Updates to sanitizer; all-in with inline HTML
authorCharlie DeTar <cfd@media.mit.edu>
Sat, 14 Feb 2015 23:27:17 +0000 (16:27 -0700)
committerBryan <btbonval@gmail.com>
Fri, 27 Feb 2015 01:08:12 +0000 (20:08 -0500)
 - Remove iframe and "static hosting" as a strategy for showing a note.
   Only show inline HTML.
 - Add a "format preserving" sanitizer that does XSS filtering and prep
   for inline HTML, but doesn't remove visual markup.
 - Remove javascript pertinent to PDF viewing. Handle zoom buttons using
   CSS transforms on the HTML container.
 - Add notion of "editability" for Notes.  Notes will save with an
   "editable" sanitizer that strips to markdown-caliber HTML which the
   client side WYSIWYG can handle if the note is an editable type, and
   renders with a "format preserving" sanitizer that uses complex ugly
   junk as the HTML if it's not meant to be edited.
 - Add and improve tests for this stuff.

karmaworld/apps/notes/forms.py
karmaworld/apps/notes/migrations/0020_markdown_to_html.py
karmaworld/apps/notes/models.py
karmaworld/apps/notes/sanitizer.py
karmaworld/apps/notes/tests.py
karmaworld/assets/js/note-detail.js
karmaworld/templates/notes/note_base.html
karmaworld/templates/notes/note_detail.html
requirements.txt

index 4bf660b529ec202be97d7fc099ce72df4d530e48..a6a92f639625db4d8d7e1eedf4b1fc8ba1a72a54 100644 (file)
@@ -15,13 +15,12 @@ class NoteForm(ModelForm):
 
     def save(self, *args, **kwargs):
         # TODO: use transaction.atomic for this when we switch to Django 1.6+
-        print self.cleaned_data
         instance = super(NoteForm, self).save(*args, **kwargs)
         instance.tags.set(*self.cleaned_data['tags'])
         if instance.is_hidden:
             instance.is_hidden = False
             instance.save()
-        if self.cleaned_data.get('html'):
+        if instance.is_editable() and self.cleaned_data.get('html'):
             try:
                 note_markdown = instance.notemarkdown
             except NoteMarkdown.DoesNotExist:
index 4efa2ac5d257240e764b8491e9891787e7f722f8..0db844f097adbb9fbc5ad48441fefa127d7b92b8 100644 (file)
@@ -5,7 +5,7 @@ from south.v2 import DataMigration
 from django.db import models
 import markdown
 from notes.models import NoteMarkdown
-from notes.sanitizer import sanitize_html
+from notes import sanitizer
 
 class Migration(DataMigration):
 
@@ -15,7 +15,7 @@ class Migration(DataMigration):
         # Use orm.ModelName to refer to models in this application,
         # and orm['appname.ModelName'] for models in other applications.
         for notemarkdown in orm['notes.NoteMarkdown'].objects.exclude(markdown=""):
-            notemarkdown.html = sanitize_html(markdown.markdown(notemarkdown.markdown))
+            notemarkdown.html = sanitizer.sanitize_html_to_editable(markdown.markdown(notemarkdown.markdown))
             notemarkdown.save()
 
     def backwards(self, orm):
index 68db5bca04fbfcdb67aa23c9fc5bcc6edc22793c..9bb0a1cb8b6a6d9ec983449db187ab20f3c92a1d 100644 (file)
@@ -34,7 +34,7 @@ import django_filepicker
 from taggit.managers import TaggableManager
 import markdown
 
-from karmaworld.apps.notes.sanitizer import sanitize_html
+from karmaworld.apps.notes import sanitizer
 from karmaworld.apps.courses.models import Course
 from karmaworld.apps.licenses.models import License
 from karmaworld.apps.notes.search import SearchIndex
@@ -80,6 +80,8 @@ class Document(models.Model):
         (ASSIGNMENT, 'Assignment'),
         (OTHER, 'Other'),
     )
