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