Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ide/models/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from django.conf import settings
from django.db import models
from ide.models.project import Project
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _

from ide.models.meta import IdeModel
from ide.utils.regexes import regexes

import utils.s3 as s3
__author__ = 'katharine'
Expand All @@ -34,7 +35,7 @@ class BuildResult(IdeModel):
DEBUG_WORKER = 1

project = models.ForeignKey(Project, related_name='builds')
uuid = models.CharField(max_length=36, default=lambda:str(uuid.uuid4()))
uuid = models.CharField(max_length=36, default=lambda: str(uuid.uuid4()), validators=regexes.validator('uuid', _('Invalid UUID.')))
state = models.IntegerField(choices=STATE_CHOICES, default=STATE_WAITING)
started = models.DateTimeField(auto_now_add=True, db_index=True)
finished = models.DateTimeField(blank=True, null=True)
Expand Down
8 changes: 4 additions & 4 deletions ide/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ide.models.s3file import S3File
from ide.models.textfile import TextFile
from ide.models.meta import IdeModel
from ide.utils.regexes import regexes

__author__ = 'katharine'

Expand All @@ -28,7 +29,7 @@ class ResourceFile(IdeModel):
('pbi', _('1-bit Pebble image')),
)

file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_(). -]+$", message=_("Invalid filename."))])
file_name = models.CharField(max_length=100, validators=regexes.validator('resource_file_name', _("Invalid filename.")))
kind = models.CharField(max_length=9, choices=RESOURCE_KINDS)
is_menu_icon = models.BooleanField(default=False)

Expand Down Expand Up @@ -141,7 +142,6 @@ def get_tags_string(self):
return "".join(self.get_tag_names())

def save(self, *args, **kwargs):
self.full_clean()
self.resource_file.save()
super(ResourceVariant, self).save(*args, **kwargs)

Expand All @@ -168,7 +168,7 @@ class Meta(S3File.Meta):