+    EDITABLE_CATEGORIES = (LECTURE_NOTES,)
+
     category = models.CharField(max_length=50, choices=NOTE_CATEGORIES, blank=True, null=True)
 
     # license if different from default
@@ -360,6 +362,9 @@ class Note(Document):
     def is_pdf(self):
         return self.mimetype in Note.PDF_MIMETYPES
 
+    def is_editable(self):
+        return self.category in Note.EDITABLE_CATEGORIES
+
 
 class NoteMarkdown(models.Model):
     note     = models.OneToOneField(Note, primary_key=True)
@@ -369,7 +374,11 @@ class NoteMarkdown(models.Model):
     def save(self, *args, **kwargs):
         if self.markdown and not self.html:
             self.html = markdown.markdown(self.markdown)
-        self.html = sanitize_html(self.html)
+        if self.note.is_editable():
+            self.html = sanitizer.sanitize_html_to_editable(self.html)
+        else:
+            self.html = sanitizer.sanitize_html_preserve_formatting(self.html)
+
         super(NoteMarkdown, self).save(*args, **kwargs)
 
 auto_add_check_unique_together(Note)
index 162d2569d40ad699d2305f3b64d60b258529e564..abc9fc337d8f8e2539e78735cb0073d40dbc6160 100644 (file)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 import re
 import bleach
 import html5lib
@@ -18,7 +20,7 @@ def _canonical_link_predicate(tag):
         tag.has_attr('rel') and \
         u'canonical' in tag['rel']
 
-class Sanitizer(BleachSanitizer):
+class SuppressingSanitizer(BleachSanitizer):
     """
     The default bleach clean method uses a sanitizer that handles disallowed
     tags either by escaping them. With the bad HTML 
@@ -35,57 +37,155 @@ class Sanitizer(BleachSanitizer):
 
     But we want to strip both the tag and contents for certain tags like script
     and style.  This subclass does that.
-
-    Also support data URI's for some mimetypes (image/png, image/gif, image/jpeg)
     """
-    allowed_elements = bleach_whitelist.markdown_tags
-    allowed_attributes = bleach_whitelist.markdown_attrs
     suppressed_elements = ["script", "style"]
-    strip_disallowed_elements = True
-    strip_html_comments = True
 
     def __init__(self, *args, **kwargs):
         self.suppressing = None
-        super(Sanitizer, self).__init__(*args, **kwargs)
+        super(SuppressingSanitizer, self).__init__(*args, **kwargs)
 
     def sanitize_token(self, token):
         # Do sanitization on the parent.
-        result = super(Sanitizer, self).sanitize_token(token)
+        result = super(SuppressingSanitizer, self).sanitize_token(token)
+        if "name" not in token:
+            if self.suppressing:
+                return None
+            return result
+
+        tag_name = token['name']
 
         # Suppress elements like script and style entirely.
-        if token.get('name') and token['name'] in self.suppressed_elements:
+        if tag_name in self.suppressed_elements:
             if token['type'] == tokenTypes['StartTag']:
-                self.suppressing = token['name']
-            elif token['type'] == tokenTypes['EndTag'] and token['name'] == self.suppressing:
+                self.suppressing = tag_name
+            elif token['type'] == tokenTypes['EndTag'] and tag_name == self.suppressing:
                 self.suppressing = False
+
+        elif tag_name in self.required_attributes:
+            if result and result.get('type') == tokenTypes['StartTag']:
+                attr_dict = dict(result.get('data', []) or [])
+                for attr, val in self.required_attributes[tag_name].iteritems():
+                    attr_dict[attr] = val
+                result['data'] = attr_dict.items()
+
         if self.suppressing:
-            return {u'data': '', 'type': 2}
+            return None
         else:
             return result
 
