def test_html_safety(self): """ Check that we're handling HTML in a safe way """ html = markup_to_html('<p>Foo</em>', 'html') self.assertEqual(html, '<p>Foo</p>') html = markup_to_html('Foo<script>alert()</script>', 'html') self.assertEqual(html, 'Fooalert()') # some junky MSWord-like markup html = markup_to_html('Foo<p class="home"><Span style="font-size: 500%">bar</Span></P>', 'html', restricted=True) self.assertEqual(html, 'Foo<p>bar</p>') html = markup_to_html('A <p> </p><table cellpadding="10"><tr><td colspan=4>B</td></tr></table>', 'html', restricted=True) self.assertEqual(html, 'A <p> </p>B') # unsafe if we ask for it html = markup_to_html('Foo<script>alert()</script>', 'html', html_already_safe=True) self.assertEqual(html, 'Foo<script>alert()</script>') # PageVersions should be saved only with safe HTML offering = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=offering, person__userid="ggbaker") p = Page(offering=offering, label="Test") p.save() v1 = PageVersion(page=p, title="T1", wikitext='<em>Some</em> <script>HTML</script>', editor=memb) v1.set_markup('html') v1.save() self.assertEqual(v1.wikitext, '<em>Some</em> HTML')
def test_api(self): crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") person = Person.objects.get(userid='ggbaker') p = Page(offering=crs, label="PageExists") p.save() v = PageVersion(page=p, title="Page Exists", wikitext="Original Contents", editor=memb, comment="original page") v.save() from dashboard.models import new_feed_token token = new_feed_token() updata = u"""{ "userid": "ggbaker", "token": "%s", "pages": [ { "label": "Index", "title": "The Coursé Page", "can_read": "ALL", "can_write": "INST", "wikitext-base64": "VGhpcyBwYWdlIGlzIHNwZWNpYWwgaW4gKipzb21lKiogd2F5LiBcKHh+XjIrMSA9IFxmcmFjezF9ezJ9XCkuCgpHb29kYnllIHdvcmxkIQ==", "comment": "page creation comment", "use_math": true }, { "label": "PageExists", "new_label": "PageChanged", "title": "Another Page", "can_read": "STUD", "wikitext": "This is some **new** page\\n\\ncontent." } ] }""" % (token) # make a request with no auth token in place c = Client() url = reverse('pages.views.api_import', kwargs={'course_slug': crs.slug}) response = c.post(url, data=updata.encode('utf8'), content_type="application/json") self.assertEquals(response.status_code, 403) # create token and try again person.config['pages-token'] = token person.save() response = c.post(url, data=updata.encode('utf8'), content_type="application/json") self.assertEquals(response.status_code, 200) # make sure the data arrived self.assertEquals(Page.objects.filter(offering=crs, label="PageExists").count(), 0) p = Page.objects.get(offering=crs, label="PageChanged") v = p.current_version() self.assertEqual(v.title, "Another Page") self.assertEqual(v.get_wikitext(), "This is some **new** page\n\ncontent.") p = Page.objects.get(offering=crs, label="Index") v = p.current_version() self.assertEqual(v.title, u"The Cours\u00e9 Page") self.assertEqual(v.get_wikitext(), 'This page is special in **some** way. \\(x~^2+1 = \\frac{1}{2}\\).\n\nGoodbye world!') self.assert_('math' in v.config) self.assertEqual(v.config['math'], True)
def _get_creole(self): "Get a Creole class for some PageVersion for generic testing" crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) p = Page(offering=crs, label="Foo") p.save() pv = PageVersion(page=p) pv.get_creole() return pv.Creole
def save(self, editor, *args, **kwargs): # also create the PageVersion object. upfile = self.cleaned_data['file_attachment'] pv = PageVersion(file_attachment=upfile, file_mediatype=upfile.content_type, file_name=upfile.name, editor=editor) self.instance.offering = self.offering pg = super(EditFileForm, self).save(*args, **kwargs) pv.page=self.instance pv.save() return pg
def test_macros(self): """ Test macro behaviour """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Index") p.save() v = PageVersion(page=p, title="Index Page", wikitext="one +two+ three +four+", editor=memb) v.save() # no macros defined: rendered as-is self.assertEqual(p.current_version().html_contents().strip(), u"<p>one +two+ three +four+</p>") mp = Page(offering=crs, label=MACRO_LABEL) mp.save() mv = PageVersion(page=mp, title="Macros", wikitext="two: 22\nfour: 4444", editor=memb) mv.save() # macros defined: should be substituted self.assertEqual(p.current_version().html_contents().strip(), u"<p>one 22 three 4444</p>") mp.safely_delete() # macros disappear: back to original self.assertEqual(p.current_version().html_contents().strip(), u"<p>one +two+ three +four+</p>")
def _import_page(self, url): try: fh = urllib2.urlopen(url, timeout=20) if 'content-type' in fh.headers: ctype = fh.headers['content-type'].split(';')[0] is_html = ctype in ['text/html', 'application/xhtml+xml'] else: is_html = False html = fh.read() fh.close() except: raise forms.ValidationError('Could not fetch "%s".' % (url)) if not is_html: raise forms.ValidationError('Not HTML at "%s".' % (url)) try: wiki, title, urls = self.converter.from_html_full(html) except self.converter.ParseError: raise forms.ValidationError("Could not parse the HTML file %s." % (url)) label = self._labelize(url, title) if not title: title = label page = Page(offering=self.offering, label=label) pv = PageVersion(page=page, editor=self.editor, title=title, wikitext=wiki, comment="imported content") #print (page, pv, pv.title) #print [urlparse.urljoin(url, u) for u in urls] return page, pv, urls
def test_markup_choice(self): """ Check the distinction between Creole and Markdown pages """ offering = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=offering, person__userid="ggbaker") p = Page(offering=offering, label="Test") p.save() v1 = PageVersion(page=p, title="T1", wikitext='A //test//.', editor=memb, comment="original page") v1.save() self.assertEqual(v1.html_contents(), '<p>A <em>test</em>.</p>') v2 = PageVersion(page=p, title="T1", wikitext='A *test*.', editor=memb, comment="original page") v2.set_markup('markdown') v2.save() self.assertEqual(v2.html_contents(), '<p>A <em>test</em>.</p>')
def save(self, editor, *args, **kwargs): # create the PageVersion object: distribute the self.cleaned_data values appropriately wikitext = self.cleaned_data['markup_content'] comment = self.cleaned_data['comment'] title = self.cleaned_data['title'] pv = PageVersion(title=title, wikitext=wikitext, comment=comment, editor=editor) pv.set_markup(self.cleaned_data['_markup']) pv.set_math(self.cleaned_data['_math']) self.instance.offering = self.offering pg = super(EditPageForm, self).save(*args, **kwargs) pv.page=self.instance pv.save() return pg
def test_updating(self): """ Test the way the full text index updates The real-time indexing is disabled in the tests environments: slows things down too much. Disabled these tests as a result, since they don't really match the deployed or devel behaviour. """ return res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 1) self.assertEquals(res[0].object, self.offering) # don't expect CourseOfferings to update automatically self.offering.title = 'Something Else' self.offering.save() res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 1) # but a manual refresh should find changes self._update_index() res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 0) res = SearchQuerySet().models(CourseOffering).filter(text='Something') self.assertEqual(len(res), 1) # but we do update Pages in real time res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 0) # create a page p = Page(offering=self.offering, label='SomePage', can_read='ALL', can_write='STAF') p.save() pv = PageVersion(page=p, title='Some Page', wikitext='This is a page about fernwhizzles.', editor=self.instructor) pv.save() res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 1) # update a page pv = PageVersion(page=p, title='Some Page', wikitext='This is a page about bobdazzles.', editor=self.instructor) pv.save() res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 0) res = SearchQuerySet().models(Page).filter(text='bobdazzles') self.assertEqual(len(res), 1)
def convert_content(request, course_slug, page_label=None): """ Convert between wikicreole and HTML (AJAX called in editor when switching editing modes) """ if request.method != 'POST': return ForbiddenResponse(request, 'POST only') if 'to' not in request.POST: return ForbiddenResponse(request, 'must send "to" language') if 'data' not in request.POST: return ForbiddenResponse(request, 'must sent source "data"') offering = get_object_or_404(CourseOffering, slug=course_slug) to = request.POST['to'] data = request.POST['data'] if to == 'html': # convert wikitext to HTML # temporarily change the current version to get the result (but don't save) if page_label: page = get_object_or_404(Page, offering=offering, label=page_label) pv = page.current_version() else: # create temporary Page for conversion during creation p = Page(offering=offering) pv = PageVersion(page=p) pv.wikitext = data pv.diff_from = None result = {'data': pv.html_contents()} return HttpResponse(json.dumps(result), content_type="application/json") else: # convert HTML to wikitext converter = HTMLWiki([]) try: wiki = converter.from_html(data) except converter.ParseError: wiki = '' result = {'data': wiki} return HttpResponse(json.dumps(result), content_type="application/json")
def test_pages(self): """ Basic page rendering """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") person = Person.objects.get(userid='ggbaker') p = Page(offering=crs, label="Index") p.save() v = PageVersion(page=p, title="Index Page", wikitext="Original Contents", editor=memb) v.save() p = Page(offering=crs, label="OtherPage") p.save() v = PageVersion(page=p, title="Other Page", wikitext="Original Contents", editor=memb) v.save() c = Client() c.login_user('ggbaker') # test the basic rendering of the core pages test_views(self, c, 'pages.views.', ['index_page', 'all_pages', 'new_page', 'new_file', 'import_site'], {'course_slug': crs.slug}) test_views(self, c, 'pages.views.', ['view_page', 'page_history', 'edit_page', 'import_page'], {'course_slug': crs.slug, 'page_label': 'OtherPage'})
def _delete_pagefile(request, course_slug, page_label, kind): """ Delete page/file """ with django.db.transaction.atomic(): offering = get_object_or_404(CourseOffering, slug=course_slug) page = get_object_or_404(Page, offering=offering, label=page_label) version = page.current_version() member = _check_allowed(request, offering, page.can_write, page.editdate()) if not member: return ForbiddenResponse(request, 'Not allowed to edit this '+kind+'.') can_create = member.role in MEMBER_ROLES[offering.page_creators()] if not can_create: return ForbiddenResponse(request, 'Not allowed to delete pages in for this offering (must have page-creator permission).') from django.core.validators import URLValidator from django.core.exceptions import ValidationError val = URLValidator() redirect = request.POST.get('redirect', 'Index') url = request.build_absolute_uri(urljoin(page.get_absolute_url(), redirect)) try: val(url) except ValidationError: messages.error(request, "Bad redirect URL entered. Not deleted.") return HttpResponseRedirect(reverse('offering:pages:edit_page', kwargs={'course_slug': course_slug, 'page_label': page.label})) redir_version = PageVersion(page=page, title=version.title, redirect=redirect, editor=member, comment='automatically generated on deletion') redir_version.set_redirect_reason('delete') redir_version.save() messages.success(request, "Page deleted and will redirect to this location.") return HttpResponseRedirect(urljoin(page.get_absolute_url(), redirect))
def _sample_setup(self): crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Index") p.save() v = PageVersion(page=p, title="Index Page", wikitext="Original Contents", editor=memb) v.save() p = Page(offering=crs, label="OtherPage") p.save() v = PageVersion(page=p, title="Other Page", wikitext="Original Contents", editor=memb) v.save() return crs
def test_redirect(self): """ Redirecting with redirect stub """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Test") p.save() v = PageVersion(page=p, title="Test Page", wikitext="one +two+ three +four+", editor=memb) v.save() c = Client() # normal pages still viewable url = reverse('offering:pages:view_page', kwargs={'course_slug': crs.slug, 'page_label': 'Test'}) response = c.get(url) self.assertEqual(response.status_code, 200) v = PageVersion(page=p, redirect='NewLocation', editor=memb) v.save() response = c.get(url) self.assertEqual(response.status_code, 301) redir_url = reverse('offering:pages:view_page', kwargs={'course_slug': crs.slug, 'page_label': 'NewLocation'}) self.assertTrue(response['location'].endswith(redir_url))
def test_macros(self): """ Test macro behaviour """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Index") p.save() v = PageVersion(page=p, title="Index Page", wikitext="one +two+ three +four+", editor=memb) v.save() # no macros defined: rendered as-is self.assertEqual(p.current_version().html_contents().strip(), "<p>one +two+ three +four+</p>") mp = Page(offering=crs, label=MACRO_LABEL) mp.save() mv = PageVersion(page=mp, title="Macros", wikitext="two: 22\nfour: 4444", editor=memb) mv.save() # macros defined: should be substituted self.assertEqual(p.current_version().html_contents().strip(), "<p>one 22 three 4444</p>") mp.label = 'NOT_MACROS' mp.save() # macros disappear: back to original self.assertEqual(p.current_version().html_contents().strip(), "<p>one +two+ three +four+</p>")
def save(self, editor, *args, **kwargs): # also create the PageVersion object. wikitext = self.cleaned_data['wikitext'] comment = self.cleaned_data['comment'] title = self.cleaned_data['title'] pv = PageVersion(title=title, wikitext=wikitext, comment=comment, editor=editor) # set config data if 'math' in self.cleaned_data: pv.set_math(self.cleaned_data['math']) self.instance.offering = self.offering pg = super(EditPageForm, self).save(*args, **kwargs) pv.page = self.instance pv.save() return pg
def test_pages(self): """ Basic page rendering """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") person = Person.objects.get(userid='ggbaker') p = Page(offering=crs, label="Index") p.save() v = PageVersion(page=p, title="Index Page", wikitext="Original Contents", editor=memb) v.save() p = Page(offering=crs, label="OtherPage") p.save() v = PageVersion(page=p, title="Other Page", wikitext="Original Contents", editor=memb) v.save() c = Client() c.login_user('ggbaker') # test the basic rendering of the core pages test_views( self, c, 'pages.views.', ['index_page', 'all_pages', 'new_page', 'new_file', 'import_site'], {'course_slug': crs.slug}) test_views(self, c, 'pages.views.', ['view_page', 'page_history', 'edit_page', 'import_page'], { 'course_slug': crs.slug, 'page_label': 'OtherPage' })
def test_redirect(self): """ Redirecting with redirect stub """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Test") p.save() v = PageVersion(page=p, title="Test Page", wikitext="one +two+ three +four+", editor=memb) v.save() c = Client() # normal pages still viewable url = reverse('offering:pages:view_page', kwargs={ 'course_slug': crs.slug, 'page_label': 'Test' }) response = c.get(url) self.assertEqual(response.status_code, 200) v = PageVersion(page=p, redirect='NewLocation', editor=memb) v.save() response = c.get(url) self.assertEqual(response.status_code, 301) redir_url = reverse('offering:pages:view_page', kwargs={ 'course_slug': crs.slug, 'page_label': 'NewLocation' }) self.assertTrue(response['location'].endswith(redir_url))
def test_updating(self): """ Test the way the full text index updates The real-time indexing is disabled in the tests environments: slows things down too much. Disabled these tests as a result, since they don't really match the deployed or devel behaviour. """ return res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 1) self.assertEqual(res[0].object, self.offering) # don't expect CourseOfferings to update automatically self.offering.title = 'Something Else' self.offering.save() res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 1) # but a manual refresh should find changes self._update_index() res = SearchQuerySet().models(CourseOffering).filter(text='Babbling') self.assertEqual(len(res), 0) res = SearchQuerySet().models(CourseOffering).filter(text='Something') self.assertEqual(len(res), 1) # but we do update Pages in real time res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 0) # create a page p = Page(offering=self.offering, label='SomePage', can_read='ALL', can_write='STAF') p.save() pv = PageVersion(page=p, title='Some Page', wikitext='This is a page about fernwhizzles.', editor=self.instructor) pv.save() res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 1) # update a page pv = PageVersion(page=p, title='Some Page', wikitext='This is a page about bobdazzles.', editor=self.instructor) pv.save() res = SearchQuerySet().models(Page).filter(text='fernwhizzles') self.assertEqual(len(res), 0) res = SearchQuerySet().models(Page).filter(text='bobdazzles') self.assertEqual(len(res), 1)
def test_html_safety(self): """ Check that we're handling HTML in a safe way """ html = markup_to_html('<p>Foo</em>', 'html') self.assertEqual(html, '<p>Foo</p>') html = markup_to_html('Foo<script>alert()</script>', 'html') self.assertEqual(html, 'Fooalert()') # some junky MSWord-like markup html = markup_to_html( 'Foo<p class="home"><Span style="font-size: 500%">bar</Span></P>', 'html', restricted=True) self.assertEqual(html, 'Foo<p>bar</p>') html = markup_to_html( 'A <p> </p><table cellpadding="10"><tr><td colspan=4>B</td></tr></table>', 'html', restricted=True) self.assertEqual(html, 'A <p> </p>B') # unsafe if we ask for it html = markup_to_html('Foo<script>alert()</script>', 'html', html_already_safe=True) self.assertEqual(html, 'Foo<script>alert()</script>') # PageVersions should be saved only with safe HTML offering = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=offering, person__userid="ggbaker") p = Page(offering=offering, label="Test") p.save() v1 = PageVersion(page=p, title="T1", wikitext='<em>Some</em> <script>HTML</script>', editor=memb) v1.set_markup('html') v1.save() self.assertEqual(v1.wikitext, '<em>Some</em> HTML')
def test_permissions(self): """ Test page access control behaviour. """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.filter(offering=crs, role='INST').first() inst = memb.person ta = Member.objects.filter(offering=crs, role='TA').first().person stud = Member.objects.filter(offering=crs, role='STUD').first().person non_member = Person.objects.get(userid='dixon') assert not Member.objects.filter(offering=crs, person=non_member) p = Page(offering=crs, label="Test", can_read='STAF', can_write='INST') p.save() v = PageVersion(page=p, title="Test Page", wikitext="Page contents", editor=memb) v.save() # page-viewing permissions c = Client() url = reverse('offering:pages:view_page', kwargs={'course_slug': crs.slug, 'page_label': 'Test'}) c.logout() response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(inst.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(ta.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(stud.userid) response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(non_member.userid) response = c.get(url) self.assertEqual(response.status_code, 403) # ... but with a PagePermission object, non_member can access pp = PagePermission(person=non_member, offering=crs, role='INST') pp.save() response = c.get(url) self.assertEqual(response.status_code, 200) # page-editing permissions url = reverse('offering:pages:edit_page', kwargs={'course_slug': crs.slug, 'page_label': 'Test'}) c.logout() response = c.get(url) self.assertEqual(response.status_code, 302) # redirect to log in c.login_user(inst.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(ta.userid) response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(stud.userid) response = c.get(url) self.assertEqual(response.status_code, 403) # editing with PagePermission not implemented c.login_user(non_member.userid) response = c.get(url) self.assertEqual(response.status_code, 403)
def test_version_diffs(self): "Test the old version diffing." crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.get(offering=crs, person__userid="ggbaker") p = Page(offering=crs, label="Test") p.save() v1 = PageVersion(page=p, title="T1", wikitext=contents1, editor=memb, comment="original page") v1.save() v2 = PageVersion(page=p, title="T2", wikitext=contents2, editor=memb, comment="some changes") v2.save() v3 = PageVersion(page=p, title="T3", wikitext=contents3, editor=memb, comment="total rewrite") v3.save() # refresh changes in DB v1 = PageVersion.objects.get(id=v1.id) v2 = PageVersion.objects.get(id=v2.id) v3 = PageVersion.objects.get(id=v3.id) # make sure the contents survived self.assertEqual(v1.get_wikitext(), contents1) self.assertEqual(v2.get_wikitext(), contents2) self.assertEqual(v3.get_wikitext(), contents3) self.assertEqual(v1.title, "T1") self.assertEqual(v2.title, "T2") self.assertEqual(v3.title, "T3") # make sure the diff got stored for incremental changes self.assertEqual(v1.wikitext, '') self.assertEqual(v1.diff_from_id, v2.id) # ... but big changes are stored verbatim self.assertEqual(v2.wikitext, contents2) self.assertEqual(v2.diff_from, None) # ... and the head has the current contents self.assertEqual(v3.wikitext, contents3) self.assertEqual(v3.diff_from, None)
def _pages_from_json(request, offering, data): with django.db.transaction.atomic(): try: data = data.decode('utf-8-sig') except UnicodeDecodeError: raise ValidationError(u"Bad UTF-8 data in file.") try: data = json.loads(data) except ValueError as e: raise ValidationError(u'JSON decoding error. Exception was: "' + str(e) + '"') if not isinstance(data, dict): raise ValidationError( u'Outer JSON data structure must be an object.') if 'userid' not in data or 'token' not in data: raise ValidationError( u'Outer JSON data object must contain keys "userid" and "token".' ) if 'pages' not in data: raise ValidationError( u'Outer JSON data object must contain keys "pages".') if not isinstance(data['pages'], list): raise ValidationError(u'Value for "pages" must be a list.') try: user = Person.objects.get(userid=data['userid']) member = Member.objects.exclude(role='DROP').get(person=user, offering=offering) except Person.DoesNotExist, Member.DoesNotExist: raise ValidationError(u'Person with that userid does not exist.') if 'pages-token' not in user.config or user.config[ 'pages-token'] != data['token']: e = ValidationError(u'Could not validate authentication token.') e.status = 403 raise e # if we get this far, the user is authenticated and we can start processing the pages... for i, pdata in enumerate(data['pages']): if not isinstance(pdata, dict): raise ValidationError( u'Page #%i entry structure must be an object.' % (i)) if 'label' not in pdata: raise ValidationError( u'Page #%i entry does not have a "label".' % (i)) # handle changes to the Page object pages = Page.objects.filter(offering=offering, label=pdata['label']) if pages: page = pages[0] old_ver = page.current_version() else: page = Page(offering=offering, label=pdata['label']) old_ver = None # check write permissions # mock the request object enough to satisfy _check_allowed() class FakeRequest(object): def is_authenticated(self): return True fake_request = FakeRequest() fake_request.user = FakeRequest() fake_request.user.username = user.userid if old_ver: m = _check_allowed(fake_request, offering, page.can_write, page.editdate()) else: m = _check_allowed(fake_request, offering, offering.page_creators()) if not m: raise ValidationError(u'You can\'t edit page #%i.' % (i)) # handle Page attributes if 'can_read' in pdata: if type(pdata['can_read'] ) != unicode or pdata['can_read'] not in ACL_DESC: raise ValidationError( u'Page #%i "can_read" value must be one of %s.' % (i, ','.join(ACL_DESC.keys()))) page.can_read = pdata['can_read'] if 'can_write' in pdata: if type(pdata['can_write']) != unicode or pdata[ 'can_write'] not in WRITE_ACL_DESC: raise ValidationError( u'Page #%i "can_write" value must be one of %s.' % (i, ','.join(WRITE_ACL_DESC.keys()))) if m.role == 'STUD': raise ValidationError( u'Page #%i: students can\'t change can_write value.' % (i)) page.can_write = pdata['can_write'] if 'new_label' in pdata: if type(pdata['new_label']) != unicode: raise ValidationError( u'Page #%i "new_label" value must be a string.' % (i)) if m.role == 'STUD': raise ValidationError( u'Page #%i: students can\'t change label value.' % (i)) if Page.objects.filter(offering=offering, label=pdata['new_label']): raise ValidationError( u'Page #%i: there is already a page with that "new_label".' % (i)) page.label = pdata['new_label'] page.save() # handle PageVersion changes ver = PageVersion(page=page, editor=member) if 'title' in pdata: if type(pdata['title']) != unicode: raise ValidationError( u'Page #%i "title" value must be a string.' % (i)) ver.title = pdata['title'] elif old_ver: ver.title = old_ver.title else: raise ValidationError( u'Page #%i has no "title" for new page.' % (i)) if 'comment' in pdata: if type(pdata['comment']) != unicode: raise ValidationError( u'Page #%i "comment" value must be a string.' % (i)) ver.comment = pdata['comment'] if 'use_math' in pdata: if type(pdata['use_math']) != bool: raise ValidationError( u'Page #%i "comment" value must be a boolean.' % (i)) ver.set_math(pdata['use_math']) if 'wikitext-base64' in pdata: if type(pdata['wikitext-base64']) != unicode: raise ValidationError( u'Page #%i "wikitext-base64" value must be a string.' % (i)) try: wikitext = base64.b64decode(pdata['wikitext-base64']) except TypeError: raise ValidationError( u'Page #%i "wikitext-base64" contains bad base BASE64 data.' % (i)) ver.wikitext = wikitext elif 'wikitext' in pdata: if type(pdata['wikitext']) != unicode: raise ValidationError( u'Page #%i "wikitext" value must be a string.' % (i)) ver.wikitext = pdata['wikitext'] elif old_ver: ver.wikitext = old_ver.wikitext else: raise ValidationError( u'Page #%i has no wikitext for new page.' % (i)) ver.save() return user
def _edit_pagefile(request, course_slug, page_label, kind): """ View to create and edit pages """ if request.method == 'POST' and 'delete' in request.POST and request.POST[ 'delete'] == 'yes': return _delete_pagefile(request, course_slug, page_label, kind) with django.db.transaction.atomic(): offering = get_object_or_404(CourseOffering, slug=course_slug) if page_label: page = get_object_or_404(Page, offering=offering, label=page_label) version = page.current_version() member = _check_allowed(request, offering, page.can_write, page.editdate()) old_label = page.label else: page = None version = None member = _check_allowed( request, offering, offering.page_creators()) # users who can create pages old_label = None if isinstance(member, PagePermission): return ForbiddenResponse( request, 'Editing of pages by additional-permission holders is not implemented. Sorry' ) # make sure we're looking at the right "kind" (page/file) if not kind: kind = "file" if version.is_filepage() else "page" # get the form class we need if kind == "page": Form = EditPageForm else: Form = EditFileForm # check that we have an allowed member of the course (and can continue) if not member: return ForbiddenResponse( request, 'Not allowed to edit/create this ' + kind + '.') restricted = False if member.role == 'STUD': # students get the restricted version of the form Form = Form.restricted_form restricted = True if request.method == 'POST': form = Form(instance=page, offering=offering, data=request.POST, files=request.FILES) if form.is_valid(): instance = form.save(editor=member) # clean up weirdness from restricted form if 'label' not in form.cleaned_data: # happens when student edits an existing page instance.label = page.label if 'can_write' not in form.cleaned_data: # happens only when students create a page instance.can_write = 'STUD' if not restricted and 'releasedate' in form.cleaned_data: instance.set_releasedate(form.cleaned_data['releasedate']) elif not restricted: instance.set_releasedate(None) if not restricted and 'editdate' in form.cleaned_data: instance.set_editdate(form.cleaned_data['editdate']) elif not restricted: instance.set_editdate(None) instance.redirect = None if old_label and old_label != instance.label: # page has been moved to a new URL: leave a redirect in its place redir_page = Page(offering=instance.offering, label=old_label, can_read=instance.can_read, can_write=offering.page_creators()) redir_page.set_releasedate(instance.releasedate()) redir_page.set_editdate(instance.editdate()) redir_page.save() redir_version = PageVersion( page=redir_page, title=version.title, redirect=instance.label, editor=member, comment='automatically generated on label change') redir_version.set_redirect_reason('rename') redir_version.save() messages.info( request, 'Page label changed: the old location (%s) will redirect to this page.' % (old_label, )) instance.save() #LOG EVENT# l = LogEntry(userid=request.user.username, description="Edited page %s in %s." % (instance.label, offering), related_object=instance) l.save() if page: messages.success( request, "Edited " + kind + " \"%s\"." % (instance.label)) else: messages.success( request, "Created " + kind + " \"%s\"." % (instance.label)) if not page and instance.label == 'Index' and not offering.url( ): # new Index page but no existing course URL: set as course web page url = settings.BASE_ABS_URL + instance.get_absolute_url() offering.set_url(url) offering.save() messages.info(request, "Set course URL to new Index page.") return HttpResponseRedirect( reverse('offering:pages:view_page', kwargs={ 'course_slug': course_slug, 'page_label': instance.label })) else: form = Form(instance=page, offering=offering) if 'label' in request.GET: label = request.GET['label'] if label == 'Index': form.initial['title'] = offering.name() form.fields[ 'label'].help_text += u'\u2014the label "Index" indicates the front page for this course.' elif label == MACRO_LABEL: form.initial['can_read'] = 'INST' form.initial['can_write'] = 'INST' form.initial['title'] = MACRO_LABEL else: form.initial['title'] = label.title() form.initial['label'] = label context = { 'offering': offering, 'page': page, 'form': form, 'kind': kind.title(), 'is_macro_page': form.initial.get('title', None) == MACRO_LABEL, } return render(request, 'pages/edit_page.html', context)
def _edit_pagefile(request, course_slug, page_label, kind): """ View to create and edit pages """ if request.method == 'POST' and 'delete' in request.POST and request.POST['delete'] == 'yes': return _delete_pagefile(request, course_slug, page_label, kind) with django.db.transaction.atomic(): offering = get_object_or_404(CourseOffering, slug=course_slug) if page_label: page = get_object_or_404(Page, offering=offering, label=page_label) version = page.current_version() member = _check_allowed(request, offering, page.can_write, page.editdate()) old_label = page.label else: page = None version = None member = _check_allowed(request, offering, offering.page_creators()) # users who can create pages old_label = None if isinstance(member, PagePermission): return ForbiddenResponse(request, 'Editing of pages by additional-permission holders is not implemented. Sorry') # make sure we're looking at the right "kind" (page/file) if not kind: kind = "file" if version.is_filepage() else "page" # get the form class we need if kind == "page": Form = EditPageForm else: Form = EditFileForm # check that we have an allowed member of the course (and can continue) if not member: return ForbiddenResponse(request, 'Not allowed to edit/create this '+kind+'.') restricted = False if member.role == 'STUD': # students get the restricted version of the form Form = Form.restricted_form restricted = True if request.method == 'POST': form = Form(instance=page, offering=offering, data=request.POST, files=request.FILES) if form.is_valid(): instance = form.save(editor=member) # clean up weirdness from restricted form if 'label' not in form.cleaned_data: # happens when student edits an existing page instance.label = page.label if 'can_write' not in form.cleaned_data: # happens only when students create a page instance.can_write = 'STUD' if not restricted and 'releasedate' in form.cleaned_data: instance.set_releasedate(form.cleaned_data['releasedate']) elif not restricted: instance.set_releasedate(None) if not restricted and 'editdate' in form.cleaned_data: instance.set_editdate(form.cleaned_data['editdate']) elif not restricted: instance.set_editdate(None) instance.redirect = None if old_label and old_label != instance.label: # page has been moved to a new URL: leave a redirect in its place redir_page = Page(offering=instance.offering, label=old_label, can_read=instance.can_read, can_write=offering.page_creators()) redir_page.set_releasedate(instance.releasedate()) redir_page.set_editdate(instance.editdate()) redir_page.save() redir_version = PageVersion(page=redir_page, title=version.title, redirect=instance.label, editor=member, comment='automatically generated on label change') redir_version.set_redirect_reason('rename') redir_version.save() messages.info(request, 'Page label changed: the old location (%s) will redirect to this page.' % (old_label,)) instance.save() #LOG EVENT# l = LogEntry(userid=request.user.username, description="Edited page %s in %s." % (instance.label, offering), related_object=instance) l.save() if page: messages.success(request, "Edited "+kind+" \"%s\"." % (instance.label)) else: messages.success(request, "Created "+kind+" \"%s\"." % (instance.label)) if not page and instance.label == 'Index' and not offering.url(): # new Index page but no existing course URL: set as course web page url = settings.BASE_ABS_URL + instance.get_absolute_url() offering.set_url(url) offering.save() messages.info(request, "Set course URL to new Index page.") return HttpResponseRedirect(reverse('offering:pages:view_page', kwargs={'course_slug': course_slug, 'page_label': instance.label})) else: form = Form(instance=page, offering=offering) if 'label' in request.GET: label = request.GET['label'] if label == 'Index': form.initial['title'] = offering.name() form.fields['label'].help_text += '\u2014the label "Index" indicates the front page for this course.' elif label == MACRO_LABEL: form.initial['can_read'] = 'INST' form.initial['can_write'] = 'INST' form.initial['title'] = MACRO_LABEL else: form.initial['title'] = label.title() form.initial['label'] = label context = { 'offering': offering, 'page': page, 'form': form, 'kind': kind.title(), 'is_macro_page': form.initial.get('title', None) == MACRO_LABEL, } return render(request, 'pages/edit_page.html', context)
def test_permissions(self): """ Test page access control behaviour. """ crs = CourseOffering.objects.get(slug=TEST_COURSE_SLUG) memb = Member.objects.filter(offering=crs, role='INST').first() inst = memb.person ta = Member.objects.filter(offering=crs, role='TA').first().person stud = Member.objects.filter(offering=crs, role='STUD').first().person non_member = Person.objects.get(userid='dixon') assert not Member.objects.filter(offering=crs, person=non_member) p = Page(offering=crs, label="Test", can_read='STAF', can_write='INST') p.save() v = PageVersion(page=p, title="Test Page", wikitext="Page contents", editor=memb) v.save() # page-viewing permissions c = Client() url = reverse('offering:pages:view_page', kwargs={ 'course_slug': crs.slug, 'page_label': 'Test' }) c.logout() response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(inst.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(ta.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(stud.userid) response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(non_member.userid) response = c.get(url) self.assertEqual(response.status_code, 403) # ... but with a PagePermission object, non_member can access pp = PagePermission(person=non_member, offering=crs, role='INST') pp.save() response = c.get(url) self.assertEqual(response.status_code, 200) # page-editing permissions url = reverse('offering:pages:edit_page', kwargs={ 'course_slug': crs.slug, 'page_label': 'Test' }) c.logout() response = c.get(url) self.assertEqual(response.status_code, 302) # redirect to log in c.login_user(inst.userid) response = c.get(url) self.assertEqual(response.status_code, 200) c.login_user(ta.userid) response = c.get(url) self.assertEqual(response.status_code, 403) c.login_user(stud.userid) response = c.get(url) self.assertEqual(response.status_code, 403) # editing with PagePermission not implemented c.login_user(non_member.userid) response = c.get(url) self.assertEqual(response.status_code, 403)
def _pages_from_json(request, offering, data): with django.db.transaction.atomic(): try: data = data.decode('utf-8-sig') except UnicodeDecodeError: raise ValidationError("Bad UTF-8 data in file.") try: data = json.loads(data) except ValueError as e: raise ValidationError('JSON decoding error. Exception was: "' + str(e) + '"') if not isinstance(data, dict): raise ValidationError('Outer JSON data structure must be an object.') if 'userid' not in data or 'token' not in data: raise ValidationError('Outer JSON data object must contain keys "userid" and "token".') if 'pages' not in data: raise ValidationError('Outer JSON data object must contain keys "pages".') if not isinstance(data['pages'], list): raise ValidationError('Value for "pages" must be a list.') try: user = Person.objects.get(userid=data['userid']) member = Member.objects.exclude(role='DROP').get(person=user, offering=offering) except (Person.DoesNotExist, Member.DoesNotExist): raise ValidationError('Person with that userid does not exist.') if 'pages-token' not in user.config or user.config['pages-token'] != data['token']: e = ValidationError('Could not validate authentication token.') e.status = 403 raise e # if we get this far, the user is authenticated and we can start processing the pages... for i, pdata in enumerate(data['pages']): if not isinstance(pdata, dict): raise ValidationError('Page #%i entry structure must be an object.' % (i)) if 'label' not in pdata: raise ValidationError('Page #%i entry does not have a "label".' % (i)) # handle changes to the Page object pages = Page.objects.filter(offering=offering, label=pdata['label']) if pages: page = pages[0] old_ver = page.current_version() else: page = Page(offering=offering, label=pdata['label']) old_ver = None # check write permissions # mock the request object enough to satisfy _check_allowed() class FakeRequest(object): is_authenticated = True fake_request = FakeRequest() fake_request.user = FakeRequest() fake_request.user.username = user.userid if old_ver: m = _check_allowed(fake_request, offering, page.can_write, page.editdate()) else: m = _check_allowed(fake_request, offering, offering.page_creators()) if not m: raise ValidationError('You can\'t edit page #%i.' % (i)) # handle Page attributes if 'can_read' in pdata: if type(pdata['can_read']) != str or pdata['can_read'] not in ACL_DESC: raise ValidationError('Page #%i "can_read" value must be one of %s.' % (i, ','.join(list(ACL_DESC.keys())))) page.can_read = pdata['can_read'] if 'can_write' in pdata: if type(pdata['can_write']) != str or pdata['can_write'] not in WRITE_ACL_DESC: raise ValidationError('Page #%i "can_write" value must be one of %s.' % (i, ','.join(list(WRITE_ACL_DESC.keys())))) if m.role == 'STUD': raise ValidationError('Page #%i: students can\'t change can_write value.' % (i)) page.can_write = pdata['can_write'] if 'new_label' in pdata: if type(pdata['new_label']) != str: raise ValidationError('Page #%i "new_label" value must be a string.' % (i)) if m.role == 'STUD': raise ValidationError('Page #%i: students can\'t change label value.' % (i)) if Page.objects.filter(offering=offering, label=pdata['new_label']): raise ValidationError('Page #%i: there is already a page with that "new_label".' % (i)) page.label = pdata['new_label'] page.save() # handle PageVersion changes ver = PageVersion(page=page, editor=member) if 'title' in pdata: if type(pdata['title']) != str: raise ValidationError('Page #%i "title" value must be a string.' % (i)) ver.title = pdata['title'] elif old_ver: ver.title = old_ver.title else: raise ValidationError('Page #%i has no "title" for new page.' % (i)) if 'comment' in pdata: if type(pdata['comment']) != str: raise ValidationError('Page #%i "comment" value must be a string.' % (i)) ver.comment = pdata['comment'] if 'use_math' in pdata: if type(pdata['use_math']) != bool: raise ValidationError('Page #%i "comment" value must be a boolean.' % (i)) ver.set_math(pdata['use_math']) if 'markup' in pdata: if isinstance(pdata['markup'], str): raise ValidationError('Page #%i "markup" value must be a string.' % (i)) ver.set_markup(pdata['markup']) if 'wikitext-base64' in pdata: if type(pdata['wikitext-base64']) != str: raise ValidationError('Page #%i "wikitext-base64" value must be a string.' % (i)) try: wikitext = base64.b64decode(pdata['wikitext-base64']).decode('utf8') except TypeError: raise ValidationError('Page #%i "wikitext-base64" contains bad base BASE64 data.' % (i)) ver.wikitext = wikitext elif 'wikitext' in pdata: if type(pdata['wikitext']) != str: raise ValidationError('Page #%i "wikitext" value must be a string.' % (i)) ver.wikitext = pdata['wikitext'] elif old_ver: ver.wikitext = old_ver.wikitext else: raise ValidationError('Page #%i has no wikitext for new page.' % (i)) ver.save() return user