From 11ea3e19bed6bda27f125d2e8b30a9520bee4e85 Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Fri, 11 Jul 2014 16:37:48 +1000 Subject: [PATCH 01/19] Catch 'jav\tascript:alert("XSS")' as a bad URL The URL checker from html5lib was used as inspiration. As html5lib is based on the same ideas that browsers (should) use for parsing and checking, this solution should be much more robust. --- wagtail/wagtailcore/tests/test_whitelist.py | 7 ++++++ wagtail/wagtailcore/whitelist.py | 26 ++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/wagtail/wagtailcore/tests/test_whitelist.py b/wagtail/wagtailcore/tests/test_whitelist.py index d96100382..6589ecab5 100644 --- a/wagtail/wagtailcore/tests/test_whitelist.py +++ b/wagtail/wagtailcore/tests/test_whitelist.py @@ -17,6 +17,13 @@ class TestCheckUrl(TestCase): def test_disallowed_url_scheme(self): self.assertFalse(bool(check_url("invalid://url"))) + def test_crafty_disallowed_url_scheme(self): + """ + Some URL parsers do not parse 'jav\tascript:' as a valid scheme. + Browsers, however, do. The checker needs to catch these crafty schemes + """ + self.assertFalse(bool(check_url("jav\tascript:alert('XSS')"))) + class TestAttributeRule(TestCase): def setUp(self): diff --git a/wagtail/wagtailcore/whitelist.py b/wagtail/wagtailcore/whitelist.py index dba8982fd..356be9ecb 100644 --- a/wagtail/wagtailcore/whitelist.py +++ b/wagtail/wagtailcore/whitelist.py @@ -2,19 +2,33 @@ A generic HTML whitelisting engine, designed to accommodate subclassing to override specific rules. """ -from six.moves.urllib.parse import urlparse +import re + from bs4 import BeautifulSoup, NavigableString, Tag -ALLOWED_URL_SCHEMES = ['', 'http', 'https', 'ftp', 'mailto', 'tel'] +ALLOWED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'tel'] + +PROTOCOL_RE = re.compile("^[a-z0-9][-+.a-z0-9]*:") def check_url(url_string): - # TODO: more paranoid checks (urlparse doesn't catch - # "jav\tascript:alert('XSS')") - url = urlparse(url_string) - return (url_string if url.scheme in ALLOWED_URL_SCHEMES else None) + # Remove control characters and other disallowed characters + # Browsers sometimes ignore these, so that 'jav\tascript:alert("XSS")' + # is treated as a valid javascript: link + + unescaped = url_string.lower() + unescaped = unescaped.replace("<", "<") + unescaped = unescaped.replace(">", ">") + unescaped = unescaped.replace("&", "&") + unescaped = re.sub("[`\000-\040\177-\240\s]+", '', unescaped) + unescaped = unescaped.replace("\ufffd", "") + if PROTOCOL_RE.match(unescaped): + protocol = unescaped.split(':', 1)[0] + if protocol not in ALLOWED_URL_SCHEMES: + return None + return url_string def attribute_rule(allowed_attrs): From 01042d809ff08ec379b077fbeb7d383b1a0de487 Mon Sep 17 00:00:00 2001 From: Nick Smith Date: Wed, 15 Oct 2014 15:31:07 +0100 Subject: [PATCH 02/19] Tabbing from password field goes to Submit --- wagtail/wagtailadmin/forms.py | 8 ++++++-- wagtail/wagtailadmin/templates/wagtailadmin/login.html | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailadmin/forms.py b/wagtail/wagtailadmin/forms.py index 866ce66a1..c94315271 100644 --- a/wagtail/wagtailadmin/forms.py +++ b/wagtail/wagtailadmin/forms.py @@ -40,10 +40,14 @@ class EmailLinkChooserWithLinkTextForm(forms.Form): class LoginForm(AuthenticationForm): username = forms.CharField( max_length=254, - widget=forms.TextInput(attrs={'placeholder': ugettext_lazy("Enter your username")}), + widget=forms.TextInput(attrs={'placeholder': ugettext_lazy("Enter your username"), + 'tabindex': '1', + }), ) password = forms.CharField( - widget=forms.PasswordInput(attrs={'placeholder': ugettext_lazy("Enter password")}), + widget=forms.PasswordInput(attrs={'placeholder': ugettext_lazy("Enter password"), + 'tabindex': '2', + }), ) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/login.html b/wagtail/wagtailadmin/templates/wagtailadmin/login.html index 52448ffe5..7772f44b9 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/login.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/login.html @@ -55,7 +55,7 @@ {% endcomment %}
  • - +
  • @@ -68,4 +68,4 @@ $('form input[name=username]').focus(); }) -{% endblock %} \ No newline at end of file +{% endblock %} From 7659bf23d3ba0ad18338329ecf066b70b6585efc Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 17 Oct 2014 17:16:36 +0100 Subject: [PATCH 03/19] Use latest_revision_created_at in sitemap gen Much faster than looking it up manually for every page in the site --- wagtail/wagtailcore/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a4d0bed92..06ffb32a4 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -794,12 +794,10 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed return ['/'] def get_sitemap_urls(self): - latest_revision = self.get_latest_revision() - return [ { 'location': self.full_url, - 'lastmod': latest_revision.created_at if latest_revision else None + 'lastmod': self.latest_revision_created_at } ] From 6e7a360cfef38d0f3a9f6b55996e41450a1f7434 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 22 Oct 2014 16:17:57 +0100 Subject: [PATCH 04/19] Added failing test --- wagtail/wagtailcore/tests/test_page_model.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py index 804b2c4e1..b54b22b4a 100644 --- a/wagtail/wagtailcore/tests/test_page_model.py +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -443,6 +443,23 @@ class TestCopyPage(TestCase): # Check that the new revision is not submitted for moderation self.assertFalse(new_christmas_event.revisions.first().submitted_for_moderation) + def test_copy_page_copies_revisions_and_doesnt_change_created_at(self): + christmas_event = EventPage.objects.get(url_path='/home/events/christmas/') + christmas_event.save_revision(submitted_for_moderation=True) + + # Set the created_at of the revision to a time in the past + revision = christmas_event.get_latest_revision() + revision.created_at = datetime.datetime(2014, 1, 1) + revision.save() + + # Copy it + new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'}) + + # Check that the created_at time is the same + christmas_event_created_at = christmas_event.get_latest_revision().created_at + new_christmas_event_created_at = new_christmas_event.get_latest_revision().created_at + self.assertEqual(christmas_event_created_at, new_christmas_event_created_at) + def test_copy_page_copies_revisions_and_doesnt_schedule(self): christmas_event = EventPage.objects.get(url_path='/home/events/christmas/') christmas_event.save_revision(approved_go_live_at=datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=pytz.utc)) From c6462b8d38c33b6ec654a7e444d197ac9807a655 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 22 Oct 2014 16:25:20 +0100 Subject: [PATCH 05/19] created_at of PageRevisions is now copied on page copy --- wagtail/wagtailcore/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a4d0bed92..94759b9be 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -917,7 +917,7 @@ class SubmittedRevisionsManager(models.Manager): class PageRevision(models.Model): page = models.ForeignKey('Page', related_name='revisions') submitted_for_moderation = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) + created_at = models.DateTimeField() user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) content_json = models.TextField() approved_go_live_at = models.DateTimeField(null=True, blank=True) @@ -926,6 +926,12 @@ class PageRevision(models.Model): submitted_revisions = SubmittedRevisionsManager() def save(self, *args, **kwargs): + # Set default value for created_at to now + # We cannot use auto_now_add as that will override + # any value that is set before saving + if self.created_at is None: + self.created_at = timezone.now() + super(PageRevision, self).save(*args, **kwargs) if self.submitted_for_moderation: # ensure that all other revisions of this page have the 'submitted for moderation' flag unset From bbc8985968936ea728c6440a73fb1dec8ab06d63 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 22 Oct 2014 16:34:14 +0100 Subject: [PATCH 06/19] Added Django 1.7 migration --- ...to_now_add_from_pagerevision_created_at.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 wagtail/wagtailcore/migrations/0009_remove_auto_now_add_from_pagerevision_created_at.py diff --git a/wagtail/wagtailcore/migrations/0009_remove_auto_now_add_from_pagerevision_created_at.py b/wagtail/wagtailcore/migrations/0009_remove_auto_now_add_from_pagerevision_created_at.py new file mode 100644 index 000000000..3dbd7e409 --- /dev/null +++ b/wagtail/wagtailcore/migrations/0009_remove_auto_now_add_from_pagerevision_created_at.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0008_populate_latest_revision_created_at'), + ] + + operations = [ + migrations.AlterField( + model_name='pagerevision', + name='created_at', + field=models.DateTimeField(), + ), + ] From 0edb1244a121d45fd40f3dd3eb82ae95a5aaf5ef Mon Sep 17 00:00:00 2001 From: John-Scott Atlakson Date: Thu, 23 Oct 2014 03:18:14 -0400 Subject: [PATCH 07/19] `unicode()` doesn't exist in PY3, changed to `str()` --- wagtail/wagtailcore/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a4d0bed92..5bb21d7ef 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -996,7 +996,7 @@ class PageRevision(models.Model): page_published.send(sender=page.specific_class, instance=page.specific) def __str__(self): - return '"' + unicode(self.page) + '" at ' + unicode(self.created_at) + return '"' + str(self.page) + '" at ' + str(self.created_at) PAGE_PERMISSION_TYPE_CHOICES = [ From ad687c500ff926e7062b72ead845b399609d63b0 Mon Sep 17 00:00:00 2001 From: John-Scott Atlakson Date: Thu, 23 Oct 2014 04:18:01 -0400 Subject: [PATCH 08/19] Switched usage of `str()` to `six.text_type()` Removed unused import --- wagtail/wagtailcore/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 5bb21d7ef..de1a20ae2 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1,7 +1,6 @@ import warnings import six -from six import string_types from six import StringIO from six.moves.urllib.parse import urlparse @@ -996,7 +995,7 @@ class PageRevision(models.Model): page_published.send(sender=page.specific_class, instance=page.specific) def __str__(self): - return '"' + str(self.page) + '" at ' + str(self.created_at) + return '"' + six.text_type(self.page) + '" at ' + six.text_type(self.created_at) PAGE_PERMISSION_TYPE_CHOICES = [ From 2bf8e4470723865d79498460ccf288f5aa3271b6 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 23 Oct 2014 12:57:04 +0100 Subject: [PATCH 09/19] release note for #463 --- CHANGELOG.txt | 1 + docs/releases/0.8.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3f58b758b..93ced2a2f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -9,6 +9,7 @@ Changelog * Fix: Replaced references of .username with .get_username() on users for better custom user model support (John-Scott Atlakson) * Fix: Unpinned dependency versions for six and requests to help prevent dependency conflicts * Fix: Fixed TypeError when getting embed HTML with oembed on Python 3 (John-Scott Atlakson) + * Fix: Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as "jav\tascript:" (Tim Heap) 0.7 (09.10.2014) ~~~~~~~~~~~~~~~~ diff --git a/docs/releases/0.8.rst b/docs/releases/0.8.rst index 80526919b..12a31121e 100644 --- a/docs/releases/0.8.rst +++ b/docs/releases/0.8.rst @@ -25,6 +25,8 @@ Bug fixes * Replaced references of .username with .get_username() on users for better custom user model support * Unpinned dependency versions for six and requests to help prevent dependency conflicts * Fixed TypeError when getting embed HTML with oembed on Python 3 + * Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as ``jav\tascript:`` + Upgrade considerations ====================== From 568ba42c95c2b10567c2a5362c3cb505a8b5fa90 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 22 Aug 2014 16:20:50 +0100 Subject: [PATCH 10/19] Added logging for most page operations --- wagtail/wagtailcore/models.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a4d0bed92..a2d23ee66 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1,3 +1,4 @@ +import logging import warnings import six @@ -37,6 +38,9 @@ from wagtail.wagtailsearch import index from wagtail.wagtailsearch.backends import get_search_backend +logger = logging.getLogger('wagtail.core') + + class SiteManager(models.Manager): def get_by_natural_key(self, hostname, port): return self.get(hostname=hostname, port=port) @@ -316,8 +320,9 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed @transaction.atomic # ensure that changes are only committed when we have updated all descendant URL paths, to preserve consistency def save(self, *args, **kwargs): update_descendant_url_paths = False + is_new = self.id is None - if self.id is None: + if is_new: # we are creating a record. If we're doing things properly, this should happen # through a treebeard method like add_child, in which case the 'path' field # has been set and so we can safely call get_parent @@ -341,6 +346,11 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed if Site.objects.filter(root_page=self).exists(): cache.delete('wagtail_site_root_paths') + # Log + if is_new: + cls = type(self) + logger.info("Page created: \"%s\" id=%d content_type=%s.%s path=%s", self.title, self.id, cls._meta.app_label, cls.__name__, self.url_path) + return result def _update_descendant_url_paths(self, old_url_path, new_url_path): @@ -412,6 +422,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed raise Http404 def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None): + # Create revision revision = self.revisions.create( content_json=self.to_json(), user=user, @@ -422,6 +433,12 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed self.latest_revision_created_at = revision.created_at self.save(update_fields=['latest_revision_created_at']) + # Log + logger.info("Page edited: \"%s\" id=%d revision_id=%d", self.title, self.id, revision.id) + + if submitted_for_moderation: + logger.info("Page submitted for moderation: \"%s\" id=%d revision_id=%d", self.title, self.id, revision.id) + return revision def get_latest_revision(self): @@ -448,6 +465,8 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed page_unpublished.send(sender=self.specific_class, instance=self.specific) + logger.info("Page unpublished: \"%s\" id=%d", self.title, self.id) + self.revisions.update(approved_go_live_at=None) def get_context(self, request, *args, **kwargs): @@ -650,6 +669,9 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed new_self.save() new_self._update_descendant_url_paths(old_url_path, new_url_path) + # Log + logger.info("Page moved: \"%s\" id=%d path=%s", self.title, self.id, self.url_path) + def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True): # Make a copy page_copy = Page.objects.get(id=self.id).specific @@ -689,6 +711,9 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed revision.page = page_copy revision.save() + # Log + logger.info("Page copied: \"%s\" id=%d from=%d", page_copy.title, page_copy.id, self.id) + # Copy child pages if recursive: for child_page in self.get_children(): @@ -995,6 +1020,10 @@ class PageRevision(models.Model): if page.live: page_published.send(sender=page.specific_class, instance=page.specific) + logger.info("Page published: \"%s\" id=%d revision_id=%d", page.title, page.id, self.id) + elif page.go_live_at: + logger.info("Page scheduled for publish: \"%s\" id=%d revision_id=%d go_live_at=%s", page.title, page.id, self.id, page.go_live_at.isoformat()) + def __str__(self): return '"' + unicode(self.page) + '" at ' + unicode(self.created_at) From 4027262433862e59603a05137a9465cc761b388b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 26 Aug 2014 10:57:29 +0100 Subject: [PATCH 11/19] Log page deletions --- wagtail/wagtailcore/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a2d23ee66..1c0467acb 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -10,7 +10,7 @@ from modelcluster.models import ClusterableModel, get_all_child_relations from django.db import models, connection, transaction from django.db.models import Q -from django.db.models.signals import pre_delete +from django.db.models.signals import pre_delete, post_delete from django.dispatch.dispatcher import receiver from django.http import Http404 from django.core.cache import cache @@ -924,6 +924,11 @@ def unpublish_page_before_delete(sender, instance, **kwargs): instance.unpublish(commit=False) +@receiver(post_delete, sender=Page) +def log_page_deletion(sender, instance, **kwargs): + logger.info("Page deleted: \"%s\" id=%d", instance.title, instance.id) + + class Orderable(models.Model): sort_order = models.IntegerField(null=True, blank=True, editable=False) sort_order_field = 'sort_order' From df7110625ac751b11c7da1711a0c5710e3e5823a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 27 Aug 2014 14:55:13 +0100 Subject: [PATCH 12/19] Added logging for moderation approval/rejection --- wagtail/wagtailcore/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 1c0467acb..c2fe81431 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -985,10 +985,12 @@ class PageRevision(models.Model): def approve_moderation(self): if self.submitted_for_moderation: + logger.info("Page moderation approved: \"%s\" id=%d revision_id=%d", self.page.title, self.page.id, self.id) self.publish() def reject_moderation(self): if self.submitted_for_moderation: + logger.info("Page moderation rejected: \"%s\" id=%d revision_id=%d", self.page.title, self.page.id, self.id) self.submitted_for_moderation = False self.save(update_fields=['submitted_for_moderation']) From 3de6cb5ff4f90e76933c5c989f944830923360b8 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 23 Oct 2014 16:08:08 +0100 Subject: [PATCH 13/19] Use new_url_path in page move log --- wagtail/wagtailcore/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index c2fe81431..a51f54aed 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -670,7 +670,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed new_self._update_descendant_url_paths(old_url_path, new_url_path) # Log - logger.info("Page moved: \"%s\" id=%d path=%s", self.title, self.id, self.url_path) + logger.info("Page moved: \"%s\" id=%d path=%s", self.title, self.id, new_url_path) def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True): # Make a copy From 1871a29f300b1201211cfabc01afa1a275596d8e Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 23 Oct 2014 17:31:54 +0100 Subject: [PATCH 14/19] release note for #569 --- CHANGELOG.txt | 1 + docs/releases/0.8.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 93ced2a2f..dcd608bae 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -4,6 +4,7 @@ Changelog 0.8 (xx.xx.2014) ~~~~~~~~~~~~~~~~ + * Added logging for page operations * The save/publish/submit buttons on the page edit page now redirects the user back to the edit page instead of the explorer * Signal handlers for ``wagtail.wagtailsearch`` and ``wagtail.contrib.wagtailfrontendcache`` are now automatically registered when using Django 1.7 or above. (Tim Heap) * Fix: Replaced references of .username with .get_username() on users for better custom user model support (John-Scott Atlakson) diff --git a/docs/releases/0.8.rst b/docs/releases/0.8.rst index 12a31121e..9c03ae9d9 100644 --- a/docs/releases/0.8.rst +++ b/docs/releases/0.8.rst @@ -15,6 +15,7 @@ What's new Minor features ~~~~~~~~~~~~~~ + * Page operations (creation, publishing, copying etc) are now logged via Python's ``logging`` framework; to configure this, add a logger entry for ``'wagtail'`` or ``'wagtail.core'`` to the ``LOGGING`` setup in your settings file. * The save/publish/submit buttons on the page edit page now redirects the user back to the edit page instead of the explorer * Signal handlers for ``wagtail.wagtailsearch`` and ``wagtail.contrib.wagtailfrontendcache`` are now automatically registered when using Django 1.7 or above. From 13e8e73830bff357f17cc1f2250a6bf1251375a9 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 23 Oct 2014 20:06:26 +0100 Subject: [PATCH 15/19] Project template: Fixed migration dependency The project template currently crashes on first migrate because it tries to run the create homepage migration before the Page.locked field is created. This commit fixes (and hopefully, future proofs) this by telling Django that the core app migrations depend on the latest wagtailcore migration, which should make Django fully migrate wagtailcore before starting the core app. --- wagtail/project_template/core/migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/project_template/core/migrations/0001_initial.py b/wagtail/project_template/core/migrations/0001_initial.py index 9bf12542d..77e0625d7 100644 --- a/wagtail/project_template/core/migrations/0001_initial.py +++ b/wagtail/project_template/core/migrations/0001_initial.py @@ -7,7 +7,7 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('wagtailcore', '0002_initial_data'), + ('wagtailcore', '__latest__'), ] operations = [ From 3503981802b444fd6e3f392f3ec376010d38f22b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 24 Oct 2014 10:32:20 +0100 Subject: [PATCH 16/19] Fix revisions still copying for child pages When Page.copy runs recursively, it forgot to pass through the copy_revisions value that the user specified causing revisions to always be copied for subpages regardless of what the user wanted --- wagtail/wagtailcore/models.py | 2 +- wagtail/wagtailcore/tests/test_page_model.py | 30 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 5db4e42af..3122e9c95 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -716,7 +716,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed # Copy child pages if recursive: for child_page in self.get_children(): - child_page.specific.copy(recursive=True, to=page_copy) + child_page.specific.copy(recursive=True, to=page_copy, copy_revisions=copy_revisions) return page_copy diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py index 804b2c4e1..134836963 100644 --- a/wagtail/wagtailcore/tests/test_page_model.py +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -456,6 +456,19 @@ class TestCopyPage(TestCase): # Check that the new revision is not scheduled self.assertEqual(new_christmas_event.revisions.first().approved_go_live_at, None) + def test_copy_page_doesnt_copy_revisions_if_told_not_to_do_so(self): + christmas_event = EventPage.objects.get(url_path='/home/events/christmas/') + christmas_event.save_revision() + + # Copy it + new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'}, copy_revisions=False) + + # Check that the revisions weren't copied + self.assertEqual(new_christmas_event.revisions.count(), 0, "Revisions were copied") + + # Check that the revisions weren't removed from old page + self.assertEqual(christmas_event.revisions.count(), 1, "Revisions were removed from the original page") + def test_copy_page_copies_child_objects_with_nonspecific_class(self): # Get chrismas page as Page instead of EventPage christmas_event = Page.objects.get(url_path='/home/events/christmas/') @@ -519,6 +532,23 @@ class TestCopyPage(TestCase): # Check that the revisions weren't removed from old page self.assertEqual(old_christmas_event.specific.revisions.count(), 1, "Revisions were removed from the original page") + def test_copy_page_copies_recursively_but_doesnt_copy_revisions_if_told_not_to_do_so(self): + events_index = EventIndex.objects.get(url_path='/home/events/') + old_christmas_event = events_index.get_children().filter(slug='christmas').first() + old_christmas_event.save_revision() + + # Copy it + new_events_index = events_index.copy(recursive=True, update_attrs={'title': "New events index", 'slug': 'new-events-index'}, copy_revisions=False) + + # Get christmas event + new_christmas_event = new_events_index.get_children().filter(slug='christmas').first() + + # Check that the revisions weren't copied + self.assertEqual(new_christmas_event.specific.revisions.count(), 0, "Revisions were copied") + + # Check that the revisions weren't removed from old page + self.assertEqual(old_christmas_event.specific.revisions.count(), 1, "Revisions were removed from the original page") + class TestSubpageTypeBusinessRules(TestCase): def test_allowed_subpage_types(self): From e7be80af50d45107181c2bef19f9706b2f001fc3 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 24 Oct 2014 10:52:03 +0100 Subject: [PATCH 17/19] release note for #732 --- CHANGELOG.txt | 1 + docs/releases/0.8.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dcd608bae..a4e0eed83 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -11,6 +11,7 @@ Changelog * Fix: Unpinned dependency versions for six and requests to help prevent dependency conflicts * Fix: Fixed TypeError when getting embed HTML with oembed on Python 3 (John-Scott Atlakson) * Fix: Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as "jav\tascript:" (Tim Heap) + * Fix: created_at timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence 0.7 (09.10.2014) ~~~~~~~~~~~~~~~~ diff --git a/docs/releases/0.8.rst b/docs/releases/0.8.rst index 9c03ae9d9..b7e51161d 100644 --- a/docs/releases/0.8.rst +++ b/docs/releases/0.8.rst @@ -27,6 +27,7 @@ Bug fixes * Unpinned dependency versions for six and requests to help prevent dependency conflicts * Fixed TypeError when getting embed HTML with oembed on Python 3 * Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as ``jav\tascript:`` + * ``created_at`` timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence Upgrade considerations From 92d0dd9895ae50696318fe2ff86e30a8deb42298 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 24 Oct 2014 10:58:54 +0100 Subject: [PATCH 18/19] Release note for #748 --- CHANGELOG.txt | 1 + docs/releases/0.8.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a4e0eed83..057393ebf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -12,6 +12,7 @@ Changelog * Fix: Fixed TypeError when getting embed HTML with oembed on Python 3 (John-Scott Atlakson) * Fix: Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as "jav\tascript:" (Tim Heap) * Fix: created_at timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence + * Fix: When copying pages recursively, revisions of sub-pages were being copied regardless of the copy_revisions flag 0.7 (09.10.2014) ~~~~~~~~~~~~~~~~ diff --git a/docs/releases/0.8.rst b/docs/releases/0.8.rst index b7e51161d..d2bb55aea 100644 --- a/docs/releases/0.8.rst +++ b/docs/releases/0.8.rst @@ -28,6 +28,7 @@ Bug fixes * Fixed TypeError when getting embed HTML with oembed on Python 3 * Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as ``jav\tascript:`` * ``created_at`` timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence + * When copying pages recursively, revisions of sub-pages were being copied regardless of the ``copy_revisions`` flag Upgrade considerations From 793ebd4cc2c24e51ce0ccfe8e7fff7fde9c4669c Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 24 Oct 2014 16:42:38 +0100 Subject: [PATCH 19/19] Release note for #743 --- CHANGELOG.txt | 1 + docs/releases/0.8.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 057393ebf..5332c555c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -13,6 +13,7 @@ Changelog * Fix: Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as "jav\tascript:" (Tim Heap) * Fix: created_at timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence * Fix: When copying pages recursively, revisions of sub-pages were being copied regardless of the copy_revisions flag + * Fix: Updated the migration dependencies within the project template to ensure that Wagtail's own migrations consistently apply first. 0.7 (09.10.2014) ~~~~~~~~~~~~~~~~ diff --git a/docs/releases/0.8.rst b/docs/releases/0.8.rst index d2bb55aea..94cff5937 100644 --- a/docs/releases/0.8.rst +++ b/docs/releases/0.8.rst @@ -29,6 +29,7 @@ Bug fixes * Made HTML whitelisting in rich text fields more robust at catching disallowed URL schemes such as ``jav\tascript:`` * ``created_at`` timestamps on page revisions were not being preserved on page copy, causing revisions to get out of sequence * When copying pages recursively, revisions of sub-pages were being copied regardless of the ``copy_revisions`` flag + * Updated the migration dependencies within the project template to ensure that Wagtail's own migrations consistently apply first Upgrade considerations