+class EditableSanitizer(SuppressingSanitizer):
+    """
+    Sanitizer tokenizer optimized for producing HTML suitable for editing in
+    client-side WYSIWYG's.
+    """
+    allowed_elements = bleach_whitelist.markdown_tags
+    allowed_attributes = {
+        "img": ["src", "width", "height"],
+        "a": ["href", "target", "rel"],
+    }
+    required_attributes = {
+        "a": { "rel": "nofollow", "target": "_blank" }
+    }
+    suppressed_elements = ["script", "style"]
+    strip_disallowed_elements = True
+    strip_html_comments = True
+
+class PreserveFormattingSanitizer(SuppressingSanitizer):
+    """
+    Lax sanitizer tokenizer optimized for preserving the formatting in the
+    incoming HTML, while still removing XSS threats.
+    """
+    allowed_elements = bleach_whitelist.generally_xss_safe
+    allowed_attributes = {
+        "*": ["class", "style"],
+        "img": ["src", "width", "height"],
+        "a": ["href", "target", "rel"],
+    }
+    required_attributes = {
+        "a": { "rel": "nofollow", "target": "_blank" }
+    }
+    allowed_css_properties = bleach_whitelist.all_styles
+    suppressed_elements = ["script"]
+    strip_disallowed_elements = True
+    strip_html_comments = True
+
 class DataUriReplacer(HTMLTokenizer, HTMLSanitizerMixin):
     """
     Convert any valid image data URI's to files, and upload them to s3. Replace
     the data URI with a link to the file in s3.
     """
-    VALID_URI = "^data:image/(png|gif|jpeg);base64,[A-Za-z0-9+/=]+$"
+    VALID_IMAGE_URI = "^data:image/(png|gif|jpeg);base64,[A-Za-z0-9+/=]+$"
+    VALID_FONT_FACE_FORMATS = ["woff"]
+
+    def __init__(self, *args, **kwargs):
+        self.in_style = False
+        self.style_content = []
+        self.font_face_cache = {}
+        super(DataUriReplacer, self).__init__(*args, **kwargs)
 
     def sanitize_token(self, token):
-        if token.get('name') == u"img":
+        # Handle images
+        tag_name = token.get("name")
+        if tag_name == u"img":
             attrs = dict([(name, val) for name, val in token['data'][::-1]])
             if 'src' in attrs:
                 src = attrs['src']
-                if re.match(self.VALID_URI, src):
+                if re.match(self.VALID_IMAGE_URI, src):
                     url = self._upload_image(src)
                     attrs['src'] = url
                     token['data'] = [(k,v) for k,v in attrs.iteritems()]
+
+        # Handle font-faces.
+        if self.in_style:
+            if 'data' in token and token['type'] == 1:
+                token['data'] = self._extract_and_upload_data_uri_fonts(token['data'])
+
+        if tag_name == u"style":
+            if token['type'] == tokenTypes['StartTag']:
+                self.in_style = True
+            elif token['type'] == tokenTypes['EndTag']:
+                self.in_style = False
+
         return token
 
-    def _upload_image(self, data_uri):
-        from django.core.files.storage import default_storage
-        from karmaworld.apps.notes.models import all_read_xml_acl
-        from django.conf import settings
+    def _extract_and_upload_data_uri_fonts(self, raw_css):
+        # Might be better to use a full-fledged CSS parser, but it has to be
+        # modern enough to support font-faces and data-uri's.
+        pattern = r"""(?P<intro>
+                @font-face\s*{      # font-face opening
+                (?:[^\}]+;)?        # any parts before the src
+                \s*src\s*:\s*             # src declaration
+                  url\(['"])?data:      # url opener
+                    (?P<mimetype>application/font-(?P<ext>%s)) # mimetype
+                    ;base64, 
+                    (?P[A-Za-z0-9+/=]+) # data-uri itself
+                 (?P<outro>['"]?\)) # url closer 
+        """ % "|".join(self.VALID_FONT_FACE_FORMATS)
+        return re.sub(pattern,
+                repl=self._upload_font_match,
+                string=raw_css,
+                flags=re.VERBOSE | re.DOTALL)
+
+    def _upload_font_match(self, match):
+        url = ""
+        if match:
+            ext = match.group('ext')
+            mimetype = match.group('mimetype')
+            data_uri = match.group('data_uri')
+            if ext in self.VALID_FONT_FACE_FORMATS:
+                filepath = "fonts/{}.{}".format(uuid.uuid4(), ext)
+                sio = StringIO()
+                sio.write(base64.b64decode(data_uri))
+                sio.seek(0)
+                value = sio.getvalue()
+                if value in self.font_face_cache:
+                    url = self.font_face_cache[value]
+                else:
+                    url = self._s3_upload(filepath, mimetype, sio.getvalue())
+                    self.font_face_cache[value] = url
+        return u"".join((match.group('intro'), url, match.group('outro')))
 