class ResourceIdentifier(IdeModel):
resource_file = models.ForeignKey(ResourceFile, related_name='identifiers')
resource_id = models.CharField(max_length=100)
resource_id = models.CharField(max_length=100, validators=regexes.validator('c_identifier', _("Invalid resource ID.")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite right; resource IDs are prefixed and so don't have the usual start-of-identifier rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

character_regex = models.CharField(max_length=100, blank=True, null=True)
tracking = models.IntegerField(blank=True, null=True)
compatibility = models.CharField(max_length=10, blank=True, null=True)
Expand Down Expand Up @@ -225,7 +225,7 @@ def save(self, *args, **kwargs):

class SourceFile(TextFile):
project = models.ForeignKey('Project', related_name='source_files')
file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_.-]+\.(c|h|js)$", message=_("Invalid filename."))])
file_name = models.CharField(max_length=100, validators=regexes.validator('source_file_name', _('Invalid file name.')))
folder = 'sources'

TARGETS = (
Expand Down
10 changes: 9 additions & 1 deletion ide/models/meta.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from django.db import models

from django.db.models.signals import pre_save
from django.dispatch import receiver

class IdeModel(models.Model):
class Meta:
abstract = True
app_label = "ide"


@receiver(pre_save)
def pre_save_full_clean_handler(sender, instance, *args, **kwargs):
""" Force IdeModels to call full_clean before save """
if isinstance(instance, IdeModel):
instance.full_clean()
8 changes: 4 additions & 4 deletions ide/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

from django.contrib.auth.models import User
from django.db import models, transaction
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError

from ide.models.files import ResourceFile, ResourceIdentifier, SourceFile, ResourceVariant
from ide.models.dependency import Dependency
from ide.models.meta import IdeModel
from ide.utils import generate_half_uuid
from ide.utils.regexes import regexes
from ide.utils.version import version_to_semver, semver_to_version, parse_sdk_version

__author__ = 'katharine'
Expand Down Expand Up @@ -42,7 +43,7 @@ class Project(IdeModel):
sdk_version = models.CharField(max_length=6, choices=SDK_VERSIONS, default='2')

# New settings for 2.0
app_uuid = models.CharField(max_length=36, blank=True, null=True, default=generate_half_uuid)
app_uuid = models.CharField(max_length=36, blank=True, null=True, default=generate_half_uuid, validators=regexes.validator('uuid', _('Invalid UUID.')))
app_company_name = models.CharField(max_length=100, blank=True, null=True)
app_short_name = models.CharField(max_length=100, blank=True, null=True)
app_long_name = models.CharField(max_length=100, blank=True, null=True)
Expand Down Expand Up @@ -85,7 +86,6 @@ def set_dependencies(self, dependencies):
Dependency.objects.filter(project=self).delete()
for name, version in dependencies.iteritems():
dep = Dependency.objects.create(project=self, name=name, version=version)
dep.full_clean()
dep.save()

@property
Expand Down Expand Up @@ -118,7 +118,7 @@ def get_parsed_appkeys(self):
else:
parsed_keys = []
for appkey in app_keys:
parsed = re.match(r'^([a-zA-Z_][_a-zA-Z\d]*)(?:\[(\d+)\])?$', appkey)
parsed = re.match(regexes.C_IDENTIFIER_WITH_INDEX, appkey)
if not parsed:
raise ValueError("Bad Appkey %s" % appkey)
parsed_keys.append((parsed.group(1), parsed.group(2) or 1))
Expand Down
9 changes: 6 additions & 3 deletions ide/static/ide/js/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ CloudPebble.Resources = (function() {
var identifier = resource.identifiers[0];
resource.identifiers = [identifier + '_WHITE', identifier + '_BLACK'];
}
CloudPebble.Sidebar.SetPopover('resource-' + resource.id, ngettext('Identifier', 'Identifiers', resource.identifiers.length), resource.identifiers.join('<br>'));
var popover_content = _.map(resource.identifiers, function(identifier) {
return $('<span>').text(identifier).html()
}).join('<br>');
CloudPebble.Sidebar.SetPopover('resource-' + resource.id, ngettext('Identifier', 'Identifiers', resource.identifiers.length), popover_content);
// We need to update code completion so it can include these identifiers.
// However, don't do this during initial setup; the server handle it for us.
if(CloudPebble.Ready) {
Expand Down Expand Up @@ -297,7 +300,7 @@ CloudPebble.Resources = (function() {
}

// Validate the file name
if (!/^[a-zA-Z0-9_(). -]+$/.test(name)) {
if (!REGEXES.resource_file_name.test(name)) {
throw new Error(gettext("You must provide a valid filename. Only alphanumerics and characters in the set \"_(). -\" are allowed."));
}

Expand Down Expand Up @@ -887,7 +890,7 @@ CloudPebble.Resources = (function() {
};

var validate_resource_id = function(id) {
return !/[^a-zA-Z0-9_]/.test(id);
return REGEXES.c_identifier.test(id);

};

Expand Down
4 changes: 2 additions & 2 deletions ide/static/ide/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ CloudPebble.Settings = (function() {
// This is not an appropriate use of a regex, but we have to have it for the HTML5 pattern attribute anyway,
// so we may as well reuse the effort here.
// It validates that the format matches x[.y] with x, y in [0, 255].
if(!version_label.match(VERSION_REGEX)) {
if(!version_label.match(REGEXES.sdk_version)) {
throw new Error(gettext("You must specify a valid version number."));
}
if(!app_uuid.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/)) {
if(!app_uuid.match(REGEXES.uuid)) {
throw new Error(gettext("You must specify a valid UUID (of the form 00000000-0000-0000-0000-000000000000)"));
}

Expand Down
1 change: 0 additions & 1 deletion ide/tasks/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ def make_valid_filename(zip_entry):
for root_file_name, loaded in file_exists_for_root.iteritems():
if not loaded:
raise KeyError("No file was found to satisfy the manifest filename: {}".format(root_file_name))
project.full_clean()
project.save()
send_td_event('cloudpebble_zip_import_succeeded', project=project)

Expand Down
3 changes: 2 additions & 1 deletion ide/templates/ide/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,8 @@ <h3>{% trans 'Compass and Accelerometer' %}</h3>
};
var DOC_JSON = "{% static 'ide/documentation.json' %}";
var LIBPEBBLE_PROXY = {{ libpebble_proxy | safe }};
var VERSION_REGEX = "{{version_regex|escapejs}}";
var REGEXES = _.mapObject(JSON.parse('{{regexes_json|escapejs}}'), function(v) { return new RegExp(v)});

</script>
<script src="{% static 'CodeMirror/lib/codemirror.js' %}"></script>
<script src="{% static 'CodeMirror/addon/dialog/dialog.js' %}"></script>
Expand Down
4 changes: 2 additions & 2 deletions ide/templates/ide/project/resource.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<div class="control-group" id="edit-resource-file-name_section">
<label class="control-label" for="edit-resource-file-name">{% trans 'File name' %}</label>
<div class="controls">
<input type="text" id="edit-resource-file-name" name="file_name">
<input type="text" id="edit-resource-file-name" name="file_name" pattern="{{regexes.resource_file_name}}">
</div>
</div>
</div>
Expand All @@ -40,7 +40,7 @@
<div class="controls">
{# TODO: placeholder/pattern thingybobs #}
<input type="text" class="span6 edit-resource-id font-only" placeholder="FONT_EXAMPLE_BOLD_SUBSET_24" pattern="[A-Za-z0-9_]*[0-9]+">
<input type="text" class="span6 edit-resource-id non-font-only" placeholder="IMAGE_EXAMPLE_IDENTIFIER" pattern="[A-Za-z0-9_]+">
<input type="text" class="span6 edit-resource-id non-font-only" placeholder="IMAGE_EXAMPLE_IDENTIFIER" pattern="{{ regexes.c_identifier }}">
<span class="help-block">{% trans 'This is used in your code and must be a valid C identifier.' %}<br>
<span class="font-only">{% trans 'It must end with the desired font size.' %}</span>
</span>
Expand Down
4 changes: 2 additions & 2 deletions ide/templates/ide/project/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
<label class="control-label" for="settings-version-label">{% trans "Version label" %}</label>
<div class="controls">
<input type="text" id="settings-version-label" placeholder="0.1" value="{{project.app_version_label}}"
pattern={{version_regex}}>
pattern="{{ regexes.sdk_version }}">
<span class="help-block">
{% trans "App version. Takes the format major[.minor], where major and minor are between 0 and 255." %}
</span>
Expand All @@ -130,7 +130,7 @@
<label class="control-label" for="settings-uuid">{% trans "App UUID" %}</label>
<div class="controls">
<div class="settings-uuid-control">
<input type="text" id="settings-uuid" pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}" placeholder="00000000-0000-0000-0000-000000000000" value="{{project.app_uuid}}">
<input type="text" id="settings-uuid" pattern="{{ regexes.uuid }}" placeholder="00000000-0000-0000-0000-000000000000" value="{{project.app_uuid}}">
<button id="uuid-generate" class="btn btn-small">{% trans "Generate" %}</button>
</div>
<span class="help-block">
Expand Down
36 changes: 24 additions & 12 deletions ide/tests/test_import_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@

fake_s3 = FakeS3()

RESOURCE_SPEC = {
'resources': {
'media': [{
'file': 'images/blah.png',
'name': 'IMAGE_BLAH',
'type': 'bitmap'
}]
}
}


@mock.patch('ide.models.s3file.s3', fake_s3)
class TestImportProject(CloudpebbleTestCase):
def setUp(self):
self.login()

@staticmethod
def make_resource_spec(name='IMAGE_BLAH'):
return {
'resources': {
'media': [{
'file': 'images/blah.png',
'name': name,
'type': 'bitmap'
}]
}
}

def test_import_basic_bundle_with_appinfo(self):
""" Check that a minimal bundle imports without error """
bundle = build_bundle({
Expand Down Expand Up @@ -97,7 +99,7 @@ def test_import_appinfo_with_resources(self):
bundle = build_bundle({
'src/main.c': '',
'resources/images/blah.png': 'contents!',
'appinfo.json': make_appinfo(options=RESOURCE_SPEC)
'appinfo.json': make_appinfo(options=self.make_resource_spec())
})
do_import_archive(self.project_id, bundle)
project = Project.objects.get(pk=self.project_id)
Expand All @@ -108,7 +110,7 @@ def test_import_package_with_resources(self):
bundle = build_bundle({
'src/main.c': '',
'resources/images/blah.png': 'contents!',
'package.json': make_package(pebble_options=RESOURCE_SPEC)
'package.json': make_package(pebble_options=self.make_resource_spec())
})
do_import_archive(self.project_id, bundle)
project = Project.objects.get(pk=self.project_id)
Expand Down Expand Up @@ -151,3 +153,13 @@ def test_throws_if_importing_array_appkeys_without_npm_manifest_support(self):
})
with self.assertRaises(InvalidProjectArchiveException):
do_import_archive(self.project_id, bundle)

def test_invalid_resource_id(self):
bundle = build_bundle({
'src/main.c': '',
'resources/images/blah.png': 'contents!',
'package.json': make_package(pebble_options=self.make_resource_spec("<>"))
})

with self.assertRaises(ValidationError):
do_import_archive(self.project_id, bundle)
4 changes: 2 additions & 2 deletions ide/utils/cloudpebble_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def make_appinfo(options=None):
},
"sdkVersion": "3",
"shortName": "test",
"uuid": "666x6666-x66x-66x6-x666-666666666666",
"uuid": "123e4567-e89b-42d3-a456-426655440000",
"versionLabel": "1.0",
"watchapp": {
"watchface": False
Expand Down Expand Up @@ -92,7 +92,7 @@ def make_package(package_options=None, pebble_options=None, no_pebble=False):
"media": []
},
"sdkVersion": "3",
"uuid": '666x6666-x66x-66x6-x666-666666666666',
"uuid": '123e4567-e89b-42d3-a456-426655440000',
"watchapp": {
"watchface": False
}
Expand Down
35 changes: 35 additions & 0 deletions ide/utils/regexes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.core.validators import RegexValidator


class RegexHolder(object):
regex_dictionary = {
# Match major[.minor], where major and minor are numbers between 0 and 255 with no leading 0s
'sdk_version': r'^(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])(\.(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5]))?$',

# Match major.minor.0, where major and minor are numbers between 0 and 255 with no leading 0s
'semver': r'^(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.0$',

# Match a string of letters and numbers separated with underscores but not starting with a digit
'c_identifier': r'^([a-zA-Z_][_a-zA-Z\d]*)$',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More broadly, every use of this has the same property: it has a prefix when used in C, and so actually can start with a digit.

Copy link
Contributor Author

@Spacerat Spacerat Jul 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thing I can change every usage in one place! I suppose I could actually just use ^\w+$.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget c_identifier_with_index, which is the same thing with an added suffix. ;)


# Match a C identifier optionally followed by an [array index]
'c_identifier_with_index': r'^([a-zA-Z_][_a-zA-Z\d]*)(?:\[(\d+)\])?$',

# Match a UUID4
'uuid': r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$',

# Match a valid resource file name
'resource_file_name': r'^[/a-zA-Z0-9_(). -]+$',

# Match a valid c/h/js file name
'source_file_name': r'^[/a-zA-Z0-9_.-]+\.(c|h|js)$'
}

def validator(self, key, message):
return [RegexValidator(self.regex_dictionary[key], message=message)]

def __getattr__(self, key):
return self.regex_dictionary[key.lower()]


regexes = RegexHolder()
10 changes: 3 additions & 7 deletions ide/utils/version.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import re

# Match major[.minor], where major and minor are numbers between 0 and 255 with no leading 0s
SDK_VERSION_REGEX = r"^(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])(\.(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5]))?$"

# Match major.minor.0, where major and minor are numbers between 0 and 255 with no leading 0s
SEMVER_REGEX = r"^(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.0$"
from ide.utils.regexes import regexes


def parse_sdk_version(version):
""" Parse an SDK compatible version string
:param version: should be "major[.minor]"
:return: (major, minor)
"""
parsed = re.match(SDK_VERSION_REGEX, version)
parsed = re.match(regexes.SDK_VERSION, version)
if not parsed:
raise ValueError("Invalid version {}".format(version))
major = parsed.group(1)
Expand All @@ -33,7 +29,7 @@ def parse_semver(semver):
:param semver: should be "major.minor.0"
:return: (major, minor)
"""
parsed = re.match(SEMVER_REGEX, semver)
parsed = re.match(regexes.SEMVER, semver)
if not parsed:
raise ValueError("Invalid semver {}".format(semver))
return parsed.group(1), parsed.group(2)
Expand Down
6 changes: 4 additions & 2 deletions ide/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from ide.tasks.git import hooked_commit
from ide.utils import generate_half_uuid
from utils.td_helper import send_td_event
from ide.utils.version import SDK_VERSION_REGEX
from ide.utils.regexes import regexes

__author__ = 'katharine'


Expand Down Expand Up @@ -49,7 +50,8 @@ def view_project(request, project_id):
'token': token,
'phone_shorturl': settings.PHONE_SHORTURL,
'supported_platforms': supported_platforms,
'version_regex': SDK_VERSION_REGEX,
'regexes': regexes,
'regexes_json': json.dumps(regexes.regex_dictionary),
'npm_manifest_support_enabled': settings.NPM_MANIFEST_SUPPORT
})

Expand Down