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