+    def _upload_image(self, data_uri):
         mimetype, data = data_uri.split(";base64,")
         sio = StringIO()
         sio.write(base64.b64decode(data))
@@ -101,35 +201,47 @@ class DataUriReplacer(HTMLTokenizer, HTMLSanitizerMixin):
         image.save(image_data, format=fmt)
 
         filepath = "images/{}.{}".format(uuid.uuid4(), fmt)
+        return self._s3_upload(filepath, mimetype, image_data.getvalue())
+
+    def _s3_upload(self, filepath, mimetype, data):
+        from django.core.files.storage import default_storage
+        from karmaworld.apps.notes.models import all_read_xml_acl
+        from django.conf import settings
+
         new_key = default_storage.bucket.new_key(filepath)
-        new_key.set_contents_from_string(image_data.getvalue(), {"Content-Type": mimetype})
+        new_key.set_contents_from_string(data, {"Content-Type": mimetype})
         new_key.set_xml_acl(all_read_xml_acl)
         parts = [settings.S3_URL, filepath]
         if parts[0].startswith("//"):
-            # Fully resolve the URL as https for happiness in all things.
+            # Fully resolve the URL as https for happiness in all things. ॐ
             parts.insert(0, "https:")
         return "".join(parts)
 
+
     def __iter__(self):
         for token in HTMLTokenizer.__iter__(self):
             token = self.sanitize_token(token)
             if token:
                 yield token
 
-def sanitize_html(raw_html):
+def _sanitize_html(raw_html, tokenizer):
+    parser = html5lib.HTMLParser(tokenizer=tokenizer)
+    clean = _render(parser.parseFragment(raw_html))
+    return clean
+
+def sanitize_html_to_editable(raw_html):
     """
-    Sanitize the given raw_html.
+    Sanitize the given raw_html, with the result in a format suitable for
+    editing in client-side HTML WYSIWYG editors.
     """
-    # Strip tags to the few that we like
-    parser = html5lib.HTMLParser(tokenizer=Sanitizer)
-    clean = _render(parser.parseFragment(raw_html))
+    return _sanitize_html(raw_html, EditableSanitizer)
 
-    # Set anchor tags' targets
-    clean = bleach.linkify(clean, callbacks=[
-        bleach.callbacks.nofollow,
-        bleach.callbacks.target_blank
-    ], tokenizer=Sanitizer)
-    return clean
+def sanitize_html_preserve_formatting(raw_html):
+    """
+    Sanitize the given HTML, preserving as much of the original formatting as
+    possible.
+    """
+    return _sanitize_html(raw_html, PreserveFormattingSanitizer)
 
 def data_uris_to_s3(raw_html):
     parser = html5lib.HTMLParser(tokenizer=DataUriReplacer)
index e4524209141bd711f600610f61300f15fc032cc4..7a52d1515146825158dfee44bad6508e0b7d00cc 100644 (file)
@@ -36,6 +36,7 @@ class TestNotes(TestCase):
         self.note = Note()
         self.note.course = self.course
         self.note.name = u"Lecture notes concerning the use of therefore ∴"
+        self.note.category = Note.LECTURE_NOTES
         self.note.uploaded_at = self.now
         self.note.text = "This is the plaintext version of a note. It's pretty cool. Alpaca."
         self.note.save()
@@ -97,11 +98,11 @@ class TestNotes(TestCase):
             <h2>OK</h2>
             &amp;
             \u201d
