diff --git a/TODO.md b/TODO.md index 6ebea6af..5b3c4475 100644 --- a/TODO.md +++ b/TODO.md @@ -160,3 +160,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * prevent adding local email addresses on account.contacts account.email * Resource monitoring without ROUTE alert or explicit error + + +* Domain validation has to be done with injected records and subdomains diff --git a/orchestra/apps/accounts/migrations/0001_initial.py b/orchestra/apps/accounts/migrations/0001_initial.py deleted file mode 100644 index 97154ba7..00000000 --- a/orchestra/apps/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import django.utils.timezone -import django.core.validators - - -class Migration(migrations.Migration): - - dependencies = [ - ('systemusers', '__first__'), - ] - - operations = [ - migrations.CreateModel( - name='Account', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')), - ('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and ./-/_ only.', unique=True, max_length=64, verbose_name='username', validators=[django.core.validators.RegexValidator(b'^[\\w.-]+$', 'Enter a valid username.', b'invalid')])), - ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), - ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), - ('email', models.EmailField(help_text='Used for password recovery', max_length=75, verbose_name='email address')), - ('type', models.CharField(default=b'INDIVIDUAL', max_length=32, verbose_name='type', choices=[(b'INDIVIDUAL', 'Individual'), (b'ASSOCIATION', 'Association'), (b'CUSTOMER', 'Customer'), (b'STAFF', 'Staff')])), - ('language', models.CharField(default=b'ca', max_length=2, verbose_name='language', choices=[(b'ca', 'Catalan'), (b'es', 'Spanish'), (b'en', 'English')])), - ('comments', models.TextField(max_length=256, verbose_name='comments', blank=True)), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('main_systemuser', models.ForeignKey(related_name='accounts_main', to='systemusers.SystemUser', null=True)), - ], - options={ - 'abstract': False, - }, - bases=(models.Model,), - ), - ] diff --git a/orchestra/apps/accounts/migrations/0002_auto_20141027_1414.py b/orchestra/apps/accounts/migrations/0002_auto_20141027_1414.py deleted file mode 100644 index 7cced87f..00000000 --- a/orchestra/apps/accounts/migrations/0002_auto_20141027_1414.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='account', - name='first_name', - ), - migrations.RemoveField( - model_name='account', - name='last_name', - ), - migrations.AddField( - model_name='account', - name='full_name', - field=models.CharField(default='', max_length=30, verbose_name='full name'), - preserve_default=False, - ), - migrations.AddField( - model_name='account', - name='short_name', - field=models.CharField(default='', max_length=30, verbose_name='short name', blank=True), - preserve_default=False, - ), - ] diff --git a/orchestra/apps/accounts/migrations/__init__.py b/orchestra/apps/accounts/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index b2891956..dfbbba1f 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -14,13 +14,13 @@ from . import settings class Account(auth.AbstractBaseUser): username = models.CharField(_("username"), max_length=64, unique=True, - help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), + help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."), validators=[validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')]) main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True, - related_name='accounts_main') - short_name = models.CharField(_("short name"), max_length=30, blank=True) - full_name = models.CharField(_("full name"), max_length=30) + related_name='accounts_main', editable=False) + short_name = models.CharField(_("short name"), max_length=64, blank=True) + full_name = models.CharField(_("full name"), max_length=256) email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES, max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE) diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index 66100d37..f2dd8847 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -15,7 +15,7 @@ ACCOUNTS_DEFAULT_TYPE = getattr(settings, 'ACCOUNTS_DEFAULT_TYPE', 'INDIVIDUAL') ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', ( - ('en', _('English')), + ('EN', _('English')), )) @@ -23,7 +23,7 @@ ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL', 'systemusers.SystemUser') -ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') +ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'EN') ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index 72406880..52bd6765 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -1,6 +1,6 @@ from dateutil.relativedelta import relativedelta -from django.core.validators import ValidationError +from django.core.validators import ValidationError, RegexValidator from django.db import models from django.template import loader, Context from django.utils import timezone @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.apps.accounts.models import Account from orchestra.apps.contacts.models import Contact -from orchestra.core import accounts +from orchestra.core import accounts, validators from orchestra.utils.html import html_to_pdf from . import settings @@ -24,8 +24,11 @@ class BillContact(models.Model): address = models.TextField(_("address")) city = models.CharField(_("city"), max_length=128, default=settings.BILLS_CONTACT_DEFAULT_CITY) - zipcode = models.PositiveIntegerField(_("zip code")) + zipcode = models.CharField(_("zip code"), max_length=10, + validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', + _("Enter a valid zipcode."), 'invalid')]) country = models.CharField(_("country"), max_length=20, + choices=settings.BILLS_CONTACT_COUNTRIES, default=settings.BILLS_CONTACT_DEFAULT_COUNTRY) vat = models.CharField(_("VAT number"), max_length=64) @@ -34,6 +37,12 @@ class BillContact(models.Model): def get_name(self): return self.name or self.account.get_full_name() + + def clean(self): + self.vat = self.vat.strip() + self.city = self.city.strip() + validators.validate_vat(self.vat, self.country) + validators.validate_zipcode(self.zipcode, self.country) class BillManager(models.Manager): diff --git a/orchestra/apps/bills/settings.py b/orchestra/apps/bills/settings.py index b4e30ada..000c18b2 100644 --- a/orchestra/apps/bills/settings.py +++ b/orchestra/apps/bills/settings.py @@ -1,4 +1,5 @@ from django.conf import settings +from django_countries import data BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4) @@ -57,4 +58,9 @@ BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order') BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona') -BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'Spain') +BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', data.COUNTRIES) + + +BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'ES') + + diff --git a/orchestra/apps/contacts/models.py b/orchestra/apps/contacts/models.py index 5a0482c2..9e10d95b 100644 --- a/orchestra/apps/contacts/models.py +++ b/orchestra/apps/contacts/models.py @@ -1,12 +1,17 @@ +from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.db import models from django.utils.translation import ugettext_lazy as _ -from orchestra.core import accounts +from orchestra.core import accounts, validators from orchestra.models.fields import MultiSelectField from . import settings +validate_phone = lambda p: validators.validate_phone(p, settings.CONTACTS_DEFAULT_COUNTRY) + + class ContactQuerySet(models.QuerySet): def filter(self, *args, **kwargs): usages = kwargs.pop('email_usages', []) @@ -37,14 +42,17 @@ class Contact(models.Model): email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True, choices=EMAIL_USAGES, default=settings.CONTACTS_DEFAULT_EMAIL_USAGES) - phone = models.CharField(_("phone"), max_length=32, blank=True) - phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True) + phone = models.CharField(_("phone"), max_length=32, blank=True, + validators=[validate_phone]) + phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True, + validators=[validate_phone]) address = models.TextField(_("address"), blank=True) - city = models.CharField(_("city"), max_length=128, blank=True, - default=settings.CONTACTS_DEFAULT_CITY) - zipcode = models.PositiveIntegerField(_("zip code"), null=True, blank=True) + city = models.CharField(_("city"), max_length=128, blank=True) + zipcode = models.CharField(_("zip code"), max_length=10, blank=True, + validators=[RegexValidator(r'^[0-9,A-Z]{3,10}$', + _("Enter a valid zipcode."), 'invalid')]) country = models.CharField(_("country"), max_length=20, blank=True, - default=settings.CONTACTS_DEFAULT_COUNTRY) + choices=settings.CONTACTS_COUNTRIES) def __unicode__(self): return self.short_name @@ -57,6 +65,12 @@ class Contact(models.Model): self.address = self.address.strip() self.city = self.city.strip() self.country = self.country.strip() + if self.address and not (self.city and self.zipcode and self.country): + raise ValidationError(_("City, zipcode and country must be provided when address is provided.")) + if self.zipcode and not self.country: + raise ValidationError(_("Country must be provided when zipcode is provided.")) + elif self.zipcode and self.country: + validators.validate_zipcode(self.zipcode, self.country) accounts.register(Contact) diff --git a/orchestra/apps/contacts/settings.py b/orchestra/apps/contacts/settings.py index d84c0259..8c663d15 100644 --- a/orchestra/apps/contacts/settings.py +++ b/orchestra/apps/contacts/settings.py @@ -1,4 +1,5 @@ from django.conf import settings +from django_countries import data CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', @@ -9,7 +10,7 @@ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona') -CONTACTS_DEFAULT_PROVINCE = getattr(settings, 'CONTACTS_DEFAULT_PROVINCE', 'Barcelona') +CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', data.COUNTRIES) -CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'Spain') +CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'ES') diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 6ac12dda..6722601f 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -2,8 +2,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orchestra.core import services -from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address, - validate_hostname, validate_ascii) +from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii from orchestra.utils.python import AttrDict from . import settings, validators, utils @@ -11,11 +10,11 @@ from . import settings, validators, utils class Domain(models.Model): name = models.CharField(_("name"), max_length=256, unique=True, - validators=[validate_hostname, validators.validate_allowed_domain], + validators=[validators.validate_domain_name, validators.validate_allowed_domain], help_text=_("Domain or subdomain name.")) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='domains', blank=True, help_text=_("Automatically selected for subdomains.")) - top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains') + top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains', editable=False) serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, help_text=_("Serial number")) diff --git a/orchestra/apps/domains/validators.py b/orchestra/apps/domains/validators.py index 891476f3..e5d366a9 100644 --- a/orchestra/apps/domains/validators.py +++ b/orchestra/apps/domains/validators.py @@ -4,6 +4,7 @@ import re from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ +from orchestra.core.validators import validate_hostname from orchestra.utils import paths from orchestra.utils.system import run @@ -23,6 +24,15 @@ def validate_allowed_domain(value): raise ValidationError(_("This domain name is not allowed")) +def validate_domain_name(value): + # SRV records may use '_' in the domain name + value = value.lstrip('*.').replace('_', '') + try: + validate_hostname(value) + except ValidationError: + raise ValidationError(_("Not a valid domain name.")) + + def validate_zone_interval(value): try: int(value) diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index d22969ae..d3d9e1fc 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -24,6 +24,11 @@ class PaymentMethod(plugins.Plugin): plugins.append(import_class(cls)) return plugins + @classmethod + def clean_data(cls, data): + """ model clean """ + return data + def get_form(self): self.form.plugin = self self.form.plugin_field = 'method' diff --git a/orchestra/apps/payments/methods/sepadirectdebit.py b/orchestra/apps/payments/methods/sepadirectdebit.py index 7bd55c3b..64380795 100644 --- a/orchestra/apps/payments/methods/sepadirectdebit.py +++ b/orchestra/apps/payments/methods/sepadirectdebit.py @@ -23,24 +23,12 @@ class SEPADirectDebitForm(PluginDataForm): widget=forms.TextInput(attrs={'size': '50'})) name = forms.CharField(max_length=128, label=_("Name"), widget=forms.TextInput(attrs={'size': '50'})) - - def clean_iban(self): - return self.cleaned_data['iban'].strip() - - def clean_name(self): - return self.cleaned_data['name'].strip() class SEPADirectDebitSerializer(serializers.Serializer): iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) name = serializers.CharField(label=_("Name"), max_length=128) - - def clean_iban(self, attrs, source): - return attrs[source].strip() - - def clean_name(self, attrs, source): - return attrs[source].strip() class SEPADirectDebit(PaymentMethod): @@ -56,6 +44,13 @@ class SEPADirectDebit(PaymentMethod): return _("This bill will been automatically charged to your bank account " " with IBAN number
%s.") % source.number + @classmethod + def clean_data(cls, data): + data['iban'] = data['iban'].strip() + data['name'] = data['name'].strip() + IBANValidator()(data['iban']) + return data + @classmethod def process(cls, transactions): debts = [] diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 3ad2f259..afe52588 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -49,6 +49,9 @@ class PaymentSource(models.Model): def get_due_delta(self): return self.method_class().due_delta + + def clean(self): + self.data = self.method_class().clean_data(self.data) class TransactionQuerySet(models.QuerySet): diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index 63e1a362..5265117f 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -23,7 +23,7 @@ class SystemUserQuerySet(models.QuerySet): class SystemUser(models.Model): """ System users """ username = models.CharField(_("username"), max_length=64, unique=True, - help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), + help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."), validators=[validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')]) password = models.CharField(_("password"), max_length=128) diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index 86b121dd..e013737c 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -61,6 +61,14 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { }) +WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) +for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): + if value is None: + WEBAPPS_TYPES.pop(webapp_type, None) + else: + WEBAPPS_TYPES[webapp_type] = value + + WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5') @@ -74,51 +82,91 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { # PHP 'enabled_functions': ( _("PHP - Enabled functions"), - r'^[\w.,-]+$' - ), - 'PHP-register_globals': ( - _("PHP - Register globals"), - r'^(On|Off|on|off)$' + r'^[\w\.,-]+$' ), 'PHP-allow_url_include': ( _("PHP - Allow URL include"), r'^(On|Off|on|off)$' ), + 'PHP-allow_url_fopen': ( + _("PHP - allow_url_fopen"), + r'^(On|Off|on|off)$' + ), 'PHP-auto_append_file': ( _("PHP - Auto append file"), - r'^none$' + r'^[\w\.,-/]+$' + ), + 'PHP-auto_prepend_file': ( + _("PHP - Auto prepend file"), + r'^[\w\.,-/]+$' + ), + 'PHP-date.timezone': ( + _("PHP - date.timezone"), + r'^\w+/\w+$' ), 'PHP-default_socket_timeout': ( _("PHP - Default socket timeout"), - r'P^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'PHP-display_errors': ( _("PHP - Display errors"), r'^(On|Off|on|off)$' ), + 'PHP-extension': ( + _("PHP - Extension"), + r'^[^ ]+$' + ), 'PHP-magic_quotes_gpc': ( _("PHP - Magic quotes GPC"), r'^(On|Off|on|off)$' ), + 'PHP-magic_quotes_runtime': ( + _("PHP - Magic quotes runtime"), + r'^(On|Off|on|off)$' + ), + 'PHP-magic_quotes_sybase': ( + _("PHP - Magic quotes sybase"), + r'^(On|Off|on|off)$' + ), 'PHP-max_execution_time': ( _("PHP - Max execution time"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'PHP-max_input_time': ( _("PHP - Max input time"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'PHP-memory_limit': ( _("PHP - Memory limit"), - r'^[0-9][0-9]?[0-9]?M$' + r'^[0-9]{1,3}M$' ), 'PHP-mysql.connect_timeout': ( _("PHP - Mysql connect timeout"), - r'^[0-9][0-9]?[0-9]?$' + r'^([0-9]){1,3}$' + ), + 'PHP-output_buffering': ( + _("PHP - output_buffering"), + r'^(On|Off|on|off)$' + ), + 'PHP-register_globals': ( + _("PHP - Register globals"), + r'^(On|Off|on|off)$' ), 'PHP-post_max_size': ( _("PHP - Post max size"), - r'^[0-9][0-9]?M$' + r'^[0-9]{1,3}M$' + ), + 'PHP-sendmail_path': ( + _("PHP - sendmail_path"), + r'^[^ ]+$' + ), + 'PHP-session.bug_compat_warn': ( + _("PHP - session.bug_compat_warn"), + r'^(On|Off|on|off)$' + ), + 'PHP-session.auto_start': ( + _("PHP - session.auto_start"), + r'^(On|Off|on|off)$' ), 'PHP-safe_mode': ( _("PHP - Safe mode"), @@ -126,32 +174,48 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { ), 'PHP-suhosin.post.max_vars': ( _("PHP - Suhosin post max vars"), - r'^[0-9][0-9]?[0-9]?[0-9]?$' + r'^[0-9]{1,4}$' ), 'PHP-suhosin.request.max_vars': ( _("PHP - Suhosin request max vars"), - r'^[0-9][0-9]?[0-9]?[0-9]?$' + r'^[0-9]{1,4}$' + ), + 'PHP-suhosin.session.encrypt': ( + _("PHP - suhosin.session.encrypt"), + r'^(On|Off|on|off)$' ), 'PHP-suhosin.simulation': ( _("PHP - Suhosin simulation"), r'^(On|Off|on|off)$' ), + 'PHP-suhosin.executor.include.whitelist': ( + _("PHP - suhosin.executor.include.whitelist"), + r'^(upload|phar)$' + ), + 'PHP-upload_max_filesize': ( + _("PHP - upload_max_filesize"), + r'^[0-9]{1,3}M$' + ), + 'PHP-zend_extension': ( + _("PHP - zend_extension"), + r'^[^ ]+$' + ), # FCGID 'FcgidIdleTimeout': ( _("FCGI - Idle timeout"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'FcgidBusyTimeout': ( _("FCGI - Busy timeout"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'FcgidConnectTimeout': ( _("FCGI - Connection timeout"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), 'FcgidIOTimeout': ( _("FCGI - IO timeout"), - r'^[0-9][0-9]?[0-9]?$' + r'^[0-9]{1,3}$' ), }) diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 88e3b3ad..0888a06e 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -24,6 +24,7 @@ class Apache2Backend(ServiceController): if site.protocol is 'https': extra_conf += self.get_ssl(site) extra_conf += self.get_security(site) + extra_conf += self.get_redirect(site) context['extra_conf'] = extra_conf apache_conf = Template(textwrap.dedent("""\ @@ -89,7 +90,7 @@ class Apache2Backend(ServiceController): Options +ExecCGI AddHandler fcgid-script .php - FcgidWrapper %(fcgid_path)s + FcgidWrapper %(fcgid_path)s\ """ % context) for option in content.webapp.options.filter(name__startswith='Fcgid'): fcgid += " %s %s\n" % (option.name, option.value) @@ -101,10 +102,12 @@ class Apache2Backend(ServiceController): custom_cert = site.options.filter(name='ssl') if custom_cert: cert = tuple(custom_cert[0].value.split()) + # TODO separate directtives? directives = textwrap.dedent("""\ SSLEngine on SSLCertificateFile %s - SSLCertificateKeyFile %s""" % cert + SSLCertificateKeyFile %s\ + """ % cert ) return directives @@ -112,14 +115,24 @@ class Apache2Backend(ServiceController): directives = '' for rules in site.options.filter(name='sec_rule_remove'): for rule in rules.value.split(): - directives += "SecRuleRemoveById %i" % int(rule) - + directives += "SecRuleRemoveById %i\n" % int(rule) for modsecurity in site.options.filter(name='sec_rule_off'): directives += textwrap.dedent("""\ SecRuleEngine Off - + \ """ % modsecurity.value) + if directives: + directives = '\n%s\n' % directives + return directives + + def get_redirect(self, site): + directives = '' + for redirect in site.options.filter(name='redirect'): + if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value): + directives += "RedirectMatch %s" % redirect.value + else: + directives += "Redirect %s" % redirect.value return directives def get_protections(self, site): diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 1068f45c..8290b584 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -17,25 +17,34 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') +# TODO ssl ca, ssl cert, ssl key WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { # { name: ( verbose_name, validation_regex ) } 'directory_protection': ( _("HTTPD - Directory protection"), r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$' ), - 'redirection': ( + 'redirect': ( _("HTTPD - Redirection"), - r'^.*\s+.*$' + r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$' ), - 'ssl': ( - _("HTTPD - SSL"), - r'^.*\s+.*$' + 'ssl_ca': ( + _("HTTPD - SSL CA"), + r'^[^ ]+$' + ), + 'ssl_cert': ( + _("HTTPD - SSL cert"), + r'^[^ ]+$' + ), + 'ssl_key': ( + _("HTTPD - SSL key"), + r'^[^ ]+$' ), 'sec_rule_remove': ( _("HTTPD - SecRuleRemoveById"), - r'^[0-9,\s]+$' + r'^[0-9\s]+$' ), - 'sec_rule_off': ( + 'sec_engine': ( _("HTTPD - Disable Modsecurity"), r'^[\w/_]+$' ), diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index 86558885..eccf6b9e 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -155,7 +155,10 @@ function install_requirements () { lxml==3.3.5 \ python-dateutil==2.2 \ django-iban==0.3.0 \ - requests" + requests \ + phonenumbers \ + django-countries \ + django-localflavor" if $testing; then APT="${APT} \ diff --git a/orchestra/core/validators.py b/orchestra/core/validators.py index 15977cc8..2f237bfb 100644 --- a/orchestra/core/validators.py +++ b/orchestra/core/validators.py @@ -1,12 +1,16 @@ import re import crack +import localflavor +import phonenumbers from django.core import validators from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from IPy import IP +from ..utils.python import import_class + def validate_ipv4_address(value): msg = _("%s is not a valid IPv4 address") % value @@ -18,7 +22,6 @@ def validate_ipv4_address(value): raise ValidationError(msg) - def validate_ipv6_address(value): msg = _("%s is not a valid IPv6 address") % value try: @@ -61,11 +64,12 @@ def validate_hostname(hostname): http://stackoverflow.com/a/2532344 """ if len(hostname) > 255: - return False - if hostname[-1] == ".": - hostname = hostname[:-1] # strip exactly one dot from the right, if present - allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?'): port = 443 + wrapper_root = None + webalizer = False + webappname = None + elif line.startswith("DocumentRoot"): + __, path = line.split() + webappname = path.rstrip('/').split('/')[-1] + if webappname == 'public_html': + webappname = '' elif line.startswith("ServerName"): - domain = line.split()[1] - name = domain + __, domain = line.split() + sitename = domain domains.append("'%s'" % domain) elif line.startswith("ServerAlias"): for domain in line.split()[1:]: domains.append("'%s'" % domain) elif line.startswith("Alias /fcgi-bin/"): - fcgid = line.split('/')[-1] or line.split('/')[-2] - fcgid = fcgid.split('-')[0] - apps.append((name, fcgid, '/')) + __, __, wrapper_root = line.split() + elif line.startswith('Action php-fcgi'): + __, __, wrapper_name = line.split() + wrapper_name = wrapper_name.split('/')[-1] elif line.startswith("Alias /webalizer"): - apps.append(('webalizer', 'webalizer', '/webalizer')) + webalizer = True elif line == '': if port == 443: - name += '-ssl' - print "# SITE" - print "website, __ = Website.objects.get_or_create(name='%s', account=account, port=%d)" % (name, port) - domains = ', '.join(domains) - print "for domain in [%s]:" % str(domains) - print " try:" - print " domain = Domain.objects.get(name=domain)" - print " except:" - print " domain = Domain.objects.create(name=domain, account=account)" - print " else:" - print " domain.account = account" - print " domain.save()" - print " website.domains.add(domain)" - print "" - for name, type, path in apps: - print "try:" - print " webapp = WebApp.objects.get(account=account, name='%s')" % name - print "except:" - print " webapp = WebApp.objects.create(account=account, name='%s', type='%s')" % (name, type) - print "else:" - print " webapp.type = '%s'" % type - print " webapp.save()" - print "" - print "Content.objects.get_or_create(website=website, webapp=webapp, path='%s')" % path + sitename += '-ssl' + context = { + 'sitename': sitename, + 'port': port, + 'domains': ', '.join(domains), + } + print textwrap.dedent("""\ + # SITE" + website, __ = Website.objects.get_or_create(name='%(sitename)s', account=account, port=%(port)d) + for domain in [%(domains)s]: + try: + domain = Domain.objects.get(name=domain) + except: + domain = Domain.objects.create(name=domain, account=account) + else: + domain.account = account + domain.save() + website.domains.add(domain) + """ % context) + if wrapper_root: + wrapper = os.join(wrapper_root, wrapper_name) + fcgid = run('grep "^\s*exec " %s' % wrapper).stdout + type = fcgid.split()[1].split('/')[-1].split('-')[0] + for option in fcgid.split('-d'): + print option + print_webapp({ + 'name': webappname, + 'path': '/', + 'type': type, + }) + if webalizer: + print_webapp({ + 'name': 'webalizer-%s' % sitename, + 'path': '/webalizer', + 'type': 'webalizer', + }) print '\n' diff --git a/scripts/migration/virtusertable.sh b/scripts/migration/virtusertable.sh index 212f73a8..5e793bd2 100644 --- a/scripts/migration/virtusertable.sh +++ b/scripts/migration/virtusertable.sh @@ -1,34 +1,64 @@ #!/bin/bash -VIRTUALTABLE="/etc/postfix/virtusertable" +VIRTUALTABLE=${1-"/etc/postfix/virtusertable"} -echo "from orchestra.apps.users import User" -echo "from orchestra.apps.users.roles.mailbox import Address, Mailbox" -echo "from orchestra.apps.domains import Domain" +echo "from orchestra.apps.accounts.models import Account" +echo "from orchestra.apps.mailboxes.models import Address, Mailbox" +echo "from orchestra.apps.domains.models import Domain" +echo "main_account = Account.objects.get(id=1)" cat "$VIRTUALTABLE"|grep -v "^\s*$"|while read line; do NAME=$(echo "$line" | awk {'print $1'} | cut -d'@' -f1) DOMAIN=$(echo "$line" | awk {'print $1'} | cut -d'@' -f2) DESTINATION=$(echo "$line" | awk '{$1=""; print $0}' | sed -e 's/^ *//' -e 's/ *$//') echo "domain = Domain.objects.get(name='$DOMAIN')" + echo "mailboxes = []" + echo "account = main_account" + NEW_DESTINATION="" for PLACE in $DESTINATION; do if [[ ! $(echo $PLACE | grep '@') ]]; then - echo "try:" - echo " user = User.objects.get(username='$PLACE')" - echo "except:" - echo " print 'User $PLACE does not exists'" - echo "else:" - echo " mailbox, __ = Mailbox.objects.get_or_create(user=user)" - echo " if user.account_id != 1:" - echo " user.account=domain.account" - echo " user.save()" - echo "" + if [[ $(grep "^${PLACE}:" /etc/shadow) ]]; then + PASSWORD=$(grep "^${PLACE}:" /etc/shadow | cut -d':' -f2) + echo "if account == main_account and domain.account != main_account:" + echo " account = domain.account" + echo "else:" + echo " try:" + echo " account = Account.objects.get(username='${PLACE}')" + echo " except:" + echo " pass" + echo "mailboxes.append(('${PLACE}', '${PASSWORD}'))" + else + NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}" + fi + else + NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}" fi done - echo "address, __ = Address.objects.get_or_create(name='$NAME', domain=domain)" - echo "address.account=domain.account" - echo "address.destination='$DESTINATION'" - echo "address.save()" + echo "for mailbox, password in mailboxes:" + echo " mailbox = mailbox.strip()" + echo " try:" + echo " mailbox = Mailbox.objects.get(username=mailbox)" + echo " except:" + echo " mailbox = Mailbox(username=mailbox, password=password, account=account)" + echo " try:" + echo " mailbox.full_clean()" + echo " except:" + echo " sys.stderr.write('cleaning')" + echo " else:" + echo " mailbox.save()" + echo " else:" + echo " if mailbox.account != account:" + echo " sys.stderr.write('%s != %s' % (mailbox.account, account))" + echo " if domain.account != account:" + echo " sys.stderr.write('%s != %s' % (domain.account, account))" + echo " address = Address(name='${NAME}', domain=domain, account=account, destination='${NEW_DESTINATION}')" + echo " try:" + echo " address.full_clean()" + echo " except:" + echo " sys.stderr.write('cleaning address')" + echo " else:" + echo " address.save()" + echo " domain = None" done