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