-            <a>This stuff</a>
+            <a target='_blank' rel='nofollow'>This stuff</a>
             <a href="http://google.com" target="_blank" rel="nofollow">That guy</a>
         """)
 
-class TestSanitizer(TestCase):
+class TestSanitizeToEditable(TestCase):
     def test_clean(self):
         dirty = """
             <script>unsafe</script>
@@ -117,12 +118,12 @@ class TestSanitizer(TestCase):
             </section>
         """
 
-        self.assertHTMLEqual(sanitizer.sanitize_html(dirty), u"""
+        self.assertHTMLEqual(sanitizer.sanitize_html_to_editable(dirty), u"""
             <h1>Something</h1>
             <h2>OK</h2>
             &amp;
             \u201d
-            <a>This stuff</a>
+            <a target="_blank" rel="nofollow">This stuff</a>
             <a href="http://google.com" target="_blank" rel="nofollow">That guy</a>
             <h3>This should show up</h3>
         """)
@@ -135,18 +136,34 @@ class TestSanitizer(TestCase):
     def test_data_uri(self):
         # Strip out all data URIs.
         html = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==">'
-        self.assertHTMLEqual(sanitizer.sanitize_html(html), "<img/>")
+        self.assertHTMLEqual(sanitizer.sanitize_html_to_editable(html), "<img/>")
 
         # Strip out non-image data URI's
         html = '<img src="data:application/pdf;base64,blergh">'
-        self.assertHTMLEqual(sanitizer.sanitize_html(html), "<img/>")
+        self.assertHTMLEqual(sanitizer.sanitize_html_to_editable(html), "<img/>")
 
 class TestDataUriToS3(TestCase):
-    def test_data_uri(self):
+    def test_image_data_uri(self):
         html = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==">'
         s3ified = sanitizer.data_uris_to_s3(html)
         soup = BeautifulSoup(s3ified)
-        print s3ified
         regex = r'^https?://.*$'
         self.assertTrue(bool(re.match(regex, soup.img['src'])),
                 "{} does not match {}".format(s3ified, regex))
