source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/applications/admin/controllers/default.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 73.6 KB
Line 
1# -*- coding: utf-8 -*-
2
3EXPERIMENTAL_STUFF = True
4MAXNFILES = 1000
5
6if EXPERIMENTAL_STUFF:
7    if is_mobile:
8        response.view = response.view.replace('default/', 'default.mobile/')
9        response.menu = []
10
11import re
12from gluon.admin import *
13from gluon.fileutils import abspath, read_file, write_file
14from gluon.utils import web2py_uuid
15from gluon.tools import Config
16from gluon.compileapp import find_exposed_functions
17from glob import glob
18from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native
19import gluon.rewrite
20import shutil
21import platform
22
23try:
24    import git
25    if git.__version__ < '0.3.1':
26        raise ImportError("Your version of git is %s. Upgrade to 0.3.1 or better." % git.__version__)
27    have_git = True
28except ImportError as e:
29    have_git = False
30    GIT_MISSING = 'Requires gitpython module, but not installed or incompatible version: %s' % e
31
32from gluon.languages import (read_possible_languages, read_dict, write_dict,
33                             read_plural_dict, write_plural_dict)
34
35
36if DEMO_MODE and request.function in ['change_password', 'pack',
37                                      'pack_custom', 'pack_plugin', 'upgrade_web2py', 'uninstall',
38                                      'cleanup', 'compile_app', 'remove_compiled_app', 'delete',
39                                      'delete_plugin', 'create_file', 'upload_file', 'update_languages',
40                                      'reload_routes', 'git_push', 'git_pull', 'install_plugin']:
41    session.flash = T('disabled in demo mode')
42    redirect(URL('site'))
43
44if is_gae and request.function in ('edit', 'edit_language',
45                                   'edit_plurals', 'update_languages', 'create_file', 'install_plugin'):
46    session.flash = T('disabled in GAE mode')
47    redirect(URL('site'))
48
49if not is_manager() and request.function in ['change_password', 'upgrade_web2py']:
50    session.flash = T('disabled in multi user mode')
51    redirect(URL('site'))
52
53if FILTER_APPS and request.args(0) and not request.args(0) in FILTER_APPS:
54    session.flash = T('disabled in demo mode')
55    redirect(URL('site'))
56
57
58if not session.token:
59    session.token = web2py_uuid()
60
61
62def count_lines(data):
63    return len([line for line in data.split('\n') if line.strip() and not line.startswith('#')])
64
65
66def log_progress(app, mode='EDIT', filename=None, progress=0):
67    progress_file = os.path.join(apath(app, r=request), 'progress.log')
68    now = str(request.now)[:19]
69    if not os.path.exists(progress_file):
70        safe_open(progress_file, 'w').write('[%s] START\n' % now)
71    if filename:
72        safe_open(progress_file, 'a').write(
73            '[%s] %s %s: %s\n' % (now, mode, filename, progress))
74
75
76def safe_open(a, b):
77    if (DEMO_MODE or is_gae) and ('w' in b or 'a' in b):
78        class tmp:
79
80            def write(self, data):
81                pass
82
83            def close(self):
84                pass
85        return tmp()
86    if PY2 or 'b' in b:
87        return open(a, b)
88    else:
89        return open(a, b, encoding="utf8")
90
91
92def safe_read(a, b='r'):
93    safe_file = safe_open(a, b)
94    try:
95        return safe_file.read()
96    finally:
97        safe_file.close()
98
99
100def safe_write(a, value, b='w'):
101    safe_file = safe_open(a, b)
102    try:
103        safe_file.write(value)
104    finally:
105        safe_file.close()
106
107
108def get_app(name=None):
109    app = name or request.args(0)
110    if (app and os.path.exists(apath(app, r=request)) and
111        (not MULTI_USER_MODE or is_manager() or
112         db(db.app.name == app)(db.app.owner == auth.user.id).count())):
113        return app
114    session.flash = T('App does not exist or you are not authorized')
115    redirect(URL('site'))
116
117
118def index():
119    """ Index handler """
120
121    send = request.vars.send
122    if DEMO_MODE:
123        session.authorized = True
124        session.last_time = t0
125    if not send:
126        send = URL('site')
127    if session.authorized:
128        redirect(send)
129    elif failed_login_count() >= allowed_number_of_attempts:
130        time.sleep(2 ** allowed_number_of_attempts)
131        raise HTTP(403)
132    elif request.vars.password:
133        if verify_password(request.vars.password[:1024]):
134            session.authorized = True
135            login_record(True)
136
137            if CHECK_VERSION:
138                session.check_version = True
139            else:
140                session.check_version = False
141
142            session.last_time = t0
143            if isinstance(send, list):  # ## why does this happen?
144                send = str(send[0])
145
146            redirect(send)
147        else:
148            times_denied = login_record(False)
149            if times_denied >= allowed_number_of_attempts:
150                response.flash = \
151                    T('admin disabled because too many invalid login attempts')
152            elif times_denied == allowed_number_of_attempts - 1:
153                response.flash = \
154                    T('You have one more login attempt before you are locked out')
155            else:
156                response.flash = T('invalid password.')
157    return dict(send=send)
158
159
160def check_version():
161    """ Checks if web2py is up to date """
162
163    session.forget()
164    session._unlock(response)
165
166    new_version, version = check_new_version(request.env.web2py_version,
167                                             WEB2PY_VERSION_URL)
168
169    if new_version in (-1, -2):
170        return A(T('Unable to check for upgrades'), _href=WEB2PY_URL)
171    elif not new_version:
172        return A(T('web2py is up to date'), _href=WEB2PY_URL)
173    elif platform.system().lower() in ('windows', 'win32', 'win64') and os.path.exists("web2py.exe"):
174        return SPAN('You should upgrade to %s' % version.split('(')[0])
175    else:
176        return sp_button(URL('upgrade_web2py'), T('upgrade now to %s') % version.split('(')[0])
177
178
179def logout():
180    """ Logout handler """
181    session.authorized = None
182    if MULTI_USER_MODE:
183        redirect(URL('user/logout'))
184    redirect(URL('index'))
185
186
187def change_password():
188
189    if session.pam_user:
190        session.flash = T(
191            'PAM authenticated user, cannot change password here')
192        redirect(URL('site'))
193    form = SQLFORM.factory(Field('current_admin_password', 'password'),
194                           Field('new_admin_password',
195                                 'password', requires=IS_STRONG()),
196                           Field('new_admin_password_again', 'password'),
197                           _class="span4 well")
198    if form.accepts(request.vars):
199        if not verify_password(request.vars.current_admin_password):
200            form.errors.current_admin_password = T('invalid password')
201        elif form.vars.new_admin_password != form.vars.new_admin_password_again:
202            form.errors.new_admin_password_again = T('no match')
203        else:
204            path = abspath('parameters_%s.py' % request.env.server_port)
205            safe_write(path, 'password="%s"' % CRYPT()(
206                request.vars.new_admin_password)[0])
207            session.flash = T('password changed')
208            redirect(URL('site'))
209    return dict(form=form)
210
211
212def site():
213    """ Site handler """
214
215    myversion = request.env.web2py_version
216
217    # Shortcut to make the elif statements more legible
218    file_or_appurl = 'file' in request.vars or 'appurl' in request.vars
219
220    class IS_VALID_APPNAME(object):
221
222        def __call__(self, value):
223            if not re.compile('^\w+$').match(value):
224                return (value, T('Invalid application name'))
225            if not request.vars.overwrite and \
226                    os.path.exists(os.path.join(apath(r=request), value)):
227                return (value, T('Application exists already'))
228            return (value, None)
229
230    is_appname = IS_VALID_APPNAME()
231    form_create = SQLFORM.factory(Field('name', requires=is_appname),
232                                  table_name='appcreate')
233    form_update = SQLFORM.factory(Field('name', requires=is_appname),
234                                  Field('file', 'upload', uploadfield=False),
235                                  Field('url'),
236                                  Field('overwrite', 'boolean'),
237                                  table_name='appupdate')
238    form_create.process()
239    form_update.process()
240
241    if DEMO_MODE:
242        pass
243
244    elif form_create.accepted:
245        # create a new application
246        appname = cleanpath(form_create.vars.name)
247        created, error = app_create(appname, request, info=True)
248        if created:
249            if MULTI_USER_MODE:
250                db.app.insert(name=appname, owner=auth.user.id)
251            log_progress(appname)
252            session.flash = T('new application "%s" created', appname)
253            gluon.rewrite.load()
254            redirect(URL('design', args=appname))
255        else:
256            session.flash = \
257                DIV(T('unable to create application "%s"', appname),
258                    PRE(error))
259        redirect(URL(r=request))
260
261    elif form_update.accepted:
262        if (form_update.vars.url or '').endswith('.git'):
263            if not have_git:
264                session.flash = GIT_MISSING
265                redirect(URL(r=request))
266            target = os.path.join(apath(r=request), form_update.vars.name)
267            try:
268                new_repo = git.Repo.clone_from(form_update.vars.url, target)
269                session.flash = T('new application "%s" imported',
270                                  form_update.vars.name)
271                gluon.rewrite.load()
272            except git.GitCommandError as err:
273                session.flash = T('Invalid git repository specified.')
274            redirect(URL(r=request))
275
276        elif form_update.vars.url:
277            # fetch an application via URL or file upload
278            try:
279                f = urlopen(form_update.vars.url)
280                if f.code == 404:
281                    raise Exception("404 file not found")
282            except Exception as e:
283                session.flash = \
284                    DIV(T('Unable to download app because:'), PRE(repr(e)))
285                redirect(URL(r=request))
286            fname = form_update.vars.url
287
288        elif form_update.accepted and form_update.vars.file:
289            fname = request.vars.file.filename
290            f = request.vars.file.file
291
292        else:
293            session.flash = 'No file uploaded and no URL specified'
294            redirect(URL(r=request))
295
296        if f:
297            appname = cleanpath(form_update.vars.name)
298            installed = app_install(appname, f,
299                                    request, fname,
300                                    overwrite=form_update.vars.overwrite)
301        if f and installed:
302            msg = 'application %(appname)s installed with md5sum: %(digest)s'
303            if MULTI_USER_MODE:
304                db.app.insert(name=appname, owner=auth.user.id)
305            log_progress(appname)
306            session.flash = T(msg, dict(appname=appname,
307                                        digest=md5_hash(installed)))
308            gluon.rewrite.load()
309        else:
310            msg = 'unable to install application "%(appname)s"'
311            session.flash = T(msg, dict(appname=form_update.vars.name))
312        redirect(URL(r=request))
313
314    regex = re.compile('^\w+$')
315
316    if is_manager():
317        apps = [a for a in os.listdir(apath(r=request)) if regex.match(a) and
318                a != '__pycache__']
319    else:
320        apps = [a.name for a in db(db.app.owner == auth.user_id).select()]
321
322    if FILTER_APPS:
323        apps = [a for a in apps if a in FILTER_APPS]
324
325    apps = sorted(apps, key=lambda a: a.upper())
326    myplatform = platform.python_version()
327    return dict(app=None, apps=apps, myversion=myversion, myplatform=myplatform,
328                form_create=form_create, form_update=form_update)
329
330
331def report_progress(app):
332    import datetime
333    progress_file = os.path.join(apath(app, r=request), 'progress.log')
334    regex = re.compile('\[(.*?)\][^\:]+\:\s+(\-?\d+)')
335    if not os.path.exists(progress_file):
336        return []
337    matches = regex.findall(open(progress_file, 'r').read())
338    events, counter = [], 0
339    for m in matches:
340        if not m:
341            continue
342        days = -(request.now - datetime.datetime.strptime(m[0],
343                                                          '%Y-%m-%d %H:%M:%S')).days
344        counter += int(m[1])
345        events.append([days, counter])
346    return events
347
348
349def pack():
350    app = get_app()
351
352    try:
353        if len(request.args) == 1:
354            fname = 'web2py.app.%s.w2p' % app
355            filename = app_pack(app, request, raise_ex=True)
356        else:
357            fname = 'web2py.app.%s.compiled.w2p' % app
358            filename = app_pack_compiled(app, request, raise_ex=True)
359    except Exception as e:
360        filename = None
361
362    if filename:
363        response.headers['Content-Type'] = 'application/w2p'
364        disposition = 'attachment; filename=%s' % fname
365        response.headers['Content-Disposition'] = disposition
366        return safe_read(filename, 'rb')
367    else:
368        session.flash = T('internal error: %s', e)
369        redirect(URL('site'))
370
371
372def pack_plugin():
373    app = get_app()
374    if len(request.args) == 2:
375        fname = 'web2py.plugin.%s.w2p' % request.args[1]
376        filename = plugin_pack(app, request.args[1], request)
377    if filename:
378        response.headers['Content-Type'] = 'application/w2p'
379        disposition = 'attachment; filename=%s' % fname
380        response.headers['Content-Disposition'] = disposition
381        return safe_read(filename, 'rb')
382    else:
383        session.flash = T('internal error')
384        redirect(URL('plugin', args=request.args))
385
386
387def pack_exe(app, base, filenames=None):
388    import urllib
389    import zipfile
390    # Download latest web2py_win and open it with zipfile
391    download_url = 'http://www.web2py.com/examples/static/web2py_win.zip'
392    out = StringIO()
393    out.write(urlopen(download_url).read())
394    web2py_win = zipfile.ZipFile(out, mode='a')
395    # Write routes.py with the application as default
396    routes = u'# -*- coding: utf-8 -*-\nrouters = dict(BASE=dict(default_application="%s"))' % app
397    web2py_win.writestr('web2py/routes.py', routes.encode('utf-8'))
398    # Copy the application into the zipfile
399    common_root = os.path.dirname(base)
400    for filename in filenames:
401        fname = os.path.join(base, filename)
402        arcname = os.path.join('web2py/applications', app, filename)
403        web2py_win.write(fname, arcname)
404    web2py_win.close()
405    response.headers['Content-Type'] = 'application/zip'
406    response.headers['Content-Disposition'] = 'attachment; filename=web2py.app.%s.zip' % app
407    out.seek(0)
408    return response.stream(out)
409
410
411def pack_custom():
412    app = get_app()
413    base = apath(app, r=request)
414
415    def ignore(fs):
416        return [f for f in fs if not (
417                f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
418    files = {}
419    for (r, d, f) in os.walk(base):
420        files[r] = {'folders': ignore(d), 'files': ignore(f)}
421
422    if request.post_vars.file:
423        valid_set = set(os.path.relpath(os.path.join(r, f), base) for r in files for f in files[r]['files'])
424        files = request.post_vars.file
425        files = [files] if not isinstance(files, list) else files
426        files = [file for file in files if file in valid_set]
427
428        if request.post_vars.doexe is None:
429            fname = 'web2py.app.%s.w2p' % app
430            try:
431                filename = app_pack(app, request, raise_ex=True, filenames=files)
432            except Exception as e:
433                filename = None
434            if filename:
435                response.headers['Content-Type'] = 'application/w2p'
436                disposition = 'attachment; filename=%s' % fname
437                response.headers['Content-Disposition'] = disposition
438                return safe_read(filename, 'rb')
439            else:
440                session.flash = T('internal error: %s', e)
441                redirect(URL(args=request.args))
442        else:
443            return pack_exe(app, base, files)
444
445    return locals()
446
447
448def upgrade_web2py():
449    dialog = FORM.confirm(T('Upgrade'),
450                          {T('Cancel'): URL('site')})
451    if dialog.accepted:
452        (success, error) = upgrade(request)
453        if success:
454            session.flash = T('web2py upgraded; please restart it')
455        else:
456            session.flash = T('unable to upgrade because "%s"', error)
457        redirect(URL('site'))
458    return dict(dialog=dialog)
459
460
461def uninstall():
462    app = get_app()
463
464    dialog = FORM.confirm(T('Uninstall'),
465                          {T('Cancel'): URL('site')})
466    dialog['_id'] = 'confirm_form'
467    dialog['_class'] = 'well'
468    for component in dialog.components:
469        component['_class'] = 'btn'
470
471    if dialog.accepted:
472        if MULTI_USER_MODE:
473            if is_manager() and db(db.app.name == app).delete():
474                pass
475            elif db(db.app.name == app)(db.app.owner == auth.user.id).delete():
476                pass
477            else:
478                session.flash = T('no permission to uninstall "%s"', app)
479                redirect(URL('site'))
480        try:
481            filename = app_pack(app, request, raise_ex=True)
482        except:
483            session.flash = T('unable to uninstall "%s"', app)
484        else:
485            if app_uninstall(app, request):
486                session.flash = T('application "%s" uninstalled', app)
487            else:
488                session.flash = T('unable to uninstall "%s"', app)
489        redirect(URL('site'))
490    return dict(app=app, dialog=dialog)
491
492
493def cleanup():
494    app = get_app()
495    clean = app_cleanup(app, request)
496    if not clean:
497        session.flash = T("some files could not be removed")
498    else:
499        session.flash = T('cache, errors and sessions cleaned')
500
501    redirect(URL('site'))
502
503
504def compile_app():
505    app = get_app()
506    c = app_compile(app, request,
507                    skip_failed_views=(request.args(1) == 'skip_failed_views'))
508    if not c:
509        session.flash = T('application compiled')
510    elif isinstance(c, list):
511        session.flash = DIV(*[T('application compiled'), BR(), BR(),
512                              T('WARNING: The following views could not be compiled:'), BR()] +
513                            [CAT(BR(), view) for view in c] +
514                            [BR(), BR(), T('DO NOT use the "Pack compiled" feature.')])
515    else:
516        session.flash = DIV(T('Cannot compile: there are errors in your app:'),
517                            CODE(c))
518    redirect(URL('site'))
519
520
521def remove_compiled_app():
522    """ Remove the compiled application """
523    app = get_app()
524    remove_compiled_application(apath(app, r=request))
525    session.flash = T('compiled application removed')
526    redirect(URL('site'))
527
528
529def delete():
530    """ Object delete handler """
531    app = get_app()
532    filename = '/'.join(request.args)
533    sender = request.vars.sender
534
535    if isinstance(sender, list):  # ## fix a problem with Vista
536        sender = sender[0]
537
538    dialog = FORM.confirm(T('Delete'),
539                          {T('Cancel'): URL(sender, anchor=request.vars.id)})
540
541    if dialog.accepted:
542        try:
543            full_path = apath(filename, r=request)
544            lineno = count_lines(open(full_path, 'r').read())
545            os.unlink(full_path)
546            log_progress(app, 'DELETE', filename, progress=-lineno)
547            session.flash = T('file "%(filename)s" deleted',
548                              dict(filename=filename))
549        except Exception:
550            session.flash = T('unable to delete file "%(filename)s"',
551                              dict(filename=filename))
552        redirect(URL(sender, anchor=request.vars.id2))
553    return dict(dialog=dialog, filename=filename)
554
555def enable():
556    if not URL.verify(request, hmac_key=session.hmac_key): raise HTTP(401)
557    app = get_app()
558    filename = os.path.join(apath(app, r=request), 'DISABLED')
559    if is_gae:
560        return SPAN(T('Not supported'), _style='color:yellow')
561    elif os.path.exists(filename):
562        os.unlink(filename)
563        return SPAN(T('Disable'), _style='color:green')
564    else:
565        if PY2:
566            safe_open(filename, 'wb').write('disabled: True\ntime-disabled: %s' % request.now)
567        else:
568            str_ = 'disabled: True\ntime-disabled: %s' % request.now
569            safe_open(filename, 'wb').write(str_.encode('utf-8'))
570        return SPAN(T('Enable'), _style='color:red')
571
572
573def peek():
574    """ Visualize object code """
575    app = get_app(request.vars.app)
576    filename = '/'.join(request.args)
577    if request.vars.app:
578        path = abspath(filename)
579    else:
580        path = apath(filename, r=request)
581    try:
582        data = safe_read(path).replace('\r', '')
583    except IOError:
584        session.flash = T('file does not exist')
585        redirect(URL('site'))
586
587    extension = filename[filename.rfind('.') + 1:].lower()
588
589    return dict(app=app,
590                filename=filename,
591                data=data,
592                extension=extension)
593
594
595def test():
596    """ Execute controller tests """
597    app = get_app()
598    if len(request.args) > 1:
599        file = request.args[1]
600    else:
601        file = '.*\.py'
602
603    controllers = listdir(
604        apath('%s/controllers/' % app, r=request), file + '$')
605
606    return dict(app=app, controllers=controllers)
607
608
609def keepalive():
610    return ''
611
612
613def search():
614    keywords = request.vars.keywords or ''
615    app = get_app()
616
617    def match(filename, keywords):
618        filename = os.path.join(apath(app, r=request), filename)
619        if keywords in read_file(filename, 'rb'):
620            return True
621        return False
622    path = apath(request.args[0], r=request)
623    files1 = glob(os.path.join(path, '*/*.py'))
624    files2 = glob(os.path.join(path, '*/*.html'))
625    files3 = glob(os.path.join(path, '*/*/*.html'))
626    files = [x[len(path) + 1:].replace(
627        '\\', '/') for x in files1 + files2 + files3 if match(x, keywords)]
628    return response.json(dict(files=files, message=T.M('Searching: **%s** %%{file}', len(files))))
629
630
631def edit():
632    """ File edit handler """
633    # Load json only if it is ajax edited...
634    app = get_app(request.vars.app)
635    app_path = apath(app, r=request)
636    preferences = {'theme': 'web2py', 'editor': 'default', 'closetag': 'true', 'codefolding': 'false', 'tabwidth': '4', 'indentwithtabs': 'false', 'linenumbers': 'true', 'highlightline': 'true'}
637    config = Config(os.path.join(request.folder, 'settings.cfg'),
638                    section='editor', default_values={})
639    preferences.update(config.read())
640
641    if not(request.ajax) and not(is_mobile):
642        # return the scaffolding, the rest will be through ajax requests
643        response.title = T('Editing %s') % app
644        return response.render('default/edit.html', dict(app=app, editor_settings=preferences))
645
646    # show settings tab and save prefernces
647    if 'settings' in request.vars:
648        if request.post_vars:  # save new preferences
649            if PY2:
650                post_vars = request.post_vars.items()
651            else:
652                post_vars = list(request.post_vars.items())
653            # Since unchecked checkbox are not serialized, we must set them as false by hand to store the correct preference in the settings
654            post_vars += [(opt, 'false') for opt in preferences if opt not in request.post_vars]
655            if config.save(post_vars):
656                response.headers["web2py-component-flash"] = T('Preferences saved correctly')
657            else:
658                response.headers["web2py-component-flash"] = T('Preferences saved on session only')
659            response.headers["web2py-component-command"] = "update_editor(%s);$('a[href=#editor_settings] button.close').click();" % response.json(config.read())
660            return
661        else:
662            details = {'realfilename': 'settings', 'filename': 'settings', 'id': 'editor_settings', 'force': False}
663            details['plain_html'] = response.render('default/editor_settings.html', {'editor_settings': preferences})
664            return response.json(details)
665
666    """ File edit handler """
667    # Load json only if it is ajax edited...
668    app = get_app(request.vars.app)
669    filename = '/'.join(request.args)
670    realfilename = request.args[-1]
671    if request.vars.app:
672        path = abspath(filename)
673    else:
674        path = apath(filename, r=request)
675    # Try to discover the file type
676    if filename[-3:] == '.py':
677        filetype = 'python'
678    elif filename[-5:] == '.html':
679        filetype = 'html'
680    elif filename[-5:] == '.load':
681        filetype = 'html'
682    elif filename[-4:] == '.css':
683        filetype = 'css'
684    elif filename[-3:] == '.js':
685        filetype = 'javascript'
686    else:
687        filetype = 'html'
688
689    # ## check if file is not there
690    if ('revert' in request.vars) and os.path.exists(path + '.bak'):
691        try:
692            data = safe_read(path + '.bak')
693            data1 = safe_read(path)
694        except IOError:
695            session.flash = T('Invalid action')
696            if 'from_ajax' in request.vars:
697                return response.json({'error': str(T('Invalid action'))})
698            else:
699                redirect(URL('site'))
700
701        safe_write(path, data)
702        file_hash = md5_hash(data)
703        saved_on = time.ctime(os.stat(path)[stat.ST_MTIME])
704        safe_write(path + '.bak', data1)
705        response.flash = T('file "%s" of %s restored', (filename, saved_on))
706    else:
707        try:
708            data = safe_read(path)
709        except IOError:
710            session.flash = T('Invalid action')
711            if 'from_ajax' in request.vars:
712                return response.json({'error': str(T('Invalid action'))})
713            else:
714                redirect(URL('site'))
715
716        lineno_old = count_lines(data)
717        file_hash = md5_hash(data)
718        saved_on = time.ctime(os.stat(path)[stat.ST_MTIME])
719
720        if request.vars.file_hash and request.vars.file_hash != file_hash:
721            session.flash = T('file changed on disk')
722            data = request.vars.data.replace('\r\n', '\n').strip() + '\n'
723            safe_write(path + '.1', data)
724            if 'from_ajax' in request.vars:
725                return response.json({'error': str(T('file changed on disk')),
726                                      'redirect': URL('resolve',
727                                                      args=request.args)})
728            else:
729                redirect(URL('resolve', args=request.args))
730        elif request.vars.data:
731            safe_write(path + '.bak', data)
732            data = request.vars.data.replace('\r\n', '\n').strip() + '\n'
733            safe_write(path, data)
734            lineno_new = count_lines(data)
735            log_progress(
736                app, 'EDIT', filename, progress=lineno_new - lineno_old)
737            file_hash = md5_hash(data)
738            saved_on = time.ctime(os.stat(path)[stat.ST_MTIME])
739            response.flash = T('file saved on %s', saved_on)
740
741    data_or_revert = (request.vars.data or request.vars.revert)
742
743    # Check compile errors
744    highlight = None
745    if filetype == 'python' and request.vars.data:
746        import _ast
747        try:
748            code = request.vars.data.rstrip().replace('\r\n', '\n') + '\n'
749            compile(code, path, "exec", _ast.PyCF_ONLY_AST)
750        except Exception as e:
751            # offset calculation is only used for textarea (start/stop)
752            start = sum([len(line) + 1 for l, line
753                         in enumerate(request.vars.data.split("\n"))
754                         if l < e.lineno - 1])
755            if e.text and e.offset:
756                offset = e.offset - (len(e.text) - len(
757                    e.text.splitlines()[-1]))
758            else:
759                offset = 0
760            highlight = {'start': start, 'end': start +
761                         offset + 1, 'lineno': e.lineno, 'offset': offset}
762            try:
763                ex_name = e.__class__.__name__
764            except:
765                ex_name = 'unknown exception!'
766            response.flash = DIV(T('failed to compile file because:'), BR(),
767                                 B(ex_name), ' ' + T('at line %s', e.lineno),
768                                 offset and ' ' +
769                                 T('at char %s', offset) or '',
770                                 PRE(repr(e)))
771    if data_or_revert and request.args[1] == 'modules':
772        # Lets try to reload the modules
773        try:
774            mopath = '.'.join(request.args[2:])[:-3]
775            exec('import applications.%s.modules.%s' % (
776                request.args[0], mopath))
777            reload(sys.modules['applications.%s.modules.%s'
778                               % (request.args[0], mopath)])
779        except Exception as e:
780            response.flash = DIV(
781                T('failed to reload module because:'), PRE(repr(e)))
782
783    edit_controller = None
784    editviewlinks = None
785    view_link = None
786    if filetype == 'html' and len(request.args) >= 3:
787        cfilename = os.path.join(request.args[0], 'controllers',
788                                 request.args[2] + '.py')
789        if os.path.exists(apath(cfilename, r=request)):
790            edit_controller = URL('edit', args=[cfilename.replace(os.sep, "/")])
791            view = request.args[3].replace('.html', '')
792            view_link = URL(request.args[0], request.args[2], view)
793    elif filetype == 'python' and request.args[1] == 'controllers':
794        # it's a controller file.
795        # Create links to all of the associated view files.
796        app = get_app()
797        viewname = os.path.splitext(request.args[2])[0]
798        viewpath = os.path.join(app, 'views', viewname)
799        aviewpath = apath(viewpath, r=request)
800        viewlist = []
801        if os.path.exists(aviewpath):
802            if os.path.isdir(aviewpath):
803                viewlist = glob(os.path.join(aviewpath, '*.html'))
804        elif os.path.exists(aviewpath + '.html'):
805            viewlist.append(aviewpath + '.html')
806        if len(viewlist):
807            editviewlinks = []
808            for v in sorted(viewlist):
809                vf = os.path.split(v)[-1]
810                vargs = "/".join([viewpath.replace(os.sep, "/"), vf])
811                editviewlinks.append(A(vf.split(".")[0],
812                                       _class="editor_filelink",
813                                       _href=URL('edit', args=[vargs])))
814
815    if len(request.args) > 2 and request.args[1] == 'controllers':
816        controller = (request.args[2])[:-3]
817        try:
818            functions = find_exposed_functions(data)
819            functions = functions and sorted(functions) or []
820        except SyntaxError as err:
821            functions = ['SyntaxError:Line:%d' % err.lineno]
822    else:
823        (controller, functions) = (None, None)
824
825    if 'from_ajax' in request.vars:
826        return response.json({'file_hash': file_hash, 'saved_on': saved_on, 'functions': functions, 'controller': controller, 'application': request.args[0], 'highlight': highlight})
827    else:
828        file_details = dict(app=request.args[0],
829                            lineno=request.vars.lineno or 1,
830                            editor_settings=preferences,
831                            filename=filename,
832                            realfilename=realfilename,
833                            filetype=filetype,
834                            data=data,
835                            edit_controller=edit_controller,
836                            file_hash=file_hash,
837                            saved_on=saved_on,
838                            controller=controller,
839                            functions=functions,
840                            view_link=view_link,
841                            editviewlinks=editviewlinks,
842                            id=IS_SLUG()(filename)[0],
843                            force=True if (request.vars.restore or
844                                           request.vars.revert) else False)
845        plain_html = response.render('default/edit_js.html', file_details)
846        file_details['plain_html'] = plain_html
847        if is_mobile:
848            return response.render('default.mobile/edit.html',
849                                   file_details, editor_settings=preferences)
850        else:
851            return response.json(file_details)
852
853
854def todolist():
855    """ Returns all TODO of the requested app
856    """
857    app = request.vars.app or ''
858    app_path = apath('%(app)s' % {'app': app}, r=request)
859    dirs = ['models', 'controllers', 'modules', 'private']
860
861    def listfiles(app, dir, regexp='.*\.py$'):
862        files = sorted(listdir(apath('%(app)s/%(dir)s/' % {'app': app, 'dir': dir}, r=request), regexp))
863        files = [x.replace(os.path.sep, '/') for x in files if not x.endswith('.bak')]
864        return files
865
866    pattern = '#\s*(todo)+\s+(.*)'
867    regex = re.compile(pattern, re.IGNORECASE)
868
869    output = []
870    for d in dirs:
871        for f in listfiles(app, d):
872            matches = []
873            filename = apath(os.path.join(app, d, f), r=request)
874            with safe_open(filename, 'r') as f_s:
875                src = f_s.read()
876                for m in regex.finditer(src):
877                    start = m.start()
878                    lineno = src.count('\n', 0, start) + 1
879                    matches.append({'text': m.group(0), 'lineno': lineno})
880            if len(matches) != 0:
881                output.append({'filename': f, 'matches': matches, 'dir': d})
882
883    return {'todo': output, 'app': app}
884
885
886def editor_sessions():
887    config = Config(os.path.join(request.folder, 'settings.cfg'),
888                    section='editor_sessions', default_values={})
889    preferences = config.read()
890
891    if request.vars.session_name and request.vars.files:
892        session_name = request.vars.session_name
893        files = request.vars.files
894        preferences.update({session_name: ','.join(files)})
895        if config.save(preferences.items()):
896            response.headers["web2py-component-flash"] = T('Session saved correctly')
897        else:
898            response.headers["web2py-component-flash"] = T('Session saved on session only')
899
900    return response.render('default/editor_sessions.html', {'editor_sessions': preferences})
901
902
903def resolve():
904    """
905    """
906
907    filename = '/'.join(request.args)
908    # ## check if file is not there
909    path = apath(filename, r=request)
910    a = safe_read(path).split('\n')
911    try:
912        b = safe_read(path + '.1').split('\n')
913    except IOError:
914        session.flash = 'Other file, no longer there'
915        redirect(URL('edit', args=request.args))
916
917    d = difflib.ndiff(a, b)
918
919    def leading(line):
920        """  """
921
922        # TODO: we really need to comment this
923        z = ''
924        for (k, c) in enumerate(line):
925            if c == ' ':
926                z += '&nbsp;'
927            elif c == ' \t':
928                z += '&nbsp;'
929            elif k == 0 and c == '?':
930                pass
931            else:
932                break
933
934        return XML(z)
935
936    def getclass(item):
937        """ Determine item class """
938        operators = {' ': 'normal', '+': 'plus', '-': 'minus'}
939
940        return operators[item[0]]
941
942    if request.vars:
943        c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0]
944                       == ' ' or 'line%i' % i in request.vars])
945        safe_write(path, c)
946        session.flash = 'files merged'
947        redirect(URL('edit', args=request.args))
948    else:
949        # Making the short circuit compatible with <= python2.4
950        gen_data = lambda index, item: not item[:1] in ['+', '-'] and "" \
951            or INPUT(_type='checkbox',
952                     _name='line%i' % index,
953                     value=item[0] == '+')
954
955        diff = TABLE(*[TR(TD(gen_data(i, item)),
956                          TD(item[0]),
957                          TD(leading(item[2:]),
958                             TT(item[2:].rstrip())),
959                          _class=getclass(item))
960                       for (i, item) in enumerate(d) if item[0] != '?'])
961
962    return dict(diff=diff, filename=filename)
963
964
965def edit_language():
966    """ Edit language file """
967    app = get_app()
968    filename = '/'.join(request.args)
969    response.title = request.args[-1]
970    strings = read_dict(apath(filename, r=request))
971
972    if '__corrupted__' in strings:
973        form = SPAN(strings['__corrupted__'], _class='error')
974        return dict(filename=filename, form=form)
975
976    keys = sorted(strings.keys(), key=lambda x: to_native(x).lower())
977    rows = []
978    rows.append(H2(T('Original/Translation')))
979
980    for key in keys:
981        name = md5_hash(key)
982        s = strings[key]
983        (prefix, sep, key) = key.partition('\x01')
984        if sep:
985            prefix = SPAN(prefix + ': ', _class='tm_ftag')
986            k = key
987        else:
988            (k, prefix) = (prefix, '')
989
990        _class = 'untranslated' if k == s else 'translated'
991
992        if len(s) <= 40:
993            elem = INPUT(_type='text', _name=name, value=s,
994                         _size=70, _class=_class)
995        else:
996            elem = TEXTAREA(_name=name, value=s, _cols=70,
997                            _rows=5, _class=_class)
998
999        # Making the short circuit compatible with <= python2.4
1000        k = (s != k) and k or B(k)
1001
1002        new_row = DIV(LABEL(prefix, k, _style="font-weight:normal;"),
1003                      CAT(elem, '\n', TAG.BUTTON(
1004                          T('delete'),
1005                          _onclick='return delkey("%s")' % name,
1006                          _class='btn')), _id=name, _class='span6 well well-small')
1007
1008        rows.append(DIV(new_row, _class="row-fluid"))
1009    rows.append(DIV(INPUT(_type='submit', _value=T('update'), _class="btn btn-primary"), _class='controls'))
1010    form = FORM(*rows)
1011    if form.accepts(request.vars, keepvalues=True):
1012        strs = dict()
1013        for key in keys:
1014            name = md5_hash(key)
1015            if form.vars[name] == chr(127):
1016                continue
1017            strs[key] = form.vars[name]
1018        write_dict(apath(filename, r=request), strs)
1019        session.flash = T('file saved on %(time)s', dict(time=time.ctime()))
1020        redirect(URL(r=request, args=request.args))
1021    return dict(app=request.args[0], filename=filename, form=form)
1022
1023
1024def edit_plurals():
1025    """ Edit plurals file """
1026    app = get_app()
1027    filename = '/'.join(request.args)
1028    plurals = read_plural_dict(
1029        apath(filename, r=request))  # plural forms dictionary
1030    nplurals = int(request.vars.nplurals) - 1  # plural forms quantity
1031    xnplurals = xrange(nplurals)
1032
1033    if '__corrupted__' in plurals:
1034        # show error message and exit
1035        form = SPAN(plurals['__corrupted__'], _class='error')
1036        return dict(filename=filename, form=form)
1037
1038    keys = sorted(plurals.keys(), lambda x, y: cmp(
1039        unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower()))
1040    tab_rows = []
1041    for key in keys:
1042        name = md5_hash(key)
1043        forms = plurals[key]
1044
1045        if len(forms) < nplurals:
1046            forms.extend(None for i in xrange(nplurals - len(forms)))
1047        tab_col1 = DIV(CAT(LABEL(T("Singular Form")), B(key,
1048                                                        _class='fake-input')))
1049        tab_inputs = [SPAN(LABEL(T("Plural Form #%s", n + 1)), INPUT(_type='text', _name=name + '_' + str(n), value=forms[n], _size=20), _class='span6') for n in xnplurals]
1050        tab_col2 = DIV(CAT(*tab_inputs))
1051        tab_col3 = DIV(CAT(LABEL(XML('&nbsp;')), TAG.BUTTON(T('delete'), _onclick='return delkey("%s")' % name, _class='btn'), _class='span6'))
1052        tab_row = DIV(DIV(tab_col1, '\n', tab_col2, '\n', tab_col3, _class='well well-small'), _id=name, _class='row-fluid tab_row')
1053        tab_rows.append(tab_row)
1054
1055    tab_rows.append(DIV(TAG['button'](T('update'), _type='submit',
1056                                      _class='btn btn-primary'),
1057                        _class='controls'))
1058    tab_container = DIV(*tab_rows, **dict(_class="row-fluid"))
1059
1060    form = FORM(tab_container)
1061    if form.accepts(request.vars, keepvalues=True):
1062        new_plurals = dict()
1063        for key in keys:
1064            name = md5_hash(key)
1065            if form.vars[name + '_0'] == chr(127):
1066                continue
1067            new_plurals[key] = [form.vars[name + '_' + str(n)]
1068                                for n in xnplurals]
1069        write_plural_dict(apath(filename, r=request), new_plurals)
1070        session.flash = T('file saved on %(time)s', dict(time=time.ctime()))
1071        redirect(URL(r=request, args=request.args, vars=dict(
1072            nplurals=request.vars.nplurals)))
1073    return dict(app=request.args[0], filename=filename, form=form)
1074
1075
1076def about():
1077    """ Read about info """
1078    app = get_app()
1079    # ## check if file is not there
1080    about = safe_read(apath('%s/ABOUT' % app, r=request))
1081    license = safe_read(apath('%s/LICENSE' % app, r=request))
1082    return dict(app=app, about=MARKMIN(about), license=MARKMIN(license), progress=report_progress(app))
1083
1084
1085def design():
1086    """ Application design handler """
1087    app = get_app()
1088
1089    if not response.flash and app == request.application:
1090        msg = T('ATTENTION: you cannot edit the running application!')
1091        response.flash = msg
1092
1093    if request.vars and not request.vars.token == session.token:
1094        redirect(URL('logout'))
1095
1096    if request.vars.pluginfile is not None and not isinstance(request.vars.pluginfile, str):
1097        filename = os.path.basename(request.vars.pluginfile.filename)
1098        if plugin_install(app, request.vars.pluginfile.file,
1099                          request, filename):
1100            session.flash = T('new plugin installed')
1101            redirect(URL('design', args=app))
1102        else:
1103            session.flash = \
1104                T('unable to install plugin "%s"', filename)
1105        redirect(URL(r=request, args=app))
1106    elif isinstance(request.vars.pluginfile, str):
1107        session.flash = T('plugin not specified')
1108        redirect(URL(r=request, args=app))
1109
1110    # If we have only pyc files it means that
1111    # we cannot design
1112    if os.path.exists(apath('%s/compiled' % app, r=request)):
1113        session.flash = \
1114            T('application is compiled and cannot be designed')
1115        redirect(URL('site'))
1116
1117    # Get all models
1118    models = listdir(apath('%s/models/' % app, r=request), '.*\.py$')
1119    models = [x.replace('\\', '/') for x in models]
1120    defines = {}
1121    for m in models:
1122        data = safe_read(apath('%s/models/%s' % (app, m), r=request))
1123        defines[m] = re.findall(REGEX_DEFINE_TABLE, data, re.MULTILINE)
1124        defines[m].sort()
1125
1126    # Get all controllers
1127    controllers = sorted(
1128        listdir(apath('%s/controllers/' % app, r=request), '.*\.py$'))
1129    controllers = [x.replace('\\', '/') for x in controllers]
1130    functions = {}
1131    for c in controllers:
1132        data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
1133        try:
1134            items = find_exposed_functions(data)
1135            functions[c] = items and sorted(items) or []
1136        except SyntaxError as err:
1137            functions[c] = ['SyntaxError:Line:%d' % err.lineno]
1138
1139    # Get all views
1140    views = sorted(
1141        listdir(apath('%s/views/' % app, r=request), '[\w/\-]+(\.\w+)+$'))
1142    views = [x.replace('\\', '/') for x in views if not x.endswith('.bak')]
1143    extend = {}
1144    include = {}
1145    for c in views:
1146        data = safe_read(apath('%s/views/%s' % (app, c), r=request))
1147        items = re.findall(REGEX_EXTEND, data, re.MULTILINE)
1148
1149        if items:
1150            extend[c] = items[0][1]
1151
1152        items = re.findall(REGEX_INCLUDE, data)
1153        include[c] = [i[1] for i in items]
1154
1155    # Get all modules
1156    modules = listdir(apath('%s/modules/' % app, r=request), '.*\.py$')
1157    modules = modules = [x.replace('\\', '/') for x in modules]
1158    modules.sort()
1159
1160    # Get all private files
1161    privates = listdir(apath('%s/private/' % app, r=request), '[^\.#].*')
1162    privates = [x.replace('\\', '/') for x in privates]
1163    privates.sort()
1164
1165    # Get all static files
1166    statics = listdir(apath('%s/static/' % app, r=request), '[^\.#].*',
1167                      maxnum=MAXNFILES)
1168    statics = [x.replace(os.path.sep, '/') for x in statics]
1169    statics.sort()
1170
1171    # Get all languages
1172    langpath = os.path.join(apath(app, r=request), 'languages')
1173    languages = dict([(lang, info) for lang, info
1174                      in iteritems(read_possible_languages(langpath))
1175                      if info[2] != 0])  # info[2] is langfile_mtime:
1176    # get only existed files
1177
1178    # Get crontab
1179    cronfolder = apath('%s/cron' % app, r=request)
1180    crontab = apath('%s/cron/crontab' % app, r=request)
1181    if not is_gae:
1182        if not os.path.exists(cronfolder):
1183            os.mkdir(cronfolder)
1184        if not os.path.exists(crontab):
1185            safe_write(crontab, '#crontab')
1186
1187    plugins = []
1188
1189    def filter_plugins(items, plugins):
1190        plugins += [item[7:].split('/')[0].split(
1191            '.')[0] for item in items if item.startswith('plugin_')]
1192        plugins[:] = list(set(plugins))
1193        plugins.sort()
1194        return [item for item in items if not item.startswith('plugin_')]
1195
1196    return dict(app=app,
1197                models=filter_plugins(models, plugins),
1198                defines=defines,
1199                controllers=filter_plugins(controllers, plugins),
1200                functions=functions,
1201                views=filter_plugins(views, plugins),
1202                modules=filter_plugins(modules, plugins),
1203                extend=extend,
1204                include=include,
1205                privates=filter_plugins(privates, plugins),
1206                statics=filter_plugins(statics, plugins),
1207                languages=languages,
1208                crontab=crontab,
1209                plugins=plugins)
1210
1211
1212def delete_plugin():
1213    """ Object delete handler """
1214    app = request.args(0)
1215    plugin = request.args(1)
1216    plugin_name = 'plugin_' + plugin
1217
1218    dialog = FORM.confirm(
1219        T('Delete'),
1220        {T('Cancel'): URL('design', args=app)})
1221
1222    if dialog.accepted:
1223        try:
1224            for folder in ['models', 'views', 'controllers', 'static', 'modules', 'private']:
1225                path = os.path.join(apath(app, r=request), folder)
1226                for item in os.listdir(path):
1227                    if item.rsplit('.', 1)[0] == plugin_name:
1228                        filename = os.path.join(path, item)
1229                        if os.path.isdir(filename):
1230                            shutil.rmtree(filename)
1231                        else:
1232                            os.unlink(filename)
1233            session.flash = T('plugin "%(plugin)s" deleted',
1234                              dict(plugin=plugin))
1235        except Exception:
1236            session.flash = T('unable to delete file plugin "%(plugin)s"',
1237                              dict(plugin=plugin))
1238        redirect(URL('design', args=request.args(0), anchor=request.vars.id2))
1239    return dict(dialog=dialog, plugin=plugin)
1240
1241
1242def plugin():
1243    """ Application design handler """
1244    app = get_app()
1245    plugin = request.args(1)
1246
1247    if not response.flash and app == request.application:
1248        msg = T('ATTENTION: you cannot edit the running application!')
1249        response.flash = msg
1250
1251    # If we have only pyc files it means that
1252    # we cannot design
1253    if os.path.exists(apath('%s/compiled' % app, r=request)):
1254        session.flash = \
1255            T('application is compiled and cannot be designed')
1256        redirect(URL('site'))
1257
1258    # Get all models
1259    models = listdir(apath('%s/models/' % app, r=request), '.*\.py$')
1260    models = [x.replace('\\', '/') for x in models]
1261    defines = {}
1262    for m in models:
1263        data = safe_read(apath('%s/models/%s' % (app, m), r=request))
1264        defines[m] = regex_tables.findall(data)
1265        defines[m].sort()
1266
1267    # Get all controllers
1268    controllers = sorted(
1269        listdir(apath('%s/controllers/' % app, r=request), '.*\.py$'))
1270    controllers = [x.replace('\\', '/') for x in controllers]
1271    functions = {}
1272    for c in controllers:
1273        data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
1274        try:
1275            items = find_exposed_functions(data)
1276            functions[c] = items and sorted(items) or []
1277        except SyntaxError as err:
1278            functions[c] = ['SyntaxError:Line:%d' % err.lineno]
1279
1280    # Get all views
1281    views = sorted(
1282        listdir(apath('%s/views/' % app, r=request), '[\w/\-]+\.\w+$'))
1283    views = [x.replace('\\', '/') for x in views]
1284    extend = {}
1285    include = {}
1286    for c in views:
1287        data = safe_read(apath('%s/views/%s' % (app, c), r=request))
1288        items = re.findall(REGEX_EXTEND, data, re.MULTILINE)
1289        if items:
1290            extend[c] = items[0][1]
1291
1292        items = re.findall(REGEX_INCLUDE, data)
1293        include[c] = [i[1] for i in items]
1294
1295    # Get all modules
1296    modules = listdir(apath('%s/modules/' % app, r=request), '.*\.py$')
1297    modules = modules = [x.replace('\\', '/') for x in modules]
1298    modules.sort()
1299
1300    # Get all private files
1301    privates = listdir(apath('%s/private/' % app, r=request), '[^\.#].*')
1302    privates = [x.replace('\\', '/') for x in privates]
1303    privates.sort()
1304
1305    # Get all static files
1306    statics = listdir(apath('%s/static/' % app, r=request), '[^\.#].*',
1307                      maxnum=MAXNFILES)
1308    statics = [x.replace(os.path.sep, '/') for x in statics]
1309    statics.sort()
1310
1311    # Get all languages
1312    languages = sorted([lang + '.py' for lang, info in
1313                        iteritems(T.get_possible_languages_info())
1314                        if info[2] != 0])  # info[2] is langfile_mtime:
1315    # get only existed files
1316
1317    # Get crontab
1318    crontab = apath('%s/cron/crontab' % app, r=request)
1319    if not os.path.exists(crontab):
1320        safe_write(crontab, '#crontab')
1321
1322    def filter_plugins(items):
1323        regex = re.compile('^plugin_' + plugin + '(/.*|\..*)?$')
1324        return [item for item in items if item and regex.match(item)]
1325
1326    return dict(app=app,
1327                models=filter_plugins(models),
1328                defines=defines,
1329                controllers=filter_plugins(controllers),
1330                functions=functions,
1331                views=filter_plugins(views),
1332                modules=filter_plugins(modules),
1333                extend=extend,
1334                include=include,
1335                privates=filter_plugins(privates),
1336                statics=filter_plugins(statics),
1337                languages=languages,
1338                crontab=crontab)
1339
1340
1341def create_file():
1342    """ Create files handler """
1343    if request.vars and not request.vars.token == session.token:
1344        redirect(URL('logout'))
1345    try:
1346        anchor = '#' + request.vars.id if request.vars.id else ''
1347        if request.vars.app:
1348            app = get_app(request.vars.app)
1349            path = abspath(request.vars.location)
1350        else:
1351            if request.vars.dir:
1352                request.vars.location += request.vars.dir + '/'
1353            app = get_app(name=request.vars.location.split('/')[0])
1354            path = apath(request.vars.location, r=request)
1355        filename = re.sub('[^\w./-]+', '_', request.vars.filename)
1356        if path[-7:] == '/rules/':
1357            # Handle plural rules files
1358            if len(filename) == 0:
1359                raise SyntaxError
1360            if not filename[-3:] == '.py':
1361                filename += '.py'
1362            lang = re.match('^plural_rules-(.*)\.py$', filename).group(1)
1363            langinfo = read_possible_languages(apath(app, r=request))[lang]
1364            text = dedent("""
1365                   #!/usr/bin/env python
1366                   # -*- coding: utf-8 -*-
1367                   # Plural-Forms for %(lang)s (%(langname)s)
1368
1369                   nplurals=2  # for example, English language has 2 forms:
1370                               # 1 singular and 1 plural
1371
1372                   # Determine plural_id for number *n* as sequence of positive
1373                   # integers: 0,1,...
1374                   # NOTE! For singular form ALWAYS return plural_id = 0
1375                   get_plural_id = lambda n: int(n != 1)
1376
1377                   # Construct and return plural form of *word* using
1378                   # *plural_id* (which ALWAYS>0). This function will be executed
1379                   # for words (or phrases) not found in plural_dict dictionary.
1380                   # By default this function simply returns word in singular:
1381                   construct_plural_form = lambda word, plural_id: word
1382                   """)[1:] % dict(lang=langinfo[0], langname=langinfo[1])
1383
1384        elif path[-11:] == '/languages/':
1385            # Handle language files
1386            if len(filename) == 0:
1387                raise SyntaxError
1388            if not filename[-3:] == '.py':
1389                filename += '.py'
1390            path = os.path.join(apath(app, r=request), 'languages', filename)
1391            if not os.path.exists(path):
1392                safe_write(path, '')
1393            # create language xx[-yy].py file:
1394            findT(apath(app, r=request), filename[:-3])
1395            session.flash = T('language file "%(filename)s" created/updated',
1396                              dict(filename=filename))
1397            redirect(request.vars.sender + anchor)
1398
1399        elif path[-8:] == '/models/':
1400            # Handle python models
1401            if not filename[-3:] == '.py':
1402                filename += '.py'
1403
1404            if len(filename) == 3:
1405                raise SyntaxError
1406
1407            text = '# -*- coding: utf-8 -*-\n'
1408
1409        elif path[-13:] == '/controllers/':
1410            # Handle python controllers
1411            if not filename[-3:] == '.py':
1412                filename += '.py'
1413
1414            if len(filename) == 3:
1415                raise SyntaxError
1416
1417            text = '# -*- coding: utf-8 -*-\n# %s\ndef index(): return dict(message="hello from %s")'
1418            text = text % (T('try something like'), filename)
1419
1420        elif path[-7:] == '/views/':
1421            if request.vars.plugin and not filename.startswith('plugin_%s/' % request.vars.plugin):
1422                filename = 'plugin_%s/%s' % (request.vars.plugin, filename)
1423            # Handle template (html) views
1424            if filename.find('.') < 0:
1425                filename += '.html'
1426            extension = filename.split('.')[-1].lower()
1427
1428            if len(filename) == 5:
1429                raise SyntaxError
1430
1431            msg = T(
1432                'This is the %(filename)s template', dict(filename=filename))
1433            if extension == 'html':
1434                text = dedent("""
1435                   {{extend 'layout.html'}}
1436                   <h1>%s</h1>
1437                   {{=BEAUTIFY(response._vars)}}""" % msg)[1:]
1438            else:
1439                generic = os.path.join(path, 'generic.' + extension)
1440                if os.path.exists(generic):
1441                    text = read_file(generic)
1442                else:
1443                    text = ''
1444
1445        elif path[-9:] == '/modules/':
1446            if request.vars.plugin and not filename.startswith('plugin_%s/' % request.vars.plugin):
1447                filename = 'plugin_%s/%s' % (request.vars.plugin, filename)
1448            # Handle python module files
1449            if not filename[-3:] == '.py':
1450                filename += '.py'
1451
1452            if len(filename) == 3:
1453                raise SyntaxError
1454
1455            text = dedent("""
1456                   #!/usr/bin/env python
1457                   # -*- coding: utf-8 -*-
1458                   from gluon import *\n""")[1:]
1459
1460        elif (path[-8:] == '/static/') or (path[-9:] == '/private/'):
1461            if (request.vars.plugin and
1462                    not filename.startswith('plugin_%s/' % request.vars.plugin)):
1463                filename = 'plugin_%s/%s' % (request.vars.plugin, filename)
1464            text = ''
1465
1466        else:
1467            redirect(request.vars.sender + anchor)
1468
1469        full_filename = os.path.join(path, filename)
1470        dirpath = os.path.dirname(full_filename)
1471
1472        if not os.path.exists(dirpath):
1473            os.makedirs(dirpath)
1474
1475        if os.path.exists(full_filename):
1476            raise SyntaxError
1477
1478        safe_write(full_filename, text)
1479        log_progress(app, 'CREATE', filename)
1480        if request.vars.dir:
1481            result = T('file "%(filename)s" created',
1482                       dict(filename=full_filename[len(path):]))
1483        else:
1484            session.flash = T('file "%(filename)s" created',
1485                              dict(filename=full_filename[len(path):]))
1486        vars = {}
1487        if request.vars.id:
1488            vars['id'] = request.vars.id
1489        if request.vars.app:
1490            vars['app'] = request.vars.app
1491        redirect(URL('edit',
1492                     args=[os.path.join(request.vars.location, filename)], vars=vars))
1493
1494    except Exception as e:
1495        if not isinstance(e, HTTP):
1496            session.flash = T('cannot create file')
1497
1498    if request.vars.dir:
1499        response.flash = result
1500        response.headers['web2py-component-content'] = 'append'
1501        response.headers['web2py-component-command'] = "%s %s %s" % (
1502            "$.web2py.invalidate('#files_menu');",
1503            "load_file('%s');" % URL('edit', args=[app, request.vars.dir, filename]),
1504            "$.web2py.enableElement($('#form form').find($.web2py.formInputClickSelector));")
1505        return ''
1506    else:
1507        redirect(request.vars.sender + anchor)
1508
1509
1510def listfiles(app, dir, regexp='.*\.py$'):
1511    files = sorted(
1512        listdir(apath('%(app)s/%(dir)s/' % {'app': app, 'dir': dir}, r=request), regexp))
1513    files = [x.replace('\\', '/') for x in files if not x.endswith('.bak')]
1514    return files
1515
1516
1517def editfile(path, file, vars={}, app=None):
1518    args = (path, file) if 'app' in vars else (app, path, file)
1519    url = URL('edit', args=args, vars=vars)
1520    return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;')
1521
1522
1523def files_menu():
1524    app = request.vars.app or 'welcome'
1525    dirs = [{'name': 'models', 'reg': '.*\.py$'},
1526            {'name': 'controllers', 'reg': '.*\.py$'},
1527            {'name': 'views', 'reg': '[\w/\-]+(\.\w+)+$'},
1528            {'name': 'modules', 'reg': '.*\.py$'},
1529            {'name': 'static', 'reg': '[^\.#].*'},
1530            {'name': 'private', 'reg': '.*\.py$'}]
1531    result_files = []
1532    for dir in dirs:
1533        result_files.append(TAG[''](LI(dir['name'], _class="nav-header component", _onclick="collapse('" + dir['name'] + "_files');"),
1534                                    LI(UL(*[LI(editfile(dir['name'], f, dict(id=dir['name'] + f.replace('.', '__')), app), _style="overflow:hidden", _id=dir['name'] + "__" + f.replace('.', '__'))
1535                                            for f in listfiles(app, dir['name'], regexp=dir['reg'])],
1536                                          _class="nav nav-list small-font"),
1537                                       _id=dir['name'] + '_files', _style="display: none;")))
1538    return dict(result_files=result_files)
1539
1540
1541def upload_file():
1542    """ File uploading handler """
1543    if request.vars and not request.vars.token == session.token:
1544        redirect(URL('logout'))
1545    try:
1546        filename = None
1547        app = get_app(name=request.vars.location.split('/')[0])
1548        path = apath(request.vars.location, r=request)
1549
1550        if request.vars.filename:
1551            filename = re.sub('[^\w\./]+', '_', request.vars.filename)
1552        else:
1553            filename = os.path.split(request.vars.file.filename)[-1]
1554
1555        if path[-8:] == '/models/' and not filename[-3:] == '.py':
1556            filename += '.py'
1557
1558        if path[-9:] == '/modules/' and not filename[-3:] == '.py':
1559            filename += '.py'
1560
1561        if path[-13:] == '/controllers/' and not filename[-3:] == '.py':
1562            filename += '.py'
1563
1564        if path[-7:] == '/views/' and not filename[-5:] == '.html':
1565            filename += '.html'
1566
1567        if path[-11:] == '/languages/' and not filename[-3:] == '.py':
1568            filename += '.py'
1569
1570        filename = os.path.join(path, filename)
1571        dirpath = os.path.dirname(filename)
1572
1573        if not os.path.exists(dirpath):
1574            os.makedirs(dirpath)
1575
1576        data = request.vars.file.file.read()
1577        lineno = count_lines(data)
1578        safe_write(filename, data, 'wb')
1579        log_progress(app, 'UPLOAD', filename, lineno)
1580        session.flash = T('file "%(filename)s" uploaded',
1581                          dict(filename=filename[len(path):]))
1582    except Exception:
1583        if filename:
1584            d = dict(filename=filename[len(path):])
1585        else:
1586            d = dict(filename='unknown')
1587        session.flash = T('cannot upload file "%(filename)s"', d)
1588
1589    redirect(request.vars.sender)
1590
1591
1592def errors():
1593    """ Error handler """
1594    import operator
1595    import os
1596    import hashlib
1597
1598    app = get_app()
1599    if is_gae:
1600        method = 'dbold' if ('old' in
1601                             (request.args(1) or '')) else 'dbnew'
1602    else:
1603        method = request.args(1) or 'new'
1604    db_ready = {}
1605    db_ready['status'] = get_ticket_storage(app)
1606    db_ready['errmessage'] = T(
1607        "No ticket_storage.txt found under /private folder")
1608    db_ready['errlink'] = "http://web2py.com/books/default/chapter/29/13#Collecting-tickets"
1609
1610    if method == 'new':
1611        errors_path = apath('%s/errors' % app, r=request)
1612
1613        delete_hashes = []
1614        for item in request.vars:
1615            if item[:7] == 'delete_':
1616                delete_hashes.append(item[7:])
1617
1618        hash2error = dict()
1619
1620        for fn in listdir(errors_path, '^[a-fA-F0-9.\-]+$'):
1621            fullpath = os.path.join(errors_path, fn)
1622            if not os.path.isfile(fullpath):
1623                continue
1624            try:
1625                fullpath_file = safe_open(fullpath, 'rb')
1626                try:
1627                    error = pickle.load(fullpath_file)
1628                finally:
1629                    fullpath_file.close()
1630            except IOError:
1631                continue
1632            except EOFError:
1633                continue
1634
1635            hash = hashlib.md5(to_bytes(error['traceback'])).hexdigest()
1636
1637            if hash in delete_hashes:
1638                os.unlink(fullpath)
1639            else:
1640                try:
1641                    hash2error[hash]['count'] += 1
1642                except KeyError:
1643                    error_lines = error['traceback'].split("\n")
1644                    last_line = error_lines[-2] if len(error_lines) > 1 else 'unknown'
1645                    error_causer = os.path.split(error['layer'])[1]
1646                    hash2error[hash] = dict(count=1, pickel=error,
1647                                            causer=error_causer,
1648                                            last_line=last_line,
1649                                            hash=hash, ticket=fn)
1650
1651        decorated = [(x['count'], x) for x in hash2error.values()]
1652        decorated.sort(key=operator.itemgetter(0), reverse=True)
1653
1654        return dict(errors=[x[1] for x in decorated], app=app, method=method, db_ready=db_ready)
1655
1656    elif method == 'dbnew':
1657        errors_path = apath('%s/errors' % app, r=request)
1658        tk_db, tk_table = get_ticket_storage(app)
1659
1660        delete_hashes = []
1661        for item in request.vars:
1662            if item[:7] == 'delete_':
1663                delete_hashes.append(item[7:])
1664
1665        hash2error = dict()
1666
1667        for fn in tk_db(tk_table.id > 0).select():
1668            try:
1669                error = pickle.loads(fn.ticket_data)
1670                hash = hashlib.md5(error['traceback']).hexdigest()
1671
1672                if hash in delete_hashes:
1673                    tk_db(tk_table.id == fn.id).delete()
1674                    tk_db.commit()
1675                else:
1676                    try:
1677                        hash2error[hash]['count'] += 1
1678                    except KeyError:
1679                        error_lines = error['traceback'].split("\n")
1680                        last_line = error_lines[-2]
1681                        error_causer = os.path.split(error['layer'])[1]
1682                        hash2error[hash] = dict(count=1,
1683                                                pickel=error, causer=error_causer,
1684                                                last_line=last_line, hash=hash,
1685                                                ticket=fn.ticket_id)
1686            except AttributeError as e:
1687                tk_db(tk_table.id == fn.id).delete()
1688                tk_db.commit()
1689
1690        decorated = [(x['count'], x) for x in hash2error.values()]
1691        decorated.sort(key=operator.itemgetter(0), reverse=True)
1692        return dict(errors=[x[1] for x in decorated], app=app,
1693                    method=method, db_ready=db_ready)
1694
1695    elif method == 'dbold':
1696        tk_db, tk_table = get_ticket_storage(app)
1697        for item in request.vars:
1698            if item[:7] == 'delete_':
1699                tk_db(tk_table.ticket_id == item[7:]).delete()
1700                tk_db.commit()
1701        tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id,
1702                                                 tk_table.created_datetime,
1703                                                 orderby=~tk_table.created_datetime)
1704        tickets = [row.ticket_id for row in tickets_]
1705        times = dict([(row.ticket_id, row.created_datetime) for
1706                      row in tickets_])
1707        return dict(app=app, tickets=tickets, method=method,
1708                    times=times, db_ready=db_ready)
1709
1710    else:
1711        for item in request.vars:
1712            # delete_all rows doesn't contain any ticket
1713            # Remove anything else as requested
1714            if item[:7] == 'delete_' and (not item == "delete_all}"):
1715                os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request))
1716        func = lambda p: os.stat(apath('%s/errors/%s' %
1717                                       (app, p), r=request)).st_mtime
1718        tickets = sorted(
1719            listdir(apath('%s/errors/' % app, r=request), '^\w.*'),
1720            key=func,
1721            reverse=True)
1722
1723        return dict(app=app, tickets=tickets, method=method, db_ready=db_ready)
1724
1725
1726def get_ticket_storage(app):
1727    private_folder = apath('%s/private' % app, r=request)
1728    ticket_file = os.path.join(private_folder, 'ticket_storage.txt')
1729    if os.path.exists(ticket_file):
1730        db_string = safe_open(ticket_file).read()
1731        db_string = db_string.strip().replace('\r', '').replace('\n', '')
1732    elif is_gae:
1733        # use Datastore as fallback if there is no ticket_file
1734        db_string = "google:datastore"
1735    else:
1736        return False
1737    tickets_table = 'web2py_ticket'
1738    tablename = tickets_table + '_' + app
1739    db_path = apath('%s/databases' % app, r=request)
1740    ticketsdb = DAL(db_string, folder=db_path, auto_import=True)
1741    if not ticketsdb.get(tablename):
1742        table = ticketsdb.define_table(
1743            tablename,
1744            Field('ticket_id', length=100),
1745            Field('ticket_data', 'text'),
1746            Field('created_datetime', 'datetime'),
1747        )
1748    return ticketsdb, ticketsdb.get(tablename)
1749
1750
1751def make_link(path):
1752    """ Create a link from a path """
1753    tryFile = path.replace('\\', '/')
1754
1755    if os.path.isabs(tryFile) and os.path.isfile(tryFile):
1756        (folder, filename) = os.path.split(tryFile)
1757        (base, ext) = os.path.splitext(filename)
1758        app = get_app()
1759
1760        editable = {'controllers': '.py', 'models': '.py', 'views': '.html'}
1761        for key in editable.keys():
1762            check_extension = folder.endswith("%s/%s" % (app, key))
1763            if ext.lower() == editable[key] and check_extension:
1764                return to_native(A('"' + tryFile + '"',
1765                                   _href=URL(r=request,
1766                                   f='edit/%s/%s/%s' % (app, key, filename))).xml())
1767    return ''
1768
1769
1770def make_links(traceback):
1771    """ Make links using the given traceback """
1772
1773    lwords = traceback.split('"')
1774
1775    # Making the short circuit compatible with <= python2.4
1776    result = (len(lwords) != 0) and lwords[0] or ''
1777
1778    i = 1
1779
1780    while i < len(lwords):
1781        link = make_link(lwords[i])
1782
1783        if link == '':
1784            result += '"' + lwords[i]
1785        else:
1786            result += link
1787
1788            if i + 1 < len(lwords):
1789                result += lwords[i + 1]
1790                i = i + 1
1791
1792        i = i + 1
1793
1794    return result
1795
1796
1797class TRACEBACK(object):
1798    """ Generate the traceback """
1799
1800    def __init__(self, text):
1801        """ TRACEBACK constructor """
1802
1803        self.s = make_links(CODE(text).xml())
1804
1805    def xml(self):
1806        """ Returns the xml """
1807
1808        return self.s
1809
1810
1811def ticket():
1812    """ Ticket handler """
1813
1814    if len(request.args) != 2:
1815        session.flash = T('invalid ticket')
1816        redirect(URL('site'))
1817
1818    app = get_app()
1819    myversion = request.env.web2py_version
1820    ticket = request.args[1]
1821    e = RestrictedError()
1822    e.load(request, app, ticket)
1823
1824    return dict(app=app,
1825                ticket=ticket,
1826                output=e.output,
1827                traceback=(e.traceback and TRACEBACK(e.traceback)),
1828                snapshot=e.snapshot,
1829                code=e.code,
1830                layer=e.layer,
1831                myversion=myversion)
1832
1833
1834def ticketdb():
1835    """ Ticket handler """
1836
1837    if len(request.args) != 2:
1838        session.flash = T('invalid ticket')
1839        redirect(URL('site'))
1840
1841    app = get_app()
1842    myversion = request.env.web2py_version
1843    ticket = request.args[1]
1844    e = RestrictedError()
1845    request.tickets_db = get_ticket_storage(app)[0]
1846    e.load(request, app, ticket)
1847    response.view = 'default/ticket.html'
1848    return dict(app=app,
1849                ticket=ticket,
1850                output=e.output,
1851                traceback=(e.traceback and TRACEBACK(e.traceback)),
1852                snapshot=e.snapshot,
1853                code=e.code,
1854                layer=e.layer,
1855                myversion=myversion)
1856
1857
1858def error():
1859    """ Generate a ticket (for testing) """
1860    raise RuntimeError('admin ticket generator at your service')
1861
1862
1863def update_languages():
1864    """ Update available languages """
1865
1866    app = get_app()
1867    update_all_languages(apath(app, r=request))
1868    session.flash = T('Language files (static strings) updated')
1869    redirect(URL('design', args=app, anchor='languages'))
1870
1871
1872def user():
1873    if MULTI_USER_MODE:
1874        if not db(db.auth_user).count():
1875            auth.settings.registration_requires_approval = False
1876        return dict(form=auth())
1877    else:
1878        return dict(form=T("Disabled"))
1879
1880
1881def reload_routes():
1882    """ Reload routes.py """
1883    gluon.rewrite.load()
1884    redirect(URL('site'))
1885
1886
1887def manage_students():
1888    if not (MULTI_USER_MODE and is_manager()):
1889        session.flash = T('Not Authorized')
1890        redirect(URL('site'))
1891    db.auth_user.registration_key.writable = True
1892    grid = SQLFORM.grid(db.auth_user)
1893    return locals()
1894
1895
1896def bulk_register():
1897    if not (MULTI_USER_MODE and is_manager()):
1898        session.flash = T('Not Authorized')
1899        redirect(URL('site'))
1900    form = SQLFORM.factory(Field('emails', 'text'))
1901    if form.process().accepted:
1902        emails = [x.strip() for x in form.vars.emails.split('\n') if x.strip()]
1903        n = 0
1904        for email in emails:
1905            if not db.auth_user(email=email):
1906                n += db.auth_user.insert(email=email) and 1 or 0
1907        session.flash = T('%s students registered', n)
1908        redirect(URL('site'))
1909    return locals()
1910
1911# Begin experimental stuff need fixes:
1912# 1) should run in its own process - cannot os.chdir
1913# 2) should not prompt user at console
1914# 3) should give option to force commit and not reuqire manual merge
1915
1916
1917def git_pull():
1918    """ Git Pull handler """
1919    app = get_app()
1920    if not have_git:
1921        session.flash = GIT_MISSING
1922        redirect(URL('site'))
1923    dialog = FORM.confirm(T('Pull'),
1924                          {T('Cancel'): URL('site')})
1925    if dialog.accepted:
1926        try:
1927            repo = git.Repo(os.path.join(apath(r=request), app))
1928            origin = repo.remotes.origin
1929            origin.fetch()
1930            origin.pull()
1931            session.flash = T("Application updated via git pull")
1932            redirect(URL('site'))
1933
1934        except git.CheckoutError:
1935            session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.")
1936            redirect(URL('site'))
1937        except git.UnmergedEntriesError:
1938            session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
1939            redirect(URL('site'))
1940        except git.GitCommandError:
1941            session.flash = T(
1942                "Pull failed, git exited abnormally. See logs for details.")
1943            redirect(URL('site'))
1944        except AssertionError:
1945            session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
1946            redirect(URL('site'))
1947    elif 'cancel' in request.vars:
1948        redirect(URL('site'))
1949    return dict(app=app, dialog=dialog)
1950
1951
1952def git_push():
1953    """ Git Push handler """
1954    app = get_app()
1955    if not have_git:
1956        session.flash = GIT_MISSING
1957        redirect(URL('site'))
1958    form = SQLFORM.factory(Field('changelog', requires=IS_NOT_EMPTY()))
1959    form.element('input[type=submit]')['_value'] = T('Push')
1960    form.add_button(T('Cancel'), URL('site'))
1961    form.process()
1962    if form.accepted:
1963        try:
1964            repo = git.Repo(os.path.join(apath(r=request), app))
1965            index = repo.index
1966            index.add([apath(r=request) + app + '/*'])
1967            new_commit = index.commit(form.vars.changelog)
1968            origin = repo.remotes.origin
1969            origin.push()
1970            session.flash = T(
1971                "Git repo updated with latest application changes.")
1972            redirect(URL('site'))
1973        except git.UnmergedEntriesError:
1974            session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.")
1975            redirect(URL('site'))
1976    return dict(app=app, form=form)
1977
1978
1979def plugins():
1980    app = request.args(0)
1981    from gluon.serializers import loads_json
1982    if not session.plugins:
1983        try:
1984            rawlist = urlopen("http://www.web2pyslices.com/" +
1985                              "public/api.json/action/list/content/Package?package" +
1986                              "_type=plugin&search_index=false").read()
1987            session.plugins = loads_json(rawlist)
1988        except:
1989            response.flash = T('Unable to download the list of plugins')
1990            session.plugins = []
1991    return dict(plugins=session.plugins["results"], app=request.args(0))
1992
1993
1994def install_plugin():
1995    app = request.args(0)
1996    source = request.vars.source
1997    plugin = request.vars.plugin
1998    if not (source and app):
1999        raise HTTP(500, T("Invalid request"))
2000    # make sure no XSS attacks in source
2001    if not source.lower().split('://')[0] in ('http','https'):
2002        raise HTTP(500, T("Invalid request"))
2003    form = SQLFORM.factory()
2004    result = None
2005    if form.process().accepted:
2006        # get w2p plugin
2007        if "web2py.plugin." in source:
2008            filename = "web2py.plugin.%s.w2p" % \
2009                source.split("web2py.plugin.")[-1].split(".w2p")[0]
2010        else:
2011            filename = "web2py.plugin.%s.w2p" % cleanpath(plugin)
2012        if plugin_install(app, urlopen(source),
2013                          request, filename):
2014            session.flash = T('New plugin installed: %s', filename)
2015        else:
2016            session.flash = \
2017                T('unable to install plugin "%s"', filename)
2018        redirect(URL(f="plugins", args=[app, ]))
2019    return dict(form=form, app=app, plugin=plugin, source=source)
Note: See TracBrowser for help on using the repository browser.