From f1acb3c617941676e6e7eedd9faffcabb66674e3 Mon Sep 17 00:00:00 2001
From: jorgepastorr <jorge.pastor.rr@gmail.com>
Date: Tue, 25 Jul 2023 16:59:58 +0200
Subject: [PATCH] webapp static form

---
 .Trash-1000/files/forms.py                    | 43 +++++++++++++++
 .Trash-1000/info/forms.py.trashinfo           |  3 ++
 orchestra/contrib/databases/admin.py          |  2 -
 .../migrations/0003_auto_20230724_1813.py     | 23 ++++++++
 orchestra/contrib/webapps/admin.py            |  3 +-
 orchestra/contrib/webapps/backends/static.py  | 20 +++++--
 .../webapps/migrations/0001_initial.py        | 51 ++++++++++++++++++
 .../migrations/0002_webapp_sftpuser.py        | 20 +++++++
 .../contrib/webapps/migrations/__init__.py    |  0
 orchestra/contrib/webapps/models.py           |  2 +
 orchestra/contrib/webapps/types/misc.py       | 53 ++++++++++++++++++-
 11 files changed, 212 insertions(+), 8 deletions(-)
 create mode 100644 .Trash-1000/files/forms.py
 create mode 100644 .Trash-1000/info/forms.py.trashinfo
 create mode 100644 orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py
 create mode 100644 orchestra/contrib/webapps/migrations/0001_initial.py
 create mode 100644 orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py
 create mode 100644 orchestra/contrib/webapps/migrations/__init__.py

diff --git a/.Trash-1000/files/forms.py b/.Trash-1000/files/forms.py
new file mode 100644
index 00000000..0f18be8a
--- /dev/null
+++ b/.Trash-1000/files/forms.py
@@ -0,0 +1,43 @@
+from django import forms
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+from django.core.exceptions import ValidationError
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+from orchestra.core import validators
+from orchestra.contrib.systemusers.models import WebappUsers
+from .models import WebApp
+
+
+class WebappCreationForm(forms.ModelForm):
+    username = forms.CharField(label=_("Username"), max_length=16,
+        required=False, validators=[validators.validate_name],
+        help_text=_("Required. 16 characters or fewer. Letters, digits and "
+                    "@/./+/-/_ only."),
+        error_messages={
+            'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and "
+                         "@/./+/-/_ characters.")})
+    user = forms.ModelChoiceField(required=False, queryset=WebappUsers.objects)
+    password1 = forms.CharField(label=_("Password"), required=False,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
+        validators=[validators.validate_password])
+    password2 = forms.CharField(label=_("Password confirmation"), required=False,
+        widget=forms.PasswordInput,
+        help_text=_("Enter the same password as above, for verification."))
+
+    class Meta:
+        model = WebApp
+        fields = ('username', 'account', 'type')
+
+    def __init__(self, *args, **kwargs):
+        super(WebappCreationForm, self).__init__(*args, **kwargs)
+    
+
+    def clean_password2(self):
+        password1 = self.cleaned_data.get("password1")
+        password2 = self.cleaned_data.get("password2")
+        if password1 and password2 and password1 != password2:
+            msg = _("The two password fields didn't match.")
+            raise ValidationError(msg)
+        return password2
diff --git a/.Trash-1000/info/forms.py.trashinfo b/.Trash-1000/info/forms.py.trashinfo
new file mode 100644
index 00000000..c239f037
--- /dev/null
+++ b/.Trash-1000/info/forms.py.trashinfo
@@ -0,0 +1,3 @@
+[Trash Info]
+Path=orchestra/contrib/webapps/forms.py
+DeletionDate=2023-07-24T18:44:25
diff --git a/orchestra/contrib/databases/admin.py b/orchestra/contrib/databases/admin.py
index 21ca6da9..42e4952f 100644
--- a/orchestra/contrib/databases/admin.py
+++ b/orchestra/contrib/databases/admin.py
@@ -49,8 +49,6 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
     add_form = DatabaseCreationForm
     readonly_fields = ('account_link', 'display_users',)
     filter_horizontal = ['users']
