]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/web/directory.py
Implementation, tests and docs for blacklists. This version allows listing directorie...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / directory.py
index b57971c102dae63560a4ef11cfd4b2a407ba97eb..b04cbe758ee58174d512e460b615ed8463c0824c 100644 (file)
@@ -14,19 +14,23 @@ from foolscap.api import fireEventually
 
 from allmydata.util import base32, time_format
 from allmydata.uri import from_string_dirnode
-from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \
-     IFilesystemNode, ExistingChildError, NoSuchChildError
+from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
+     IImmutableFileNode, IMutableFileNode, ExistingChildError, \
+     NoSuchChildError, EmptyPathnameComponentError, SDMF_VERSION, MDMF_VERSION
+from allmydata.blacklist import ProhibitedNode
 from allmydata.monitor import Monitor, OperationCancelledError
 from allmydata import dirnode
 from allmydata.web.common import text_plain, WebError, \
      IOpHandleTable, NeedOperationHandleError, \
      boolean_of_arg, get_arg, get_root, parse_replace_arg, \
      should_create_intermediate_directories, \
-     getxmlfile, RenderMixin, humanize_failure
+     getxmlfile, RenderMixin, humanize_failure, convert_children_json, \
+     parse_mutable_type_arg
 from allmydata.web.filenode import ReplaceMeMixin, \
      FileNodeHandler, PlaceHolderNodeHandler
 from allmydata.web.check_results import CheckResults, \
-     CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
+     CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults, \
+     LiteralCheckResults
 from allmydata.web.info import MoreInfo
 from allmydata.web.operations import ReloadMixin
 from allmydata.web.check_results import json_check_results, \
@@ -40,8 +44,6 @@ class BlockingFileError(Exception):
 def make_handler_for(node, client, parentnode=None, name=None):
     if parentnode:
         assert IDirectoryNode.providedBy(parentnode)
-    if IMutableFileNode.providedBy(node):
-        return FileNodeHandler(client, node, parentnode, name)
     if IFileNode.providedBy(node):
         return FileNodeHandler(client, node, parentnode, name)
     if IDirectoryNode.providedBy(node):
@@ -60,8 +62,9 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         self.name = name
 
     def childFactory(self, ctx, name):
-        req = IRequest(ctx)
         name = name.decode("utf-8")
+        if not name:
+            raise EmptyPathnameComponentError()
         d = self.node.get(name)
         d.addBoth(self.got_child, ctx, name)
         # got_child returns a handler resource: FileNodeHandler or
@@ -86,17 +89,38 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
                 if should_create_intermediate_directories(req):
                     # create intermediate directories
                     if DEBUG: print " making intermediate directory"
-                    d = self.node.create_empty_directory(name)
+                    d = self.node.create_subdirectory(name)
                     d.addCallback(make_handler_for,
                                   self.client, self.node, name)
                     return d
             else:
                 if DEBUG: print " terminal"
                 # terminal node
-                if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
+                if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir"),
+                                   ("POST", "mkdir-with-children"),
+                                   ("POST", "mkdir-immutable") ]:
                     if DEBUG: print " making final directory"
                     # final directory
-                    d = self.node.create_empty_directory(name)
+                    kids = {}
+                    if t in ("mkdir-with-children", "mkdir-immutable"):
+                        req.content.seek(0)
+                        kids_json = req.content.read()
+                        kids = convert_children_json(self.client.nodemaker,
+                                                     kids_json)
+                    mutable = True
+                    if t == "mkdir-immutable":
+                        mutable = False
+
+                    mt = None
+                    if mutable:
+                        arg = get_arg(req, "mutable-type", None)
+                        mt = parse_mutable_type_arg(arg)
+                        if mt is "invalid":
+                            raise WebError("Unknown type: %s" % arg,
+                                           http.BAD_REQUEST)
+                    d = self.node.create_subdirectory(name, kids,
+                                                      mutable=mutable,
+                                                      mutable_version=mt)
                     d.addCallback(make_handler_for,
                                   self.client, self.node, name)
                     return d
