About Download Docs Changes

Support directory trees

Radomir…

Below is a patch file from this newbie. Randy at data-warp dot com

Update: 2009-11-27 by Randy I've cleaned up my hack for page trees under docs. Back links seem to be working too (with the latest development version of Hatta).

Question: how should I submit them? Should I push them to the Hatta Mercurial repository. Do I need push permission? I don't want to break anything… Should I email them? Put them into a Wiki page (like I did below)?

--- hatta-2009-10-06.py	2009-10-10 13:17:04.000000000 -0500
+++ hatta-2009-10-06-new.py	2009-10-10 13:17:35.000000000 -0500
@@ -14,12 +14,79 @@
 from the wiki or with a text editor -- in either case the changes committed to
 the repository will appear in the recent changes and in page's history.

+# 2009-10-09 RLS (Randy Selzler): FIXME_RLS
+# Based upon Hatta development version dated 2009-10-06.
+# Hacked to support links to pages in docs/ trees.
+# Strategy: preserve "/" in page links for docs/ trees,
+# or url_quote them for flat Wikis.
+#
+# Radomir: use or reject my hacks as you see fit.
+# Thanks for the encouragement !!!
+#
+# See FIXME_RLS for unresolved issues, including:
+# * --file-tree option should be frozen when Wiki (repository?) is created.
+#   Accessing the same Wiki with and without would cause problems.
+# * Why offer both flat and tree Wiki styles (when is flat better)?
+#   I advocate discarding flat support.
+# * title_safe is hardwired within WikiTitleConverter.
+#   FIXME_RLS I don't know how to fix this !!!
+# * mercurial support fails when saving pages via symbolic links.
+#   The file is changed, but commit complains about link traversal.
+#   Perhaps Hatta could implicitly make them read-only?
+# * Much more testing is needed !!!
+# * DELETE/RELOCATE THESE COMMENTS ONCE THE ISSUES ARE RESOLVED.
+#
+# Discussion: if --file-tree is specified on the command line, then docs
+# supports a directory tree for Wiki pages, i.e. docs/ need not be flat.
+# Examples:
+# |=Link                   |=Path/Filename             |
+# |[[foofoo]]              |.../hatta/docs/foofoo      |
+# |[[foo.hat]]             |.../hatta/docs/foo.hat     |
+# |[[foo/bar.hat]]         |.../hatta/docs/foo/bar.hat |
+# |[[foo/gee.hat|Foo Gee]] |.../hatta/docs/foo/gee.hat |
+# |[[foo/../bad.hat]]      |REJECTED, see below        |
+#
+# Link paths that contain "../" are rejected (not followed).
+# This prevents Wiki users from escaping the docs directory,
+# simply by creating a ../ link, i.e. [[../../../peek-a-boo.hat]].
+# Wiki admin can create symbolic links to directories outside docs/
+# to provide controlled access.
+#
+# Security via os.path.abspath() comparisons was considered and rejected.
+# Symbolic links that led outside docs/ (to their natural location) would not
+# begin with the same path as those within docs/.  Natural locations could be
+# supported with a list of alternative docs/ that were allowed, but this
+# creates an ugly coordination problem between the list and actual links.
+#
+# Symbolic links to Wiki pages in their natural location have several uses:
+# * Cross links between source code and documentation trees.
+# * Wiki access to open source pages versus open source + proprietary pages.
+# * Wiki access for project foo and bar version combinations, i.e.
+#   production release links to foo-v1.0 and bar-v1.1
+#   beta test  release links to foo-v1.3 and bar-v1.1
+# * Repository isolation for open source and proprietary components.
+# * Wiki views for project combinations...
+#   Example symbolic links -> :
+#   hatta1/docs/
+#       a -> /data/a/
+#       b -> /data/b/
+#       c -> /data/c/
+#   hatta2/docs/
+#       b -> /data/b/
+#       c -> /data/c/
+#   hatta3/docs/
+#       a -> /data/a/
+#       c -> /data/c/
+#   Where 3 distinct repositories/trees service /data/a, b, c
+#
+
 Usage: hatta.py [options]

 Options:
   -h, --help            show this help message and exit
   -d DIR, --pages-dir=DIR
                         Store pages in DIR
+  -f, --file-tree       Whether the wiki should be hierarchical
   -t DIR, --cache-dir=DIR
                         Store cache in DIR
   -i INT, --interface=INT
@@ -174,6 +241,8 @@

         add('-d', '--pages-dir', dest='pages_path',
             help='Store pages in DIR', metavar='DIR')