+
+        resanitize = sanitizer.data_uris_to_s3(s3ified)
+        self.assertHTMLEqual(s3ified, resanitize)
+
+    def test_font_face_data_uri(self):
+        # Note: this data-uri is not a valid font (it's the red dot).
+        html = '''<style>@font-face { src: url('data:application/font-woff;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='); }</style>'''
+
+        s3ified = sanitizer.data_uris_to_s3(html)
+        self.assertFalse(re.search(r"url\('data:application", s3ified),
+                "data URL not removed: {}".format(s3ified))
+        self.assertTrue(re.search(r"url\('https?://[^\)]+\)", s3ified),
+                "URL not inserted: {}".format(s3ified))
+
+        # Ensure that cleaning is idempotent.
+        self.assertHTMLEqual(s3ified,
+                sanitizer.sanitize_html_preserve_formatting(s3ified))
index d2e14134ca458054dc9f518183923298acb64568..0ea09a2a3468be3a11c6d4580c3ce24f04fd395a 100644 (file)
@@ -1,96 +1,3 @@
-
-function rescalePdf(viewer) {
-  var scaleBase = 750;
-  var outlineWidth = 250;
-  var frameWidth = parseInt($('#note_container')[0].clientWidth);
-  var pdfWidth = frameWidth;
-
-  if ($(viewer.sidebar).hasClass('opened')){
-    pdfWidth = pdfWidth - 250;
-  }
-
-  var newPdfScale = pdfWidth / scaleBase;
-  viewer.rescale(newPdfScale);
-}
-
-function setupPdfViewer(noteframe, pdfViewer) {
-
-  $('#plus-btn').click(function (){
-    pdfViewer.rescale(1.20, true, [0,0]);
-  });
-
-  $('#minus-btn').click(function (){
-    pdfViewer.rescale(0.80, true, [0,0]);
-  });
-
-  // detect if the PDF viewer wants to show an outline
-  // at all
-  if ($(pdfViewer.sidebar).hasClass('opened')) {
-    var body = $(document.body);
-    // if the screen is less than 64em wide, hide the outline
-    if (parseInt($(body.width()).toEm({scope: body})) < 64) {
-      $(pdfViewer.sidebar).removeClass('opened');
-    }
-  }
-
-  $('#outline-btn').click(function() {
-    $(pdfViewer.sidebar).toggleClass('opened');
-    // rescale the PDF to fit the available space
-    rescalePdf(pdfViewer);
-  });
-
-  $('#scroll-to').change(function() {
-    page = parseInt($(this).val());
-    pdfViewer.scroll_to(page, [0,0]);
-  });
-
-  // rescale the PDF to fit the available space
-  rescalePdf(pdfViewer);
-}
-
-function writeNoteFrame(contents) {
-  var dstFrame = document.getElementById('noteframe');
-  if (dstFrame) {
-    var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
-    dstDoc.write(contents);
-    dstDoc.close();
-  }
-}
-
-function setupAnnotator(noteElement, readOnly) {
-  noteElement.annotator({readOnly: readOnly});
-  noteElement.annotator('addPlugin', 'Store', {
-    prefix: '/ajax/annotations',
-    loadFromSearch: {
-      'uri': note_id
-    },
-    annotationData: {
-      'uri': note_id
-    }
-  });
-}
-
-function injectRemoteScript(url, noteframe, onload) {
-  var injectScript = noteframe.document.createElement("script");
-  injectScript.src = url;
-  injectScript.onload = onload;
-  noteframe.document.head.appendChild(injectScript);
-}
-
-function injectScript(scriptText, noteframe) {
-  var injectScript = noteframe.document.createElement("script");
-  injectScript.innerHTML = scriptText;
-  noteframe.document.body.appendChild(injectScript);
-}
-
-function injectRemoteCSS(url, noteframe) {
-  var injectCSS = noteframe.document.createElement("link");
-  injectCSS.href = url;
-  injectCSS.type = 'text/css';
-  injectCSS.rel = 'stylesheet';
-  noteframe.document.head.appendChild(injectCSS);
-}
-
 function tabHandler(event) {
   // check for:
   // key pressed was TAB
@@ -133,6 +40,32 @@ function addForm(event) {
   keywordInput.focus();
 }
 
+/**
+ * Scale the html given by the selector by the factor.
+ * @param {string|jQuery|dom} container - Selector to scale
+ * @param {Number} factorDelta - Amount to change the current scaling factor by
+ * (e.g. 1.1, 0.9, 2.0).
+ */
+function scaleHTML(container, factorDelta) {
+  var el = $(container);
+  var currentFactor = parseFloat(el.attr("data-scale-factor") || 1);
+  var factor = currentFactor * factorDelta;
+  
+  var parent = el.parent()
+  var origHeight = parent.height() / currentFactor;
+  var destHeight = origHeight * factor;
+  // Set the new parent height
+  parent.height(destHeight);
+
+  el.attr("data-scale-factor", factor);
+  var matrix = "matrix(" + factor + ", 0, 0, " + factor + ", " +
+                       "0," + ((destHeight - origHeight) * 0.5) + ")";
+  el.css("-webkit-transform", matrix);
+  el.css("-moz-transform", matrix);
+  el.css("-ms-transform", matrix);
+  el.css("transform", matrix);
+}
+
 function initNoteContentPage() {
 
   $("#thank-button").click(function(event) {
@@ -216,69 +149,16 @@ function initNoteContentPage() {
     }
   });
 
-  // Embed the converted markdown if it is on the page, else default to the iframe
-  if ($('#note-markdown').length > 0) {
-    var note_markdown = $('#note-markdown');
-    note_markdown.html(marked(note_markdown.data('markdown')));
-    setupAnnotator(note_markdown, !user_authenticated);
-  } else {
-    $.ajax(note_contents_url, {
-      type: 'GET',
-      xhrFields: {
-        onprogress: function (progress) {
-          var percentage = Math.floor((progress.loaded / progress.total) * 100);
-          writeNoteFrame("<h3 style='text-align: center'>" + percentage + "%</h3>");
-        }
-      },
-      success: function(data, textStatus, jqXHR) {
-        writeNoteFrame(data);
-
-        // run setupAnnotator in frame context
-        var parentFrame = document.getElementById('noteframe');
-        if (!parentFrame) {
-          return;
-        }
-        var noteframe = parentFrame.contentWindow;
-
-        injectRemoteCSS(annotator_css_url, noteframe);
-        injectScript("csrf_token = '" + csrf_token + "';", noteframe);
-
-        injectRemoteScript("https://code.jquery.com/jquery-2.1.0.min.js", noteframe,
-          function() {
-            injectRemoteScript(setup_ajax_url, noteframe);
-            injectRemoteScript(annotator_js_url, noteframe,
-              function() {
-                var js = "$(function() { \
-                  var document_selector = $('body'); \
-                  if ($('#page-container').length > 0) { \
-                    document_selector = $('#page-container'); \
-                  } \
-                  document_selector.annotator({readOnly: " + !user_authenticated + "}); \
-                  document_selector.annotator('addPlugin', 'Store', { \
-                    prefix: '/ajax/annotations', \
-                    loadFromSearch: { \
-                    'uri': " + note_id + " \
-                  }, \
-                  annotationData: { \
-                    'uri': " + note_id + " \
-                  } \
-                }); })";
-                injectScript(js, noteframe);
-
-                if (pdfControls == true) {
-                  var pdfViewer = noteframe.pdf2htmlEX.defaultViewer;
-                  $(noteframe.document).ready(function() {
-                    setupPdfViewer(noteframe, pdfViewer);
-                  });
-                }
-              });
-          });
-      },
-      error: function(data, textStatus, jqXHR) {
-        writeNoteFrame("<h3 style='text-align: center'>Sorry, your note could not be retrieved.</h3>");
-      }
-    });
-  }
+  $("#note-html").annotator({
+    readOnly: false
+  }).annotator('addPlugin', 'Store', {
+    prefix: '/ajax/annotations',
+    loadFromSearch: { 'uri': note_id },
+    annotationData: { 'uri': note_id }
+  });
+
+  $("#plus-btn").click(function() { scaleHTML("#note-html", 1.1); });
+  $("#minus-btn").click(function() { scaleHTML("#note-html", 1 / 1.1); });
 }
 
 function initNoteKeywordsPage() {
index 766c44dc7889116172cd908e66058cd7cced8832..d700d504e7168fad382cb390574e692cde81aed7 100644 (file)
       });
     {% endif %}
   </script>