@@ -137,7 +161,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         if not t:
             # render the directory as HTML, using the docFactory and Nevow's
             # whole templating thing.
-            return DirectoryAsHTML(self.node)
+            return DirectoryAsHTML(self.node,
+                                   self.client.mutable_file_default)
 
         if t == "json":
             return DirectoryJSONMetadata(ctx, self.node)
@@ -178,6 +203,10 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
 
         if t == "mkdir":
             d = self._POST_mkdir(req)
+        elif t == "mkdir-with-children":
+            d = self._POST_mkdir_with_children(req)
+        elif t == "mkdir-immutable":
+            d = self._POST_mkdir_immutable(req)
         elif t == "mkdir-p":
             # TODO: docs, tests
             d = self._POST_mkdir_p(req)
@@ -185,8 +214,8 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             d = self._POST_upload(ctx) # this one needs the context
         elif t == "uri":
             d = self._POST_uri(req)
-        elif t == "delete":
-            d = self._POST_delete(req)
+        elif t == "delete" or t == "unlink":
+            d = self._POST_unlink(req)
         elif t == "rename":
             d = self._POST_rename(req)
         elif t == "check":
@@ -203,8 +232,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             d = self._POST_start_deep_stats(ctx)
         elif t == "stream-manifest":
             d = self._POST_stream_manifest(ctx)
-        elif t == "set_children":
-            # TODO: docs
+        elif t == "set_children" or t == "set-children":
             d = self._POST_set_children(req)
         else:
             raise WebError("POST to a directory with bad t=%s" % t)
@@ -222,7 +250,56 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             return defer.succeed(self.node.get_uri()) # TODO: urlencode
         name = name.decode("utf-8")
         replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.create_empty_directory(name, overwrite=replace)
+        kids = {}
+        arg = get_arg(req, "mutable-type", None)
+        mt = parse_mutable_type_arg(arg)
+        if mt is not None and mt is not "invalid":
+            d = self.node.create_subdirectory(name, kids, overwrite=replace,
+                                          mutable_version=mt)
+        elif mt is "invalid":
+            raise WebError("Unknown type: %s" % arg, http.BAD_REQUEST)
+        else:
+            d = self.node.create_subdirectory(name, kids, overwrite=replace)
+        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
+        return d
+
+    def _POST_mkdir_with_children(self, req):
+        name = get_arg(req, "name", "")
+        if not name:
+            # our job is done, it was handled by the code in got_child
+            # which created the final directory (i.e. us)
+            return defer.succeed(self.node.get_uri()) # TODO: urlencode
+        name = name.decode("utf-8")
+        # TODO: decide on replace= behavior, see #903
+        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
+        req.content.seek(0)
+        kids_json = req.content.read()
+        kids = convert_children_json(self.client.nodemaker, kids_json)
+        arg = get_arg(req, "mutable-type", None)
+        mt = parse_mutable_type_arg(arg)
+        if mt is not None and mt is not "invalid":
+            d = self.node.create_subdirectory(name, kids, overwrite=False,
+                                              mutable_version=mt)
+        elif mt is "invalid":
+            raise WebError("Unknown type: %s" % arg)
+        else:
+            d = self.node.create_subdirectory(name, kids, overwrite=False)
+        d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
+        return d
+
+    def _POST_mkdir_immutable(self, req):
+        name = get_arg(req, "name", "")
+        if not name:
+            # our job is done, it was handled by the code in got_child
+            # which created the final directory (i.e. us)
+            return defer.succeed(self.node.get_uri()) # TODO: urlencode
+        name = name.decode("utf-8")
+        # TODO: decide on replace= behavior, see #903
+        #replace = boolean_of_arg(get_arg(req, "replace", "false"))
+        req.content.seek(0)
+        kids_json = req.content.read()
+        kids = convert_children_json(self.client.nodemaker, kids_json)
+        d = self.node.create_subdirectory(name, kids, overwrite=False, mutable=False)
         d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
         return d
 
@@ -247,7 +324,7 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         d = node.get(path[0])
         def _maybe_create(f):
             f.trap(NoSuchChildError)