-    # filter_by_account_fields = ('users',)
-    # list_prefetch_related = ('users',)
     actions = (list_accounts, save_selected)
 
     @mark_safe
diff --git a/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py b/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py
new file mode 100644
index 00000000..1e1a246f
--- /dev/null
+++ b/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.28 on 2023-07-24 16:13
+
+from django.db import migrations, models
+import orchestra.core.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('systemusers', '0002_webappusers'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='webappusers',
+            options={'verbose_name': 'WebAppUser', 'verbose_name_plural': 'WebappUsers'},
+        ),
+        migrations.AlterField(
+            model_name='webappusers',
+            name='home',
+            field=models.CharField(blank=True, help_text='name dir webapp /home/&lt;main&gt;/webapps/&lt;DirName&gt;', max_length=256, validators=[orchestra.core.validators.validate_string_dir], verbose_name='WebappDir'),
+        ),
+    ]
diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py
index d1dc7a18..b75ce688 100644
--- a/orchestra/contrib/webapps/admin.py
+++ b/orchestra/contrib/webapps/admin.py
@@ -57,7 +57,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
     list_filter = ('type', HasWebsiteListFilter, DetailListFilter)
     inlines = [WebAppOptionInline]
     readonly_fields = ('account_link',)
-    change_readonly_fields = ('name', 'type', 'display_websites')
+    change_readonly_fields = ('name', 'type', 'display_websites', 'sftpuser', 'target_server')
     search_fields = ('name', 'account__username', 'data', 'website__domains__name')
     list_prefetch_related = ('content_set__website', 'content_set__website__domains')
     plugin = AppType
@@ -67,6 +67,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
 
     display_type = display_plugin_field('type')
 
+
     @mark_safe
     def display_websites(self, webapp):
         websites = []
diff --git a/orchestra/contrib/webapps/backends/static.py b/orchestra/contrib/webapps/backends/static.py
index f424d505..0756759c 100644
--- a/orchestra/contrib/webapps/backends/static.py
+++ b/orchestra/contrib/webapps/backends/static.py
@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
 from orchestra.contrib.orchestration import ServiceController
 
 from . import WebAppServiceMixin
