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