-            return node.create_empty_directory(path[0])
+            return node.create_subdirectory(path[0])
         d.addErrback(_maybe_create)
         d.addCallback(self._get_or_create_directories, path[1:])
         return d
@@ -303,26 +380,31 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         charset = get_arg(req, "_charset", "utf-8")
         name = name.decode(charset)
         replace = boolean_of_arg(get_arg(req, "replace", "true"))
-        d = self.node.set_uri(name, childcap, overwrite=replace)
+
+        # We mustn't pass childcap for the readcap argument because we don't
+        # know whether it is a read cap. Passing a read cap as the writecap
+        # argument will work (it ends up calling NodeMaker.create_from_cap,
+        # which derives a readcap if necessary and possible).
+        d = self.node.set_uri(name, childcap, None, overwrite=replace)
         d.addCallback(lambda res: childcap)
         return d
 
-    def _POST_delete(self, req):
+    def _POST_unlink(self, req):
         name = get_arg(req, "name")
         if name is None:
             # apparently an <input type="hidden" name="name" value="">
             # won't show up in the resulting encoded form.. the 'name'
-            # field is completely missing. So to allow deletion of an
-            # empty file, we have to pretend that None means ''. The only
-            # downide of this is a slightly confusing error message if
-            # someone does a POST without a name= field. For our own HTML
-            # thisn't a big deal, because we create the 'delete' POST
-            # buttons ourselves.
+            # field is completely missing. So to allow unlinking of a
+            # child with a name that is the empty string, we have to
+            # pretend that None means ''. The only downside of this is
+            # a slightly confusing error message if someone does a POST
+            # without a name= field. For our own HTML this isn't a big
+            # deal, because we create the 'unlink' POST buttons ourselves.
             name = ''
         charset = get_arg(req, "_charset", "utf-8")
         name = name.decode(charset)
         d = self.node.delete(name)
-        d.addCallback(lambda res: "thing deleted")
+        d.addCallback(lambda res: "thing unlinked")
         return d
 
     def _POST_rename(self, req):
@@ -353,6 +435,11 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         d.addCallback(lambda res: "thing renamed")
         return d
 
+    def _maybe_literal(self, res, Results_Class):
+        if res:
+            return Results_Class(self.client, res)
+        return LiteralCheckResults(self.client)
+
     def _POST_check(self, req):
         # check this directory
         verify = boolean_of_arg(get_arg(req, "verify", "false"))
@@ -360,10 +447,10 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
         if repair:
             d = self.node.check_and_repair(Monitor(), verify, add_lease)
-            d.addCallback(lambda res: CheckAndRepairResults(self.client, res))
+            d.addCallback(self._maybe_literal, CheckAndRepairResults)
         else:
             d = self.node.check(Monitor(), verify, add_lease)
-            d.addCallback(lambda res: CheckResults(self.client, res))
+            d.addCallback(self._maybe_literal, CheckResults)
         return d
 
     def _start_operation(self, monitor, renderer, ctx):
@@ -468,11 +555,16 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             le.args = tuple(le.args + (body,))
             # TODO test handling of bad JSON
             raise
-        cs = []
+        cs = {}
         for name, (file_or_dir, mddict) in children.iteritems():
             name = unicode(name) # simplejson-2.0.1 returns str *or* unicode
-            cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
-            cs.append((name, cap, mddict.get('metadata')))
+            writecap = mddict.get('rw_uri')
+            if writecap is not None:
+                writecap = str(writecap)
+            readcap = mddict.get('ro_uri')
+            if readcap is not None:
+                readcap = str(readcap)
+            cs[name] = (writecap, readcap, mddict.get('metadata'))
         d = self.node.set_children(cs, replace)
         d.addCallback(lambda res: "Okay so I did it.")
         # TODO: results
@@ -488,10 +580,13 @@ class DirectoryAsHTML(rend.Page):
     docFactory = getxmlfile("directory.xhtml")
     addSlash = True
 
-    def __init__(self, node):
+    def __init__(self, node, default_mutable_format):
         rend.Page.__init__(self)
         self.node = node
 