-  <script type="text/javascript">
-    // wysihtml5 doesn't init correctly in a hidden div.  So we remove it every
-    // time before showing it in the modal.
+  {% if note.is_editable %}
+    <script type="text/javascript">
+      // wysihtml5 doesn't init correctly in a hidden div.  So we remove it every
+      // time before showing it in the modal.
 
-    // This event will be renamed "open.fndtn.reveal" in future foundation:
-    // http://foundation.zurb.com/docs/components/reveal.html#event-bindings
-    $(document).on("open", "#note-edit-dialog", function(event) {
-        var scope = $(event.currentTarget);
-        // Remove iframe.
-        $(".wysihtml5-sandbox", scope).remove();
-        // Unbind toolbar events by cloning it.
-        var toolbar = $(".wysihtml5-toolbar", scope);
-        var toolbarClone = toolbar.clone();
-        toolbar.replaceWith(toolbarClone);
-        // Unbind textarea events by cloning it.
-        var textarea = $("[role='wysihtml5-rich-text']", scope);
-        var textareaClone = textarea.clone();
-        var val = textarea.val(); // textarea value isn't copied with clone.
-        textarea.replaceWith(textareaClone);
-        textareaClone.val(val);
-        textareaClone.show();
-        // Reinitialize
-        initWysihtml5(textareaClone[0]);
-    });
-  </script>
+      // This event will be renamed "open.fndtn.reveal" in future foundation:
+      // http://foundation.zurb.com/docs/components/reveal.html#event-bindings
+      $(document).on("open", "#note-edit-dialog", function(event) {
+          var scope = $(event.currentTarget);
+          // Remove iframe.
+          $(".wysihtml5-sandbox", scope).remove();
+          // Unbind toolbar events by cloning it.
+          var toolbar = $(".wysihtml5-toolbar", scope);
+          var toolbarClone = toolbar.clone();
+          toolbar.replaceWith(toolbarClone);
+          // Unbind textarea events by cloning it.
+          var textarea = $("[role='wysihtml5-rich-text']", scope);
+          var textareaClone = textarea.clone();
+          var val = textarea.val(); // textarea value isn't copied with clone.
+          textarea.replaceWith(textareaClone);
+          textareaClone.val(val);
+          textareaClone.show();
+          // Reinitialize
+          initWysihtml5(textareaClone[0]);
+      });
+    </script>
+  {% endif %}
 {% endblock %}
 
 {% block raw_content %}
                 <p>{{ field.help_text }}</p>
               {% endwith %}
             </div>
