]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/webish.py
webish: upload+localdir=missing should give an error
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / webish.py
1
2 from base64 import b32encode
3 import os.path
4 from twisted.application import service, strports, internet
5 from twisted.web import static, resource, server, html, http
6 from twisted.python import log
7 from twisted.internet import defer
8 from twisted.internet.interfaces import IConsumer
9 from nevow import inevow, rend, loaders, appserver, url, tags as T
10 from nevow.static import File as nevow_File # TODO: merge with static.File?
11 from allmydata.util import fileutil
12 import simplejson
13 from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode, \
14      IMutableFileNode
15 from allmydata import download
16 from allmydata.upload import FileHandle, FileName
17 from allmydata import provisioning
18 from allmydata import get_package_versions_string
19 from zope.interface import implements, Interface
20 import urllib
21 from formless import webform
22
23 from nevow.util import resource_filename
24
25 def getxmlfile(name):
26     return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
27
28 class IClient(Interface):
29     pass
30 class ILocalAccess(Interface):
31     def local_access_is_allowed():
32         """Return True if t=upload&localdir= is allowed, giving anyone who
33         can talk to the webserver control over the local (disk) filesystem."""
34
35 def boolean_of_arg(arg):
36     assert arg.lower() in ("true", "t", "1", "false", "f", "0")
37     return arg.lower() in ("true", "t", "1")
38
39 def get_arg(req, argname, default=None, multiple=False):
40     """Extract an argument from either the query args (req.args) or the form
41     body fields (req.fields). If multiple=False, this returns a single value
42     (or the default, which defaults to None), and the query args take
43     precedence. If multiple=True, this returns a tuple of arguments (possibly
44     empty), starting with all those in the query args.
45     """
46     results = []
47     if argname in req.args:
48         results.extend(req.args[argname])
49     if req.fields and argname in req.fields:
50         results.append(req.fields[argname].value)
51     if multiple:
52         return tuple(results)
53     if results:
54         return results[0]
55     return default
56
57 # we must override twisted.web.http.Request.requestReceived with a version
58 # that doesn't use cgi.parse_multipart() . Since we actually use Nevow, we
59 # override the nevow-specific subclass, nevow.appserver.NevowRequest . This
60 # is an exact copy of twisted.web.http.Request (from SVN HEAD on 10-Aug-2007)
61 # that modifies the way form arguments are parsed. Note that this sort of
62 # surgery may induce a dependency upon a particular version of twisted.web
63
64 parse_qs = http.parse_qs
65 class MyRequest(appserver.NevowRequest):
66     fields = None
67     def requestReceived(self, command, path, version):
68         """Called by channel when all data has been received.
69
70         This method is not intended for users.
71         """
72         self.content.seek(0,0)
73         self.args = {}
74         self.stack = []
75
76         self.method, self.uri = command, path
77         self.clientproto = version
78         x = self.uri.split('?', 1)
79
80         if len(x) == 1:
81             self.path = self.uri
82         else:
83             self.path, argstring = x
84             self.args = parse_qs(argstring, 1)
85
86         # cache the client and server information, we'll need this later to be
87         # serialized and sent with the request so CGIs will work remotely
88         self.client = self.channel.transport.getPeer()
89         self.host = self.channel.transport.getHost()
90
91         # Argument processing.
92
93 ##      The original twisted.web.http.Request.requestReceived code parsed the
94 ##      content and added the form fields it found there to self.args . It
95 ##      did this with cgi.parse_multipart, which holds the arguments in RAM
96 ##      and is thus unsuitable for large file uploads. The Nevow subclass
97 ##      (nevow.appserver.NevowRequest) uses cgi.FieldStorage instead (putting
98 ##      the results in self.fields), which is much more memory-efficient.
99 ##      Since we know we're using Nevow, we can anticipate these arguments
100 ##      appearing in self.fields instead of self.args, and thus skip the
101 ##      parse-content-into-self.args step.
102
103 ##      args = self.args
104 ##      ctype = self.getHeader('content-type')
105 ##      if self.method == "POST" and ctype:
106 ##          mfd = 'multipart/form-data'
107 ##          key, pdict = cgi.parse_header(ctype)
108 ##          if key == 'application/x-www-form-urlencoded':
109 ##              args.update(parse_qs(self.content.read(), 1))
110 ##          elif key == mfd:
111 ##              try:
112 ##                  args.update(cgi.parse_multipart(self.content, pdict))
113 ##              except KeyError, e:
114 ##                  if e.args[0] == 'content-disposition':
115 ##                      # Parse_multipart can't cope with missing
116 ##                      # content-dispostion headers in multipart/form-data
117 ##                      # parts, so we catch the exception and tell the client
118 ##                      # it was a bad request.
119 ##                      self.channel.transport.write(
120 ##                              "HTTP/1.1 400 Bad Request\r\n\r\n")
121 ##                      self.channel.transport.loseConnection()
122 ##                      return
123 ##                  raise
124
125         self.process()
126
127 class Directory(rend.Page):
128     addSlash = True
129     docFactory = getxmlfile("directory.xhtml")
130
131     def __init__(self, rootname, dirnode, dirpath):
132         self._rootname = rootname
133         self._dirnode = dirnode
134         self._dirpath = dirpath
135
136     def dirpath_as_string(self):
137         return "/" + "/".join(self._dirpath)
138
139     def render_title(self, ctx, data):
140         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
141
142     def render_header(self, ctx, data):
143         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
144         num_dirs = len(parent_directories)
145
146         header = ["Directory '"]
147         for i,d in enumerate(parent_directories):
148             upness = num_dirs - i - 1
149             if upness:
150                 link = "/".join( ("..",) * upness )
151             else:
152                 link = "."
153             header.append(T.a(href=link)[d])
154             if upness != 0:
155                 header.append("/")
156         header.append("'")
157
158         if self._dirnode.is_readonly():
159             header.append(" (readonly)")
160         header.append(":")
161         return ctx.tag[header]
162
163     def render_welcome(self, ctx, data):
164         depth = len(self._dirpath) + 2
165         link = "/".join([".."] * depth)
166         return T.div[T.a(href=link)["Return to Welcome page"]]
167
168     def data_children(self, ctx, data):
169         d = self._dirnode.list()
170         d.addCallback(lambda dict: sorted(dict.items()))
171         return d
172
173     def render_row(self, ctx, data):
174         name, (target, metadata) = data
175
176         if self._dirnode.is_readonly():
177             delete = "-"
178             rename = "-"
179         else:
180             # this creates a button which will cause our child__delete method
181             # to be invoked, which deletes the file and then redirects the
182             # browser back to this directory
183             delete = T.form(action=url.here, method="post")[
184                 T.input(type='hidden', name='t', value='delete'),
185                 T.input(type='hidden', name='name', value=name),
186                 T.input(type='hidden', name='when_done', value=url.here),
187                 T.input(type='submit', value='del', name="del"),
188                 ]
189
190             rename = T.form(action=url.here, method="get")[
191                 T.input(type='hidden', name='t', value='rename-form'),
192                 T.input(type='hidden', name='name', value=name),
193                 T.input(type='hidden', name='when_done', value=url.here),
194                 T.input(type='submit', value='rename', name="rename"),
195                 ]
196
197         ctx.fillSlots("delete", delete)
198         ctx.fillSlots("rename", rename)
199         check = T.form(action=url.here, method="post")[
200             T.input(type='hidden', name='t', value='check'),
201             T.input(type='hidden', name='name', value=name),
202             T.input(type='hidden', name='when_done', value=url.here),
203             T.input(type='submit', value='check', name="check"),
204             ]
205         ctx.fillSlots("overwrite", self.build_overwrite(ctx, (name, target)))
206         ctx.fillSlots("check", check)
207
208         # build the base of the uri_link link url
209         uri_link = "/uri/" + urllib.quote(target.get_uri())
210
211         assert (IFileNode.providedBy(target)
212                 or IDirectoryNode.providedBy(target)
213                 or IMutableFileNode.providedBy(target)), target
214
215         if IMutableFileNode.providedBy(target):
216             # file
217
218             # add the filename to the uri_link url
219             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
220
221             # to prevent javascript in displayed .html files from stealing a
222             # secret directory URI from the URL, send the browser to a URI-based
223             # page that doesn't know about the directory at all
224             #dlurl = urllib.quote(name)
225             dlurl = uri_link
226
227             ctx.fillSlots("filename",
228                           T.a(href=dlurl)[html.escape(name)])
229             ctx.fillSlots("type", "SSK")
230
231             ctx.fillSlots("size", "?")
232
233             text_plain_link = uri_link + "?filename=foo.txt"
234             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
235
236         elif IFileNode.providedBy(target):
237             # file
238
239             # add the filename to the uri_link url
240             uri_link += '?%s' % (urllib.urlencode({'filename': name}),)
241
242             # to prevent javascript in displayed .html files from stealing a
243             # secret directory URI from the URL, send the browser to a URI-based
244             # page that doesn't know about the directory at all
245             #dlurl = urllib.quote(name)
246             dlurl = uri_link
247
248             ctx.fillSlots("filename",
249                           T.a(href=dlurl)[html.escape(name)])
250             ctx.fillSlots("type", "FILE")
251
252             ctx.fillSlots("size", target.get_size())
253
254             text_plain_link = uri_link + "?filename=foo.txt"
255             text_plain_tag = T.a(href=text_plain_link)["text/plain"]
256
257         elif IDirectoryNode.providedBy(target):
258             # directory
259             subdir_url = urllib.quote(name)
260             ctx.fillSlots("filename",
261                           T.a(href=subdir_url)[html.escape(name)])
262             if target.is_readonly():
263                 dirtype = "DIR-RO"
264             else:
265                 dirtype = "DIR"
266             ctx.fillSlots("type", dirtype)
267             ctx.fillSlots("size", "-")
268             text_plain_tag = None
269
270         childdata = [T.a(href="%s?t=json" % name)["JSON"], ", ",
271                      T.a(href="%s?t=uri" % name)["URI"], ", ",
272                      T.a(href="%s?t=readonly-uri" % name)["readonly-URI"], ", ",
273                      T.a(href=uri_link)["URI-link"],
274                      ]
275         if text_plain_tag:
276             childdata.extend([", ", text_plain_tag])
277
278         ctx.fillSlots("data", childdata)
279
280         try:
281             checker = IClient(ctx).getServiceNamed("checker")
282         except KeyError:
283             checker = None
284         if checker:
285             d = defer.maybeDeferred(checker.checker_results_for,
286                                     target.get_verifier())
287             def _got(checker_results):
288                 recent_results = reversed(checker_results[-5:])
289                 if IFileNode.providedBy(target):
290                     results = ("[" +
291                                ", ".join(["%d/%d" % (found, needed)
292                                           for (when,
293                                                (needed, total, found, sharemap))
294                                           in recent_results]) +
295                                "]")
296                 elif IDirectoryNode.providedBy(target):
297                     results = ("[" +
298                                "".join([{True:"+",False:"-"}[res]
299                                         for (when, res) in recent_results]) +
300                                "]")
301                 else:
302                     results = "%d results" % len(checker_results)
303                 return results
304             d.addCallback(_got)
305             results = d
306         else:
307             results = "--"
308         # TODO: include a link to see more results, including timestamps
309         # TODO: use a sparkline
310         ctx.fillSlots("checker_results", results)
311
312         return ctx.tag
313
314     def render_forms(self, ctx, data):
315         if self._dirnode.is_readonly():
316             return T.div["No upload forms: directory is read-only"]
317         mkdir = T.form(action=".", method="post",
318                        enctype="multipart/form-data")[
319             T.fieldset[
320             T.input(type="hidden", name="t", value="mkdir"),
321             T.input(type="hidden", name="when_done", value=url.here),
322             T.legend(class_="freeform-form-label")["Create a new directory"],
323             "New directory name: ",
324             T.input(type="text", name="name"), " ",
325             T.input(type="submit", value="Create"),
326             ]]
327
328         upload = T.form(action=".", method="post",
329                         enctype="multipart/form-data")[
330             T.fieldset[
331             T.input(type="hidden", name="t", value="upload"),
332             T.input(type="hidden", name="when_done", value=url.here),
333             T.legend(class_="freeform-form-label")["Upload a file to this directory"],
334             "Choose a file to upload: ",
335             T.input(type="file", name="file", class_="freeform-input-file"),
336             " ",
337             T.input(type="submit", value="Upload"),
338             " Mutable?:",
339             T.input(type="checkbox", name="mutable"),
340             ]]
341
342         mount = T.form(action=".", method="post",
343                         enctype="multipart/form-data")[
344             T.fieldset[
345             T.input(type="hidden", name="t", value="uri"),
346             T.input(type="hidden", name="when_done", value=url.here),
347             T.legend(class_="freeform-form-label")["Attach a file or directory"
348                                                    " (by URI) to this"
349                                                    " directory"],
350             "New child name: ",
351             T.input(type="text", name="name"), " ",
352             "URI of new child: ",
353             T.input(type="text", name="uri"), " ",
354             T.input(type="submit", value="Attach"),
355             ]]
356         return [T.div(class_="freeform-form")[mkdir],
357                 T.div(class_="freeform-form")[upload],
358                 T.div(class_="freeform-form")[mount],
359                 ]
360
361     def build_overwrite(self, ctx, data):
362         name, target = data
363         if IMutableFileNode.providedBy(target) and not target.is_readonly():
364             action="/uri/" + urllib.quote(target.get_uri())
365             overwrite = T.form(action=action, method="post",
366                                enctype="multipart/form-data")[
367                 T.fieldset[
368                 T.input(type="hidden", name="t", value="overwrite"),
369                 T.input(type='hidden', name='name', value=name),
370                 T.input(type='hidden', name='when_done', value=url.here),
371                 T.legend(class_="freeform-form-label")["Overwrite"],
372                 "Choose new file: ",
373                 T.input(type="file", name="file", class_="freeform-input-file"),
374                 " ",
375                 T.input(type="submit", value="Overwrite")
376                 ]]
377             return [T.div(class_="freeform-form")[overwrite],]
378         else:
379             return []
380
381     def render_results(self, ctx, data):
382         req = inevow.IRequest(ctx)
383         return get_arg(req, "results", "")
384
385 class WebDownloadTarget:
386     implements(IDownloadTarget, IConsumer)
387     def __init__(self, req, content_type, content_encoding, save_to_file):
388         self._req = req
389         self._content_type = content_type
390         self._content_encoding = content_encoding
391         self._opened = False
392         self._producer = None
393         self._save_to_file = save_to_file
394
395     def registerProducer(self, producer, streaming):
396         self._req.registerProducer(producer, streaming)
397     def unregisterProducer(self):
398         self._req.unregisterProducer()
399
400     def open(self, size):
401         self._opened = True
402         self._req.setHeader("content-type", self._content_type)
403         if self._content_encoding:
404             self._req.setHeader("content-encoding", self._content_encoding)
405         self._req.setHeader("content-length", str(size))
406         if self._save_to_file is not None:
407             # tell the browser to save the file rather display it
408             # TODO: quote save_to_file properly
409             self._req.setHeader("content-disposition",
410                                 'attachment; filename="%s"'
411                                 % self._save_to_file)
412
413     def write(self, data):
414         self._req.write(data)
415     def close(self):
416         self._req.finish()
417
418     def fail(self, why):
419         if self._opened:
420             # The content-type is already set, and the response code
421             # has already been sent, so we can't provide a clean error
422             # indication. We can emit text (which a browser might interpret
423             # as something else), and if we sent a Size header, they might
424             # notice that we've truncated the data. Keep the error message
425             # small to improve the chances of having our error response be
426             # shorter than the intended results.
427             #
428             # We don't have a lot of options, unfortunately.
429             self._req.write("problem during download\n")
430         else:
431             # We haven't written anything yet, so we can provide a sensible
432             # error message.
433             msg = str(why.type)
434             msg.replace("\n", "|")
435             self._req.setResponseCode(http.GONE, msg)
436             self._req.setHeader("content-type", "text/plain")
437             # TODO: HTML-formatted exception?
438             self._req.write(str(why))
439         self._req.finish()
440
441     def register_canceller(self, cb):
442         pass
443     def finish(self):
444         pass
445
446 class FileDownloader(resource.Resource):
447     def __init__(self, filenode, name):
448         assert (IFileNode.providedBy(filenode)
449                 or IMutableFileNode.providedBy(filenode))
450         self._filenode = filenode
451         self._name = name
452
453     def render(self, req):
454         gte = static.getTypeAndEncoding
455         type, encoding = gte(self._name,
456                              static.File.contentTypes,
457                              static.File.contentEncodings,
458                              defaultType="text/plain")
459         save_to_file = None
460         if get_arg(req, "save", False):
461             # TODO: make the API specification clear: should "save=" or
462             # "save=false" count?
463             save_to_file = self._name
464         wdt = WebDownloadTarget(req, type, encoding, save_to_file)
465         d = self._filenode.download(wdt)
466         # exceptions during download are handled by the WebDownloadTarget
467         d.addErrback(lambda why: None)
468         return server.NOT_DONE_YET
469
470 class BlockingFileError(Exception):
471     """We cannot auto-create a parent directory, because there is a file in
472     the way"""
473 class NoReplacementError(Exception):
474     """There was already a child by that name, and you asked me to not replace it"""
475 class NoLocalDirectoryError(Exception):
476     """The localdir= directory didn't exist"""
477
478 LOCALHOST = "127.0.0.1"
479
480 class NeedLocalhostError:
481     implements(inevow.IResource)
482
483     def renderHTTP(self, ctx):
484         req = inevow.IRequest(ctx)
485         req.setResponseCode(http.FORBIDDEN)
486         req.setHeader("content-type", "text/plain")
487         return "localfile= or localdir= requires a local connection"
488
489 class NeedAbsolutePathError:
490     implements(inevow.IResource)
491
492     def renderHTTP(self, ctx):
493         req = inevow.IRequest(ctx)
494         req.setResponseCode(http.FORBIDDEN)
495         req.setHeader("content-type", "text/plain")
496         return "localfile= or localdir= requires an absolute path"
497
498 class LocalAccessDisabledError:
499     implements(inevow.IResource)
500
501     def renderHTTP(self, ctx):
502         req = inevow.IRequest(ctx)
503         req.setResponseCode(http.FORBIDDEN)
504         req.setHeader("content-type", "text/plain")
505         return "local file access is disabled"
506
507
508 class LocalFileDownloader(resource.Resource):
509     def __init__(self, filenode, local_filename):
510         self._local_filename = local_filename
511         IFileNode(filenode)
512         self._filenode = filenode
513
514     def render(self, req):
515         target = download.FileName(self._local_filename)
516         d = self._filenode.download(target)
517         def _done(res):
518             req.write(self._filenode.get_uri())
519             req.finish()
520         d.addCallback(_done)
521         return server.NOT_DONE_YET
522
523
524 class FileJSONMetadata(rend.Page):
525     def __init__(self, filenode):
526         self._filenode = filenode
527
528     def renderHTTP(self, ctx):
529         req = inevow.IRequest(ctx)
530         req.setHeader("content-type", "text/plain")
531         return self.renderNode(self._filenode)
532
533     def renderNode(self, filenode):
534         file_uri = filenode.get_uri()
535         data = ("filenode",
536                 {'ro_uri': file_uri,
537                  'size': filenode.get_size(),
538                  })
539         return simplejson.dumps(data, indent=1)
540
541 class FileURI(FileJSONMetadata):
542     def renderNode(self, filenode):
543         file_uri = filenode.get_uri()
544         return file_uri
545
546 class FileReadOnlyURI(FileJSONMetadata):
547     def renderNode(self, filenode):
548         if filenode.is_readonly():
549             return filenode.get_uri()
550         else:
551             return filenode.get_readonly().get_uri()
552
553 class DirnodeWalkerMixin:
554     """Visit all nodes underneath (and including) the rootnode, one at a
555     time. For each one, call the visitor. The visitor will see the
556     IDirectoryNode before it sees any of the IFileNodes inside. If the
557     visitor returns a Deferred, I do not call the visitor again until it has
558     fired.
559     """
560
561 ##    def _walk_if_we_could_use_generators(self, rootnode, rootpath=()):
562 ##        # this is what we'd be doing if we didn't have the Deferreds and
563 ##        # thus could use generators
564 ##        yield rootpath, rootnode
565 ##        for childname, childnode in rootnode.list().items():
566 ##            childpath = rootpath + (childname,)
567 ##            if IFileNode.providedBy(childnode):
568 ##                yield childpath, childnode
569 ##            elif IDirectoryNode.providedBy(childnode):
570 ##                for res in self._walk_if_we_could_use_generators(childnode,
571 ##                                                                 childpath):
572 ##                    yield res
573
574     def walk(self, rootnode, visitor, rootpath=()):
575         d = rootnode.list()
576         def _listed(listing):
577             return listing.items()
578         d.addCallback(_listed)
579         d.addCallback(self._handle_items, visitor, rootpath)
580         return d
581
582     def _handle_items(self, items, visitor, rootpath):
583         if not items:
584             return
585         childname, (childnode, metadata) = items[0]
586         childpath = rootpath + (childname,)
587         d = defer.maybeDeferred(visitor, childpath, childnode, metadata)
588         if IDirectoryNode.providedBy(childnode):
589             d.addCallback(lambda res: self.walk(childnode, visitor, childpath))
590         d.addCallback(lambda res:
591                       self._handle_items(items[1:], visitor, rootpath))
592         return d
593
594 class LocalDirectoryDownloader(resource.Resource, DirnodeWalkerMixin):
595     def __init__(self, dirnode, localdir):
596         self._dirnode = dirnode
597         self._localdir = localdir
598
599     def _handle(self, path, node, metadata):
600         localfile = os.path.join(self._localdir, os.sep.join(path))
601         if IDirectoryNode.providedBy(node):
602             fileutil.make_dirs(localfile)
603         elif IFileNode.providedBy(node):
604             target = download.FileName(localfile)
605             return node.download(target)
606
607     def render(self, req):
608         d = self.walk(self._dirnode, self._handle)
609         def _done(res):
610             req.setHeader("content-type", "text/plain")
611             return "operation complete"
612         d.addCallback(_done)
613         return d
614
615 class DirectoryJSONMetadata(rend.Page):
616     def __init__(self, dirnode):
617         self._dirnode = dirnode
618
619     def renderHTTP(self, ctx):
620         req = inevow.IRequest(ctx)
621         req.setHeader("content-type", "text/plain")
622         return self.renderNode(self._dirnode)
623
624     def renderNode(self, node):
625         d = node.list()
626         def _got(children):
627             kids = {}
628             for name, (childnode, metadata) in children.iteritems():
629                 if IFileNode.providedBy(childnode):
630                     kiduri = childnode.get_uri()
631                     kiddata = ("filenode",
632                                {'ro_uri': kiduri,
633                                 'size': childnode.get_size(),
634                                 })
635                 else:
636                     assert IDirectoryNode.providedBy(childnode), (childnode, children,)
637                     kiddata = ("dirnode",
638                                {'ro_uri': childnode.get_readonly_uri(),
639                                 })
640                     if not childnode.is_readonly():
641                         kiddata[1]['rw_uri'] = childnode.get_uri()
642                 kids[name] = kiddata
643             contents = { 'children': kids,
644                          'ro_uri': node.get_readonly_uri(),
645                          }
646             if not node.is_readonly():
647                 contents['rw_uri'] = node.get_uri()
648             data = ("dirnode", contents)
649             return simplejson.dumps(data, indent=1)
650         d.addCallback(_got)
651         return d
652
653 class DirectoryURI(DirectoryJSONMetadata):
654     def renderNode(self, node):
655         return node.get_uri()
656
657 class DirectoryReadonlyURI(DirectoryJSONMetadata):
658     def renderNode(self, node):
659         return node.get_readonly_uri()
660
661 class RenameForm(rend.Page):
662     addSlash = True
663     docFactory = getxmlfile("rename-form.xhtml")
664
665     def __init__(self, rootname, dirnode, dirpath):
666         self._rootname = rootname
667         self._dirnode = dirnode
668         self._dirpath = dirpath
669
670     def dirpath_as_string(self):
671         return "/" + "/".join(self._dirpath)
672
673     def render_title(self, ctx, data):
674         return ctx.tag["Directory '%s':" % self.dirpath_as_string()]
675
676     def render_header(self, ctx, data):
677         parent_directories = ("<%s>" % self._rootname,) + self._dirpath
678         num_dirs = len(parent_directories)
679
680         header = [ "Rename in directory '",
681                    "<%s>/" % self._rootname,
682                    "/".join(self._dirpath),
683                    "':", ]
684
685         if self._dirnode.is_readonly():
686             header.append(" (readonly)")
687         return ctx.tag[header]
688
689     def render_when_done(self, ctx, data):
690         return T.input(type="hidden", name="when_done", value=url.here)
691
692     def render_get_name(self, ctx, data):
693         req = inevow.IRequest(ctx)
694         name = get_arg(req, "name", "")
695         ctx.tag.attributes['value'] = name
696         return ctx.tag
697
698 class POSTHandler(rend.Page):
699     def __init__(self, node, replace):
700         self._node = node
701         self._replace = replace
702
703     def _check_replacement(self, name):
704         if self._replace:
705             return defer.succeed(None)
706         d = self._node.has_child(name)
707         def _got(present):
708             if present:
709                 raise NoReplacementError("There was already a child by that "
710                                          "name, and you asked me to not "
711                                          "replace it.")
712             return None
713         d.addCallback(_got)
714         return d
715
716     def renderHTTP(self, ctx):
717         req = inevow.IRequest(ctx)
718
719         t = get_arg(req, "t")
720         assert t is not None
721
722         name = get_arg(req, "name", None)
723         if name and "/" in name:
724             req.setResponseCode(http.BAD_REQUEST)
725             req.setHeader("content-type", "text/plain")
726             return "name= may not contain a slash"
727         if name is not None:
728             name = name.strip()
729         # we allow the user to delete an empty-named file, but not to create
730         # them, since that's an easy and confusing mistake to make
731
732         when_done = get_arg(req, "when_done", None)
733         if not boolean_of_arg(get_arg(req, "replace", "true")):
734             self._replace = False
735
736         if t == "mkdir":
737             if not name:
738                 raise RuntimeError("mkdir requires a name")
739             d = self._check_replacement(name)
740             d.addCallback(lambda res: self._node.create_empty_directory(name))
741             d.addCallback(lambda res: "directory created")
742         elif t == "uri":
743             if not name:
744                 raise RuntimeError("set-uri requires a name")
745             newuri = get_arg(req, "uri")
746             assert newuri is not None
747             d = self._check_replacement(name)
748             d.addCallback(lambda res: self._node.set_uri(name, newuri))
749             d.addCallback(lambda res: newuri)
750         elif t == "delete":
751             if name is None:
752                 # apparently an <input type="hidden" name="name" value="">
753                 # won't show up in the resulting encoded form.. the 'name'
754                 # field is completely missing. So to allow deletion of an
755                 # empty file, we have to pretend that None means ''. The only
756                 # downide of this is a slightly confusing error message if
757                 # someone does a POST without a name= field. For our own HTML
758                 # thisn't a big deal, because we create the 'delete' POST
759                 # buttons ourselves.
760                 name = ''
761             d = self._node.delete(name)
762             d.addCallback(lambda res: "thing deleted")
763         elif t == "rename":
764             from_name = 'from_name' in req.fields and req.fields["from_name"].value
765             if from_name is not None:
766                 from_name = from_name.strip()
767             to_name = 'to_name' in req.fields and req.fields["to_name"].value
768             if to_name is not None:
769                 to_name = to_name.strip()
770             if not from_name or not to_name:
771                 raise RuntimeError("rename requires from_name and to_name")
772             if not IDirectoryNode.providedBy(self._node):
773                 raise RuntimeError("rename must only be called on directories")
774             for k,v in [ ('from_name', from_name), ('to_name', to_name) ]:
775                 if v and "/" in v:
776                     req.setResponseCode(http.BAD_REQUEST)
777                     req.setHeader("content-type", "text/plain")
778                     return "%s= may not contain a slash" % (k,)
779             d = self._check_replacement(to_name)
780             d.addCallback(lambda res: self._node.get(from_name))
781             def add_dest(child):
782                 uri = child.get_uri()
783                 # now actually do the rename
784                 return self._node.set_uri(to_name, uri)
785             d.addCallback(add_dest)
786             def rm_src(junk):
787                 return self._node.delete(from_name)
788             d.addCallback(rm_src)
789             d.addCallback(lambda res: "thing renamed")
790
791         elif t == "upload":
792             if "mutable" in req.fields:
793                 contents = req.fields["file"]
794                 name = name or contents.filename
795                 if name is not None:
796                     name = name.strip()
797                 if not name:
798                     raise RuntimeError("upload-mutable requires a name")
799                 # SDMF: files are small, and we can only upload data.
800                 contents.file.seek(0)
801                 data = contents.file.read()
802                 uploadable = FileHandle(contents.file)
803                 d = self._check_replacement(name)
804                 d.addCallback(lambda res: self._node.has_child(name))
805                 def _checked(present):
806                     if present:
807                         # modify the existing one instead of creating a new
808                         # one
809                         d2 = self._node.get(name)
810                         def _got_newnode(newnode):
811                             d3 = newnode.replace(data)
812                             d3.addCallback(lambda res: newnode.get_uri())
813                             return d3
814                         d2.addCallback(_got_newnode)
815                     else:
816                         d2 = IClient(ctx).create_mutable_file(data)
817                         def _uploaded(newnode):
818                             d1 = self._node.set_node(name, newnode)
819                             d1.addCallback(lambda res: newnode.get_uri())
820                             return d1
821                         d2.addCallback(_uploaded)
822                     return d2
823                 d.addCallback(_checked)
824             else:
825                 contents = req.fields["file"]
826                 name = name or contents.filename
827                 if name is not None:
828                     name = name.strip()
829                 if not name:
830                     raise RuntimeError("upload requires a name")
831                 uploadable = FileHandle(contents.file)
832                 d = self._check_replacement(name)
833                 d.addCallback(lambda res: self._node.add_file(name, uploadable))
834                 def _done(newnode):
835                     return newnode.get_uri()
836                 d.addCallback(_done)
837
838         elif t == "overwrite":
839             contents = req.fields["file"]
840             # SDMF: files are small, and we can only upload data.
841             contents.file.seek(0)
842             data = contents.file.read()
843             # TODO: 'name' handling needs review
844             d = defer.succeed(self._node)
845             def _got_child_overwrite(child_node):
846                 child_node.replace(data)
847                 return child_node.get_uri()
848             d.addCallback(_got_child_overwrite)
849
850         elif t == "check":
851             d = self._node.get(name)
852             def _got_child_check(child_node):
853                 d2 = child_node.check()
854                 def _done(res):
855                     log.msg("checked %s, results %s" % (child_node, res))
856                     return str(res)
857                 d2.addCallback(_done)
858                 return d2
859             d.addCallback(_got_child_check)
860         else:
861             print "BAD t=%s" % t
862             return "BAD t=%s" % t
863         if when_done:
864             d.addCallback(lambda res: url.URL.fromString(when_done))
865         def _check_replacement(f):
866             # TODO: make this more human-friendly: maybe send them to the
867             # when_done page but with an extra query-arg that will display
868             # the error message in a big box at the top of the page. The
869             # directory page that when_done= usually points to accepts a
870             # result= argument.. use that.
871             f.trap(NoReplacementError)
872             req.setResponseCode(http.CONFLICT)
873             req.setHeader("content-type", "text/plain")
874             return str(f.value)
875         d.addErrback(_check_replacement)
876         return d
877
878 class DELETEHandler(rend.Page):
879     def __init__(self, node, name):
880         self._node = node
881         self._name = name
882
883     def renderHTTP(self, ctx):
884         req = inevow.IRequest(ctx)
885         d = self._node.delete(self._name)
886         def _done(res):
887             # what should this return??
888             return "%s deleted" % self._name
889         d.addCallback(_done)
890         def _trap_missing(f):
891             f.trap(KeyError)
892             req.setResponseCode(http.NOT_FOUND)
893             req.setHeader("content-type", "text/plain")
894             return "no such child %s" % self._name
895         d.addErrback(_trap_missing)
896         return d
897
898 class PUTHandler(rend.Page):
899     def __init__(self, node, path, t, localfile, localdir, replace):
900         self._node = node
901         self._path = path
902         self._t = t
903         self._localfile = localfile
904         self._localdir = localdir
905         self._replace = replace
906
907     def renderHTTP(self, ctx):
908         req = inevow.IRequest(ctx)
909         t = self._t
910         localfile = self._localfile
911         localdir = self._localdir
912
913         if t == "upload" and not (localfile or localdir):
914             req.setResponseCode(http.BAD_REQUEST)
915             req.setHeader("content-type", "text/plain")
916             return "t=upload requires localfile= or localdir="
917
918         # we must traverse the path, creating new directories as necessary
919         d = self._get_or_create_directories(self._node, self._path[:-1])
920         name = self._path[-1]
921         d.addCallback(self._check_replacement, name, self._replace)
922         if t == "upload":
923             if localfile:
924                 d.addCallback(self._upload_localfile, localfile, name)
925             else:
926                 # localdir
927                 # take the last step
928                 d.addCallback(self._get_or_create_directories, self._path[-1:])
929                 d.addCallback(self._upload_localdir, localdir)
930         elif t == "uri":
931             d.addCallback(self._attach_uri, req.content, name)
932         elif t == "mkdir":
933             d.addCallback(self._mkdir, name)
934         else:
935             d.addCallback(self._upload_file, req.content, name)
936
937         def _transform_error(f):
938             errors = {BlockingFileError: http.BAD_REQUEST,
939                       NoReplacementError: http.CONFLICT,
940                       NoLocalDirectoryError: http.BAD_REQUEST,
941                       }
942             for k,v in errors.items():
943                 if f.check(k):
944                     req.setResponseCode(v)
945                     req.setHeader("content-type", "text/plain")
946                     return str(f.value)
947             return f
948         d.addErrback(_transform_error)
949         return d
950
951     def _get_or_create_directories(self, node, path):
952         if not IDirectoryNode.providedBy(node):
953             # unfortunately it is too late to provide the name of the
954             # blocking directory in the error message.
955             raise BlockingFileError("cannot create directory because there "
956                                     "is a file in the way")
957         if not path:
958             return defer.succeed(node)
959         d = node.get(path[0])
960         def _maybe_create(f):
961             f.trap(KeyError)
962             return node.create_empty_directory(path[0])
963         d.addErrback(_maybe_create)
964         d.addCallback(self._get_or_create_directories, path[1:])
965         return d
966
967     def _check_replacement(self, node, name, replace):
968         if replace:
969             return node
970         d = node.has_child(name)
971         def _got(present):
972             if present:
973                 raise NoReplacementError("There was already a child by that "
974                                          "name, and you asked me to not "
975                                          "replace it.")
976             return node
977         d.addCallback(_got)
978         return d
979
980     def _mkdir(self, node, name):
981         d = node.create_empty_directory(name)
982         def _done(newnode):
983             return newnode.get_uri()
984         d.addCallback(_done)
985         return d
986
987     def _upload_file(self, node, contents, name):
988         uploadable = FileHandle(contents)
989         d = node.add_file(name, uploadable)
990         def _done(filenode):
991             log.msg("webish upload complete")
992             return filenode.get_uri()
993         d.addCallback(_done)
994         return d
995
996     def _upload_localfile(self, node, localfile, name):
997         uploadable = FileName(localfile)
998         d = node.add_file(name, uploadable)
999         d.addCallback(lambda filenode: filenode.get_uri())
1000         return d
1001
1002     def _attach_uri(self, parentnode, contents, name):
1003         newuri = contents.read().strip()
1004         d = parentnode.set_uri(name, newuri)
1005         def _done(res):
1006             return newuri
1007         d.addCallback(_done)
1008         return d
1009
1010     def _upload_localdir(self, node, localdir):
1011         # build up a list of files to upload
1012         all_files = []
1013         all_dirs = []
1014         msg = "No files to upload! %s is empty" % localdir
1015         if not os.path.exists(localdir):
1016             msg = "%s doesn't exist!" % localdir
1017             raise NoLocalDirectoryError(msg)
1018         for root, dirs, files in os.walk(localdir):
1019             if root == localdir:
1020                 path = ()
1021             else:
1022                 relative_root = root[len(localdir)+1:]
1023                 path = tuple(relative_root.split(os.sep))
1024             for d in dirs:
1025                 all_dirs.append(path + (d,))
1026             for f in files:
1027                 all_files.append(path + (f,))
1028         d = defer.succeed(msg)
1029         for dir in all_dirs:
1030             if dir:
1031                 d.addCallback(self._makedir, node, dir)
1032         for f in all_files:
1033             d.addCallback(self._upload_one_file, node, localdir, f)
1034         return d
1035
1036     def _makedir(self, res, node, dir):
1037         d = defer.succeed(None)
1038         # get the parent. As long as os.walk gives us parents before
1039         # children, this ought to work
1040         d.addCallback(lambda res: node.get_child_at_path(dir[:-1]))
1041         # then create the child directory
1042         d.addCallback(lambda parent: parent.create_empty_directory(dir[-1]))
1043         return d
1044
1045     def _upload_one_file(self, res, node, localdir, f):
1046         # get the parent. We can be sure this exists because we already
1047         # went through and created all the directories we require.
1048         localfile = os.path.join(localdir, *f)
1049         d = node.get_child_at_path(f[:-1])
1050         d.addCallback(self._upload_localfile, localfile, f[-1])
1051         return d
1052
1053
1054 class Manifest(rend.Page):
1055     docFactory = getxmlfile("manifest.xhtml")
1056     def __init__(self, dirnode, dirpath):
1057         self._dirnode = dirnode
1058         self._dirpath = dirpath
1059
1060     def dirpath_as_string(self):
1061         return "/" + "/".join(self._dirpath)
1062
1063     def render_title(self, ctx):
1064         return T.title["Manifest of %s" % self.dirpath_as_string()]
1065
1066     def render_header(self, ctx):
1067         return T.p["Manifest of %s" % self.dirpath_as_string()]
1068
1069     def data_items(self, ctx, data):
1070         return self._dirnode.build_manifest()
1071
1072     def render_row(self, ctx, refresh_cap):
1073         ctx.fillSlots("refresh_capability", refresh_cap)
1074         return ctx.tag
1075
1076 class ChildError:
1077     implements(inevow.IResource)
1078     def renderHTTP(self, ctx):
1079         req = inevow.IRequest(ctx)
1080         req.setResponseCode(http.BAD_REQUEST)
1081         req.setHeader("content-type", "text/plain")
1082         return self.text
1083
1084 def child_error(text):
1085     ce = ChildError()
1086     ce.text = text
1087     return ce, ()
1088
1089 class VDrive(rend.Page):
1090
1091     def __init__(self, node, name):
1092         self.node = node
1093         self.name = name
1094
1095     def get_child_at_path(self, path):
1096         if path:
1097             return self.node.get_child_at_path(path)
1098         return defer.succeed(self.node)
1099
1100     def locateChild(self, ctx, segments):
1101         req = inevow.IRequest(ctx)
1102         method = req.method
1103         path = segments
1104
1105         t = get_arg(req, "t", "")
1106         localfile = get_arg(req, "localfile", None)
1107         if localfile is not None:
1108             if localfile != os.path.abspath(localfile):
1109                 return NeedAbsolutePathError(), ()
1110         localdir = get_arg(req, "localdir", None)
1111         if localdir is not None:
1112             if localdir != os.path.abspath(localdir):
1113                 return NeedAbsolutePathError(), ()
1114         if localfile or localdir:
1115             if not ILocalAccess(ctx).local_access_is_allowed():
1116                 return LocalAccessDisabledError(), ()
1117             if req.getHost().host != LOCALHOST:
1118                 return NeedLocalhostError(), ()
1119         # TODO: think about clobbering/revealing config files and node secrets
1120
1121         replace = boolean_of_arg(get_arg(req, "replace", "true"))
1122
1123         if method == "GET":
1124             # the node must exist, and our operation will be performed on the
1125             # node itself.
1126             d = self.get_child_at_path(path)
1127             def file_or_dir(node):
1128                 if (IFileNode.providedBy(node)
1129                     or IMutableFileNode.providedBy(node)):
1130                     filename = "unknown"
1131                     if path:
1132                         filename = path[-1]
1133                     filename = get_arg(req, "filename", filename)
1134                     if t == "download":
1135                         if localfile:
1136                             # write contents to a local file
1137                             return LocalFileDownloader(node, localfile), ()
1138                         # send contents as the result
1139                         return FileDownloader(node, filename), ()
1140                     elif t == "":
1141                         # send contents as the result
1142                         return FileDownloader(node, filename), ()
1143                     elif t == "json":
1144                         return FileJSONMetadata(node), ()
1145                     elif t == "uri":
1146                         return FileURI(node), ()
1147                     elif t == "readonly-uri":
1148                         return FileReadOnlyURI(node), ()
1149                     else:
1150                         return child_error("bad t=%s" % t)
1151                 elif IDirectoryNode.providedBy(node):
1152                     if t == "download":
1153                         if localdir:
1154                             # recursive download to a local directory
1155                             return LocalDirectoryDownloader(node, localdir), ()
1156                         return child_error("t=download requires localdir=")
1157                     elif t == "":
1158                         # send an HTML representation of the directory
1159                         return Directory(self.name, node, path), ()
1160                     elif t == "json":
1161                         return DirectoryJSONMetadata(node), ()
1162                     elif t == "uri":
1163                         return DirectoryURI(node), ()
1164                     elif t == "readonly-uri":
1165                         return DirectoryReadonlyURI(node), ()
1166                     elif t == "manifest":
1167                         return Manifest(node, path), ()
1168                     elif t == 'rename-form':
1169                         return RenameForm(self.name, node, path), ()
1170                     else:
1171                         return child_error("bad t=%s" % t)
1172                 else:
1173                     return child_error("unknown node type")
1174             d.addCallback(file_or_dir)
1175         elif method == "POST":
1176             # the node must exist, and our operation will be performed on the
1177             # node itself.
1178             d = self.get_child_at_path(path)
1179             def _got_POST(node):
1180                 return POSTHandler(node, replace), ()
1181             d.addCallback(_got_POST)
1182         elif method == "DELETE":
1183             # the node must exist, and our operation will be performed on its
1184             # parent node.
1185             assert path # you can't delete the root
1186             d = self.get_child_at_path(path[:-1])
1187             def _got_DELETE(node):
1188                 return DELETEHandler(node, path[-1]), ()
1189             d.addCallback(_got_DELETE)
1190         elif method in ("PUT",):
1191             # the node may or may not exist, and our operation may involve
1192             # all the ancestors of the node.
1193             return PUTHandler(self.node, path, t, localfile, localdir, replace), ()
1194         else:
1195             return rend.NotFound
1196         return d
1197
1198 class URIPUTHandler(rend.Page):
1199     def renderHTTP(self, ctx):
1200         req = inevow.IRequest(ctx)
1201         assert req.method == "PUT"
1202
1203         t = get_arg(req, "t", "")
1204
1205         if t == "":
1206             # "PUT /uri", to create an unlinked file. This is like PUT but
1207             # without the associated set_uri.
1208             uploadable = FileHandle(req.content)
1209             d = IClient(ctx).upload(uploadable)
1210             # that fires with the URI of the new file
1211             return d
1212
1213         if t == "mkdir":
1214             # "PUT /uri?t=mkdir", to create an unlinked directory.
1215             d = IClient(ctx).create_empty_dirnode()
1216             d.addCallback(lambda dirnode: dirnode.get_uri())
1217             # XXX add redirect_to_result
1218             return d
1219
1220         req.setResponseCode(http.BAD_REQUEST)
1221         req.setHeader("content-type", "text/plain")
1222         return "/uri only accepts PUT and PUT?t=mkdir"
1223
1224 class URIPOSTHandler(rend.Page):
1225     def renderHTTP(self, ctx):
1226         req = inevow.IRequest(ctx)
1227         assert req.method == "POST"
1228
1229         t = get_arg(req, "t", "").strip()
1230
1231         if t in ("", "upload"):
1232             # "POST /uri", to create an unlinked file.
1233             fileobj = req.fields["file"].file
1234             uploadable = FileHandle(fileobj)
1235             d = IClient(ctx).upload(uploadable)
1236             # that fires with the URI of the new file
1237             return d
1238
1239         if t == "mkdir":
1240             # "POST /uri?t=mkdir", to create an unlinked directory.
1241             d = IClient(ctx).create_empty_dirnode()
1242             redirect = get_arg(req, "redirect_to_result", "false")
1243             if boolean_of_arg(redirect):
1244                 def _then_redir(res):
1245                     new_url = "uri/" + urllib.quote(res.get_uri())
1246                     req.setResponseCode(http.SEE_OTHER) # 303
1247                     req.setHeader('location', new_url)
1248                     req.finish()
1249                     return ''
1250                 d.addCallback(_then_redir)
1251             else:
1252                 d.addCallback(lambda dirnode: dirnode.get_uri())
1253             return d
1254
1255         req.setResponseCode(http.BAD_REQUEST)
1256         req.setHeader("content-type", "text/plain")
1257         err = "/uri accepts only PUT, PUT?t=mkdir, POST?t=upload, and POST?t=mkdir"
1258         return err
1259
1260
1261 class Root(rend.Page):
1262
1263     addSlash = True
1264     docFactory = getxmlfile("welcome.xhtml")
1265
1266     def locateChild(self, ctx, segments):
1267         client = IClient(ctx)
1268         req = inevow.IRequest(ctx)
1269
1270         segments = list(segments) # XXX HELP I AM YUCKY!
1271         while segments and not segments[-1]:
1272             segments.pop()
1273         if not segments:
1274             segments.append('')
1275         segments = tuple(segments)
1276         if segments:
1277             if segments[0] == "uri":
1278                 if len(segments) == 1 or segments[1] == '':
1279                     uri = get_arg(req, "uri", None)
1280                     if uri is not None:
1281                         there = url.URL.fromContext(ctx)
1282                         there = there.clear("uri")
1283                         there = there.child("uri").child(uri)
1284                         return there, ()
1285                 if len(segments) == 1:
1286                     # /uri
1287                     if req.method == "PUT":
1288                         # either "PUT /uri" to create an unlinked file, or
1289                         # "PUT /uri?t=mkdir" to create an unlinked directory
1290                         return URIPUTHandler(), ()
1291                     elif req.method == "POST":
1292                         # "POST /uri?t=upload&file=newfile" to upload an unlinked
1293                         # file or "POST /uri?t=mkdir" to create a new directory
1294                         return URIPOSTHandler(), ()
1295                 if len(segments) < 2:
1296                     return rend.NotFound
1297                 uri = segments[1]
1298                 d = defer.maybeDeferred(client.create_node_from_uri, uri)
1299                 d.addCallback(lambda node: VDrive(node, "from-uri"))
1300                 d.addCallback(lambda vd: vd.locateChild(ctx, segments[2:]))
1301                 def _trap_KeyError(f):
1302                     f.trap(KeyError)
1303                     return rend.FourOhFour(), ()
1304                 d.addErrback(_trap_KeyError)
1305                 return d
1306             elif segments[0] == "xmlrpc":
1307                 raise NotImplementedError()
1308         return rend.Page.locateChild(self, ctx, segments)
1309
1310     child_webform_css = webform.defaultCSS
1311     child_tahoe_css = nevow_File(resource_filename('allmydata.web', 'tahoe.css'))
1312
1313     child_provisioning = provisioning.ProvisioningTool()
1314
1315     def data_version(self, ctx, data):
1316         return get_package_versions_string()
1317
1318     def data_my_nodeid(self, ctx, data):
1319         return b32encode(IClient(ctx).nodeid).lower()
1320     def data_introducer_furl(self, ctx, data):
1321         return IClient(ctx).introducer_furl
1322     def data_connected_to_introducer(self, ctx, data):
1323         if IClient(ctx).connected_to_introducer():
1324             return "yes"
1325         return "no"
1326
1327     def data_helper_furl(self, ctx, data):
1328         try:
1329             uploader = IClient(ctx).getServiceNamed("uploader")
1330         except KeyError:
1331             return None
1332         furl, connected = uploader.get_helper_info()
1333         return furl
1334     def data_connected_to_helper(self, ctx, data):
1335         try:
1336             uploader = IClient(ctx).getServiceNamed("uploader")
1337         except KeyError:
1338             return "no" # we don't even have an Uploader
1339         furl, connected = uploader.get_helper_info()
1340         if connected:
1341             return "yes"
1342         return "no"
1343
1344     def data_num_peers(self, ctx, data):
1345         #client = inevow.ISite(ctx)._client
1346         client = IClient(ctx)
1347         return len(list(client.get_all_peerids()))
1348
1349     def data_peers(self, ctx, data):
1350         d = []
1351         client = IClient(ctx)
1352         for nodeid in sorted(client.get_all_peerids()):
1353             row = (b32encode(nodeid).lower(),)
1354             d.append(row)
1355         return d
1356
1357     def render_row(self, ctx, data):
1358         (nodeid_a,) = data
1359         ctx.fillSlots("peerid", nodeid_a)
1360         return ctx.tag
1361
1362     # this is a form where users can download files by URI
1363     def render_download_form(self, ctx, data):
1364         form = T.form(action="uri", method="get",
1365                       enctype="multipart/form-data")[
1366             T.fieldset[
1367             T.legend(class_="freeform-form-label")["download a file"],
1368             "URI of file to download: ",
1369             T.input(type="text", name="uri"), " ",
1370             "Filename to download as: ",
1371             T.input(type="text", name="filename"), " ",
1372             T.input(type="submit", value="download"),
1373             ]]
1374         return T.div[form]
1375
1376     # this is a form where users can create new directories
1377     def render_mkdir_form(self, ctx, data):
1378         form = T.form(action="uri", method="post",
1379                       enctype="multipart/form-data")[
1380             T.fieldset[
1381             T.legend(class_="freeform-form-label")["create a directory"],
1382             T.input(type="hidden", name="t", value="mkdir"),
1383             T.input(type="hidden", name="redirect_to_result", value="true"),
1384             T.input(type="submit", value="create"),
1385             ]]
1386         return T.div[form]
1387
1388
1389 class LocalAccess:
1390     implements(ILocalAccess)
1391     def __init__(self):
1392         self.local_access = False
1393     def local_access_is_allowed(self):
1394         return self.local_access
1395
1396 class WebishServer(service.MultiService):
1397     name = "webish"
1398
1399     def __init__(self, webport, nodeurl_path=None):
1400         service.MultiService.__init__(self)
1401         self.webport = webport
1402         self.root = Root()
1403         self.site = site = appserver.NevowSite(self.root)
1404         self.site.requestFactory = MyRequest
1405         self.allow_local = LocalAccess()
1406         self.site.remember(self.allow_local, ILocalAccess)
1407         s = strports.service(webport, site)
1408         s.setServiceParent(self)
1409         self.listener = s # stash it so the tests can query for the portnum
1410         self._started = defer.Deferred()
1411         if nodeurl_path:
1412             self._started.addCallback(self._write_nodeurl_file, nodeurl_path)
1413
1414     def allow_local_access(self, enable=True):
1415         self.allow_local.local_access = enable
1416
1417     def startService(self):
1418         service.MultiService.startService(self)
1419         # to make various services available to render_* methods, we stash a
1420         # reference to the client on the NevowSite. This will be available by
1421         # adapting the 'context' argument to a special marker interface named
1422         # IClient.
1423         self.site.remember(self.parent, IClient)
1424         # I thought you could do the same with an existing interface, but
1425         # apparently 'ISite' does not exist
1426         #self.site._client = self.parent
1427         self._started.callback(None)
1428
1429     def _write_nodeurl_file(self, junk, nodeurl_path):
1430         # what is our webport?
1431         s = self.listener
1432         if isinstance(s, internet.TCPServer):
1433             base_url = "http://localhost:%d" % s._port.getHost().port
1434         elif isinstance(s, internet.SSLServer):
1435             base_url = "https://localhost:%d" % s._port.getHost().port
1436         else:
1437             base_url = None
1438         if base_url:
1439             f = open(nodeurl_path, 'wb')
1440             # this file is world-readable
1441             f.write(base_url + "\n")
1442             f.close()
1443