+        assert default_mutable_format in (MDMF_VERSION, SDMF_VERSION)
+        self.default_mutable_format = default_mutable_format
+
     def beforeRender(self, ctx):
         # attempt to get the dirnode's children, stashing them (or the
         # failure that results) for later use
@@ -525,8 +620,12 @@ class DirectoryAsHTML(rend.Page):
 
     def render_title(self, ctx, data):
         si_s = abbreviated_dirnode(self.node)
-        header = ["TahoeLAFS - Directory SI=%s" % si_s]
-        if self.node.is_readonly():
+        header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
+        if self.node.is_unknown():
+            header.append(" (unknown)")
+        elif not self.node.is_mutable():
+            header.append(" (immutable)")
+        elif self.node.is_readonly():
             header.append(" (read-only)")
         else:
             header.append(" (modifiable)")
@@ -534,17 +633,21 @@ class DirectoryAsHTML(rend.Page):
 
     def render_header(self, ctx, data):
         si_s = abbreviated_dirnode(self.node)
-        header = ["TahoeLAFS Directory SI=", T.span(class_="data-chars")[si_s]]
-        if self.node.is_readonly():
+        header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
+        if self.node.is_unknown():
+            header.append(" (unknown)")
+        elif not self.node.is_mutable():
+            header.append(" (immutable)")
+        elif self.node.is_readonly():
             header.append(" (read-only)")
         return ctx.tag[header]
 
     def render_welcome(self, ctx, data):
         link = get_root(ctx)
-        return T.div[T.a(href=link)["Return to Welcome page"]]
+        return ctx.tag[T.a(href=link)["Return to Welcome page"]]
 
     def render_show_readonly(self, ctx, data):
-        if self.node.is_readonly():
+        if self.node.is_unknown() or self.node.is_readonly():
             return ""
         rocap = self.node.get_readonly_uri()
         root = get_root(ctx)
@@ -571,18 +674,18 @@ class DirectoryAsHTML(rend.Page):
 
         root = get_root(ctx)
         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
-        if self.node.is_readonly():
-            delete = "-"
+        if self.node.is_unknown() or self.node.is_readonly():
+            unlink = "-"
             rename = "-"
         else:
-            # this creates a button which will cause our child__delete method
-            # to be invoked, which deletes the file and then redirects the
+            # this creates a button which will cause our _POST_unlink method
+            # to be invoked, which unlinks the file and then redirects the
             # browser back to this directory