-            <div class="small-12 columns">
-              {% with note_edit_form.html as field %}
-                {{ field.errors|safe }}
-                <label for="{{ field.id_for_label }}">{{ field.label }}:</label>
-                {{ field }}
-                <p>{{ field.help_text }}</p>
-              {% endwith %}
-            </div>
+            {% if note.is_editable %}
+              <div class="small-12 columns">
+                {% with note_edit_form.html as field %}
+                  {{ field.errors|safe }}
+                  <label for="{{ field.id_for_label }}">{{ field.label }}:</label>
+                  {{ field }}
+                  <p>{{ field.help_text }}</p>
+                {% endwith %}
+              </div>
+            {% endif %}
             <div class="small-12 columns text-center">
               <button type="submit"><i class="fa fa-save"></i> Save</button>
             </div>
index fb8ca9f3f6bbf62a0ee2cef74351dba4fc9ada30..3facd1691f5290d234f10c56fd2fbf5de3fe02dc 100644 (file)
@@ -1,13 +1,6 @@
 <div id="note_container" class="">
   {% if pdf_controls %}
     <div id="zoom-buttons" class="row show-for-medium-up">
-      <div id="outline-btn-wrapper" class="small-1 columns hide show-for-medium-up">
-        <i id="outline-btn" class="zoom-button fa fa-bars fa-2x"></i>
-      </div>
-      <div class="small-4 columns">
-        <span>Jump to page:
-        <input id="scroll-to" type="text" style="width: 3em; display: inline" /></span>
-      </div>
       <div class="small-2 small-centered columns center">
         <i id="minus-btn" class="zoom-button fa fa-search-minus fa-2x"></i>
         <i id="plus-btn" class="zoom-button fa fa-search-plus fa-2x"></i>
     <div class="small-12 small-centered columns medium-12 large-12 body_copy">
       <div id="note-content-wrapper" class="note-text">
         {% if note.has_markdown %}
-          <div class='note-html'>
+          <div class='note-html' id='note-html'>
             {{ note.notemarkdown.html|safe }}
           </div>
-        {% else %}
-          <iframe style="border:none; width:100%; min-height: 1000px;"
-                id="noteframe"></iframe>
-          <noscript>
-            {{ note.text }}
-          </noscript>
         {% endif %}
       </div> <!-- .note-text -->
     </div><!-- /body_copy -->
index f758b1478692dd20cadba8564d3bd1804a8a65b5..88000ab05c9f0d18551a192ee686532b93c1010f 100644 (file)
@@ -37,5 +37,5 @@ amqplib==1.0.2
 django-sslify>=0.2
 newrelic==2.40.0.34
 bleach==1.4
-bleach-whitelist==0.0.4
+bleach-whitelist==0.0.7
 Markdown==2.5.2