-
+from ..settings import WEBAPP_NEW_SERVERS
 
 class StaticController(WebAppServiceMixin, ServiceController):
     """
@@ -15,9 +15,21 @@ class StaticController(WebAppServiceMixin, ServiceController):
     
     def save(self, webapp):
         context = self.get_context(webapp)
-        self.create_webapp_dir(context)
-        self.set_under_construction(context)
+        if context.get('target_server').name in WEBAPP_NEW_SERVERS:
+            self.check_webapp_dir(context)
+            self.set_under_construction(context)
+            # TODO: crea el usuario sftp
+            # webapp.name = webapp.sftpuser.directory.replace("webapps/", "")
+            # webapp.save()
+            
+        else:
+            self.create_webapp_dir(context)
+            self.set_under_construction(context)
     
     def delete(self, webapp):
         context = self.get_context(webapp)
-        self.delete_webapp_dir(context)
+        if context.get('target_server').name not in WEBAPP_NEW_SERVERS:
+            self.delete_webapp_dir(context)
+        else:
+            # TODO: elimina el usuario sftp
+            pass
diff --git a/orchestra/contrib/webapps/migrations/0001_initial.py b/orchestra/contrib/webapps/migrations/0001_initial.py
new file mode 100644
index 00000000..041f7932
--- /dev/null
+++ b/orchestra/contrib/webapps/migrations/0001_initial.py
@@ -0,0 +1,51 @@
+# Generated by Django 2.2.28 on 2023-07-24 16:08
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import jsonfield.fields
+import orchestra.core.validators
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('orchestration', '__first__'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='WebApp',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(help_text='The app will be installed in %(home)s/webapps/%(app_name)s', max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='name')),
+                ('type', models.CharField(choices=[('moodle-php', 'Moodle'), ('php', 'PHP'), ('python', 'Python'), ('static', 'Static'), ('symbolic-link', 'Symbolic link'), ('webalizer', 'Webalizer'), ('wordpress-php', 'WordPress')], max_length=32, verbose_name='type')),
+                ('data', jsonfield.fields.JSONField(blank=True, default={}, help_text='Extra information dependent of each service.', verbose_name='data')),
+                ('comments', models.TextField(blank=True, default='')),
+                ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
+                ('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to='orchestration.Server', verbose_name='Target Server')),
+            ],
+            options={
+                'verbose_name': 'Web App',
+                'verbose_name_plural': 'Web Apps',
+                'unique_together': {('name', 'account')},
+            },
+        ),
+        migrations.CreateModel(
+            name='WebAppOption',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(choices=[(None, '-------'), ('FileSystem', [('public-root', 'Public root')]), ('Process', [('timeout', 'Process timeout'), ('processes', 'Number of processes')]), ('PHP', [('enable_functions', 'Enable functions'), ('disable_functions', 'Disable functions'), ('allow_url_include', 'Allow URL include'), ('allow_url_fopen', 'Allow URL fopen'), ('auto_append_file', 'Auto append file'), ('auto_prepend_file', 'Auto prepend file'), ('date.timezone', 'date.timezone'), ('default_socket_timeout', 'Default socket timeout'), ('display_errors', 'Display errors'), ('extension', 'Extension'), ('include_path', 'Include path'), ('open_basedir', 'Open basedir'), ('magic_quotes_gpc', 'Magic quotes GPC'), ('magic_quotes_runtime', 'Magic quotes runtime'), ('magic_quotes_sybase', 'Magic quotes sybase'), ('max_input_time', 'Max input time'), ('max_input_vars', 'Max input vars'), ('memory_limit', 'Memory limit'), ('mysql.connect_timeout', 'Mysql connect timeout'), ('output_buffering', 'Output buffering'), ('register_globals', 'Register globals'), ('post_max_size', 'Post max size'), ('sendmail_path', 'Sendmail path'), ('session.bug_compat_warn', 'Session bug compat warning'), ('session.auto_start', 'Session auto start'), ('safe_mode', 'Safe mode'), ('suhosin.post.max_vars', 'Suhosin POST max vars'), ('suhosin.get.max_vars', 'Suhosin GET max vars'), ('suhosin.request.max_vars', 'Suhosin request max vars'), ('suhosin.session.encrypt', 'Suhosin session encrypt'), ('suhosin.simulation', 'Suhosin simulation'), ('suhosin.executor.include.whitelist', 'Suhosin executor include whitelist'), ('upload_max_filesize', 'Upload max filesize'), ('upload_tmp_dir', 'Upload tmp dir'), ('zend_extension', 'Zend extension')])], max_length=128, verbose_name='name')),
+                ('value', models.CharField(max_length=256, verbose_name='value')),
+                ('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='webapps.WebApp', verbose_name='Web application')),
+            ],
+            options={
+                'verbose_name': 'option',
+                'verbose_name_plural': 'options',
+                'unique_together': {('webapp', 'name')},
+            },
+        ),
+    ]
diff --git a/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py b/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py
new file mode 100644
index 00000000..cfbaec51
--- /dev/null
+++ b/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.28 on 2023-07-24 16:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('systemusers', '0003_auto_20230724_1813'),
+        ('webapps', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='webapp',
+            name='sftpuser',
+            field=models.ForeignKey(blank=True, help_text='This option is only required for the new webservers.', null=True, on_delete=django.db.models.deletion.CASCADE, to='systemusers.WebappUsers', verbose_name='SFTP user'),
+        ),
+    ]
diff --git a/orchestra/contrib/webapps/migrations/__init__.py b/orchestra/contrib/webapps/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py
index 21f07979..2f1fe374 100644
--- a/orchestra/contrib/webapps/models.py
+++ b/orchestra/contrib/webapps/models.py
@@ -26,6 +26,8 @@ class WebApp(models.Model):
     target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE,
         verbose_name=_("Target Server"), related_name='webapps')
     comments = models.TextField(default="", blank=True)
+    sftpuser = models.ForeignKey('systemusers.WebappUsers', blank=True, null=True,  on_delete=models.CASCADE ,
+        verbose_name=_("SFTP user"), help_text=_("This option is only required for the new webservers."))
 
     # CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them
     databases = VirtualDatabaseRelation('databases.Database')
diff --git a/orchestra/contrib/webapps/types/misc.py b/orchestra/contrib/webapps/types/misc.py
index 1619bbf3..cfcde517 100644
--- a/orchestra/contrib/webapps/types/misc.py
+++ b/orchestra/contrib/webapps/types/misc.py
@@ -2,14 +2,64 @@ import os
 
 from django import forms
 from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ValidationError
 from rest_framework import serializers
 
+from orchestra.core import validators
+from orchestra.plugins.forms import PluginDataForm
+from orchestra.utils.python import random_ascii
+
 from ..options import AppOption
+from ..settings import WEBAPP_NEW_SERVERS
 
 from . import AppType
 from .php import PHPApp, PHPAppForm, PHPAppSerializer
 
 
+class StaticForm(PluginDataForm):
+    username = forms.CharField(label=_("Username"), max_length=16,
+        required=False, validators=[validators.validate_name],
+        help_text=_("Required. 16 characters or fewer. Letters, digits and "
+                    "@/./+/-/_ only."),
+        error_messages={
+            'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and "
+                         "@/./+/-/_ characters.")})
+    password1 = forms.CharField(label=_("Password"), required=False,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
+        validators=[validators.validate_password],
+        help_text=_("Suggestion: %s") % random_ascii(15))
+    password2 = forms.CharField(label=_("Password confirmation"), required=False,
+        widget=forms.PasswordInput,
+        help_text=_("Enter the same password as above, for verification."))
+
+
+    def __init__(self, *args, **kwargs):
+        super(StaticForm, self).__init__(*args, **kwargs)
+        if self.instance.id is None:
+            self.fields['sftpuser'].widget = forms.HiddenInput()
+        else:
+            self.fields['username'].widget = forms.HiddenInput()
+            self.fields['password1'].widget = forms.HiddenInput()
+            self.fields['password2'].widget = forms.HiddenInput()
+
+    def clean(self):
+        webapp_server = self.cleaned_data.get("target_server")
+        sftpuser = self.cleaned_data.get('sftpuser')
+        if webapp_server is None:
+            self.add_error("target_server", _("choice some target_server"))
+        else:
+            if webapp_server.name in WEBAPP_NEW_SERVERS and sftpuser == None:
+                self.add_error("sftpuser", _("SFTP user is required by new webservers"))
+
+    def clean_password2(self):
+        password1 = self.cleaned_data.get("password1")
+        password2 = self.cleaned_data.get("password2")
+        if password1 and password2 and password1 != password2:
+            msg = _("The two password fields didn't match.")
+            raise ValidationError(msg)
+        return password2
+
+
 class StaticApp(AppType):
     name = 'static'
     verbose_name = "Static"
@@ -17,7 +67,8 @@ class StaticApp(AppType):
                   "Apache2 will be used to serve static content and execute CGI files.")
     icon = 'orchestra/icons/apps/Static.png'
     option_groups = (AppOption.FILESYSTEM,)
-    
+    form = StaticForm
+
     def get_directive(self):
         return ('static', self.instance.get_path())