Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/macros.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/macros.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/macros.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/macros.py Sat Nov 15 01:14:46 2014 @@ -1,15 +1,54 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +from StringIO import StringIO from datetime import datetime +import os +import shutil +import tempfile import unittest -from trac.config import Option +from trac.config import Option, ListOption, IntOption, BoolOption from trac.test import locale_en from trac.util.datefmt import format_date, utc from trac.wiki.model import WikiPage from trac.wiki.tests import formatter + +def add_pages(tc, names): + now = datetime.now(utc) + for name in names: + w = WikiPage(tc.env) + w.name = name + w.text = '--' + w.save('joe', 'the page ' + name, '::1', now) + + # == [[Image]] +def image_setup(tc): + add_pages(tc, ['page:fr']) + from trac.attachment import Attachment + tc.env.path = tempfile.mkdtemp(prefix='trac-tempenv-') + attachment = Attachment(tc.env, 'wiki', 'page:fr') + attachment.description = "image in page:fr" + attachment.insert('img.png', StringIO(''), 0, 2) + +def image_teardown(tc): + shutil.rmtree(os.path.join(tc.env.path, 'files')) + os.rmdir(tc.env.path) + tc.env.reset_db() + # Note: using `« test »` string in the following tests for checking # unicode robustness and whitespace support (first space is # normal ASCII SPACE, second is Unicode NO-BREAK SPACE). @@ -91,13 +130,39 @@ IMAGE_MACRO_TEST_CASES = u""" ------------------------------ <a style="padding:0; border:none" href="/wiki/WikiStart"><img src="/browser/%C2%AB%20test%C2%A0%C2%BB?format=raw" alt="/browser/« test »" title="/browser/« test »" /></a> ============================== Strip unicode white-spaces and ZWSPs (#10668) -[[Image(â âsource:« test ».pngã â, nolink)]] +[[Image(â âsource:« test ».pngã â, nolink, 100%ã â)]] ------------------------------ <p> -<img src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" alt="source:« test ».png" title="source:« test ».png" /> +<img width="100%" alt="source:« test ».png" title="source:« test ».png" src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" /> </p> ------------------------------ -<img src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" alt="source:« test ».png" title="source:« test ».png" /> +<img width="100%" alt="source:« test ».png" title="source:« test ».png" src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" /> +------------------------------ +============================== Attachments on page with ':' characters (#10562) +[[Image("page:fr":img.pngâ,nolink)]] +------------------------------ +<p> +<img src="/raw-attachment/wiki/page%3Afr/img.png" alt="image in page:fr" title="image in page:fr" /> +</p> +------------------------------ +<img src="/raw-attachment/wiki/page%3Afr/img.png" alt="image in page:fr" title="image in page:fr" /> +------------------------------ +============================== htdocs: Image, nolink +[[Image(htdocs:trac_logo.png, nolink)]] +------------------------------ +<p> +<img src="/chrome/site/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" /> +</p> +------------------------------ +<img src="/chrome/site/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" /> +============================== shared: Image, nolink +[[Image(shared:trac_logo.png, nolink)]] +------------------------------ +<p> +<img src="/chrome/shared/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" /> +</p> +------------------------------ +<img src="/chrome/shared/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" /> ------------------------------ """ @@ -110,14 +175,6 @@ IMAGE_MACRO_TEST_CASES = u""" # == [[TitleIndex]] -def add_pages(tc, names): - now = datetime.now(utc) - for name in names: - w = WikiPage(tc.env) - w.name = name - w.text = '--' - w.save('joe', 'the page ' + name, '::1', now) - def titleindex_teardown(tc): tc.env.reset_db() @@ -391,8 +448,36 @@ TRACINI_MACRO_TEST_CASES = u"""\ </p><div class="tracini">\ <h3 id="section-42-section"><code>[section-42]</code></h3>\ <table class="wiki"><tbody>\ -<tr><td><tt>option1</tt></td><td></td><td class="default"><code>value</code></td></tr>\ -<tr><td><tt>option2</tt></td><td>blah</td><td class="default"><code>value</code></td></tr>\ +<tr class="even"><td><tt>option1</tt></td><td></td><td class="default"><code>value</code></td></tr>\ +<tr class="odd"><td><tt>option2</tt></td><td>blah</td><td class="default"><code>value</code></td></tr>\ +</tbody></table>\ +</div><p> +</p> +------------------------------ +============================== TracIni, list option with sep=| (#11074) +[[TracIni(section-list)]] +------------------------------ +<p> +</p><div class="tracini">\ +<h3 id="section-list-section"><code>[section-list]</code></h3>\ +<table class="wiki"><tbody>\ +<tr class="even"><td><tt>option1</tt></td><td></td><td class="default"><code>4.2|42|42||0|enabled</code></td></tr>\ +</tbody></table>\ +</div><p> +</p> +------------------------------ +============================== TracIni, option with "false" value as default +[[TracIni(section-def)]] +------------------------------ +<p> +</p><div class="tracini">\ +<h3 id="section-def-section"><code>[section-def]</code></h3>\ +<table class="wiki"><tbody>\ +<tr class="even"><td><tt>option1</tt></td><td></td><td class="nodefault">(no default)</td></tr>\ +<tr class="odd"><td><tt>option2</tt></td><td></td><td class="nodefault">(no default)</td></tr>\ +<tr class="even"><td><tt>option3</tt></td><td></td><td class="default"><code>0</code></td></tr>\ +<tr class="odd"><td><tt>option4</tt></td><td></td><td class="default"><code>disabled</code></td></tr>\ +<tr class="even"><td><tt>option5</tt></td><td></td><td class="default"><code></code></td></tr>\ </tbody></table>\ </div><p> </p> @@ -404,6 +489,13 @@ def tracini_setup(tc): class Foo(object): option_a1 = (Option)('section-42', 'option1', 'value', doc='') option_a2 = (Option)('section-42', 'option2', 'value', doc='blah') + option_l1 = (ListOption)('section-list', 'option1', + [4.2, '42', 42, None, 0, True], sep='|') + option_d1 = (Option)('section-def', 'option1', None) + option_d2 = (Option)('section-def', 'option2', '') + option_d3 = (IntOption)('section-def', 'option3', 0) + option_d4 = (BoolOption)('section-def', 'option4', False) + option_d5 = (ListOption)('section-def', 'option5', []) def tracini_teardown(tc): Option.registry = tc._orig_registry @@ -411,7 +503,9 @@ def tracini_teardown(tc): def suite(): suite = unittest.TestSuite() - suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__)) + suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__, + setup=image_setup, + teardown=image_teardown)) suite.addTest(formatter.suite(TITLEINDEX1_MACRO_TEST_CASES, file=__file__)) suite.addTest(formatter.suite(TITLEINDEX2_MACRO_TEST_CASES, file=__file__, setup=titleindex2_setup,
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/model.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/model.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/model.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/model.py Sat Nov 15 01:14:46 2014 @@ -1,16 +1,28 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. from __future__ import with_statement from datetime import datetime -import os.path import shutil from StringIO import StringIO import tempfile import unittest +import trac.tests.compat from trac.attachment import Attachment from trac.core import * +from trac.resource import Resource from trac.test import EnvironmentStub from trac.tests.resource import TestResourceChangeListener from trac.util.datefmt import utc, to_utimestamp @@ -48,8 +60,7 @@ class WikiPageTestCase(unittest.TestCase def setUp(self): self.env = EnvironmentStub() - self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv') - os.mkdir(self.env.path) + self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-') def tearDown(self): shutil.rmtree(self.env.path) @@ -57,14 +68,14 @@ class WikiPageTestCase(unittest.TestCase def test_new_page(self): page = WikiPage(self.env) - self.assertEqual(False, page.exists) - self.assertEqual(None, page.name) + self.assertFalse(page.exists) + self.assertIsNone(page.name) self.assertEqual(0, page.version) self.assertEqual('', page.text) self.assertEqual(0, page.readonly) self.assertEqual('', page.author) self.assertEqual('', page.comment) - self.assertEqual(None, page.time) + self.assertIsNone(page.time) def test_existing_page(self): t = datetime(2001, 1, 1, 1, 1, 1, 0, utc) @@ -74,10 +85,10 @@ class WikiPageTestCase(unittest.TestCase 'Testing', 0)) page = WikiPage(self.env, 'TestPage') - self.assertEqual(True, page.exists) + self.assertTrue(page.exists) self.assertEqual('TestPage', page.name) self.assertEqual(1, page.version) - self.assertEqual(None, page.resource.version) # FIXME: Intentional? + self.assertIsNone(page.resource.version) # FIXME: Intentional? self.assertEqual('Bla bla', page.text) self.assertEqual(0, page.readonly) self.assertEqual('joe', page.author) @@ -90,6 +101,11 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage', 1) self.assertEqual(1, page.resource.version) + self.assertEqual(1, page.version) + + resource = Resource('wiki', 'TestPage') + page = WikiPage(self.env, resource, 1) + self.assertEqual(1, page.version) def test_create_page(self): page = WikiPage(self.env) @@ -98,7 +114,7 @@ class WikiPageTestCase(unittest.TestCase t = datetime(2001, 1, 1, 1, 1, 1, 0, utc) page.save('joe', 'Testing', '::1', t) - self.assertEqual(True, page.exists) + self.assertTrue(page.exists) self.assertEqual(1, page.version) self.assertEqual(1, page.resource.version) self.assertEqual(0, page.readonly) @@ -165,7 +181,7 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage') page.delete() - self.assertEqual(False, page.exists) + self.assertFalse(page.exists) self.assertEqual([], self.env.db_query(""" SELECT version, time, author, ipnr, text, comment, readonly @@ -184,7 +200,7 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage') page.delete(version=2) - self.assertEqual(True, page.exists) + self.assertTrue(page.exists) self.assertEqual( [(1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0)], self.env.db_query(""" @@ -203,7 +219,7 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage') page.delete(version=1) - self.assertEqual(False, page.exists) + self.assertFalse(page.exists) self.assertEqual([], self.env.db_query(""" SELECT version, time, author, ipnr, text, comment, readonly @@ -224,6 +240,7 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage') page.rename('PageRenamed') self.assertEqual('PageRenamed', page.name) + self.assertEqual('PageRenamed', page.resource.id) self.assertEqual([data], self.env.db_query(""" SELECT version, time, author, ipnr, text, comment, readonly @@ -236,8 +253,7 @@ class WikiPageTestCase(unittest.TestCase Attachment.delete_all(self.env, 'wiki', 'PageRenamed') old_page = WikiPage(self.env, 'TestPage') - self.assertEqual(False, old_page.exists) - + self.assertFalse(old_page.exists) self.assertEqual([], self.env.db_query(""" SELECT version, time, author, ipnr, text, comment, readonly @@ -267,6 +283,24 @@ class WikiPageTestCase(unittest.TestCase page = WikiPage(self.env, 'TestPage') self.assertRaises(TracError, page.rename, name) + def test_invalid_version(self): + data = (1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0) + self.env.db_transaction( + "INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)", + ('TestPage',) + data) + + self.assertRaises(ValueError, WikiPage, self.env, + 'TestPage', '1abc') + + resource = Resource('wiki', 'TestPage') + self.assertRaises(ValueError, WikiPage, self.env, + resource, '1abc') + + resource = Resource('wiki', 'TestPage', '1abc') + page = WikiPage(self.env, resource) + self.assertEqual(1, page.version) + + class WikiResourceChangeListenerTestCase(unittest.TestCase): INITIAL_NAME = "Wiki page 1" INITIAL_TEXT = "some text" @@ -332,12 +366,13 @@ class WikiResourceChangeListenerTestCase self.wiki_name = resource.name self.wiki_text = resource.text + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(WikiPageTestCase, 'test')) - suite.addTest(unittest.makeSuite( - WikiResourceChangeListenerTestCase, 'test')) + suite.addTest(unittest.makeSuite(WikiPageTestCase)) + suite.addTest(unittest.makeSuite(WikiResourceChangeListenerTestCase)) return suite + if __name__ == '__main__': unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wiki-tests.txt URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wiki-tests.txt?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wiki-tests.txt (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wiki-tests.txt Sat Nov 15 01:14:46 2014 @@ -316,6 +316,13 @@ nolink:"<blink>" nolink:"<blink>" </p> ------------------------------ +============================== Bracketed links +See <http://en.wikipedia.org/wiki/Mornington_Crescent_(game)> +------------------------------ +<p> +See <<a class="ext-link" href="http://en.wikipedia.org/wiki/Mornington_Crescent_(game)"><span class="icon"></span>http://en.wikipedia.org/wiki/Mornington_Crescent_(game)</a>> +</p> +------------------------------ ============================================================ Other Links @@ -1177,6 +1184,35 @@ Inline comment </p> ------------------------------ Inline comment +============================== Exception with ascii bytes +[[ValueErrorWithUtf8(error)]] +------------------------------ +<p> +<div class="system-message"><strong>Error: Macro ValueErrorWithUtf8(error) failed</strong><pre>error</pre></div> +</p> +------------------------------ +============================== Exception with utf-8 bytes +{{{#!ValueErrorWithUtf8 +Ãrrör +}}} +[[ValueErrorWithUtf8(érrör)]] +------------------------------ +<div class="system-message"><strong>Error: Processor ValueErrorWithUtf8 failed</strong><pre>Ãrrör +</pre></div><p> +<div class="system-message"><strong>Error: Macro ValueErrorWithUtf8(érrör) failed</strong><pre>érrör</pre></div> +</p> +------------------------------ +============================== TracError with unicode +{{{#!TracErrorWithUnicode +Ãrrör +}}} +[[TracErrorWithUnicode(érrör)]] +------------------------------ +<div class="system-message"><strong>Error: Processor TracErrorWithUnicode failed</strong><pre>Ãrrör +</pre></div><p> +<div class="system-message"><strong>Error: Macro TracErrorWithUnicode(érrör) failed</strong><pre>érrör</pre></div> +</p> +------------------------------ ============================================================ Headings @@ -2573,3 +2609,96 @@ c ------------------------------ [â¦] > c +============================== List immediately followed by binary inline markup, #11009 +*** +* list +*** +------------------------------ +<p> +<strong>* +</strong></p> +<ul><li>list +</li></ul><p> +<strong>* +</strong></p> +------------------------------ +============================== List immediately followed by binary inline markup 1, #11373 + 1. normal in list +''italic +in paragraph + + 1. normal in list +//italic +in paragraph +------------------------------ +<ol><li>normal in list +</li></ol><p> +<em>italic +in paragraph +</em></p> +<ol><li>normal in list +</li></ol><p> +<em>italic +in paragraph +</em></p> +------------------------------ +============================== List immediately followed by binary inline markup 2, #11373 + 1. //italic in list +''italic +in paragraph + + 1. ''italic in list +//italic +in paragraph + +1. ''italic in list +''italic +in paragraph + +1. //italic in list +//italic +in paragraph +------------------------------ +<ol><li><em>italic in list +</em></li></ol><p> +<em>italic +in paragraph +</em></p> +<ol><li><em>italic in list +</em></li></ol><p> +<em>italic +in paragraph +</em></p> +<ol><li><em>italic in list +</em></li></ol><p> +<em>italic +in paragraph +</em></p> +<ol><li><em>italic in list +</em></li></ol><p> +<em>italic +in paragraph +</em></p> +------------------------------ +============================== List immediately followed by binary inline markup 3, #11373 + 1. ''italic in list +'''''bolditalic +in paragraph''''' +------------------------------ +<ol><li><em>italic in list +</em></li></ol><p> +<strong><em>bolditalic +in paragraph</em></strong> +</p> +------------------------------ +============================== List immediately followed by binary inline markup 4, #11373 + 1. '''bold in list +'''''bolditalic +in paragraph''''' +------------------------------ +<ol><li><strong>bold in list +</strong></li></ol><p> +<strong><em>bolditalic +in paragraph</em></strong> +</p> +------------------------------ Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wikisyntax.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wikisyntax.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wikisyntax.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/tests/wikisyntax.py Sat Nov 15 01:14:46 2014 @@ -1,4 +1,15 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. from datetime import datetime import unittest @@ -7,6 +18,7 @@ from trac.util.datefmt import utc from trac.wiki.model import WikiPage from trac.wiki.tests import formatter + TEST_CASES = u""" ============================== wiki: link resolver wiki:TestPage @@ -711,6 +723,8 @@ nolink http://noweb w.text = '--' w.save('joe', 'other third level of hierarchy', '::1', now) + tc.env.db_transaction("INSERT INTO ticket (id) VALUES ('123')") + def wiki_teardown(tc): tc.env.reset_db() Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/web_ui.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/web_ui.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/web_ui.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/wiki/web_ui.py Sat Nov 15 01:14:46 2014 @@ -37,10 +37,10 @@ from trac.util.text import shorten_line from trac.util.translation import _, tag_ from trac.versioncontrol.diff import get_diff_options, diff_blocks from trac.web.api import IRequestHandler -from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider, - add_ctxtnav, add_link, add_notice, add_script, - add_stylesheet, add_warning, prevnext_nav, - web_context) +from trac.web.chrome import (Chrome, INavigationContributor, + ITemplateProvider, add_ctxtnav, add_link, + add_notice, add_script, add_stylesheet, + add_warning, prevnext_nav, web_context) from trac.wiki.api import IWikiPageManipulator, WikiSystem, validate_page_name from trac.wiki.formatter import format_to, OneLinerFormatter from trac.wiki.model import WikiPage @@ -55,14 +55,14 @@ class InvalidWikiPage(TracError): class WikiModule(Component): - implements(IContentConverter, INavigationContributor, IPermissionRequestor, - IRequestHandler, ITimelineEventProvider, ISearchSource, - ITemplateProvider) + implements(IContentConverter, INavigationContributor, + IPermissionRequestor, IRequestHandler, ITimelineEventProvider, + ISearchSource, ITemplateProvider) page_manipulators = ExtensionPoint(IWikiPageManipulator) max_size = IntOption('wiki', 'max_size', 262144, - """Maximum allowed wiki page size in bytes. (''since 0.11.2'')""") + """Maximum allowed wiki page size in characters. (''since 0.11.2'')""") PAGE_TEMPLATES_PREFIX = 'PageTemplates/' DEFAULT_PAGE_TEMPLATE = 'DefaultPage' @@ -70,11 +70,11 @@ class WikiModule(Component): # IContentConverter methods def get_supported_conversions(self): - yield ('txt', _('Plain Text'), 'txt', 'text/x-trac-wiki', 'text/plain', - 9) + yield ('txt', _("Plain Text"), 'txt', 'text/x-trac-wiki', + 'text/plain', 9) def convert_content(self, req, mimetype, content, key): - return (content, 'text/plain;charset=utf-8') + return content, 'text/plain;charset=utf-8' # INavigationContributor methods @@ -82,11 +82,12 @@ class WikiModule(Component): return 'wiki' def get_navigation_items(self, req): - if 'WIKI_VIEW' in req.perm('wiki'): + if 'WIKI_VIEW' in req.perm('wiki', 'WikiStart'): yield ('mainnav', 'wiki', - tag.a(_('Wiki'), href=req.href.wiki(), accesskey=1)) + tag.a(_("Wiki"), href=req.href.wiki(), accesskey=1)) + if 'WIKI_VIEW' in req.perm('wiki', 'TracGuide'): yield ('metanav', 'help', - tag.a(_('Help/Guide'), href=req.href.wiki('TracGuide'), + tag.a(_("Help/Guide"), href=req.href.wiki('TracGuide'), accesskey=6)) # IPermissionRequestor methods @@ -119,10 +120,17 @@ class WikiModule(Component): raise TracError(_("Invalid Wiki page name '%(name)s'", name=pagename)) + if version is not None: + try: + version = int(version) + except (ValueError, TypeError): + raise ResourceNotFound( + _('No version "%(num)s" for Wiki page "%(name)s"', + num=version, name=pagename)) + page = WikiPage(self.env, pagename) versioned_page = WikiPage(self.env, pagename, version=version) - req.perm(page.resource).require('WIKI_VIEW') req.perm(versioned_page.resource).require('WIKI_VIEW') if version and versioned_page.version != int(version): @@ -147,7 +155,8 @@ class WikiModule(Component): if action == 'edit' and not has_collision and valid: return self._do_save(req, versioned_page) else: - return self._render_editor(req, page, action, has_collision) + return self._render_editor(req, page, action, + has_collision) elif action == 'delete': self._do_delete(req, versioned_page) elif action == 'rename': @@ -192,8 +201,8 @@ class WikiModule(Component): # Validate page size if len(req.args.get('text', '')) > self.max_size: - add_warning(req, _('The wiki page is too long (must be less ' - 'than %(num)s characters)', + add_warning(req, _("The wiki page is too long (must be less " + "than %(num)s characters)", num=self.max_size)) valid = False @@ -202,12 +211,12 @@ class WikiModule(Component): for field, message in manipulator.validate_wiki_page(req, page): valid = False if field: - add_warning(req, _("The Wiki page field '%(field)s' is " - "invalid: %(message)s", - field=field, message=message)) + add_warning(req, tag_("The Wiki page field '%(field)s'" + " is invalid: %(message)s", + field=field, message=message)) else: - add_warning(req, _("Invalid Wiki page: %(message)s", - message=message)) + add_warning(req, tag_("Invalid Wiki page: %(message)s", + message=message)) return valid def _page_data(self, req, page, action=''): @@ -233,9 +242,10 @@ class WikiModule(Component): def version_info(v, last=0): return {'path': get_resource_name(self.env, page.resource), # TRANSLATOR: wiki page - 'rev': v or _('currently edited'), + 'rev': v or _("currently edited"), 'shortrev': v or last + 1, - 'href': req.href.wiki(page.name, version=v) if v else None} + 'href': req.href.wiki(page.name, version=v) + if v else None} changes = [{'diffs': diffs, 'props': [], 'new': version_info(new_version, old_version), 'old': version_info(old_version)}] @@ -271,12 +281,12 @@ class WikiModule(Component): req.redirect(req.href.wiki()) else: if version and old_version and version > old_version + 1: - add_notice(req, _('The versions %(from_)d to %(to)d of the ' - 'page %(name)s have been deleted.', - from_=old_version + 1, to=version, name=page.name)) + add_notice(req, _("The versions %(from_)d to %(to)d of the " + "page %(name)s have been deleted.", + from_=old_version + 1, to=version, name=page.name)) else: - add_notice(req, _('The version %(version)d of the page ' - '%(name)s has been deleted.', + add_notice(req, _("The version %(version)d of the page " + "%(name)s has been deleted.", version=version, name=page.name)) req.redirect(req.href.wiki(page.name)) @@ -319,6 +329,14 @@ class WikiModule(Component): new_name, old_version, old_name, new_name) redirection.save(author, comment, req.remote_addr) + add_notice(req, _("The page %(old_name)s has been renamed to " + "%(new_name)s.", old_name=old_name, + new_name=new_name)) + if redirect: + add_notice(req, _("The page %(old_name)s has been recreated " + "with a redirect to %(new_name)s.", + old_name=old_name, new_name=new_name)) + req.redirect(req.href.wiki(old_name if redirect else new_name)) def _do_save(self, req, page): @@ -394,8 +412,8 @@ class WikiModule(Component): def _render_diff(self, req, page): if not page.exists: - raise TracError(_('Version %(num)s of page "%(name)s" does not ' - 'exist', + raise TracError(_("Version %(num)s of page \"%(name)s\" does not " + "exist", num=req.args.get('version'), name=page.name)) old_version = req.args.get('old_version') @@ -446,13 +464,13 @@ class WikiModule(Component): if prev_version: add_link(req, 'prev', req.href.wiki(page.name, action='diff', version=prev_version), - _('Version %(num)s', num=prev_version)) + _("Version %(num)s", num=prev_version)) add_link(req, 'up', req.href.wiki(page.name, action='history'), _('Page history')) if next_version: add_link(req, 'next', req.href.wiki(page.name, action='diff', version=next_version), - _('Version %(num)s', num=next_version)) + _("Version %(num)s", num=next_version)) data = self._page_data(req, page, 'diff') data.update({ @@ -465,8 +483,8 @@ class WikiModule(Component): 'changes': changes, 'diff': diff_data, }) - prevnext_nav(req, _('Previous Change'), _('Next Change'), - _('Wiki History')) + prevnext_nav(req, _("Previous Change"), _("Next Change"), + _("Wiki History")) return 'wiki_diff.html', data, None def _render_editor(self, req, page, action='edit', has_collision=False): @@ -479,6 +497,8 @@ class WikiModule(Component): if page.readonly: req.perm(page.resource).require('WIKI_ADMIN') + elif not page.exists: + req.perm(page.resource).require('WIKI_CREATE') else: req.perm(page.resource).require('WIKI_MODIFY') original_text = page.text @@ -489,7 +509,7 @@ class WikiModule(Component): template = self.PAGE_TEMPLATES_PREFIX + req.args.get('template') template_page = WikiPage(self.env, template) if template_page and template_page.exists and \ - 'WIKI_VIEW' in req.perm(template_page.resource): + 'WIKI_VIEW' in req.perm(template_page.resource): page.text = template_page.text elif 'version' in req.args: old_page = WikiPage(self.env, page.name, @@ -528,9 +548,11 @@ class WikiModule(Component): data = self._page_data(req, page, action) context = web_context(req, page.resource) data.update({ + 'context': context, 'author': author, 'comment': comment, - 'edit_rows': editrows, 'sidebyside': sidebyside, + 'edit_rows': editrows, + 'sidebyside': sidebyside, 'scroll_bar_pos': req.args.get('scroll_bar_pos', ''), 'diff': None, 'attachments': AttachmentModule(self.env).attachment_data(context), @@ -574,7 +596,7 @@ class WikiModule(Component): }) data.update({'history': history, 'resource': page.resource}) add_ctxtnav(req, _("Back to %(wikipage)s", wikipage=page.name), - req.href.wiki(page.name)) + req.href.wiki(page.name)) return 'history_view.html', data, None def _render_view(self, req, page): @@ -582,13 +604,10 @@ class WikiModule(Component): # Add registered converters if page.exists: - for conversion in Mimeview(self.env).get_supported_conversions( - 'text/x-trac-wiki'): + for conversion in Mimeview(self.env) \ + .get_supported_conversions('text/x-trac-wiki'): conversion_href = req.href.wiki(page.name, version=version, format=conversion[0]) - # or... - conversion_href = get_resource_url(self.env, page.resource, - req.href, format=conversion[0]) add_link(req, 'alternate', conversion_href, conversion[1], conversion[3]) @@ -601,7 +620,7 @@ class WikiModule(Component): higher, related = [], [] if not page.exists: if 'WIKI_CREATE' not in req.perm(page.resource): - raise ResourceNotFound(_('Page %(name)s not found', + raise ResourceNotFound(_("Page %(name)s not found", name=page.name)) formatter = OneLinerFormatter(self.env, context) if '/' in page.name: @@ -610,7 +629,7 @@ class WikiModule(Component): name = '/'.join(parts[:i] + [parts[-1]]) if not ws.has_page(name): higher.append(ws._format_link(formatter, 'wiki', - '/' + name, name, False)) + '/' + name, name, False)) else: name = page.name name = name.lower() @@ -623,7 +642,6 @@ class WikiModule(Component): for each in related] latest_page = WikiPage(self.env, page.name, version=None) - req.perm(latest_page.resource).require('WIKI_VIEW') prev_version = next_version = None if version: @@ -650,12 +668,12 @@ class WikiModule(Component): if prev_version: add_link(req, 'prev', req.href.wiki(page.name, version=prev_version), - _('Version %(num)s', num=prev_version)) + _("Version %(num)s", num=prev_version)) parent = None if version: add_link(req, 'up', req.href.wiki(page.name, version=None), - _('View latest version')) + _("View latest version")) elif '/' in page.name: parent = page.name[:page.name.rindex('/')] add_link(req, 'up', req.href.wiki(parent, version=None), @@ -668,8 +686,8 @@ class WikiModule(Component): # Add ctxtnav entries if version: - prevnext_nav(req, _('Previous Version'), _('Next Version'), - _('View Latest Version')) + prevnext_nav(req, _("Previous Version"), _("Next Version"), + _("View Latest Version")) else: if parent: add_ctxtnav(req, _('Up'), req.href.wiki(parent)) @@ -697,10 +715,10 @@ class WikiModule(Component): def _wiki_ctxtnav(self, req, page): """Add the normal wiki ctxtnav entries.""" - add_ctxtnav(req, _('Start Page'), req.href.wiki('WikiStart')) - add_ctxtnav(req, _('Index'), req.href.wiki('TitleIndex')) + add_ctxtnav(req, _("Start Page"), req.href.wiki('WikiStart')) + add_ctxtnav(req, _("Index"), req.href.wiki('TitleIndex')) if page.exists: - add_ctxtnav(req, _('History'), req.href.wiki(page.name, + add_ctxtnav(req, _("History"), req.href.wiki(page.name, action='history')) # ITimelineEventProvider methods @@ -724,7 +742,7 @@ class WikiModule(Component): # Attachments for event in AttachmentModule(self.env).get_timeline_events( - req, wiki_realm, start, stop): + req, wiki_realm, start, stop): yield event def render_timeline_event(self, context, field, event): @@ -734,9 +752,9 @@ class WikiModule(Component): elif field == 'title': name = tag.em(get_resource_name(self.env, wiki_page)) if wiki_page.version > 1: - return tag_('%(page)s edited', page=name) + return tag_("%(page)s edited", page=name) else: - return tag_('%(page)s created', page=name) + return tag_("%(page)s created", page=name) elif field == 'description': markup = format_to(self.env, None, context.child(resource=wiki_page), comment) @@ -744,7 +762,7 @@ class WikiModule(Component): diff_href = context.href.wiki( wiki_page.id, version=wiki_page.version, action='diff') markup = tag(markup, - ' (', tag.a(_('diff'), href=diff_href), ')') + " (", tag.a(_("diff"), href=diff_href), ")") return markup # ISearchSource methods @@ -775,5 +793,5 @@ class WikiModule(Component): # Attachments for result in AttachmentModule(self.env).get_search_results( - req, wiki_realm, terms): + req, wiki_realm, terms): yield result Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/enscript.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/enscript.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/enscript.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/enscript.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2004-2009 Edgewall Software +# Copyright (C) 2004-2013 Edgewall Software # Copyright (C) 2004 Daniel Lundin <dan...@edgewall.com> # Copyright (C) 2005 Christopher Lenz <cml...@gmx.de> # All rights reserved. Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/php.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/php.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/php.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/php.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2005-2009 Edgewall Software +# Copyright (C) 2005-2013 Edgewall Software # Copyright (C) 2005 Christian Boos <cb...@bct-technology.com> # Copyright (C) 2005 Christopher Lenz <cml...@gmx.de> # All rights reserved. Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/silvercity.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/silvercity.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/silvercity.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/silvercity.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2004-2009 Edgewall Software +# Copyright (C) 2004-2013 Edgewall Software # Copyright (C) 2004 Daniel Lundin <dan...@edgewall.com> # All rights reserved. # Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009 Edgewall Software +# Copyright (C) 2009-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/php.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/php.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/php.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/mimeview/tests/php.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C)2006-2009 Edgewall Software +# Copyright (C) 2006-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -140,8 +140,8 @@ def suite(): suite = unittest.TestSuite() php = locate("php") if php: - suite.addTest(unittest.makeSuite(PhpDeuglifierTestCase, 'test')) - suite.addTest(unittest.makeSuite(PhpRendererTestCase, 'test')) + suite.addTest(unittest.makeSuite(PhpDeuglifierTestCase)) + suite.addTest(unittest.makeSuite(PhpRendererTestCase)) else: print("SKIP: tracopt/mimeview/tests/php.py (php cli binary, 'php', " "not found)") Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/authz_policy.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/authz_policy.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/authz_policy.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/authz_policy.py Sat Nov 15 01:14:46 2014 @@ -14,17 +14,19 @@ # # Author: Alec Thomas <a...@swapoff.org> -from fnmatch import fnmatch +from fnmatch import fnmatchcase from itertools import groupby import os from trac.core import * -from trac.config import Option +from trac.config import ConfigurationError, Option from trac.perm import PermissionSystem, IPermissionPolicy +from trac.util import lazy +from trac.util.text import to_unicode ConfigObj = None try: - from configobj import ConfigObj + from configobj import ConfigObj, ConfigObjError except ImportError: pass @@ -138,12 +140,8 @@ class AuthzPolicy(Component): # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): - if ConfigObj is None: - self.log.error('configobj package not found') - return None - - if self.authz_file and not self.authz_mtime or \ - os.path.getmtime(self.get_authz_file()) > self.authz_mtime: + if not self.authz_mtime or \ + os.path.getmtime(self.get_authz_file) > self.authz_mtime: self.parse_authz() resource_key = self.normalise_resource(resource) self.log.debug('Checking %s on %s', action, resource_key) @@ -166,19 +164,42 @@ class AuthzPolicy(Component): # Internal methods + @lazy def get_authz_file(self): - f = self.authz_file - return f if os.path.isabs(f) else os.path.join(self.env.path, f) + if not self.authz_file: + self.log.error('The `[authz_policy] authz_file` configuration ' + 'option in trac.ini is empty or not defined.') + raise ConfigurationError() + + authz_file = self.authz_file if os.path.isabs(self.authz_file) \ + else os.path.join(self.env.path, + self.authz_file) + try: + os.stat(authz_file) + except OSError, e: + self.log.error("Error parsing authz permission policy file: %s", + to_unicode(e)) + raise ConfigurationError() + return authz_file def parse_authz(self): + if ConfigObj is None: + self.log.error('ConfigObj package not found.') + raise ConfigurationError() self.log.debug('Parsing authz security policy %s', - self.get_authz_file()) - self.authz = ConfigObj(self.get_authz_file(), encoding='utf8') + self.get_authz_file) + try: + self.authz = ConfigObj(self.get_authz_file, encoding='utf8', + raise_errors=True) + except ConfigObjError, e: + self.log.error("Error parsing authz permission policy file: %s", + to_unicode(e)) + raise ConfigurationError() groups = {} for group, users in self.authz.get('groups', {}).iteritems(): if isinstance(users, basestring): users = [users] - groups[group] = users + groups[group] = map(to_unicode, users) self.groups_by_user = {} @@ -192,31 +213,32 @@ class AuthzPolicy(Component): for group, users in groups.iteritems(): add_items('@' + group, users) - self.authz_mtime = os.path.getmtime(self.get_authz_file()) + self.authz_mtime = os.path.getmtime(self.get_authz_file) def normalise_resource(self, resource): + def to_descriptor(resource): + id = resource.id + return '%s:%s@%s' % (resource.realm or '*', + id if id is not None else '*', + resource.version or '*') + def flatten(resource): if not resource: return ['*:*@*'] - if not (resource.realm or resource.id): - return ['%s:%s@%s' % (resource.realm or '*', - resource.id or '*', - resource.version or '*')] + descriptor = to_descriptor(resource) + if not resource.realm and resource.id is None: + return [descriptor] # XXX Due to the mixed functionality in resource we can end up with # ticket, ticket:1, ticket:1@10. This code naively collapses all # subsets of the parent resource into one. eg. ticket:1@10 parent = resource.parent - while parent and (resource.realm == parent.realm or - (resource.realm == parent.realm and - resource.id == parent.id)): + while parent and resource.realm == parent.realm: parent = parent.parent if parent: - parent = flatten(parent) + return flatten(parent) + [descriptor] else: - parent = [] - return parent + ['%s:%s@%s' % (resource.realm or '*', - resource.id or '*', - resource.version or '*')] + return [descriptor] + return '/'.join(flatten(resource)) def authz_permissions(self, resource_key, username): @@ -227,14 +249,15 @@ class AuthzPolicy(Component): else: valid_users = ['*', 'anonymous'] for resource_section in [a for a in self.authz.sections - if a != 'groups']: - resource_glob = resource_section + if a != 'groups']: + resource_glob = to_unicode(resource_section) if '@' not in resource_glob: resource_glob += '@*' - if fnmatch(resource_key, resource_glob): + if fnmatchcase(resource_key, resource_glob): section = self.authz[resource_section] for who, permissions in section.iteritems(): + who = to_unicode(who) if who in valid_users or \ who in self.groups_by_user.get(username, []): self.log.debug('%s matched section %s for user %s', Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/config_perm_provider.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/config_perm_provider.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/config_perm_provider.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/config_perm_provider.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009 Edgewall Software +# Copyright (C) 2009-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -17,7 +17,10 @@ from trac.perm import IPermissionRequest class ExtraPermissionsProvider(Component): - """Extra permission provider.""" + """Define arbitrary permissions. + + Documentation can be found on the [wiki:TracIni#extra-permissions-section] + page after enabling the component.""" implements(IPermissionRequestor) @@ -31,17 +34,21 @@ class ExtraPermissionsProvider(Component and a comma-separated list of permissions. For example: {{{ [extra-permissions] - extra_admin = extra_view, extra_modify, extra_delete + EXTRA_ADMIN = EXTRA_VIEW, EXTRA_MODIFY, EXTRA_DELETE }}} This entry will define three new permissions `EXTRA_VIEW`, `EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions `EXTRA_ADMIN` that grants all three permissions. + The permissions are created in upper-case characters regardless of + the casing of the definitions in `trac.ini`. For example, the + definition `extra_view` would create the permission `EXTRA_VIEW`. + If you don't want a meta-permission, start the meta-name with an underscore (`_`): {{{ [extra-permissions] - _perms = extra_view, extra_modify + _perms = EXTRA_VIEW, EXTRA_MODIFY }}} """) Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012 Edgewall Software +# Copyright (C) 2012-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/authz_policy.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/authz_policy.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/authz_policy.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/perm/tests/authz_policy.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012 Edgewall Software +# Copyright (C) 2012-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -19,9 +19,13 @@ try: except ImportError: ConfigObj = None +import trac.tests.compat +from trac.config import ConfigurationError +from trac.perm import PermissionCache from trac.resource import Resource -from trac.test import EnvironmentStub +from trac.test import EnvironmentStub, Mock from trac.util import create_file +from trac.versioncontrol.api import Repository from tracopt.perm.authz_policy import AuthzPolicy @@ -45,8 +49,33 @@ administrators = éat änon = @administrators = WIKI_VIEW * = + +# Tickets +[ticket:43] +änon = TICKET_VIEW +@administrators = +* = + +[ticket:*] +änon = +@administrators = TICKET_VIEW +* = + +# Default repository +[repository:@*] +änon = +@administrators = BROWSER_VIEW, FILE_VIEW +* = + +# Non-default repository +[repository:bláh@*] +änon = BROWSER_VIEW, FILE_VIEW +@administrators = BROWSER_VIEW, FILE_VIEW +* = """) - self.env = EnvironmentStub(enable=[AuthzPolicy]) + self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir) + self.env.config.set('trac', 'permission_policies', + 'AuthzPolicy, DefaultPermissionPolicy') self.env.config.set('authz_policy', 'authz_file', self.authz_file) self.authz_policy = AuthzPolicy(self.env) @@ -57,32 +86,168 @@ administrators = éat def check_permission(self, action, user, resource, perm): return self.authz_policy.check_permission(action, user, resource, perm) + def get_repository(self, reponame): + params = {'id': 1, 'name': reponame} + return Mock(Repository, 'mock', params, self.env.log) + + def get_perm(self, username, *args): + perm = PermissionCache(self.env, username) + if args: + return perm(*args) + return perm + def test_unicode_username(self): resource = Resource('wiki', 'WikiStart') + + perm = self.get_perm('anonymous') self.assertEqual( False, - self.check_permission('WIKI_VIEW', 'anonymous', resource, None)) + self.check_permission('WIKI_VIEW', 'anonymous', resource, perm)) + self.assertNotIn('WIKI_VIEW', perm) + self.assertNotIn('WIKI_VIEW', perm(resource)) + + perm = self.get_perm(u'änon') self.assertEqual( True, - self.check_permission('WIKI_VIEW', u'änon', resource, None)) + self.check_permission('WIKI_VIEW', u'änon', resource, perm)) + self.assertNotIn('WIKI_VIEW', perm) + self.assertIn('WIKI_VIEW', perm(resource)) def test_unicode_resource_name(self): resource = Resource('wiki', u'résumé') + + perm = self.get_perm('anonymous') self.assertEqual( False, - self.check_permission('WIKI_VIEW', 'anonymous', resource, None)) + self.check_permission('WIKI_VIEW', 'anonymous', resource, perm)) + self.assertNotIn('WIKI_VIEW', perm) + self.assertNotIn('WIKI_VIEW', perm(resource)) + + perm = self.get_perm(u'änon') self.assertEqual( False, - self.check_permission('WIKI_VIEW', u'änon', resource, None)) + self.check_permission('WIKI_VIEW', u'änon', resource, perm)) + self.assertNotIn('WIKI_VIEW', perm) + self.assertNotIn('WIKI_VIEW', perm(resource)) + + perm = self.get_perm(u'éat') self.assertEqual( True, - self.check_permission('WIKI_VIEW', u'éat', resource, None)) + self.check_permission('WIKI_VIEW', u'éat', resource, perm)) + self.assertNotIn('WIKI_VIEW', perm) + self.assertIn('WIKI_VIEW', perm(resource)) + + def test_resource_without_id(self): + perm = self.get_perm('anonymous') + self.assertNotIn('TICKET_VIEW', perm) + self.assertNotIn('TICKET_VIEW', perm('ticket')) + self.assertNotIn('TICKET_VIEW', perm('ticket', 42)) + self.assertNotIn('TICKET_VIEW', perm('ticket', 43)) + + perm = self.get_perm(u'änon') + self.assertNotIn('TICKET_VIEW', perm) + self.assertNotIn('TICKET_VIEW', perm('ticket')) + self.assertNotIn('TICKET_VIEW', perm('ticket', 42)) + self.assertIn('TICKET_VIEW', perm('ticket', 43)) + + perm = self.get_perm(u'éat') + self.assertNotIn('TICKET_VIEW', perm) + self.assertIn('TICKET_VIEW', perm('ticket')) + self.assertIn('TICKET_VIEW', perm('ticket', 42)) + self.assertNotIn('TICKET_VIEW', perm('ticket', 43)) + + def test_default_repository(self): + repos = self.get_repository('') + self.assertEqual(False, repos.is_viewable(self.get_perm('anonymous'))) + self.assertEqual(False, repos.is_viewable(self.get_perm(u'änon'))) + self.assertEqual(True, repos.is_viewable(self.get_perm(u'éat'))) + + def test_non_default_repository(self): + repos = self.get_repository(u'bláh') + self.assertEqual(False, repos.is_viewable(self.get_perm('anonymous'))) + self.assertEqual(True, repos.is_viewable(self.get_perm(u'änon'))) + self.assertEqual(True, repos.is_viewable(self.get_perm(u'éat'))) + + def test_case_sensitive_resource(self): + resource = Resource('WIKI', 'wikistart') + self.assertEqual( + None, + self.check_permission('WIKI_VIEW', 'anonymous', resource, None)) + self.assertEqual( + None, + self.check_permission('WIKI_VIEW', u'änon', resource, None)) + + def test_get_authz_file(self): + """get_authz_file should resolve a relative path and lazily compute. + """ + authz_file = self.authz_policy.get_authz_file + self.assertEqual(os.path.join(self.env.path, 'trac-authz-policy'), + authz_file) + self.assertIs(authz_file, self.authz_policy.get_authz_file) + + def test_get_authz_file_notfound_raises(self): + """ConfigurationError exception should be raised if file not found.""" + authz_file = os.path.join(self.env.path, 'some-nonexistent-file') + self.env.config.set('authz_policy', 'authz_file', authz_file) + self.assertRaises(ConfigurationError, getattr, self.authz_policy, + 'get_authz_file') + + def test_get_authz_file_notdefined_raises(self): + """ConfigurationError exception should be raised if the option + `[authz_policy] authz_file` is not specified in trac.ini.""" + self.env.config.remove('authz_policy', 'authz_file') + self.assertRaises(ConfigurationError, getattr, self.authz_policy, + 'get_authz_file') + + def test_get_authz_file_empty_raises(self): + """ConfigurationError exception should be raised if the option + `[authz_policy] authz_file` is empty.""" + self.env.config.set('authz_policy', 'authz_file', '') + self.assertRaises(ConfigurationError, getattr, self.authz_policy, + 'get_authz_file') + + def test_parse_authz_empty(self): + """Allow the file to be empty.""" + create_file(self.authz_file, '') + self.authz_policy.parse_authz() + self.assertFalse(self.authz_policy.authz) + + def test_parse_authz_no_settings(self): + """Allow the file to have no settings.""" + create_file(self.authz_file, """\ +# [wiki:WikiStart] +# änon = WIKI_VIEW +# * = +""") + self.authz_policy.parse_authz() + self.assertFalse(self.authz_policy.authz) + + def test_parse_authz_malformed_raises(self): + """ConfigurationError should be raised if the file is malformed.""" + create_file(self.authz_file, """\ +wiki:WikiStart] +änon = WIKI_VIEW +* = +""") + self.assertRaises(ConfigurationError, self.authz_policy.parse_authz) + + def test_parse_authz_duplicated_sections_raises(self): + """ConfigurationError should be raised if the file has duplicate + sections.""" + create_file(self.authz_file, """\ +[wiki:WikiStart] +änon = WIKI_VIEW + +[wiki:WikiStart] +änon = WIKI_VIEW +""") + self.assertRaises(ConfigurationError, self.authz_policy.parse_authz) def suite(): suite = unittest.TestSuite() if ConfigObj: - suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test')) + suite.addTest(unittest.makeSuite(AuthzPolicyTestCase)) else: print "SKIP: tracopt/perm/tests/authz_policy.py (no configobj " + \ "installed)" Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/commit_updater.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/commit_updater.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/commit_updater.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/commit_updater.py Sat Nov 15 01:14:46 2014 @@ -50,7 +50,7 @@ from trac.ticket import Ticket from trac.ticket.notification import TicketNotifyEmail from trac.util.datefmt import utc from trac.util.text import exception_to_unicode -from trac.util.translation import cleandoc_ +from trac.util.translation import _, cleandoc_ from trac.versioncontrol import IRepositoryChangeListener, RepositoryManager from trac.versioncontrol.web_ui.changeset import ChangesetModule from trac.wiki.formatter import format_to_html @@ -182,10 +182,11 @@ class CommitTicketUpdater(Component): def _parse_message(self, message): """Parse the commit message and return the ticket references.""" - cmd_groups = self.command_re.findall(message) + cmd_groups = self.command_re.finditer(message) functions = self._get_functions() tickets = {} - for cmd, tkts in cmd_groups: + for m in cmd_groups: + cmd, tkts = m.group('action', 'ticket') func = functions.get(cmd.lower()) if not func and self.commands_refs.strip() == '<ALL>': func = self.cmd_refs @@ -208,7 +209,8 @@ In [changeset:"%s"]: def _update_tickets(self, tickets, changeset, comment, date): """Update the tickets with the given comment.""" - perm = PermissionCache(self.env, changeset.author) + authname = self._authname(changeset) + perm = PermissionCache(self.env, authname) for tkt_id, cmds in tickets.iteritems(): try: self.log.debug("Updating ticket #%d", tkt_id) @@ -220,7 +222,7 @@ In [changeset:"%s"]: if cmd(ticket, changeset, ticket_perm) is not False: save = True if save: - ticket.save_changes(changeset.author, comment, date) + ticket.save_changes(authname, comment, date) if save: self._notify(ticket, date) except Exception, e: @@ -250,23 +252,29 @@ In [changeset:"%s"]: functions[cmd] = func return functions + def _authname(self, changeset): + return changeset.author.lower() \ + if self.env.config.getbool('trac', 'ignore_auth_case') \ + else changeset.author + # Command-specific behavior # The ticket isn't updated if all extracted commands return False. def cmd_close(self, ticket, changeset, perm): + authname = self._authname(changeset) if self.check_perms and not 'TICKET_MODIFY' in perm: self.log.info("%s doesn't have TICKET_MODIFY permission for #%d", - changeset.author, ticket.id) + authname, ticket.id) return False ticket['status'] = 'closed' ticket['resolution'] = 'fixed' if not ticket['owner']: - ticket['owner'] = changeset.author + ticket['owner'] = authname def cmd_refs(self, ticket, changeset, perm): if self.check_perms and not 'TICKET_APPEND' in perm: self.log.info("%s doesn't have TICKET_APPEND permission for #%d", - changeset.author, ticket.id) + self._authname(changeset), ticket.id) return False @@ -302,8 +310,8 @@ class CommitTicketReferenceMacro(WikiMac ticket_re = CommitTicketUpdater.ticket_re if not any(int(tkt_id) == int(formatter.context.resource.id) for tkt_id in ticket_re.findall(message)): - return tag.p("(The changeset message doesn't reference this " - "ticket)", class_='hint') + return tag.p(_("(The changeset message doesn't reference this " + "ticket)"), class_='hint') if ChangesetModule(self.env).wiki_format_messages: return tag.div(format_to_html(self.env, formatter.context.child('changeset', rev, parent=resource), Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/templates/ticket_delete.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/templates/ticket_delete.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/templates/ticket_delete.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/ticket/templates/ticket_delete.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2010-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> @@ -38,8 +48,8 @@ </p> </div> <div class="buttons"> + <input type="submit" class="trac-disable-on-submit" value="${_('Delete ticket')}"/> <input type="submit" name="cancel" value="${_('Cancel')}"/> - <input type="submit" value="${_('Delete ticket')}"/> </div> </form> </py:when> @@ -62,8 +72,8 @@ This is an irreversible operation.</p> </div> <div class="buttons"> + <input type="submit" class="trac-disable-on-submit" value="${_('Delete comment')}"/> <input type="submit" name="cancel" value="${_('Cancel')}"/> - <input type="submit" value="${_('Delete comment')}"/> </div> </form> </py:otherwise> Modified: bloodhound/branches/trac-1.0.2-integration/trac/tracopt/versioncontrol/git/PyGIT.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/tracopt/versioncontrol/git/PyGIT.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/tracopt/versioncontrol/git/PyGIT.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/tracopt/versioncontrol/git/PyGIT.py Sat Nov 15 01:14:46 2014 @@ -28,36 +28,12 @@ from threading import Lock import time import weakref +from trac.util import terminate +from trac.util.text import to_unicode __all__ = ['GitError', 'GitErrorSha', 'Storage', 'StorageFactory'] -def terminate(process): - """Python 2.5 compatibility method. - os.kill is not available on Windows before Python 2.7. - In Python 2.6 subprocess.Popen has a terminate method. - (It also seems to have some issues on Windows though.) - """ - - def terminate_win(process): - import ctypes - PROCESS_TERMINATE = 1 - handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, - False, - process.pid) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) - - def terminate_nix(process): - import os - import signal - return os.kill(process.pid, signal.SIGTERM) - - if sys.platform == 'win32': - return terminate_win(process) - return terminate_nix(process) - - class GitError(Exception): pass @@ -95,12 +71,29 @@ def parse_commit(raw): return '\n'.join(lines), props +_unquote_re = re.compile(r'\\(?:[abtnvfr"\\]|[0-7]{3})') +_unquote_chars = {'a': '\a', 'b': '\b', 't': '\t', 'n': '\n', 'v': '\v', + 'f': '\f', 'r': '\r', '"': '"', '\\': '\\'} + + +def _unquote(path): + if path.startswith('"') and path.endswith('"'): + def replace(match): + s = match.group(0)[1:] + if len(s) == 3: + return chr(int(s, 8)) # \ooo + return _unquote_chars[s] + path = _unquote_re.sub(replace, path[1:-1]) + return path + + class GitCore(object): """Low-level wrapper around git executable""" - def __init__(self, git_dir=None, git_bin='git'): + def __init__(self, git_dir=None, git_bin='git', log=None): self.__git_bin = git_bin self.__git_dir = git_dir + self.__log = log def __repr__(self): return '<GitCore bin="%s" dir="%s">' % (self.__git_bin, @@ -132,7 +125,10 @@ class GitCore(object): p = self.__pipe(git_cmd, stdout=PIPE, stderr=PIPE, *cmd_args) stdout_data, stderr_data = p.communicate() - #TODO, do something with p.returncode, e.g. raise exception + if self.__log and (p.returncode != 0 or stderr_data): + self.__log.debug('%s exits with %d, dir: %r, args: %s %r, ' + 'stderr: %r', self.__git_bin, p.returncode, + self.__git_dir, git_cmd, cmd_args, stderr_data) return stdout_data @@ -201,21 +197,23 @@ class StorageFactory(object): self.logger = log with StorageFactory.__dict_lock: + if weak: + # remove additional reference which is created + # with non-weak argument + try: + del StorageFactory.__dict_nonweak[repo] + except KeyError: + pass + try: i = StorageFactory.__dict[repo] except KeyError: i = Storage(repo, log, git_bin, git_fs_encoding) StorageFactory.__dict[repo] = i - # create or remove additional reference depending on 'weak' - # argument - if weak: - try: - del StorageFactory.__dict_nonweak[repo] - except KeyError: - pass - else: - StorageFactory.__dict_nonweak[repo] = i + # create additional reference depending on 'weak' argument + if not weak: + StorageFactory.__dict_nonweak[repo] = i self.__inst = i self.__repo = repo @@ -227,13 +225,19 @@ class StorageFactory(object): self.__repo)) return self.__inst + @classmethod + def _clean(cls): + """For testing purpose only""" + with StorageFactory.__dict_lock: + cls.__dict.clear() + cls.__dict_nonweak.clear() + class Storage(object): """High-level wrapper around GitCore with in-memory caching""" __SREV_MIN = 4 # minimum short-rev length - class RevCache(tuple): """RevCache(youngest_rev, oldest_rev, rev_dict, tag_set, srev_dict, branch_dict) @@ -383,18 +387,29 @@ class Storage(object): # simple sanity checking __git_file_path = partial(os.path.join, git_dir) - if not all(map(os.path.exists, - map(__git_file_path, - ['HEAD','objects','refs']))): - self.logger.error("GIT control files missing in '%s'" % git_dir) - if os.path.exists(__git_file_path('.git')): - self.logger.error("entry '.git' found in '%s'" - " -- maybe use that folder instead..." + control_files = ['HEAD', 'objects', 'refs'] + control_files_exist = \ + lambda p: all(map(os.path.exists, map(p, control_files))) + if not control_files_exist(__git_file_path): + __git_file_path = partial(os.path.join, git_dir, '.git') + if os.path.exists(__git_file_path()) and \ + control_files_exist(__git_file_path): + git_dir = __git_file_path() + else: + self.logger.error("GIT control files missing in '%s'" % git_dir) - raise GitError("GIT control files not found, maybe wrong " - "directory?") + raise GitError("GIT control files not found, maybe wrong " + "directory?") + # at least, check that the HEAD file is readable + head_file = os.path.join(git_dir, 'HEAD') + try: + with open(head_file, 'rb') as f: + pass + except IOError, e: + raise GitError("Make sure the Git repository '%s' is readable: %s" + % (git_dir, to_unicode(e))) - self.repo = GitCore(git_dir, git_bin=git_bin) + self.repo = GitCore(git_dir, git_bin=git_bin, log=log) self.logger.debug("PyGIT.Storage instance %d constructed" % id(self)) @@ -410,25 +425,29 @@ class Storage(object): # # called by Storage.sync() - def __rev_cache_sync(self, youngest_rev=None): + def __rev_cache_sync(self): """invalidates revision db cache if necessary""" + branches = self._get_branches() + with self.__rev_cache_lock: need_update = False - if self.__rev_cache: - last_youngest_rev = self.__rev_cache.youngest_rev - if last_youngest_rev != youngest_rev: - self.logger.debug("invalidated caches (%s != %s)" - % (last_youngest_rev, youngest_rev)) - need_update = True - else: + if not self.__rev_cache: need_update = True # almost NOOP + elif branches != self.__rev_cache.branch_dict: + self.logger.debug('invalidated caches for %d cause repository ' + 'has been changed', id(self)) + need_update = True if need_update: self.__rev_cache = None - return need_update + def invalidate_rev_cache(self): + with self.__rev_cache_lock: + self.__rev_cache = None + self.logger.debug('invalidated caches for %d', id(self)) + def get_rev_cache(self): """Retrieve revision cache @@ -463,7 +482,7 @@ class Storage(object): for k, v in self._get_branches()] head_revs = set(v for _, v in new_branches) - rev = ord_rev = 0 + rev = ord_rev = None for ord_rev, revs in enumerate( self.repo.rev_list('--parents', '--topo-order', @@ -565,8 +584,12 @@ class Storage(object): """ result = [] - for e in self.repo.branch('-v', '--no-abbrev').splitlines(): - bname, bsha = e[1:].strip().split()[:2] + for e in self.repo.branch('-v', '--no-abbrev').rstrip('\n') \ + .split('\n'): + tokens = e[1:].strip().split()[:2] + if len(tokens) != 2: + continue + bname, bsha = tokens if e.startswith('*'): result.insert(0, (bname, bsha)) else: @@ -640,8 +663,8 @@ class Storage(object): def get_commit_encoding(self): if self.commit_encoding is None: self.commit_encoding = \ - self.repo.repo_config("--get", "i18n.commitEncoding") \ - .strip() or 'utf-8' + self.repo.config('--get', 'i18n.commitEncoding').strip() or \ + 'utf-8' return self.commit_encoding @@ -812,7 +835,7 @@ class Storage(object): raise GitErrorSha with self.__commit_msg_lock: - if self.__commit_msg_cache.has_key(commit_id): + if commit_id in self.__commit_msg_cache: # cache hit result = self.__commit_msg_cache[commit_id] return result[0], dict(result[1]) @@ -882,15 +905,14 @@ class Storage(object): return self.get_commits().iterkeys() def sync(self): - rev = self.repo.rev_list('--max-count=1', '--topo-order', '--all') \ - .strip() - return self.__rev_cache_sync(rev) + return self.__rev_cache_sync() @contextmanager def get_historian(self, sha, base_path): p = [] change = {} next_path = [] + base_path = self._fs_from_unicode(base_path) def name_status_gen(): p[:] = [self.repo.log_pipe('--pretty=format:%n%H', @@ -904,6 +926,8 @@ class Storage(object): if l == '\n': break _, path = l.rstrip('\n').split('\t', 1) + # git-log without -z option quotes each pathname + path = _unquote(path) while path not in change: change[path] = old_sha if next_path == [path]: @@ -921,6 +945,7 @@ class Storage(object): gen = name_status_gen() def historian(path): + path = self._fs_from_unicode(path) try: return change[path] except KeyError: @@ -936,34 +961,32 @@ class Storage(object): def last_change(self, sha, path, historian=None): if historian is not None: return historian(path) - return self.repo.rev_list('--max-count=1', - sha, '--', - self._fs_from_unicode(path)).strip() or None + tmp = self.history(sha, path, limit=1) + return tmp[0] if tmp else None def history(self, sha, path, limit=None): if limit is None: limit = -1 - tmp = self.repo.rev_list('--max-count=%d' % limit, str(sha), '--', - self._fs_from_unicode(path)) - - return [ rev.strip() for rev in tmp.splitlines() ] + args = ['--max-count=%d' % limit, str(sha)] + if path: + args.extend(('--', self._fs_from_unicode(path))) + tmp = self.repo.rev_list(*args) + return [rev.strip() for rev in tmp.splitlines()] def history_timerange(self, start, stop): + # retrieve start <= committer-time < stop, + # see CachedRepository.get_changesets() return [ rev.strip() for rev in \ - self.repo.rev_list('--reverse', + self.repo.rev_list('--date-order', '--max-age=%d' % start, - '--min-age=%d' % stop, + '--min-age=%d' % (stop - 1), '--all').splitlines() ] def rev_is_anchestor_of(self, rev1, rev2): """return True if rev2 is successor of rev1""" - rev1 = rev1.strip() - rev2 = rev2.strip() - rev_dict = self.get_commits() - return (rev2 in rev_dict and rev2 in self.children_recursive(rev1, rev_dict))