+        add('-f', '--file-tree', dest='file_tree', default=False,
+            help='Whether the wiki should be hierarchical', action="store_true")
         add('-t', '--cache-dir', dest='cache_path',
             help='Store cache in DIR', metavar='DIR')
         add('-i', '--interface', dest='interface',
@@ -309,7 +378,7 @@
     change history, using Mercurial repository as the storage method.
     """

-    def __init__(self, path, charset=None):
+    def __init__(self, path, file_tree, charset=None):
         """
         Takes the path to the directory where the pages are to be kept.
         If the directory doen't exist, it will be created. If it's inside
@@ -318,6 +387,13 @@
         """

         self.charset = charset or 'utf-8'
+        self.file_tree = file_tree
+        if self.file_tree:
+            self.title_safe = '/'
+        else:
+            self.title_safe = ''
+        # RegEx matching ".." path components ("..", "../" or "../").
+        self.dot_dot_re = re.compile('(^|/)\.\.(?=(/|$))')
         self.path = path
         if not os.path.exists(self.path):
             os.makedirs(self.path)
@@ -356,11 +432,17 @@
         return path

     def _file_path(self, title):
-        return os.path.join(self.path, werkzeug.url_quote(title, safe=''))
+        if self.file_tree and self.dot_dot_re.search(title):
+            raise werkzeug.exceptions.Forbidden(
+                    u'_file_path: Link contains "../": %s' % title)
+        return os.path.join(self.path, werkzeug.url_quote(title, safe=self.title_safe))

     def _title_to_file(self, title):
+        if self.file_tree and self.dot_dot_re.search(title):
+            raise werkzeug.exceptions.Forbidden(
+                    u'_title_to_file: Link contains "../": %s' % title)
         return os.path.join(self.repo_prefix,
-                            werkzeug.url_quote(title, safe=''))
+                    werkzeug.url_quote(title, safe=self.title_safe))

     def _file_to_title(self, filename):
         assert filename.startswith(self.repo_prefix)
@@ -409,12 +491,28 @@
     @locked_repo
     def save_file(self, title, file_name, author=u'', comment=u'', parent=None):
         """Save an existing file as specified page."""
+        import errno

         user = author.encode('utf-8') or _(u'anon').encode('utf-8')
         text = comment.encode('utf-8') or _(u'comment').encode('utf-8')
         repo_file = self._title_to_file(title)
         file_path = self._file_path(title)
-        mercurial.util.rename(file_name, file_path)
+        path_dir = os.path.dirname(file_path)
+
+        if os.path.isdir(file_path):
+            raise werkzeug.exceptions.Forbidden(
+                    u'save_file: Page is a directory: %s' % file_path)
+
+        try:
+            if not os.path.isdir(path_dir):
+                os.makedirs(path_dir)
+            mercurial.util.rename(file_name, file_path)
+        except OSError, err:
+            # Path component is an existing page?
+            msg = u'save_file: [Errno %s] %s: in path %s' % (err.errno,
+                    os.strerror(err.errno), file_path)
+            raise werkzeug.exceptions.Forbidden(msg)
+
         changectx = self._changectx()
         try:
             filectx_tip = changectx[repo_file]
@@ -2091,10 +2189,41 @@
         yield u'</table>'

 class WikiTitleConverter(werkzeug.routing.PathConverter):
-    """Behaves like the path converter, except that it escapes slashes."""
+    """Behaves like the path converter, except optional slash escapes."""
+
+
+# to_url appears to control "Title" rendering at top of pages...
+
+    # FIXME_RLS should be within __init__
+#    self.file_tree = True
+#    if self.file_tree:
+#        self.title_safe = '/'
+#    else:
+#        self.title_safe = ''
+#    self.dot_dot_re = re.compile('(^|/)\.\.(?=(/|$))')
+#
+#    # 2009-10-09 RLS, __init__ raises a runtime error ?
+#    # Newbie issue... how to get file_tree passed in???
+    def __init__FIXME_RLS(self, file_tree):
+        """
+        Initialize title_safe for url_quote.
+        """
+        self.file_tree = file_tree
+        if self.file_tree:
+            self.title_safe = '/'
+        else:
+            self.title_safe = ''
+        self.dot_dot_re = re.compile('(^|/)\.\.(?=(/|$))')

     def to_url(self, value):
-        return werkzeug.url_quote(value, self.map.charset, safe="")
+        # FIXME_RLS hardwired and duplicated effort
+        self.file_tree = True
+        self.title_safe = '/'
+        self.dot_dot_re = re.compile('(^|/)\.\.(?=(/|$))')
+        if self.file_tree and self.dot_dot_re.search(value):
+            raise werkzeug.exceptions.Forbidden(
+                    u'to_url: Name contains "../": %s' % value)
+        return werkzeug.url_quote(value, self.map.charset, safe=self.title_safe)

 class WikiAllConverter(werkzeug.routing.BaseConverter):
     """Matches everything."""
@@ -2108,6 +2237,7 @@
     application and most of the logic.
     """
     storage_class = WikiStorage
+    titleConverter_class = WikiTitleConverter
     index_class = WikiSearch
     mime_map = {
         'text': WikiPageText,
@@ -2149,6 +2279,7 @@
         else:
             _ = gettext.translation('hatta', fallback=True).ugettext
         self.path = os.path.abspath(config.get('pages_path', 'docs'))
+        self.file_tree = self.config.get_bool('file_tree', False)
         self.cache = os.path.abspath(config.get('cache_path', 'cache'))
         self.page_charset = config.get('page_charset', 'utf-8')
         self.menu_page = self.config.get('menu_page', u'Menu')
@@ -2161,7 +2292,16 @@
         self.script_page = self.config.get('script_page', None)
         self.icon_page = self.config.get('icon_page', None)

-        self.storage = self.storage_class(self.path, self.page_charset)
+        if self.file_tree:
+            self.title_safe = '/'
+        else:
+            self.title_safe = ''
+
+        self.storage = self.storage_class(self.path, self.title_safe, self.page_charset)
+
+        # FIXME_RLS how to pass in file_tree into title converter ???
+        # self.titleConverter = self.titleConverter_class(self.file_tree)
+
         if not os.path.isdir(self.cache):
             os.makedirs(self.cache)
             reindex = True