source: sage/server/notebook/twist.py @ 17277:2394c94f25ac

Revision 17277:2394c94f25ac, 88.4 KB checked in by Jeroen Demeyer <jdemeyer@…>, 12 months ago (diff)

Trac #11310: Replace "raise Exception" by "raise RuntimeError?", replace "except:" by "except StandardError:"

Line 
1# This file is part of the OLD Sage notebook and is NOT actively developed,
2# maintained, or supported.  As of Sage v4.1.2, all notebook development has
3# moved to the separate Sage Notebook project:
4#
5# http://nb.sagemath.org/
6#
7# The new notebook is installed in Sage as an spkg (e.g., sagenb-0.3.spkg).
8#
9# Please visit the project's home page for more information, including directions on
10# obtaining the latest source code.  For notebook-related development and support,
11# please consult the sage-notebook discussion group:
12#
13# http://groups.google.com/group/sage-notebook
14r"""
15The Sage Notebook Twisted Web Server
16
17TESTS:
18
19It is important that this file never be imported by default on
20startup by Sage, since it is very expensive, since importing Twisted
21is expensive. This doctest verifies that twist.py isn't imported on
22startup.
23
24::
25
26    sage: os.system("sage -startuptime | grep twisted\.web2 1>/dev/null") != 0  # !=0 means not found
27    True
28"""
29
30#############################################################################
31#       Copyright (C) 2007 William Stein <wstein@gmail.com>
32#  Distributed under the terms of the GNU General Public License (GPL)
33#  The full text of the GPL is available at:
34#                  http://www.gnu.org/licenses/
35#############################################################################
36
37############################################################
38# WARNING: Potential source of confusion!!
39#
40# The following three global variables get set on
41# startup by the script that is generated by run_notebook.py
42
43notebook   = None
44OPEN_MODE  = None
45SID_COOKIE = None
46
47############################################################
48
49import os, shutil, time
50import bz2
51from cgi import escape
52
53from twisted.web2 import http, resource
54from twisted.web2 import static, http_headers
55from twisted.web2.filter import gzip
56import zipfile
57
58import css, js, keyboards
59
60
61from sage.server.notebook.template import template
62
63HISTORY_MAX_OUTPUT = 92*5
64HISTORY_NCOLS = 90
65
66from sage.misc.misc import SAGE_EXTCODE, SAGE_LOCAL, SAGE_DOC, walltime, tmp_filename, tmp_dir
67
68p = os.path.join
69css_path        = p(SAGE_EXTCODE, "notebook/css")
70image_path      = p(SAGE_EXTCODE, "notebook/images")
71javascript_path = p(SAGE_EXTCODE, "notebook/javascript")
72javascript_local_path = p(SAGE_LOCAL, "notebook/javascript")
73java_path       = p(SAGE_LOCAL, "java")
74
75# the list of users waiting to register
76waiting = {}
77
78# the user database
79from user_db import UserDatabase
80users = UserDatabase()
81
82_cols = None
83def word_wrap_cols():
84    global _cols
85    if _cols is None:
86        _cols = notebook.conf()['word_wrap_cols']
87    return _cols
88
89############################
90# Encoding data to go between the server and client
91############################
92SEP = '___S_A_G_E___'
93
94def encode_list(v):
95    return SEP.join([str(x) for x in v])
96
97
98
99############################
100# Notebook autosave.
101############################
102# save if make a change to notebook and at least some seconds have elapsed since last save.
103def init_updates():
104    global save_interval, idle_interval, last_save_time, last_idle_time
105
106    save_interval = notebook.conf()['save_interval']
107    idle_interval = notebook.conf()['idle_check_interval']
108    last_save_time = walltime()
109    last_idle_time = walltime()
110
111def notebook_save_check():
112    global last_save_time
113    t = walltime()
114    if t > last_save_time + save_interval:
115        notebook.save()
116        last_save_time = t
117
118def notebook_idle_check():
119    global last_idle_time
120    t = walltime()
121    if t > last_idle_time + idle_interval:
122        notebook.quit_idle_worksheet_processes()
123        last_idle_time = t
124
125def notebook_updates():
126    notebook_save_check()
127    notebook_idle_check()
128
129######################################################################################
130# RESOURCES
131######################################################################################
132def gzip_handler(request):
133    """
134    Add gzip compression to the request if it makes sense.
135    """
136    if request.host not in ('localhost', '127.0.0.1'):
137        request.addResponseFilter(gzip.gzipfilter, atEnd=True)
138
139############################
140# An error message
141############################
142def message(msg, cont=None):
143    template_dict = {'msg': msg, 'cont': cont}
144    return template('error_message.html', **template_dict)
145
146def HTMLResponse(*args, **kwds):
147    """
148    Returns an HTMLResponse object whose 'Content-Type' header has been set
149    to ``text/html; charset=utf-8``
150
151    EXAMPLES::
152
153        sage: from sage.server.notebook.twist import HTMLResponse
154        sage: response = HTMLResponse(stream='<html><head><title>Test</title></head><body>Test</body></html>')
155        sage: response.headers
156        <Headers: Raw: {'content-type': ['text/html; charset=utf-8']} Parsed: {'content-type': <RecalcNeeded>}>
157
158    """
159    response = http.Response(*args, **kwds)
160    response.headers.addRawHeader('Content-Type', 'text/html; charset=utf-8')
161    return response
162
163
164############################
165# Create a Sage worksheet from a latex2html'd file
166############################
167doc_worksheet_number = 0
168def doc_worksheet():
169    global doc_worksheet_number
170    wnames = notebook.worksheet_names()
171    name = 'doc_browser_%s'%doc_worksheet_number
172    doc_worksheet_number = doc_worksheet_number % notebook.conf()['doc_pool_size']
173    if name in wnames:
174        W = notebook.get_worksheet_with_name(name)
175        W.clear()
176    else:
177        W = notebook.create_new_worksheet(name, '_sage_', docbrowser=True)
178    W.set_is_doc_worksheet(True)
179    return W
180
181
182class WorksheetFile(resource.Resource):
183    addSlash = False
184
185    def __init__(self, path, username):
186        self.docpath = path
187        self.username = username
188
189    def render(self, ctx=None):
190        # Create a live Sage worksheet out of self.path and render it.
191        if not os.path.exists(self.docpath):
192            return HTMLResponse(stream = message('Document does not exist.'))
193
194        doc_page_html = open(self.docpath).read()
195        from docHTMLProcessor import SphinxHTMLProcessor
196        doc_page = SphinxHTMLProcessor().process_doc_html(doc_page_html)
197
198        title = extract_title(doc_page_html).replace('&mdash;','--')
199        doc_page = title + '\nsystem:sage\n\n' + doc_page
200
201        W = doc_worksheet()
202        W.edit_save(doc_page)
203
204        #FIXME: For some reason, an extra cell gets added
205        #so we remove it here.
206        cells = W.cell_list()
207        cells.pop()
208
209        s = notebook.html(worksheet_filename = W.filename(),
210                          username = self.username)
211
212        return HTMLResponse(stream=s)
213
214    # This earlier code does not convert all reference manual pages into live
215    # worksheets, apparently:
216    #    def childFactory(self, request, name):
217    #        path = self.docpath + '/' + name
218    #        if name.endswith('.html'):
219    #            return WorksheetFile(path, self.username)
220    #        else:
221    #            return static.File(path)
222
223    # Instead, we reimplement a lower-level method to detect, convert,
224    # and render live doc worksheets.  We also return appropriate
225    # paths for "special" directories accessed via ../'s.  See, e.g.,
226    # http://twistedmatrix.com/trac/browser/trunk/doc/web2/howto/
227    # about resources and object traversal.
228    def locateChild(self, request, segments):
229        path = os.path.join(self.docpath, *segments)
230
231        if segments[-1].endswith('.html'):
232            return WorksheetFile(path, self.username), ()
233
234        for special in ['_static', '_sources', '_images']:
235            if special in segments:
236                ind = segments.index(special)
237                path = os.path.join(self.docpath, *segments[ind:])
238                break
239
240        return static.File(path), ()
241
242
243############################
244# The documentation browsers
245############################
246
247DOC = os.path.abspath(SAGE_DOC + '/output/html/en/')
248DOC_PDF = os.path.abspath(SAGE_DOC + '/output/pdf')
249DOC_REF_MEDIA = os.path.abspath(SAGE_DOC + '/en/reference/media')
250
251class DocPDF(resource.Resource):
252    addSlash = True
253
254    def render(self, ctx):
255        return static.File('%s' % DOC_PDF)
256
257    def childFactory(self, request, name):
258        gzip_handler(request)
259        return static.File('%s/%s' % (DOC_PDF, name))
260
261class DocRefMedia(resource.Resource):
262    addSlash = True
263
264    def render(self, ctx):
265        return static.File('%s' % DOC_REF_MEDIA)
266
267    def childFactory(self, request, name):
268        gzip_handler(request)
269        return static.File('%s/%s' % (DOC_REF_MEDIA, name))
270
271class DocReference(resource.Resource):
272    addSlash = True
273    child_media = DocRefMedia()
274
275    def render(self, ctx):
276        return static.File('%s/reference/index.html' % DOC)
277
278    def childFactory(self, request, name):
279        gzip_handler(request)
280        return static.File('%s/reference/%s' % (DOC, name))
281
282class DocStatic(resource.Resource):
283    addSlash = True
284    child_reference = DocReference()
285
286    def render(self, ctx):
287        return static.File('%s/index.html'%DOC)
288
289    def childFactory(self, request, name):
290        gzip_handler(request)
291        return static.File('%s/%s'%(DOC,name))
292
293class DocLive(resource.Resource):
294    addSlash = True
295
296    def __init__(self, username):
297        self.username = username
298
299    def render(self, ctx):
300        return HTMLResponse(stream=message('nothing to see.'))
301
302    def childFactory(self, request, name):
303        gzip_handler(request)
304        return WorksheetFile('%s/%s'%(DOC,name), username = self.username)
305
306class Doc(resource.Resource):
307    addSlash = True
308    child_static = DocStatic()
309
310    def __init__(self, username):
311        self.username = username
312
313    def render(self, ctx):
314        s = notebook.html_doc(username = self.username)
315        return HTMLResponse(stream=s)
316
317    def childFactory(self, request, name):
318        if name == "live":
319            gzip_handler(request)
320            return DocLive(username = self.username)
321
322
323############################
324# SageTex browser
325############################
326SAGETEX_PATH = ""
327
328
329class SageTex(resource.Resource):
330    def __init__(self, username):
331        self.username = username
332
333    def render(self, ctx):
334        s = notebook.html_doc(username = self.username)
335        return HTMLResponse(stream=s)
336
337    def childFactory(self, request, name):
338        return WorksheetFile('%s/%s'%(SAGETEX_PATH,name),
339                             username = self.username)
340
341
342############################
343# The source code browser
344############################
345
346SRC = os.path.abspath(os.environ['SAGE_ROOT'] + '/devel/sage/sage/')
347
348class SourceBrowser(resource.Resource):
349    addSlash = True
350
351    def __init__(self, username):
352        self.username = username
353
354    def render(self, ctx):
355        return static.File(SRC)
356
357    def childFactory(self, request, name):
358        return Source('%s/%s'%(SRC,name), self.username)
359
360class Source(resource.Resource):
361    addSlash = True
362
363    def __init__(self, path, username):
364        self.path = path
365        self.username = username
366
367    def render(self, ctx):
368        filename = self.path
369        if os.path.isfile(filename):
370            if not os.path.exists(filename):
371                src = "No such file '%s'"%filename
372            else:
373                src = open(filename).read()
374            src = escape(src)
375            return HTMLResponse(stream = template('source_code.html', src_filename=self.path, src=src, username=self.username))
376        else:
377            return static.File(filename)
378
379    def childFactory(self, request, name):
380        return Source(self.path + '/' + name, self.username)
381
382
383############################
384# A New Worksheet
385############################
386class NewWorksheet(resource.Resource):
387    def __init__(self, username):
388        self.username = username
389
390    def render(self, ctx):
391        W = notebook.create_new_worksheet("Untitled", self.username)
392        return http.RedirectResponse('/home/'+W.filename())
393
394
395############################
396# Uploading a saved worksheet file
397############################
398
399def redirect(url):
400    return '<html><head><meta http-equiv="REFRESH" content="0; URL=%s"></head></html>'%url
401
402class Upload(resource.Resource):
403    def __init__(self, username):
404        self.username = username
405
406    def render(self, ctx):
407        return HTMLResponse(stream = template('upload.html', username=self.username))
408
409class UploadWorksheet(resource.PostableResource):
410    def __init__(self, username):
411        self.username = username
412
413    def render(self, ctx):
414        url = ctx.args['urlField'][0].strip()
415        dir = ''  # we will delete the directory below if it is used
416        if url != '':
417            # downloading a file from the internet
418            filename = tmp_filename()+".sws"
419        else:
420            # uploading a file from the user's computer
421            dir = tmp_dir()
422            filename = ctx.files['fileField'][0][0]
423            # Make tmp file in Sage temp directory
424            filename = '%s/%s'%(dir, filename)
425            f = file(filename,'wb')
426            # Then download to that file.
427            f.write(ctx.files['fileField'][0][2].read())
428            # TODO: Server blocking issues (?!)
429            f.close()
430
431
432        #We make a callback so that we can download a file remotely
433        #while allowing the server to still serve requests.
434        def callback(result):
435
436            if ctx.args.has_key('nameField'):
437                new_name = ctx.args['nameField'][0].strip()
438            else:
439                new_name = None
440
441            try:
442                try:
443
444                    if filename.endswith('.zip'):
445                        # Extract all the .sws files from a zip file.
446                        zip_file = zipfile.ZipFile(filename)
447                        sws_file = "%s/%s" % (dir, "tmp.sws")
448                        for sws in zip_file.namelist():
449                            if sws.endswith('.sws'):
450                                open(sws_file, 'w').write(zip_file.read(sws)) # 2.6 zip_file.extract(sws, sws_file)
451                                W = notebook.import_worksheet(sws_file, self.username)
452                                if new_name:
453                                    W.set_name("%s - %s" % (new_name, W.name()))
454                        return http.RedirectResponse('/')
455
456                    else:
457                        W = notebook.import_worksheet(filename, self.username)
458
459                except IOError, msg:
460                    print msg
461                    raise ValueError, "Unfortunately, there was an error uploading the worksheet.  It could be an old unsupported format or worse.  If you desperately need its contents contact the Google group sage-support and post a link to your worksheet.  Alternatively, an sws file is just a bzip2'd tarball; take a look inside!"
462                finally:
463                    # Clean up the temporarily uploaded filename.
464                    os.unlink(filename)
465                    # if a temp directory was created, we delete it now.
466                    if dir:
467                        shutil.rmtree(dir)
468
469            except ValueError, msg:
470                s = "Error uploading worksheet '%s'."%msg
471                return HTMLResponse(stream = message(s, '/'))
472
473            # If the user requested in the form a specific title for
474            # the worksheet set it.
475            if new_name:
476                W.set_name(new_name)
477
478            return http.RedirectResponse('/home/'+W.filename())
479
480        if url != '':
481            #We use the downloadPage function which returns a
482            #deferred which we are allowed to return to the server.
483            #The server waits until the download is finished and then runs
484            #the callback function specified.
485            from twisted.web.client import downloadPage
486            d = downloadPage(url, filename)
487            d.addCallback(callback)
488            return d
489        else:
490            #If we already have the file, then we
491            #can just return the result of callback which will
492            #give us the HTMLResponse.
493            return callback(None)
494
495
496############################
497# A resource attached to a given worksheet.
498#
499# This has the name of the worksheet and the
500# worksheet object itself set as attributes.
501# It's much better to do it once-for-all here
502# instead of doing it in the derived classes
503# over and over again.
504############################
505class WorksheetResource:
506    def __init__(self, name, username):
507        self.name = name
508        self.username = username
509        self.worksheet = notebook.get_worksheet_with_filename(name)
510        if not self.worksheet.is_published():
511            self.worksheet.set_active(username)
512        owner = self.worksheet.owner()
513        if owner != '_sage_' and username != owner:
514            if not self.worksheet.is_published():
515                if not username in self.worksheet.collaborators() and user_type(username) != 'admin':
516                    raise RuntimeError, "illegal worksheet access"
517
518    def id(self, ctx):
519        return int(ctx.args['id'][0])
520
521
522###############################################
523# Worksheet data -- a file that
524# is associated with a cell in some worksheet.
525# The file is stored on the filesystem.
526#      /home/worksheet_name/data/cell_number/filename
527##############################################
528class Worksheet_savedatafile(WorksheetResource, resource.PostableResource):
529    def render(self, ctx):
530        if ctx.args.has_key('button_save'):
531            E = ctx.args['textfield'][0]
532            filename = ctx.args['filename'][0]
533            dest = '%s/%s'%(self.worksheet.data_directory(), filename)
534            open(dest,'w').write(E)
535        return http.RedirectResponse('/home/'+self.worksheet.filename())
536
537class Worksheet_link_datafile(WorksheetResource, resource.Resource):
538    def render(self, ctx):
539        target_worksheet_filename = ctx.args['target'][0]
540        data_filename = ctx.args['filename'][0]
541        src = os.path.abspath(self.worksheet.data_directory() + '/' +data_filename)
542        target_ws =  notebook.get_worksheet_with_filename(target_worksheet_filename)
543        target = os.path.abspath(target_ws.data_directory() + '/' + data_filename)
544        if target_ws.owner() != self.username and not target_ws.user_is_collaborator(self.username):
545            return HTMLResponse(stream=message("illegal link attempt!"))
546        os.system('ln "%s" "%s"'%(src, target))
547        return http.RedirectResponse('/home/'+target_ws.filename() + '/datafile?name=%s'%data_filename)
548
549
550class Worksheet_upload_data(WorksheetResource, resource.Resource):
551    def render(self, ctx):
552        return HTMLResponse(stream = notebook.html_upload_data_window(self.worksheet, self.username))
553
554class Worksheet_do_upload_data(WorksheetResource, resource.PostableResource):
555    def render(self, ctx):
556        name = ''
557        if ctx.args.has_key('newField'):
558            newfield = ctx.args['newField'][0].strip()
559        else:
560            newfield = None
561
562        if ctx.args.has_key('nameField'):
563            name = ctx.args['nameField'][0].strip()
564
565        url = ctx.args['urlField'][0].strip()
566
567        if not name:
568            name = ctx.files['fileField'][0][0]
569
570        if not name:
571            name = newfield
572
573        if url and not name:
574            name = os.path.split(url)[-1]
575
576        dest = '%s/%s'%(self.worksheet.data_directory(), name)
577        response = http.RedirectResponse('/home/'+self.worksheet.filename() + '/datafile?name=%s'%name)
578
579        if url != '':
580            #Here we use twisted's downloadPage function which
581            #returns a deferred object.  We return the deferred to the server,
582            #and it will wait until the download has finished while
583            #still serving other requests.  At the end of the deferred
584            #callback chain should be the response that we wanted to return.
585            from twisted.web.client import downloadPage
586
587            #The callback just returns the response
588            def callback(result):
589                return response
590
591            d = downloadPage(url, dest)
592            d.addCallback(callback)
593            return d
594        elif newfield:
595            open(dest,'w').close()
596            return response
597        else:
598            f = file(dest,'wb')
599            f.write(ctx.files['fileField'][0][2].read())
600            f.close()
601            return response
602
603
604##############################################
605# Download or delete a data file
606##############################################
607
608class Worksheet_datafile(WorksheetResource, resource.Resource):
609    def render(self, ctx):
610        dir = os.path.abspath(self.worksheet.data_directory())
611        filename = ctx.args['name'][0]
612        if ctx.args.has_key('action'):
613            if ctx.args['action'][0] == 'delete':
614                path = '%s/%s'%(self.worksheet.data_directory(), filename)
615                os.unlink(path)
616                return HTMLResponse(stream = message("Successfully deleted '%s'"%filename,
617                                                      '/home/' + self.worksheet.filename()))
618        s = notebook.html_download_or_delete_datafile(self.worksheet, self.username, filename)
619        return HTMLResponse(stream=s)
620
621##############################################
622# Returns an object in the datafile directory
623##############################################
624class Worksheet_data(WorksheetResource, resource.Resource):
625    addSlash = True
626
627    def render(self, ctx):
628        dir = os.path.abspath(self.worksheet.data_directory())
629        if os.path.exists(dir):
630            return static.File(dir)
631        else:
632            return HTMLResponse(stream = message("No data files",'..'))
633
634    def childFactory(self, request, name):
635        dir = os.path.abspath(self.worksheet.data_directory())
636        return static.File('%s/%s'%(dir, name))
637
638
639class CellData(resource.Resource):
640    def __init__(self, worksheet, number):
641        self.worksheet = worksheet
642        self.number = number
643
644    def render(self, ctx):
645        return HTMLResponse(stream = message("No data file (%s)"%self.number,'..'))
646
647    def childFactory(self, request, name):
648        dir = self.worksheet.directory()
649        path = '%s/cells/%s/%s'%(dir, self.number, name)
650        request.setLastModified(os.stat(filename).st_mtime)
651        return static.File(path)
652
653
654class Worksheet_cells(WorksheetResource, resource.Resource):
655    addSlash = True
656
657    def render(self, ctx):
658        return static.File(self.worksheet.cells_directory())
659
660    def childFactory(self, request, segment):
661        return static.File(self.worksheet.cells_directory() + segment)
662    #return CellData(self.worksheet, segment)
663
664
665
666
667########################################################
668# Use this to wrap a worksheet operation in a confirmation
669# request.  See WorksheetDelete and WorksheetAdd for
670# examples.
671########################################################
672## class FastRedirect(resource.Resource):
673##     def __init__(self, dest):
674##         self.dest = dest
675##     def render(self, ctx):
676##         return http.RedirectResponse(self.dest)
677## class YesNo(resource.Resource):
678##     addSlash = True
679
680##     def __init__(self, mesg, yes, no):
681##         self.mesg = mesg
682##         self.yes = yes
683##         self.no  = no
684
685##     def render(self, ctx):
686##         from sage.server.notebook.template import yes_no_template
687##         lt = yes_no_template(mesg=self.mesg)
688##         return HTMLResponse(stream = lt)
689
690##         s = '%s<br>'%self.mesg
691##         s += '<a href="yes">Yes</a> or <a href="no">No</a>'
692##         return HTMLResponse(stream = message(s))
693
694##     def childFactory(self, request, op):
695##         if op == 'yes':
696##             return FastRedirect(self.yes())
697##         elif op == 'no':
698##             return FastRedirect(self.no())
699
700
701########################################################
702# keep alive
703########################################################
704
705class Worksheet_alive(WorksheetResource, resource.Resource):
706    def render(self, ctx):
707        #self.worksheet.ping(self.username)
708        self.worksheet.ping(username=None)  # None so doesn't save a revision
709        return HTMLResponse(stream = '')
710
711########################################################
712# Worksheet configuration.
713########################################################
714class Worksheet_conf(WorksheetResource, resource.Resource):
715    def render(self, ctx):
716        conf = self.worksheet.conf()
717        s = str(conf)
718        # TODO: This should be a form that allows for configuring all options
719        # of a given worksheet, saves the result,
720        return HTMLResponse(stream = s)
721
722class TrivialResource(resource.Resource):
723    def render(self, ctx):
724        return HTMLResponse(stream="success")
725
726class Worksheet_system(WorksheetResource, resource.Resource):
727    def childFactory(self, request, system):
728        self.worksheet.set_system(system)
729        return TrivialResource()
730
731class Worksheet_pretty_print(WorksheetResource, resource.Resource):
732    def childFactory(self, request, enable):
733        self.worksheet.set_pretty_print(enable)
734        return TrivialResource()
735
736
737
738########################################################
739# Cell introspection
740########################################################
741class Worksheet_introspect(WorksheetResource, resource.PostableResource):
742    """
743    Cell introspection. This is called when the user presses the tab
744    key in the browser in order to introspect.
745    """
746    def render(self, ctx):
747        try:
748            id = int(ctx.args['id'][0])
749        except (KeyError,TypeError):
750            return HTMLResponse(stream = 'Error in introspection -- invalid cell id.')
751        try:
752            before_cursor = ctx.args['before_cursor'][0]
753        except KeyError:
754            before_cursor = ''
755        try:
756            after_cursor = ctx.args['after_cursor'][0]
757        except KeyError:
758            after_cursor = ''
759        C = self.worksheet.get_cell_with_id(id)
760        C.evaluate(introspect=[before_cursor, after_cursor])
761        return HTMLResponse(stream = encode_list([C.next_id(),'introspect',id]))
762
763########################################################
764# Edit the entire worksheet
765########################################################
766class Worksheet_edit(WorksheetResource, resource.Resource):
767    """
768    Return a window that allows the user to edit the text of the
769    worksheet with the given filename.
770    """
771    def render(self, ctx):
772        s = notebook.html_edit_window(self.worksheet, self.username)
773        return HTMLResponse(stream = s)
774
775########################################################
776# Plain text log view of worksheet
777########################################################
778class Worksheet_text(WorksheetResource, resource.Resource):
779    """
780    Return a window that allows the user to edit the text of the
781    worksheet with the given filename.
782    """
783    def render(self, ctx):
784        s = notebook.html_plain_text_window(self.worksheet, self.username)
785        return HTMLResponse(stream = s)
786
787########################################################
788# Copy a worksheet
789########################################################
790class Worksheet_copy(WorksheetResource, resource.PostableResource):
791    def render(self, ctx):
792        W = notebook.copy_worksheet(self.worksheet, self.username)
793        if 'no_load' in ctx.args:
794            return http.StatusResponse(200, '')
795        else:
796            return http.RedirectResponse('/home/' + W.filename())
797
798########################################################
799# Get a copy of a published worksheet and start editing it
800########################################################
801class Worksheet_edit_published_page(WorksheetResource, resource.Resource):
802    def render(self, ctx):
803        if user_type(self.username) == 'guest':
804            return HTMLResponse(stream = message(
805                'You must <a href="/">login first</a> in order to edit this worksheet.'))
806        ws = self.worksheet.worksheet_that_was_published()
807        if ws.owner() == self.username:
808            W = ws
809        else:
810            W = notebook.copy_worksheet(self.worksheet, self.username)
811            W.set_name(self.worksheet.name())
812        return http.RedirectResponse('/home/' + W.filename())
813
814########################################################
815# Save a worksheet
816########################################################
817class Worksheet_save(WorksheetResource, resource.PostableResource):
818    """
819    Save the contents of a worksheet after editing it in plain-text
820    edit mode.
821    """
822    def render(self, ctx):
823        if ctx.args.has_key('button_save'):
824            E = ctx.args['textfield'][0]
825            self.worksheet.edit_save(E)
826            self.worksheet.record_edit(self.username)
827        return http.RedirectResponse('/home/'+self.worksheet.filename())
828
829
830class Worksheet_save_snapshot(WorksheetResource, resource.PostableResource):
831    """
832    Save a snapshot of a worksheet.
833    """
834    def render(self, ctx):
835        self.worksheet.save_snapshot(self.username)
836        return HTMLResponse(stream="saved")
837
838class Worksheet_save_and_quit(WorksheetResource, resource.PostableResource):
839    """
840    Save a snapshot of a worksheet and quit.
841    """
842    def render(self, ctx):
843        self.worksheet.save_snapshot(self.username)
844        self.worksheet.quit()
845        return HTMLResponse(stream="saved")
846
847class Worksheet_discard_and_quit(WorksheetResource, resource.PostableResource):
848    """
849    Save a snapshot of a worksheet and quit.
850    """
851    def render(self, ctx):
852        self.worksheet.revert_to_last_saved_state()
853        self.worksheet.quit()
854        return HTMLResponse(stream="saved")
855
856class Worksheet_revert_to_last_saved_state(WorksheetResource, resource.PostableResource):
857    def render(self, ctx):
858        self.worksheet.revert_to_last_saved_state()
859        return HTMLResponse(stream="reverted")
860
861class Worksheet_save_and_close(WorksheetResource, resource.PostableResource):
862    """
863    Save a snapshot of a worksheet then quit it.
864    """
865    def render(self, ctx):
866        self.worksheet.save_snapshot(self.username)
867        self.worksheet.quit()
868        return HTMLResponse(stream="saved")
869
870########################################################
871# Collaborate with others
872########################################################
873class Worksheet_share(WorksheetResource, resource.Resource):
874    def render(self, ctx):
875        s = notebook.html_share(self.worksheet, self.username)
876        return HTMLResponse(stream = s)
877
878class Worksheet_invite_collab(WorksheetResource, resource.PostableResource):
879    def render(self, ctx):
880        if not ctx.args.has_key('collaborators'):
881            v = []
882        else:
883            collab = ctx.args['collaborators'][0]
884            v = [x.strip() for x in collab.split(',')]
885        self.worksheet.set_collaborators(v)
886        return http.RedirectResponse('.')
887
888
889#################################
890# Revisions
891#################################
892
893class PublishWorksheetRevision(resource.Resource):
894    def __init__(self, worksheet, rev):
895        self.worksheet = worksheet
896        self.rev = rev
897
898    def render(self, ctx):
899        W = notebook.publish_worksheet(self.worksheet, self.username)
900        txt = open(self.worksheet.get_snapshot_text_filename(self.rev)).read()
901        W.delete_cells_directory()
902        W.edit_save(txt)
903        return http.RedirectResponse('/home/'+W.filename())
904
905class RevertToWorksheetRevision(resource.Resource):
906    def __init__(self, worksheet, rev):
907        self.worksheet = worksheet
908        self.rev = rev
909
910    def render(self, ctx):
911        self.worksheet.save_snapshot(self.username)
912        txt = open(self.worksheet.get_snapshot_text_filename(self.rev)).read()
913        self.worksheet.delete_cells_directory()
914        self.worksheet.edit_save(txt)
915        return http.RedirectResponse('/home/'+self.worksheet.filename())
916
917def worksheet_revision_publish(worksheet, rev, username):
918    W = notebook.publish_worksheet(worksheet, username)
919    txt = bz2.decompress(open(worksheet.get_snapshot_text_filename(rev)).read())
920    W.delete_cells_directory()
921    W.edit_save(txt)
922    return http.RedirectResponse('/home/'+W.filename())
923
924def worksheet_revision_revert(worksheet, rev, username):
925    worksheet.save_snapshot(username)
926    txt = bz2.decompress(open(worksheet.get_snapshot_text_filename(rev)).read())
927    worksheet.delete_cells_directory()
928    worksheet.edit_save(txt)
929    return http.RedirectResponse('/home/'+worksheet.filename())
930
931
932class Worksheet_revisions(WorksheetResource, resource.PostableResource):
933    """
934    Show a list of revisions of this worksheet.
935    """
936    def render(self, ctx):
937        if not ctx.args.has_key('action'):
938            if ctx.args.has_key('rev'):
939                rev = ctx.args['rev'][0]
940                s = notebook.html_specific_revision(self.username, self.worksheet, rev)
941            else:
942                s = notebook.html_worksheet_revision_list(self.username, self.worksheet)
943        else:
944            rev = ctx.args['rev'][0]
945            action = ctx.args['action'][0]
946            if action == 'revert':
947                return worksheet_revision_revert(self.worksheet, rev, self.username)
948            elif action == 'publish':
949                return worksheet_revision_publish(self.worksheet, rev, self.username)
950            else:
951                s = message('Error')
952        return HTMLResponse(stream = s)
953
954
955########################################################
956# Worksheet/User/Notebooks settings and configuration
957########################################################
958
959class Worksheet_input_settings(WorksheetResource, resource.PostableResource):
960    def render(self, ctx):
961        if ctx.args.has_key('button_cancel'):
962            return http.RedirectResponse('/home/'+self.worksheet.filename())
963        if user_type(self.username) == 'admin' or \
964               self.worksheet.owner() == self.username \
965               or self.worksheet.user_is_collaborator(self.username):
966            system = ctx.args['system'][0].strip().lower()
967            self.worksheet.set_system(system)
968            if system != 'sage':
969                post = ' (%s)'%system
970                n = self.worksheet.name()
971                i = n.rfind('(')
972                if i != -1:
973                    j = n.rfind(')')
974                    if j != -1:
975                        n = n[:i]
976                n = n.strip() + post
977                self.worksheet.set_name(n)
978            return http.RedirectResponse('/home/'+ self.worksheet.filename())
979        else:
980            s = 'You must be the owner of this worksheet to configure it.'
981            return HTMLResponse(stream = message(s))
982
983class Worksheet_settings(WorksheetResource, resource.Resource):
984    def render(self, ctx):
985        if self.worksheet.owner() != self.username:
986            s = message('You must be the owner of this worksheet to configure it.')
987        else:
988            s = notebook.html_worksheet_settings(self.worksheet, self.username)
989        return HTMLResponse(stream = s)
990
991class ProcessUserSettings(resource.PostableResource):
992    def render(self, ctx):
993        pass
994
995#class UserSettings(resource.Resource):
996#    child_process = ProcessUserSettings()
997#
998#    def __init__(self, username):
999#        self.username = username
1000#
1001#    def render(self, ctx):
1002#        s = notebook.html_user_settings(self.username)
1003#        return HTMLResponse(stream = s)
1004
1005class ProcessNotebookSettings(resource.PostableResource):
1006    def render(self, ctx):
1007        pass
1008
1009class NotebookSettings(resource.Resource):
1010    child_process = ProcessNotebookSettings()
1011
1012    def __init__(self, username):
1013        self.username = username
1014
1015    def render(self, ctx):
1016        if user_type(self.username) != 'admin':
1017            s = message('You must an admin to configure the notebook.')
1018        else:
1019            s = notebook.html_notebook_settings()
1020        return HTMLResponse(stream = s)
1021
1022class SettingsPage(resource.PostableResource):
1023    def __init__(self, username):
1024        self.username = username
1025
1026    def render(self, request):
1027        error = None
1028        redirect_to_home = None
1029        redirect_to_logout  = None
1030        if 'autosave' in request.args:
1031            notebook.user(self.username)['autosave_interval'] = int(request.args['autosave'][0]) * 60
1032            redirect_to_home = True
1033
1034        if 'Newpass' in request.args or 'RetypePass' in request.args:
1035            if not 'Oldpass' in request.args:
1036                error = 'Old password not given'
1037            elif not notebook.user(self.username).password_is(request.args['Oldpass'][0]):
1038                error = 'Incorrect password given'
1039            elif not 'Newpass' in request.args:
1040                error = 'New password not given'
1041            elif not 'RetypePass' in request.args:
1042                error = 'Please type in new password again.'
1043            elif request.args['Newpass'][0] != request.args['RetypePass'][0]:
1044                error = 'The passwords you entered do not match.'
1045
1046            if not error: #webbrowser may auto fill in "old password" even though the user may not want to change her password
1047                notebook.change_password(self.username, request.args['Newpass'][0])
1048                redirect_to_logout = True
1049        if notebook.conf()['email']:
1050            if 'Newemail' in request.args:
1051                notebook.user(self.username).set_email(request.args['Newemail'][0])
1052                redirect_to_home = True
1053
1054        if error:
1055            return HTMLResponse(stream=message(error, '/settings'))
1056
1057        if redirect_to_logout:
1058            return http.RedirectResponse('/logout')
1059
1060        if redirect_to_home:
1061            return http.RedirectResponse('/home/%s' % self.username)
1062
1063        template_dict = {}
1064        template_dict['username'] = self.username
1065        template_dict['autosave_intervals'] = ((i, ' selected') if notebook.user(self.username)['autosave_interval']/60 == i else (i, '') for i in range(1, 10, 2))
1066        template_dict['email'] = notebook.conf()['email']
1067        if template_dict['email']:
1068            template_dict['email_address'] = 'None' if not notebook.user(self.username)._User__email else notebook.user(self.username)._User__email
1069            template_dict['email_confirmed'] = 'Not confirmed' if not notebook.user(self.username).is_email_confirmed() else 'Confirmed'
1070        template_dict['admin'] = user_type(self.username) == 'admin'
1071        return HTMLResponse(stream=template('account_settings.html', **template_dict))
1072
1073########################################################
1074# Set output type of a cell
1075########################################################
1076class Worksheet_set_cell_output_type(WorksheetResource, resource.PostableResource):
1077    """
1078    Set the output type of the cell.
1079
1080    This enables the type of output cell, such as to allowing wrapping
1081    for output that is very long.
1082    """
1083    def render(self, ctx):
1084        id = self.id(ctx)
1085        typ = ctx.args['type'][0]
1086        W = self.worksheet
1087        W.get_cell_with_id(id).set_cell_output_type(typ)
1088        return HTMLResponse(stream = '')
1089
1090########################################################
1091# The new cell command: /home/worksheet/new_cell?id=number
1092########################################################
1093class Worksheet_new_cell_before(WorksheetResource, resource.PostableResource):
1094    """
1095    Adds a new cell before a given cell.
1096    """
1097    def render(self, ctx):
1098        id = self.id(ctx)
1099        if not ctx.args.has_key('input'):
1100            input = ''
1101        else:
1102            input = ctx.args['input'][0]
1103
1104        cell = self.worksheet.new_cell_before(id, input=input)
1105        s = encode_list([cell.id(), cell.html(div_wrap=False), id])
1106        return HTMLResponse(stream = s)
1107
1108class Worksheet_new_text_cell_before(WorksheetResource, resource.PostableResource):
1109    """
1110    Adds a new cell before a given cell.
1111    """
1112    def render(self, ctx):
1113        id = self.id(ctx)
1114        if not ctx.args.has_key('input'):
1115            input = ''
1116        else:
1117            input = ctx.args['input'][0]
1118
1119        cell = self.worksheet.new_text_cell_before(id, input=input)
1120        s = encode_list([cell.id(), cell.html(editing=True), id])
1121        return HTMLResponse(stream = s)
1122
1123
1124class Worksheet_new_cell_after(WorksheetResource, resource.PostableResource):
1125    """
1126    Adds a new cell after a given cell.
1127    """
1128    def render(self, ctx):
1129        id = self.id(ctx)
1130        if not ctx.args.has_key('input'):
1131            input = ''
1132        else:
1133            input = ctx.args['input'][0]
1134        cell = self.worksheet.new_cell_after(id, input=input)
1135        s = encode_list([cell.id(), cell.html(div_wrap=False), id])
1136        return HTMLResponse(stream = s)
1137
1138class Worksheet_new_text_cell_after(WorksheetResource, resource.PostableResource):
1139    """
1140    Adds a new text cell after a given cell.
1141    """
1142    def render(self, ctx):
1143        id = self.id(ctx)
1144        if not ctx.args.has_key('input'):
1145            input = ''
1146        else:
1147            input = ctx.args['input'][0]
1148
1149        cell = self.worksheet.new_text_cell_after(id, input=input)
1150        s = encode_list([cell.id(), cell.html(editing=True), id])
1151        return HTMLResponse(stream = s)
1152
1153
1154########################################################
1155# The delete cell command: /home/worksheet/delete_cell?id=number
1156########################################################
1157class Worksheet_delete_cell(WorksheetResource, resource.PostableResource):
1158    """
1159    Deletes a notebook cell.
1160
1161    If there is only one cell left in a given worksheet, the request to
1162    delete that cell is ignored because there must be a least one cell
1163    at all times in a worksheet. (This requirement exists so other
1164    functions that evaluate relative to existing cells will still work,
1165    and so one can add new cells.)
1166    """
1167    def render(self, ctx):
1168        id = self.id(ctx)
1169        W = self.worksheet
1170        if len(W.compute_cell_id_list()) <= 1:
1171            s = 'ignore'
1172        else:
1173            prev_id = W.delete_cell_with_id(id)
1174            s = encode_list(['delete', id, prev_id, W.cell_id_list()])
1175        return HTMLResponse(stream = s)
1176
1177
1178############################
1179# Get the latest update on output appearing
1180# in a given output cell.
1181############################
1182class Worksheet_cell_update(WorksheetResource, resource.PostableResource):
1183    def render(self, ctx):
1184        id = self.id(ctx)
1185
1186        worksheet = self.worksheet
1187
1188        # update the computation one "step".
1189        worksheet.check_comp()
1190
1191        # now get latest status on our cell
1192        status, cell = worksheet.check_cell(id)
1193
1194        if status == 'd':
1195            new_input = cell.changed_input_text()
1196            out_html = cell.output_html()
1197            H = "Worksheet '%s' (%s)\n"%(worksheet.name(), time.strftime("%Y-%m-%d at %H:%M",time.localtime(time.time())))
1198            H += cell.edit_text(ncols=HISTORY_NCOLS, prompts=False,
1199                                max_out=HISTORY_MAX_OUTPUT)
1200            notebook.add_to_user_history(H, self.username)
1201        else:
1202            new_input = ''
1203            out_html = ''
1204
1205        if cell.interrupted():
1206            inter = 'true'
1207        else:
1208            inter = 'false'
1209
1210        raw = cell.output_text(raw=True).split("\n")
1211        if "Unhandled SIGSEGV" in raw:
1212            inter = 'restart'
1213            print "Segmentation fault detected in output!"
1214
1215        msg = '%s%s %s'%(status, cell.id(),
1216                       encode_list([cell.output_text(html=True),
1217                                    cell.output_text(word_wrap_cols(), html=True),
1218                                    out_html,
1219                                    new_input,
1220                                    inter,
1221                                    cell.introspect_html()]))
1222
1223        # There may be more computations left to do, so start one if there is one.
1224        worksheet.start_next_comp()
1225
1226        return HTMLResponse(stream=msg)
1227
1228
1229class Worksheet_eval(WorksheetResource, resource.PostableResource):
1230    """
1231    Evaluate a worksheet cell.
1232
1233    If the request is not authorized (the requester did not enter the
1234    correct password for the given worksheet), then the request to
1235    evaluate or introspect the cell is ignored.
1236
1237    If the cell contains either 1 or 2 question marks at the end (not
1238    on a comment line), then this is interpreted as a request for
1239    either introspection to the documentation of the function, or the
1240    documentation of the function and the source code of the function
1241    respectively.
1242    """
1243    def render(self, ctx):
1244        id = self.id(ctx)
1245        if not ctx.args.has_key('input'):
1246            input_text = ''
1247        else:
1248            input_text = ctx.args['input'][0]
1249            input_text = input_text.replace('\r\n', '\n')   # DOS
1250
1251        W = self.worksheet
1252        owner = W.owner()
1253        if owner != '_sage_':
1254            if W.owner() != self.username and not (self.username in W.collaborators()):
1255               return InvalidPage(msg = "can't evaluate worksheet cells", username = self.username)
1256        cell = W.get_cell_with_id(id)
1257
1258        cell.set_input_text(input_text)
1259
1260        if ctx.args.has_key('save_only') and ctx.args['save_only'][0] == '1':
1261            notebook_updates()
1262            return HTMLResponse(stream='')
1263        elif ctx.args.has_key('text_only') and ctx.args['text_only'][0] == '1':
1264            notebook_updates()
1265            return HTMLResponse(stream=encode_list([str(id), cell.html()]))
1266        else:
1267            newcell = int(ctx.args['newcell'][0])  # whether to insert a new cell or not
1268
1269        cell.evaluate(username = self.username)
1270
1271        if cell.is_last():
1272            new_cell = W.append_new_cell()
1273            s = encode_list([new_cell.id(), 'append_new_cell', new_cell.html(div_wrap=False)])
1274        elif newcell:
1275            new_cell = W.new_cell_after(id)
1276            s = encode_list([new_cell.id(), 'insert_cell', new_cell.html(div_wrap=False), str(id)])
1277        else:
1278            s = encode_list([cell.next_id(), 'no_new_cell', str(id)])
1279
1280        notebook_updates()
1281        return HTMLResponse(stream=s)
1282
1283
1284########################################################
1285# Publication and rating of a worksheet
1286########################################################
1287
1288class Worksheet_publish(WorksheetResource, resource.Resource):
1289    """
1290    This is a child resource of the Worksheet resource. It provides a
1291    frontend to the management of worksheet publication. This management
1292    functionality includes initializational of publication,
1293    re-publication, automated publication when a worksheet saved, and
1294    ending of publication.
1295    """
1296    addSlash = True
1297
1298    def render(self, ctx):
1299        # Publishes worksheet and also sets worksheet to be published automatically when saved
1300        if 'yes' in ctx.args and 'auto' in ctx.args:
1301            notebook.publish_worksheet(self.worksheet, self.username)
1302            self.worksheet.set_auto_publish()
1303            return http.RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
1304        # Just publishes worksheet
1305        elif 'yes' in ctx.args:
1306            notebook.publish_worksheet(self.worksheet, self.username)
1307            return http.RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
1308        # Stops publication of worksheet
1309        elif 'stop' in ctx.args:
1310            notebook.delete_worksheet(self.worksheet.published_version().filename())
1311            return http.RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
1312        # Re-publishes worksheet
1313        elif 're' in ctx.args:
1314            W = notebook.publish_worksheet(self.worksheet, self.username)
1315            return http.RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
1316        # Sets worksheet to be published automatically when saved
1317        elif 'auto' in ctx.args:
1318            self.worksheet.set_auto_publish()
1319            return http.RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
1320        # Returns boolean of "Is this worksheet set to be published automatically when saved?"
1321        elif 'is_auto' in ctx.args:
1322            return HTMLResponse(stream=str(self.worksheet.is_auto_publish()))
1323        # Returns the publication page
1324        else:
1325            # Page for when worksheet already published
1326            if self.worksheet.has_published_version():
1327                addr = 'http%s://' % ('' if not notebook.secure else 's')
1328                addr += notebook.address
1329                addr += ':%s' % notebook.port
1330                addr += '/home/' + self.worksheet.published_version().filename()
1331                dtime = self.worksheet.published_version().date_edited()
1332                return HTMLResponse(stream=notebook.html_afterpublish_window(self.worksheet, self.username, addr, dtime))
1333            # Page for when worksheet is not already published
1334            else:
1335                return HTMLResponse(stream=notebook.html_beforepublish_window(self.worksheet, self.username))
1336
1337
1338class Worksheet_rating_info(WorksheetResource, resource.Resource):
1339    def render(self, ctx):
1340        s = self.worksheet.html_ratings_info()
1341        return HTMLResponse(stream=message('''
1342        <h2 align=center>Ratings for %s</h2>
1343        <h3 align=center><a href='/home/%s'>Go to the worksheet.</a>
1344        <br><br>
1345        <table width=70%%align=center border=1 cellpadding=10 cellspacing=0>
1346        <tr bgcolor="#7799bb"><td width=30em>User</td><td width=10em align=center>Rating</td><td width=10em align=center width=60em>Comment</td></tr>
1347        %s
1348        </table>
1349        <br><br>
1350        '''%(self.worksheet.name(), self.worksheet.filename(), s)))
1351
1352
1353class Worksheet_rate(WorksheetResource, resource.Resource):
1354    def render(self, ctx):
1355        ret = '/home/' + self.worksheet.filename()
1356        #if self.worksheet.is_rater(self.username):
1357        #    return HTMLResponse(stream=message("You have already rated the worksheet <i><b>%s</b></i>."%self.worksheet.name(), ret))
1358        if user_type(self.username) == "guest":
1359            return HTMLResponse(stream = message(
1360                'You must <a href="/">login first</a> in order to rate this worksheet.', ret))
1361
1362        rating = int(ctx.args['rating'][0])
1363        if rating < 0 or rating >= 5:
1364            return HTMLResponse(stream = message(
1365                "Gees -- You can't fool the rating system that easily!", ret))
1366        comment = ctx.args['comment'][0]
1367        self.worksheet.rate(rating, comment, self.username)
1368        return HTMLResponse(stream=message("""
1369        Thank you for rating the worksheet <b><i>%s</i></b>!
1370        You can <a href="rating_info">see all ratings of this worksheet.</a>
1371        """%self.worksheet.name(), '/pub/'))
1372
1373
1374########################################################
1375# Downloading, moving around, renaming, etc.
1376########################################################
1377
1378
1379class Worksheet_download(WorksheetResource, resource.Resource):
1380    def childFactory(self, request, name):
1381        worksheet_name = self.name
1382        filename = tmp_filename() + '.sws'
1383        try:
1384            notebook.export_worksheet(worksheet_name, filename)
1385        except KeyError:
1386            return HTMLResponse(stream=message('No such worksheet.'))
1387        r = open(filename, 'rb').read()
1388        os.unlink(filename)
1389        return static.Data(r, 'application/sage')
1390        #return static.File(filename)
1391
1392class DownloadWorksheets(resource.Resource):
1393
1394    def __init__(self, username):
1395        self.username = username
1396
1397    def render(self, ctx):
1398        print type(ctx)
1399        worksheet_names = set()
1400        if ctx.args.has_key('filenames'):
1401            sep = ctx.args['sep'][0]
1402            worksheets = [notebook.get_worksheet_with_filename(x.strip()) for x in ctx.args['filenames'][0].split(sep) if len(x.strip()) > 0]
1403        else:
1404            worksheets = notebook.worksheet_list_for_user(self.username)
1405        zip_filename = tmp_filename() + ".zip"
1406        zip = zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_STORED)
1407        for worksheet in worksheets:
1408            sws_filename = tmp_filename() + '.sws'
1409            notebook.export_worksheet(worksheet.filename(), sws_filename)
1410            entry_name = worksheet.name()
1411            if entry_name in worksheet_names:
1412                i = 2
1413                while ("%s_%s" % (entry_name, i)) in worksheet_names:
1414                    i += 1
1415                entry_name = "%s_%s" % (entry_name, i)
1416            zip.write(sws_filename, entry_name + ".sws")
1417            os.unlink(sws_filename)
1418        zip.close()
1419        r = open(zip_filename, 'rb').read()
1420        os.unlink(zip_filename)
1421        return static.Data(r, 'application/zip')
1422
1423class Worksheet_rename(WorksheetResource, resource.PostableResource):
1424    def render(self, ctx):
1425        self.worksheet.set_name(ctx.args['name'][0])
1426        return HTMLResponse(stream='done')
1427
1428class Worksheet_restart_sage(WorksheetResource, resource.Resource):
1429    def render(self, ctx):
1430        # TODO -- this must not block long (!)
1431        self.worksheet.restart_sage()
1432        return HTMLResponse(stream='done')
1433
1434class Worksheet_quit_sage(WorksheetResource, resource.Resource):
1435    def render(self, ctx):
1436        # TODO -- this must not block long (!)
1437        self.worksheet.quit()
1438        return HTMLResponse(stream='done')
1439
1440class Worksheet_interrupt(WorksheetResource, resource.Resource):
1441    def render(self, ctx):
1442        # TODO -- this must not block long (!)
1443        s = self.worksheet.interrupt()
1444        return HTMLResponse(stream='ok' if s else 'failed')
1445
1446class Worksheet_plain(WorksheetResource, resource.Resource):
1447    def render(self, ctx):
1448        s = notebook.plain_text_worksheet_html(self.name)
1449        return HTMLResponse(stream=s)
1450
1451class Worksheet_hide_all(WorksheetResource, resource.Resource):
1452    def render(self, ctx):
1453        self.worksheet.hide_all()
1454        return HTMLResponse(stream='success')
1455
1456class Worksheet_show_all(WorksheetResource, resource.Resource):
1457    def render(self, ctx):
1458        self.worksheet.show_all()
1459        return HTMLResponse(stream='success')
1460
1461
1462# Delete all the output of cells in a worksheet.
1463class Worksheet_delete_all_output(WorksheetResource, resource.Resource):
1464    def render(self, ctx):
1465        try:
1466            self.worksheet.delete_all_output(self.username)
1467        except ValueError:
1468            return HTMLResponse(stream='fail')
1469        return HTMLResponse(stream='success')
1470
1471class Worksheet_print(WorksheetResource, resource.Resource):
1472    def render(self, ctx):
1473        s = notebook.worksheet_html(self.name, do_print=True)
1474        return HTMLResponse(stream=s)
1475
1476
1477class NotImplementedWorksheetOp(resource.Resource):
1478    def __init__(self, op, ws):
1479        self.op = op
1480        self.ws = ws
1481
1482    def render(self, ctx):
1483        return HTMLResponse(stream = message(
1484            'The worksheet operation "%s" is not defined.'%self.op,
1485            '/home/'+self.ws.filename()))
1486
1487
1488class Worksheet(WorksheetResource, resource.Resource):
1489    addSlash = True
1490
1491    def render(self, ctx):
1492        self.worksheet.sage()
1493        s = notebook.html(worksheet_filename = self.name,  username=self.username)
1494        return HTMLResponse(stream=s)
1495
1496    def childFactory(self, request, op):
1497        notebook_updates()
1498        try:
1499            # Rather than a bunch of if-else statements, we wrap
1500            # any Worksheet_... class as a subresource of a worksheet
1501            # using the following  statement:
1502            R = globals()['Worksheet_%s'%op]
1503            return R(self.name, username = self.username)
1504        except KeyError:
1505            file = self.worksheet.data_directory() + '/' + op
1506            if os.path.exists(file):
1507                return static.File(file)
1508            dir = self.worksheet.cells_directory()
1509            for F in os.listdir(dir):
1510                h = '%s/%s/%s'%(dir,F,op)
1511                if os.path.exists(h):
1512                    return static.File(h)
1513            return NotImplementedWorksheetOp(op, self.worksheet)
1514
1515
1516def render_worksheet_list(args, pub, username):
1517    """
1518    Returns a rendered worksheet listing.
1519
1520    INPUT:
1521
1522    -  ``args`` - ctx.args where ctx is the dict passed
1523       into a resource's render method
1524
1525    -  ``pub`` - boolean, True if this is a listing of
1526       public worksheets
1527
1528    -  ``username`` - the user whose worksheets we are
1529       listing
1530
1531    OUTPUT:
1532
1533    a string
1534    """
1535    from sage.server.notebook.notebook import sort_worksheet_list
1536    typ = args['typ'][0] if 'typ' in args else 'active'
1537    search = args['search'][0] if 'search' in args else None
1538    sort = args['sort'][0] if 'sort' in args else 'last_edited'
1539    reverse = (args['reverse'][0] == 'True') if 'reverse' in args else False
1540
1541    if not pub:
1542        worksheets = notebook.worksheet_list_for_user(username, typ=typ, sort=sort,
1543                                                      search=search, reverse=reverse)
1544
1545    else:
1546        worksheets = notebook.worksheet_list_for_public(username, sort=sort,
1547                                                        search=search, reverse=reverse)
1548
1549    worksheet_filenames = [x.filename() for x in worksheets]
1550
1551    if pub and (not username or username == tuple([])):
1552        username = 'pub'
1553
1554    accounts = notebook.get_accounts()
1555
1556    return template('worksheet_listing.html', **locals())
1557
1558
1559class WorksheetsByUser(resource.Resource):
1560    addSlash = True
1561
1562    def __init__(self, user, username):
1563        # user -- who we're requesting the worksheets of
1564        # username -- who is doing the requesting
1565        self.user = user
1566        self.username = username
1567
1568    def render_list(self, ctx):
1569        s = render_worksheet_list(ctx.args, pub=False, username=self.user)
1570        return HTMLResponse(stream = s)
1571
1572    def render(self, ctx):
1573        if self.user == self.username or user_type(self.username) == 'admin':
1574            return self.render_list(ctx)
1575        else:
1576            if not self.username == 'guest':
1577                s = message("User '%s' does not have permission to view the home page of '%s'."%(self.username, self.user))
1578            else:
1579                return http.RedirectResponse('/')
1580            return HTMLResponse(stream = s)
1581
1582    def childFactory(self, request, name):
1583        if name == "trash":
1584            return TrashCan(self.user)
1585
1586        filename = self.user + '/' + name
1587        try:
1588            return Worksheet(filename, self.username)
1589        except KeyError:
1590            s = "The user '%s' has no worksheet '%s'."%(self.user, name)
1591            return InvalidPage(msg = s, username = self.user)
1592        except RuntimeError:
1593            s = "You are not logged in or do not have access to the worksheet '%s'."%name
1594            return InvalidPage(msg = s, username = self.user)
1595
1596
1597
1598############################
1599# Trash can, archive and active
1600############################
1601class EmptyTrash(resource.Resource):
1602    def __init__(self, username):
1603        """
1604        This twisted resource empties the trash of the current user when it
1605        is rendered.
1606
1607        EXAMPLES:
1608
1609        We create an instance of this resource.
1610
1611        ::
1612
1613            sage: import sage.server.notebook.twist
1614            sage: E = sage.server.notebook.twist.EmptyTrash('sage'); E
1615            <sage.server.notebook.twist.EmptyTrash object at ...>
1616        """
1617        self.username = username
1618
1619    def render(self, ctx):
1620        """
1621        Rendering this resource (1) empties the trash, and (2) returns a
1622        message.
1623
1624        EXAMPLES:
1625
1626        We create a notebook with a worksheet, put it in the
1627        trash, then empty the trash by creating and rendering this
1628        worksheet.
1629
1630        ::
1631
1632            sage: n = sage.server.notebook.notebook.Notebook('notebook-test')
1633            sage: n.add_user('sage','sage','sage@sagemath.org',force=True)
1634            sage: W = n.new_worksheet_with_title_from_text('Sage', owner='sage')
1635            sage: W.move_to_trash('sage')
1636            sage: n.worksheet_names()
1637            ['sage/0']
1638            sage: sage.server.notebook.twist.notebook = n
1639            sage: E = sage.server.notebook.twist.EmptyTrash('sage'); E
1640            <sage.server.notebook.twist.EmptyTrash object at ...>
1641            sage: E.render(None)
1642            <twisted.web2.http.Response code=200, streamlen=...>
1643
1644        Finally we verify that the trashed worksheet is gone::
1645
1646            sage: n.worksheet_names()
1647            []
1648            sage: n.delete()
1649        """
1650        notebook.empty_trash(self.username)
1651        return HTMLResponse(stream = message("Trash emptied."))
1652
1653class SendWorksheetToFolder(resource.PostableResource):
1654    def __init__(self, username):
1655        self.username = username
1656
1657    def action(self, W):
1658        raise NotImplementedError
1659
1660    def render(self, ctx):
1661        X = notebook.user(self.username)
1662        if user_type(self.username) == 'guest':
1663            return HTMLResponse(stream = message("You are not authorized to move '%s'"%W.name()))
1664
1665        def send_worksheet_to_folder(filename):
1666            W = notebook.get_worksheet_with_filename(filename)
1667            self.action(W)
1668
1669        if ctx.args.has_key('filename'):
1670            filenames = [ctx.args['filename'][0]]
1671        elif ctx.args.has_key('filenames'):
1672            sep = ctx.args['sep'][0]
1673            filenames = [x for x in ctx.args['filenames'][0].split(sep) if len(x.strip()) > 0]
1674
1675        else:
1676
1677            filenames = []
1678
1679        for F in filenames:
1680            send_worksheet_to_folder(F)
1681
1682        return HTMLResponse(stream = '')
1683
1684class SendWorksheetToTrash(SendWorksheetToFolder):
1685    def action(self, W):
1686        W.move_to_trash(self.username)
1687
1688class SendWorksheetToArchive(SendWorksheetToFolder):
1689    def action(self, W):
1690        W.move_to_archive(self.username)
1691
1692class SendWorksheetToActive(SendWorksheetToFolder):
1693    def action(self, W):
1694        W.set_active(self.username)
1695
1696# Using SendWorksheet does feel somewhat hackish.  It however is
1697# exactly the right thing to actually do, and minimizes code
1698# duplication.
1699class SendWorksheetToStop(SendWorksheetToFolder):
1700    """
1701    Quits each selected worksheet.
1702    """
1703    def action(self, W):
1704        W.quit()
1705
1706############################
1707# Publicly Available Worksheets
1708############################
1709class PublicWorksheets(resource.Resource):
1710    addSlash = True
1711
1712    def __init__(self, username):
1713        self.username = username
1714
1715    def render(self, ctx):
1716        s = render_worksheet_list(ctx.args, pub=True, username=self.username)
1717        return HTMLResponse(stream = s)
1718
1719    def childFactory(self, request, name):
1720        return Worksheet('pub/' + name, username=self.username)
1721
1722class PublicWorksheetsHome(resource.Resource):
1723    addSlash = True
1724
1725    def __init__(self, username):
1726        self.username = username
1727
1728    def childFactor(self, request, name):
1729        if name == 'pub':
1730            return PublicWorksheets(self.username)
1731
1732############################
1733# Resource that gives access to worksheets
1734############################
1735
1736class Worksheets(resource.Resource):
1737    def __init__(self, username):
1738        self.username = username
1739
1740    def render(self, ctx):
1741        return HTMLResponse(stream = message("Please request a specific worksheet"))
1742
1743    def childFactory(self, request, name):
1744        return WorksheetsByUser(name, username=self.username)
1745
1746
1747class WorksheetsByUserAdmin(WorksheetsByUser):
1748    def render(self, ctx):
1749        return self.render_list(ctx)
1750
1751class WorksheetsAdmin(Worksheets):
1752    def childFactory(self, request, name):
1753        return WorksheetsByUserAdmin(name, username=self.username)
1754
1755############################
1756# Notebook configuration
1757############################
1758
1759class NotebookConf(Worksheets):
1760    def render(self, ctx):
1761        s = '<html>' + notebook.conf().html_conf_form('submit') + '</html>'
1762        return HTMLResponse(stream = s)
1763
1764
1765
1766############################
1767# Adding a new worksheet
1768############################
1769
1770class AddWorksheet(resource.Resource):
1771    def render(self, ctx):
1772        name = ctx.args['name'][0]
1773        W = notebook.create_new_worksheet(name)
1774        v = notebook.worksheet_list_html(W.name())
1775        return HTMLResponse(stream = encode_list([v, W.name()]))
1776
1777
1778############################
1779
1780class Help(resource.Resource):
1781    addSlash = True
1782    def __init__(self, username):
1783        self.username = username
1784
1785    def render(self, ctx):
1786        from tutorial import notebook_help
1787        return HTMLResponse(stream=template('docs.html', username=self.username, notebook_help=notebook_help))
1788
1789
1790############################
1791
1792############################
1793
1794class History(resource.Resource):
1795    def __init__(self, username):
1796        self.username = username
1797
1798    def render(self, ctx):
1799        t = template('history.html', username=self.username,
1800                     text = notebook.user_history_text(self.username),
1801                     actions=False)
1802        return HTMLResponse(stream=t)
1803
1804class LiveHistory(resource.Resource):
1805    def __init__(self, username):
1806        self.username = username
1807
1808    def render(self, ctx):
1809        W = notebook.create_new_worksheet_from_history('Log', self.username, 100)
1810        return http.RedirectResponse('/home/'+W.filename())
1811
1812
1813############################
1814
1815class Main_css(resource.Resource):
1816    def render(self, ctx):
1817        s = css.css()
1818        gzip_handler(ctx)
1819        response = http.Response(stream=s)
1820        response.headers.addRawHeader('Content-Type', 'text/css; charset=utf-8')
1821        return response
1822
1823class Reset_css(resource.Resource):
1824    def render(self, ctx):
1825        s = template('css/reset.css')
1826        gzip_handler(ctx)
1827        return http.Response(stream=s)
1828
1829class CSS(resource.Resource):
1830    addSlash = True
1831
1832    def render(self, ctx):
1833        return static.File(css_path)
1834
1835    def childFactory(self, request, name):
1836        gzip_handler(request)
1837        return static.File(css_path + "/" + name)
1838
1839setattr(CSS, 'child_main.css', Main_css())
1840setattr(CSS, 'child_reset.css', Reset_css())
1841
1842############################
1843
1844
1845############################
1846# Javascript resources
1847############################
1848
1849class Main_js(resource.Resource):
1850    def render(self, ctx):
1851        gzip_handler(ctx)
1852        s = js.javascript()
1853        return http.Response(stream=s)
1854
1855
1856class Keyboard_js_specific(resource.Resource):
1857    def __init__(self, browser_os):
1858        self.s = keyboards.get_keyboard(browser_os)
1859
1860    def render(self, ctx):
1861        gzip_handler(ctx)
1862        return http.Response(stream = self.s)
1863
1864
1865class Keyboard_js(resource.Resource):
1866    def childFactory(self, request, browser_os):
1867        gzip_handler(request)
1868        return Keyboard_js_specific(browser_os)
1869
1870class Javascript(resource.Resource):
1871    addSlash = True
1872    child_keyboard = Keyboard_js()
1873
1874    def render(self, ctx):
1875        return static.File(javascript_path)
1876
1877    def childFactory(self, request, name):
1878        gzip_handler(request)
1879        return static.File(javascript_path + "/" + name)
1880
1881setattr(Javascript, 'child_main.js', Main_js())
1882
1883
1884class JavascriptLocal(resource.Resource):
1885    addSlash = True
1886
1887    def render(self, ctx):
1888        return static.File(javascript_local_path)
1889
1890    def childFactory(self, request, name):
1891        gzip_handler(request)
1892        return static.File(javascript_local_path + "/" + name)
1893
1894
1895
1896############################
1897# Java resources
1898############################
1899
1900class Java(resource.Resource):
1901    addSlash = True
1902
1903    def render(self, ctx):
1904        return static.File(java_path)
1905
1906    def childFactory(self, request, name):
1907        gzip_handler(request)
1908        return static.File(java_path + "/" + name)
1909
1910############################
1911# Logout
1912############################
1913class Logout(resource.Resource):
1914    def render(self, ctx):
1915        # TODO -- actually log out.
1916        s = message("<br>Thank you for using Sage.<br><br><a href='/'>Please login and use Sage again soon.</a><br>")
1917        return HTMLResponse(stream=s)
1918
1919############################
1920# Image resource
1921############################
1922
1923class Images(resource.Resource):
1924    addSlash = True
1925
1926    def render(self, ctx):
1927        return static.File(image_path)
1928
1929    def childFactory(self, request, name):
1930        return static.File(image_path + "/" + name)
1931
1932#####################################
1933# Confirmation of registration
1934####################################
1935class RegConfirmation(resource.Resource):
1936    def render(self, request):
1937        if not notebook.conf()['email']:
1938            return HTMLResponse(stream=message('The confirmation system is not active.'))
1939        key = request.args['key'][0]
1940        invalid_confirm_key = """\
1941<h1>Invalid confirmation key</h1>
1942<p>You are reporting a confirmation key that has not been assigned by this
1943server. Please <a href="/register">register</a> with the server.</p>
1944"""
1945        key = int(key)
1946        global waiting
1947        try:
1948            username = waiting[key]
1949            user = notebook.user(username)
1950            user.set_email_confirmation(True)
1951        except KeyError:
1952            return HTMLResponse(stream=message(invalid_confirm_key, '/register'))
1953        success = """<h1>Email address confirmed for user %s</h1>""" % username
1954        del waiting[key]
1955        return HTMLResponse(stream=message(success))
1956
1957############################
1958# Registration page
1959############################
1960import re
1961re_valid_username = re.compile('[a-z|A-Z|0-9|_|.]*')
1962def is_valid_username(username):
1963    r"""
1964    Returns True if and only if ``username`` is valid,
1965    i.e., starts with a letter, is between 4 and 32 characters long,
1966    and contains only letters, numbers, underscores, and and one dot
1967    (.).
1968
1969    EXAMPLES::
1970
1971        sage: from sage.server.notebook.twist import is_valid_username
1972
1973    ``username`` must start with a letter
1974
1975    ::
1976
1977        sage: is_valid_username('mark10')
1978        True
1979        sage: is_valid_username('10mark')
1980        False
1981
1982    ``username`` must be between 4 and 32 characters long
1983
1984    ::
1985
1986        sage: is_valid_username('bob')
1987        False
1988        sage: is_valid_username('I_love_computer_science_and_maths') #33 characters long
1989        False
1990
1991    ``username`` must not have more than one dot (.)
1992
1993    ::
1994
1995        sage: is_valid_username('david.andrews')
1996        True
1997        sage: is_valid_username('david.m.andrews')
1998        False
1999        sage: is_valid_username('math125.TA.5')
2000        False
2001
2002    ``username`` must not have any spaces
2003
2004    ::
2005
2006        sage: is_valid_username('David Andrews')
2007        False
2008        sage: is_valid_username('David M. Andrews')
2009        False
2010
2011    ::
2012
2013        sage: is_valid_username('sarah_andrews')
2014        True
2015
2016    ::
2017
2018        sage: is_valid_username('TA-1')
2019        False
2020        sage: is_valid_username('math125-TA')
2021        False
2022
2023    ::
2024
2025        sage: is_valid_username('dandrews@sagemath.org')
2026        False
2027    """
2028    import string
2029
2030    if not (len(username) > 3 and len(username) < 33):
2031        return False
2032    if not username[0] in string.letters:
2033        return False
2034    if '.' in username:
2035        if username.count('.') > 1:
2036            return False
2037
2038    m = re_valid_username.match(username)
2039    return m.start() == 0 and m.end() == len(username)
2040
2041def is_valid_password(password, username):
2042    r"""
2043    Return True if and only if ``password`` is valid, i.e.,
2044    is between 6 and 32 characters long, doesn't contain space(s), and
2045    doesn't contain ``username``.
2046
2047    EXAMPLES::
2048
2049        sage: from sage.server.notebook.twist import is_valid_password
2050        sage: is_valid_password('uip@un7!', None)
2051        True
2052        sage: is_valid_password('markusup89', None)
2053        True
2054        sage: is_valid_password('8u7', None)
2055        False
2056        sage: is_valid_password('fUmDagaz8LmtonAowjSe0Pvu9C5Gvr6eKcC6wsAT', None)
2057        False
2058        sage: is_valid_password('rrcF !u78!', None)
2059        False
2060        sage: is_valid_password('markusup89', 'markus')
2061        False
2062    """
2063    import string
2064    if len(password) < 6 or len(password) > 32 or ' ' in password:
2065        return False
2066    if username:
2067        if string.lower(username) in string.lower(password):
2068            return False
2069    return True
2070
2071def do_passwords_match(pass1, pass2):
2072    """
2073    EXAMPLES::
2074
2075        sage: from sage.server.notebook.twist import do_passwords_match
2076        sage: do_passwords_match('momcat', 'mothercat')
2077        False
2078        sage: do_passwords_match('mothercat', 'mothercat')
2079        True
2080    """
2081    return pass1 == pass2
2082
2083def is_valid_email(email):
2084    """
2085    from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65215
2086
2087    EXAMPLES::
2088
2089        sage: from sage.server.notebook.twist import is_valid_email
2090        sage: is_valid_email('joe@washinton.gov')
2091        True
2092        sage: is_valid_email('joe.washington.gov')
2093        False
2094    """
2095    if len(email) > 7:
2096        if re.match("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9._%-]+\.[a-zA-Z]{2,6}$", email) != None:
2097            return True
2098    return False
2099
2100class RegistrationPage(resource.PostableResource):
2101    def __init__(self, userdb):
2102        self.userdb = userdb
2103
2104    def render(self, request):
2105        input_boxes = ['username', 'password', 'retype_password']
2106        if notebook.conf()['email']:
2107            input_boxes.append('email')
2108        is_valid_dict = {'username': is_valid_username, 'password': is_valid_password,
2109                         'retype_password': do_passwords_match, 'email': is_valid_email}
2110
2111        missing = [False] * len(input_boxes)
2112        filled_in = {}
2113        template_dict = {}
2114
2115        def errors_found():
2116            for key, value in filled_in.iteritems():
2117                template_dict[key] = value
2118            minus = 1 if 'email' in template_dict else 0
2119            size = len(template_dict) - minus
2120            count = 0
2121            boxes = []
2122            if size == 1:
2123                return ('',)
2124            for i in template_dict:
2125                if '_' in i:
2126                    count += 1
2127            plural = True if count > 1 else False
2128            template_dict['error'] = 'Es ' if plural else 'E '
2129
2130        if notebook.conf()['email']:
2131            template_dict['email'] = True
2132
2133        if set(input_boxes) <= set(request.args):
2134            for i, box in enumerate(input_boxes):
2135                filled_in[box] = request.args[box][0]
2136                if box == 'retype_password':
2137                    if not is_valid_dict[box](filled_in[box], filled_in['password']):
2138                        template_dict['passwords_dont_match'] = True
2139                elif box == 'password':
2140                    if 'username' in filled_in:
2141                        u = filled_in['username']
2142                    else:
2143                        u = None
2144                    if not is_valid_dict[box](filled_in[box], u):
2145                        template_dict['password_invalid'] = True
2146                else:
2147                    if not is_valid_dict[box](filled_in[box]):
2148                        template_dict[box + '_invalid'] = True
2149        else:
2150            for i, box in enumerate(input_boxes):
2151                if not box in request.args:
2152                    missing[i] = True
2153
2154            if set(missing) == set([True]):
2155                return HTMLResponse(stream=template('registration.html', **template_dict))
2156            elif set(missing) == set([False]):
2157                for i, box in enumerate(input_boxes):
2158                    filled_in[box] = request.args[box][0]
2159                    if box == 'retype_password':
2160                        if not is_valid_dict[box](filled_in[box], filled_in['password']):
2161                            template_dict['passwords_dont_match'] = True
2162                    elif box == 'password':
2163                        if 'username' in filled_in:
2164                            u = filled_in['username']
2165                        else:
2166                            u = None
2167                        if not is_valid_dict[box](filled_in[box], u):
2168                            template_dict['password_invalid'] = True
2169                    else:
2170                        if not is_valid_dict[box](filled_in[box]):
2171                            template_dict[box + '_invalid'] = True
2172            else:
2173                for i, value in enumerate(missing):
2174                    if value:
2175                        template_dict[input_boxes[i] + '_missing'] = True
2176                    elif not value:
2177                        filled_in[input_boxes[i]] = request.args[input_boxes[i]][0]
2178
2179        if template_dict and set(template_dict) != set(['email']):
2180            errors_found()
2181            return HTMLResponse(stream=template('registration.html', **template_dict))
2182        else:
2183            try:
2184                e = filled_in['email'] if notebook.conf()['email'] else ''
2185                self.userdb.add_user(filled_in['username'], request.args['password'][0],
2186                                     e)
2187            except ValueError:
2188                template_dict['username_taken'] = True
2189                errors_found()
2190                return HTMLResponse(stream=template('registration.html', **template_dict))
2191
2192            if notebook.conf()['email']:
2193                destaddr = filled_in['email']
2194                from sage.server.notebook.smtpsend import send_mail
2195                from sage.server.notebook.register import make_key, build_msg
2196                # TODO: make this come from the server settings
2197                key = make_key()
2198                listenaddr = notebook.address
2199                port = notebook.port
2200                fromaddr = 'no-reply@%s' % listenaddr
2201                body = build_msg(key, filled_in['username'], listenaddr, port,
2202                                 notebook.secure)
2203
2204                # Send a confirmation message to the user.
2205                try:
2206                    send_mail(fromaddr, destaddr, "Sage Notebook Registration",body)
2207                    waiting[key] = filled_in['username']
2208                except ValueError:
2209                    pass
2210
2211            template_dict = {'accounts': notebook.get_accounts(),
2212                             'default_user': notebook.default_user(),
2213                             'welcome': filled_in['username'],
2214                             'recovery': notebook.conf()['email']}
2215            return HTMLResponse(stream=template('login.html', **template_dict))
2216
2217class ForgotPassPage(resource.Resource):
2218
2219    def render(self, request):
2220        if not notebook.conf()['email']:
2221            return HTMLResponse(stream=message('The account recovery system is not active.'))
2222
2223        if request.args.has_key('username'):
2224            def error(msg):
2225                return HTMLResponse(stream=message(msg, '/forgotpass'))
2226
2227            try:
2228                import string
2229                user = notebook.user(request.args[string.strip('username')][0])
2230            except KeyError:
2231                return error('Username is invalid.')
2232
2233            if not user.is_email_confirmed():
2234                return error("The e-mail address hasn't been confirmed.")
2235
2236            from random import choice
2237            import string
2238            chara = string.letters + string.digits
2239            old_pass = user.password()
2240            password = ''.join([choice(chara) for i in range(8)])
2241            user.set_password(password)
2242
2243            from sage.server.notebook.smtpsend import send_mail
2244            from sage.server.notebook.register import build_password_msg
2245            # TODO: make this come from the server settings
2246
2247            listenaddr = notebook.address
2248            port = notebook.port
2249            fromaddr = 'no-reply@%s' % listenaddr
2250            body = build_password_msg(password, request.args[string.strip('username')][0], listenaddr, port, notebook.secure)
2251            destaddr = user.get_email()
2252            try:
2253                send_mail(fromaddr, destaddr, "Sage Notebook Account Recovery",body)
2254            except ValueError:
2255                # the email address is invalid
2256                user.set_password(oldpass)
2257                return error("The new password couldn't be sent."%destaddr)
2258
2259            return HTMLResponse(stream=message("A new password has been sent to your e-mail address.", '/'))
2260        else:
2261            s = template('account_recovery.html')
2262        return HTMLResponse(stream=s)
2263
2264class ListOfUsers(resource.Resource):
2265    addSlash = True
2266
2267    def __init__(self, username):
2268        self.username = username
2269
2270    def render(self, ctx):
2271        template_dict = {}
2272
2273        if 'reset' in ctx.args:
2274            user = ctx.args['reset'][0]
2275            from random import choice
2276            import string
2277            chara = string.letters + string.digits
2278            password = ''.join([choice(chara) for i in range(8)])
2279            try:
2280                U = notebook.user(user)
2281                U.set_password(password)
2282            except KeyError:
2283                pass
2284            template_dict['reset'] = [user, password]
2285
2286        if 'suspension' in ctx.args:
2287            user = ctx.args['suspension'][0]
2288            try:
2289                U = notebook.user(user)
2290                U.set_suspension()
2291            except KeyError:
2292                pass
2293
2294        template_dict['number_of_users'] = len(notebook.valid_login_names()) if len(notebook.valid_login_names()) > 1 else None
2295        users = sorted(notebook.valid_login_names())
2296        del users[users.index('admin')]
2297        template_dict['users'] = [notebook.user(i) for i in users]
2298        return HTMLResponse(stream = template('user_management.html', **template_dict))
2299
2300class AdminAddUser(resource.PostableResource):
2301    def __init__(self, userdb):
2302        self.userdb = userdb
2303
2304    def render(self, request):
2305
2306        if 'username' in request.args:
2307            username = request.args['username'][0]
2308            if not is_valid_username(username):
2309                return HTMLResponse(stream=template('admin_add_user.html', error='username_invalid', username=username))
2310
2311            from random import choice
2312            import string
2313            chara = string.letters + string.digits
2314            password = ''.join([choice(chara) for i in range(8)])
2315            if username in notebook.usernames():
2316                return HTMLResponse(stream=template('admin_add_user.html', error='username_taken', username=username))
2317            notebook.add_user(username, password, '', force=True)
2318            return HTMLResponse(stream=message('The temporary password for the new user <em>%s</em> is <em>%s</em>' % (username, password), '/adduser'))
2319        else:
2320            return HTMLResponse(stream=template('admin_add_user.html'))
2321
2322class InvalidPage(resource.Resource):
2323    addSlash = True
2324
2325    def __init__(self, msg, username):
2326        self.msg = msg
2327        self.username = username
2328
2329    def render(self, ctx):
2330        if self.msg:
2331            s = self.msg
2332        else:
2333            s = "This is an invalid page."
2334            if self.username == 'guest':
2335                s += ' You might have to login to view this page.'
2336        return HTMLResponse(stream = message(s, '/'))
2337
2338    def childFactory(self, request, name):
2339        return InvalidPage(msg = self.msg, username = self.username)
2340
2341
2342class RedirectLogin(resource.PostableResource):
2343    def render(self, ctx):
2344        return http.RedirectResponse('/')
2345    def childFactory(self, request, name):
2346        return RedirectLogin()
2347
2348import sage.server.simple.twist
2349
2350class Toplevel(resource.PostableResource):
2351    child_login = RedirectLogin()
2352    child_simple = sage.server.simple.twist.SimpleServer()
2353
2354    def __init__(self, cookie, username):
2355        self.cookie = cookie
2356        self.username = username if username else 'guest'
2357
2358    def render(self, ctx):
2359        template_dict = {'accounts': notebook.get_accounts(),
2360                         'default_user': notebook.default_user(),
2361                         'recovery': notebook.conf()['email']}
2362        return HTMLResponse(stream=template('login.html', **template_dict))
2363
2364    def userchildFactory(self, request, name):
2365        return InvalidPage(msg = "unauthorized request", username = self.username)
2366
2367    def childFactory(self, request, name):
2368        return self.userchildFactory(request, name)
2369
2370setattr(Toplevel, 'child_favicon.ico', static.File(image_path + '/favicon.ico'))
2371
2372class LoginResourceClass(resource.Resource):
2373    def render(self, ctx):
2374        template_dict = {'accounts': notebook.get_accounts(),
2375                         'default_user': notebook.default_user(),
2376                         'recovery': notebook.conf()['email']}
2377        return HTMLResponse(stream=template('login.html', **template_dict))
2378
2379    def childFactory(self, request, name):
2380        return LoginResource
2381
2382LoginResource = LoginResourceClass()
2383
2384class AnonymousToplevel(Toplevel):
2385    from sage.server.notebook.avatars import PasswordChecker
2386    addSlash = True
2387    child_register = RegistrationPage(PasswordChecker())
2388    child_confirm = RegConfirmation()
2389    child_forgotpass = ForgotPassPage()
2390
2391    child_images = Images()
2392    child_css = CSS()
2393    child_javascript = Javascript()
2394    child_javascript_local = JavascriptLocal()
2395    child_java = Java()
2396    child_logout = RedirectLogin()
2397
2398    def userchildFactory(self, request, name):
2399        # This is called from Toplevel above
2400        try:
2401            return AnonymousToplevel.__dict__['userchild_%s'%name](username = self.username)
2402        except KeyError:
2403            pass
2404
2405    userchild_home = Worksheets
2406    userchild_pub = PublicWorksheets
2407    userchild_src = SourceBrowser
2408
2409    #child_login = LoginResource
2410
2411    def render(self, ctx):
2412        template_dict = {'accounts': notebook.get_accounts(),
2413                         'default_user': notebook.default_user(),
2414                         'recovery': notebook.conf()['email']}
2415        response = HTMLResponse(stream=template('login.html', **template_dict))
2416        response.headers.setHeader("set-cookie", [http_headers.Cookie('cookie_test', 'cookie_test')])
2417        return response
2418
2419class FailedToplevel(Toplevel):
2420    def __init__(self, info, problem, username=None):
2421        self.info = info
2422        self.problem= problem
2423        self.username = username
2424
2425    def render(self, ctx):
2426        # Since public access is allowed, which lists usernames in the published
2427        # worksheets and ratings, this gives no new information away.
2428        # If published pages were disabled, then this should be disabled too.
2429        if self.problem == 'username':
2430            template_dict = {'accounts': notebook.get_accounts(),
2431                             'default_user': notebook.default_user(),
2432                             'username_error': True,
2433                             'recovery': notebook.conf()['email']}
2434            return HTMLResponse(stream=template('login.html', **template_dict))
2435        elif self.problem == 'password':
2436            template_dict = {'accounts': notebook.get_accounts(),
2437                             'default_user': self.username,
2438                             'password_error': True,
2439                             'recovery': notebook.conf()['email']}
2440            return HTMLResponse(stream=template('login.html', **template_dict))
2441        elif self.problem == 'suspended':
2442            return HTMLResponse(stream = message("Your account is currently suspended."))
2443        else:
2444            return HTMLResponse(stream = message("Please enable cookies and try again."))
2445
2446
2447class UserToplevel(Toplevel):
2448    addSlash = True
2449
2450    child_pdf = DocPDF()
2451    child_images = Images()
2452    child_css = CSS()
2453    child_javascript = Javascript()
2454    child_javascript_local = JavascriptLocal()
2455    child_java = Java()
2456
2457    child_logout = Logout()
2458
2459    child_confirm = RegConfirmation()
2460
2461
2462
2463    #child_login = RedirectLogin()
2464
2465    # userchild_* is like Twisted's child_, etc. except StandardError:
2466    #  (1) it also sets the username in the __init__ method, and
2467    #  (2) it calls the constructor for the object, i.e., it is
2468    #      a class rather than an object.
2469    # NOTE: If you overload childFactory in any derived class, you
2470    # better call userchildFactory it in the base class (Toplevel)!
2471    def userchildFactory(self, request, name):
2472        try:
2473            return UserToplevel.__dict__['userchild_%s'%name](username = self.username)
2474        except KeyError:
2475            pass
2476
2477    userchild_doc = Doc
2478    userchild_sagetex = SageTex
2479    userchild_help = Help
2480    userchild_history = History
2481    userchild_home = Worksheets
2482    userchild_live_history = LiveHistory
2483    userchild_new_worksheet = NewWorksheet
2484    userchild_notebook_settings = NotebookSettings
2485    userchild_settings = SettingsPage
2486    userchild_pub = PublicWorksheets
2487    userchild_upload = Upload
2488    userchild_download_worksheets = DownloadWorksheets
2489
2490    userchild_send_to_trash = SendWorksheetToTrash
2491    userchild_send_to_archive = SendWorksheetToArchive
2492    userchild_send_to_active = SendWorksheetToActive
2493    userchild_send_to_stop = SendWorksheetToStop
2494
2495    userchild_src = SourceBrowser
2496    userchild_upload_worksheet = UploadWorksheet
2497    userchild_emptytrash = EmptyTrash
2498
2499    def render(self, request):
2500        # This resource always does a redirect to the user's home directory
2501        # so that after login (which is a POST operation), the postdata will not remain
2502        # in the browser on return.  This method is sometimes
2503        # referred to as the post-redirect-get method.
2504        response = http.RedirectResponse("/home/%s" % self.username)
2505        # This allows a Notebook user to select a "remember me" checkbox and not have to
2506        # sign back in when she restarts her web browser
2507        # This works by setting an expiration date because without one the browser forgets the cookie.
2508        if 'remember' in request.args:
2509            response.headers.setHeader("set-cookie", [http_headers.Cookie('nb_session', self.cookie, expires=(time.time() + 60 * 60 * 24 * 14)), http_headers.Cookie('cookie_test', self.cookie, expires=1)])
2510        else:
2511            response.headers.setHeader("set-cookie", [http_headers.Cookie('nb_session', self.cookie), http_headers.Cookie('cookie_test', self.cookie, expires=1)])
2512        return response
2513
2514
2515class AdminToplevel(UserToplevel):
2516    addSlash = True
2517
2518    userchild_home = WorksheetsAdmin
2519    child_users = ListOfUsers
2520    child_adduser = AdminAddUser
2521
2522def user_type(username):
2523    # one of admin, guest, user
2524    try:
2525        U = notebook.user(username)
2526    except KeyError:
2527        return 'guest'
2528    return U.account_type()
2529
2530def extract_title(html_page):
2531    h = html_page.lower()
2532    i = h.find('<title>')
2533    if i == -1:
2534        return "Untitled"
2535    j = h.find('</title>')
2536    return html_page[i + len('<title>') : j]
Note: See TracBrowser for help on using the repository browser.