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