-            delete = T.form(action=here, method="post")[
-                T.input(type='hidden', name='t', value='delete'),
+            unlink = T.form(action=here, method="post")[
+                T.input(type='hidden', name='t', value='unlink'),
                 T.input(type='hidden', name='name', value=name),
                 T.input(type='hidden', name='when_done', value="."),
-                T.input(type='submit', value='del', name="del"),
+                T.input(type='submit', value='unlink', name="unlink"),
                 ]
 
             rename = T.form(action=here, method="get")[
@@ -592,7 +695,7 @@ class DirectoryAsHTML(rend.Page):
                 T.input(type='submit', value='rename', name="rename"),
                 ]
 
-        ctx.fillSlots("delete", delete)
+        ctx.fillSlots("unlink", unlink)
         ctx.fillSlots("rename", rename)
 
         times = []
@@ -619,8 +722,8 @@ class DirectoryAsHTML(rend.Page):
         ctx.fillSlots("times", times)
 
         assert IFilesystemNode.providedBy(target), target
-        writecap = target.get_uri() or ""
-        quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
+        target_uri = target.get_uri() or ""
+        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
 
         if IMutableFileNode.providedBy(target):
             # to prevent javascript in displayed .html files from stealing a
@@ -636,7 +739,7 @@ class DirectoryAsHTML(rend.Page):
 
             info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
 
-        elif IFileNode.providedBy(target):
+        elif IImmutableFileNode.providedBy(target):
             dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
 
             ctx.fillSlots("filename",
@@ -649,10 +752,12 @@ class DirectoryAsHTML(rend.Page):
 
         elif IDirectoryNode.providedBy(target):
             # directory
-            uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
+            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
             ctx.fillSlots("filename",
                           T.a(href=uri_link)[html.escape(name)])
-            if target.is_readonly():
+            if not target.is_mutable():
+                dirtype = "DIR-IMM"
+            elif target.is_readonly():
                 dirtype = "DIR-RO"
             else:
                 dirtype = "DIR"
@@ -660,19 +765,38 @@ class DirectoryAsHTML(rend.Page):
             ctx.fillSlots("size", "-")
             info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
 
+        elif isinstance(target, ProhibitedNode):
+            ctx.fillSlots("filename", T.strike[name])
+            if IDirectoryNode.providedBy(target.wrapped_node):
+                blacklisted_type = "DIR-BLACKLISTED"
+            else:
+                blacklisted_type = "BLACKLISTED"
+            ctx.fillSlots("type", blacklisted_type)
+            ctx.fillSlots("size", "-")
+            info_link = None
+            ctx.fillSlots("info", ["Access Prohibited:", T.br, target.reason])
+
         else:
             # unknown
             ctx.fillSlots("filename", html.escape(name))
-            ctx.fillSlots("type", "?")
+            if target.get_write_uri() is not None:
+                unknowntype = "?"
+            elif not self.node.is_mutable() or target.is_alleged_immutable():
+                unknowntype = "?-IMM"
+            else:
+                unknowntype = "?-RO"
+            ctx.fillSlots("type", unknowntype)
             ctx.fillSlots("size", "-")
             # use a directory-relative info link, so we can extract both the
             # writecap and the readcap
             info_link = "%s?t=info" % urllib.quote(name)
 
-        ctx.fillSlots("info", T.a(href=info_link)["More Info"])
+        if info_link:
+            ctx.fillSlots("info", T.a(href=info_link)["More Info"])
 
         return ctx.tag
 
+    # XXX: Duplicated from root.py.
     def render_forms(self, ctx, data):
         forms = []
 
@@ -681,6 +805,12 @@ class DirectoryAsHTML(rend.Page):
         if self.dirnode_children is None:
             return T.div["No upload forms: directory is unreadable"]
 
+        mdmf_directory_input = T.input(type='radio', name='mutable-type',
+                                       id='mutable-directory-mdmf',
+                                       value='mdmf')
+        sdmf_directory_input = T.input(type='radio', name='mutable-type',
+                                       id='mutable-directory-sdmf',
+                                       value='sdmf', checked='checked')
         mkdir = T.form(action=".", method="post",
                        enctype="multipart/form-data")[
             T.fieldset[
@@ -689,10 +819,34 @@ class DirectoryAsHTML(rend.Page):
             T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
             "New directory name: ",
             T.input(type="text", name="name"), " ",
+            T.label(for_='mutable-directory-sdmf')["SDMF"],
+            sdmf_directory_input,
+            T.label(for_='mutable-directory-mdmf')["MDMF"],
+            mdmf_directory_input,
             T.input(type="submit", value="Create"),
             ]]
         forms.append(T.div(class_="freeform-form")[mkdir])
 
+        # Build input elements for mutable file type. We do this outside
+        # of the list so we can check the appropriate format, based on
+        # the default configured in the client (which reflects the
+        # default configured in tahoe.cfg)
+        if self.default_mutable_format == MDMF_VERSION:
+            mdmf_input = T.input(type='radio', name='mutable-type',
+                                 id='mutable-type-mdmf', value='mdmf',
+                                 checked='checked')
+        else:
+            mdmf_input = T.input(type='radio', name='mutable-type',
+                                 id='mutable-type-mdmf', value='mdmf')
+
+        if self.default_mutable_format == SDMF_VERSION:
+            sdmf_input = T.input(type='radio', name='mutable-type',
+                                 id='mutable-type-sdmf', value='sdmf',
+                                 checked="checked")
+        else:
+            sdmf_input = T.input(type='radio', name='mutable-type',
+                                 id='mutable-type-sdmf', value='sdmf')
+
         upload = T.form(action=".", method="post",
                         enctype="multipart/form-data")[
             T.fieldset[
@@ -705,6 +859,9 @@ class DirectoryAsHTML(rend.Page):
             T.input(type="submit", value="Upload"),
             " Mutable?:",
             T.input(type="checkbox", name="mutable"),
+            sdmf_input, T.label(for_="mutable-type-sdmf")["SDMF"],
+            mdmf_input,
+            T.label(for_="mutable-type-mdmf")["MDMF (experimental)"],
             ]]
         forms.append(T.div(class_="freeform-form")[upload])
 
@@ -713,7 +870,7 @@ class DirectoryAsHTML(rend.Page):
             T.fieldset[
             T.input(type="hidden", name="t", value="uri"),
             T.input(type="hidden", name="when_done", value="."),
-            T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in TahoeLAFS."],
+            T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in Tahoe-LAFS."],
             "New child name: ",
             T.input(type="text", name="name"), " ",
             "URI of new child: ",
@@ -734,36 +891,41 @@ def DirectoryJSONMetadata(ctx, dirnode):
         kids = {}
         for name, (childnode, metadata) in children.iteritems():
             assert IFilesystemNode.providedBy(childnode), childnode
-            rw_uri = childnode.get_uri()
+            rw_uri = childnode.get_write_uri()
             ro_uri = childnode.get_readonly_uri()
-            if (IDirectoryNode.providedBy(childnode)
-                or IFileNode.providedBy(childnode)):
-                if childnode.is_readonly():
-                    rw_uri = None
             if IFileNode.providedBy(childnode):
                 kiddata = ("filenode", {'size': childnode.get_size(),
                                         'mutable': childnode.is_mutable(),
                                         })
+                if childnode.is_mutable() and \
+                    childnode.get_version() is not None:
+                    mutable_type = childnode.get_version()
+                    assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
+
+                    if mutable_type == MDMF_VERSION:
+                        mutable_type = "mdmf"
+                    else:
+                        mutable_type = "sdmf"
+                    kiddata[1]['mutable-type'] = mutable_type
+
             elif IDirectoryNode.providedBy(childnode):
-                kiddata = ("dirnode", {'mutable': childnode.is_mutable(),
-                                       })
+                kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
             else:
                 kiddata = ("unknown", {})
+
             kiddata[1]["metadata"] = metadata
-            if ro_uri:
-                kiddata[1]["ro_uri"] = ro_uri
             if rw_uri:
                 kiddata[1]["rw_uri"] = rw_uri
+            if ro_uri:
+                kiddata[1]["ro_uri"] = ro_uri
             verifycap = childnode.get_verify_cap()
             if verifycap:
                 kiddata[1]['verify_uri'] = verifycap.to_string()
+
             kids[name] = kiddata
-        if dirnode.is_readonly():
-            drw_uri = None
-            dro_uri = dirnode.get_uri()
-        else:
-            drw_uri = dirnode.get_uri()
-            dro_uri = dirnode.get_readonly_uri()
+
+        drw_uri = dirnode.get_write_uri()
+        dro_uri = dirnode.get_readonly_uri()
         contents = { 'children': kids }
         if dro_uri:
             contents['ro_uri'] = dro_uri
@@ -774,13 +936,13 @@ def DirectoryJSONMetadata(ctx, dirnode):
             contents['verify_uri'] = verifycap.to_string()
         contents['mutable'] = dirnode.is_mutable()
         data = ("dirnode", contents)
-        return simplejson.dumps(data, indent=1) + "\n"
+        json = simplejson.dumps(data, indent=1) + "\n"
+        return json
     d.addCallback(_got)
     d.addCallback(text_plain, ctx)
     return d
 
 
-
 def DirectoryURI(ctx, dirnode):
     return text_plain(dirnode.get_uri(), ctx)
 
@@ -849,9 +1011,13 @@ class ManifestResults(rend.Page, ReloadMixin):
         m = self.monitor
         s = m.get_status()
 
+        if m.origin_si:
+            origin_base32 = base32.b2a(m.origin_si)
+        else:
+            origin_base32 = ""
         status = { "stats": s["stats"],
                    "finished": m.is_finished(),
-                   "origin": base32.b2a(m.origin_si),
+                   "origin": origin_base32,
                    }
         if m.is_finished():
             # don't return manifest/verifycaps/SIs unless the operation is
@@ -872,7 +1038,10 @@ class ManifestResults(rend.Page, ReloadMixin):
         return simplejson.dumps(status, indent=1)
 
     def _si_abbrev(self):
-        return base32.b2a(self.monitor.origin_si)[:6]
+        si = self.monitor.origin_si
+        if not si:
+            return "<LIT>"
+        return base32.b2a(si)[:6]
 
     def render_title(self, ctx):
         return T.title["Manifest of SI=%s" % self._si_abbrev()]
@@ -970,17 +1139,17 @@ class ManifestStreamer(dirnode.DeepStats):
         v = node.get_verify_cap()
         if v:
             v = v.to_string()
-        d["verifycap"] = v
+        d["verifycap"] = v or ""
 
         r = node.get_repair_cap()
         if r:
             r = r.to_string()
-        d["repaircap"] = r
+        d["repaircap"] = r or ""
 
         si = node.get_storage_index()
         if si:
             si = base32.b2a(si)
-        d["storage-index"] = si
+        d["storage-index"] = si or ""
 
         j = simplejson.dumps(d, ensure_ascii=True)
         assert "\n" not in j
@@ -1022,23 +1191,25 @@ class DeepCheckStreamer(dirnode.DeepStats):
 
         if IDirectoryNode.providedBy(node):
             data["type"] = "directory"
-        else:
+        elif IFileNode.providedBy(node):
             data["type"] = "file"
+        else:
+            data["type"] = "unknown"
 
         v = node.get_verify_cap()
         if v:
             v = v.to_string()
-        data["verifycap"] = v
+        data["verifycap"] = v or ""
 
         r = node.get_repair_cap()
         if r:
             r = r.to_string()
-        data["repaircap"] = r
+        data["repaircap"] = r or ""
 
         si = node.get_storage_index()
         if si:
             si = base32.b2a(si)
-        data["storage-index"] = si
+        data["storage-index"] = si or ""
 
         if self.repair:
             d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
@@ -1072,18 +1243,45 @@ class DeepCheckStreamer(dirnode.DeepStats):
         self.req.write(j+"\n")
         return ""
 
-class UnknownNodeHandler(RenderMixin, rend.Page):
 
+class UnknownNodeHandler(RenderMixin, rend.Page):
     def __init__(self, client, node, parentnode=None, name=None):
         rend.Page.__init__(self)
         assert node
         self.node = node
+        self.parentnode = parentnode
+        self.name = name
 
     def render_GET(self, ctx):
         req = IRequest(ctx)
         t = get_arg(req, "t", "").strip()
         if t == "info":
             return MoreInfo(self.node)
-        raise WebError("GET unknown: can only do t=info, not t=%s" % t)
-
-
+        if t == "json":
+            is_parent_known_immutable = self.parentnode and not self.parentnode.is_mutable()
+            if self.parentnode and self.name:
+                d = self.parentnode.get_metadata_for(self.name)
+            else:
+                d = defer.succeed(None)
+            d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md, is_parent_known_immutable))
+            return d
+        raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
+                       "Using a webapi server that supports a later version of Tahoe "
+                       "may help." % t)
+
+def UnknownJSONMetadata(ctx, node, edge_metadata, is_parent_known_immutable):
+    rw_uri = node.get_write_uri()
+    ro_uri = node.get_readonly_uri()
+    data = ("unknown", {})
+    if ro_uri:
+        data[1]['ro_uri'] = ro_uri
+    if rw_uri:
+        data[1]['rw_uri'] = rw_uri
+        data[1]['mutable'] = True
+    elif is_parent_known_immutable or node.is_alleged_immutable():
+        data[1]['mutable'] = False
+    # else we don't know whether it is mutable.
+
+    if edge_metadata is not None:
+        data[1]['metadata'] = edge_metadata
+    return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)