summaryrefslogtreecommitdiffstats
path: root/pyload/webui/app
diff options
context:
space:
mode:
Diffstat (limited to 'pyload/webui/app')
-rw-r--r--pyload/webui/app/__init__.py3
-rw-r--r--pyload/webui/app/api.py99
-rw-r--r--pyload/webui/app/cnl.py172
-rw-r--r--pyload/webui/app/json.py314
-rw-r--r--pyload/webui/app/pyload.py526
-rw-r--r--pyload/webui/app/utils.py124
6 files changed, 1238 insertions, 0 deletions
diff --git a/pyload/webui/app/__init__.py b/pyload/webui/app/__init__.py
new file mode 100644
index 000000000..39d0fadd5
--- /dev/null
+++ b/pyload/webui/app/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from pyload.webui.app import api, cnl, json, pyload
diff --git a/pyload/webui/app/api.py b/pyload/webui/app/api.py
new file mode 100644
index 000000000..99a7c2998
--- /dev/null
+++ b/pyload/webui/app/api.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+import itertools
+import traceback
+import urllib
+
+import SafeEval
+import bottle
+
+from pyload.Api import BaseObject
+from pyload.utils import json
+from pyload.webui import PYLOAD
+from pyload.webui.app.utils import toDict, set_session
+
+
+# json encoder that accepts TBase objects
+class TBaseEncoder(json.JSONEncoder):
+
+ def default(self, o):
+ if isinstance(o, BaseObject):
+ return toDict(o)
+ return json.JSONEncoder.default(self, o)
+
+
+# accepting positional arguments, as well as kwargs via post and get
+@bottle.route('/api/<func><args:re:[a-zA-Z0-9\-_/\"\'\[\]%{},]*>')
+@bottle.route('/api/<func><args:re:[a-zA-Z0-9\-_/\"\'\[\]%{},]*>', method='POST')
+def call_api(func, args=""):
+ response.headers.replace("Content-type", "application/json")
+ response.headers.append("Cache-Control", "no-cache, must-revalidate")
+
+ s = request.environ.get('beaker.session')
+ if 'session' in request.POST:
+ s = s.get_by_id(request.POST['session'])
+
+ if not s or not s.get("authenticated", False):
+ return bottle.HTTPError(403, json.dumps("Forbidden"))
+
+ if not PYLOAD.isAuthorized(func, {"role": s['role'], "permission": s['perms']}):
+ return bottle.HTTPError(401, json.dumps("Unauthorized"))
+
+ args = args.split("/")[1:]
+ kwargs = {}
+
+ for x, y in itertools.chain(request.GET.iteritems(), request.POST.iteritems()):
+ if x == "session":
+ continue
+ kwargs[x] = urllib.unquote(y)
+
+ try:
+ return callApi(func, *args, **kwargs)
+ except Exception, e:
+ traceback.print_exc()
+ return bottle.HTTPError(500, json.dumps({"error": e.message, "traceback": traceback.format_exc()}))
+
+
+def callApi(func, *args, **kwargs):
+ if not hasattr(PYLOAD.EXTERNAL, func) or func.startswith("_"):
+ print "Invalid API call", func
+ return bottle.HTTPError(404, json.dumps("Not Found"))
+
+ result = getattr(PYLOAD, func)(*[SafeEval.const_eval(x) for x in args],
+ **dict((x, SafeEval.const_eval(y)) for x, y in kwargs.iteritems()))
+
+ # null is invalid json response
+ return json.dumps(result or True, cls=TBaseEncoder)
+
+
+# post -> username, password
+@bottle.route('/api/login', method='POST')
+def login():
+ response.headers.replace("Content-type", "application/json")
+ response.headers.append("Cache-Control", "no-cache, must-revalidate")
+
+ user = request.forms.get("username")
+ password = request.forms.get("password")
+
+ info = PYLOAD.checkAuth(user, password)
+
+ if not info:
+ return json.dumps(False)
+
+ s = set_session(request, info)
+
+ # get the session id by dirty way, documentations seems wrong
+ try:
+ sid = s._headers['cookie_out'].split("=")[1].split(";")[0]
+ return json.dumps(sid)
+ except Exception:
+ return json.dumps(True)
+
+
+@bottle.route('/api/logout')
+def logout():
+ response.headers.replace("Content-type", "application/json")
+ response.headers.append("Cache-Control", "no-cache, must-revalidate")
+
+ s = request.environ.get('beaker.session')
+ s.delete()
diff --git a/pyload/webui/app/cnl.py b/pyload/webui/app/cnl.py
new file mode 100644
index 000000000..465e087e5
--- /dev/null
+++ b/pyload/webui/app/cnl.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+import base64
+import binascii
+import os
+import re
+import urllib
+
+import bottle
+
+from pyload.webui import PYLOAD, DL_ROOT, JS
+
+
+try:
+ from Crypto.Cipher import AES
+except Exception:
+ pass
+
+
+def local_check(function):
+
+
+ def _view(*args, **kwargs):
+ if request.environ.get("REMOTE_ADDR", "0") in ("127.0.0.1", "localhost") \
+ or request.environ.get("HTTP_HOST", "0") in ("127.0.0.1:9666", "localhost:9666"):
+ return function(*args, **kwargs)
+ else:
+ return bottle.HTTPError(403, "Forbidden")
+
+ return _view
+
+
+@bottle.route('/flash')
+@bottle.route('/flash/<id>')
+@bottle.route('/flash', method='POST')
+@local_check
+def flash(id="0"):
+ return "JDownloader\r\n"
+
+
+@bottle.route('/flash/add', method='POST')
+@local_check
+def add(request):
+ package = request.POST.get('referer', None)
+ urls = filter(lambda x: x != "", request.POST['urls'].split("\n"))
+
+ if package:
+ PYLOAD.addPackage(package, urls, 0)
+ else:
+ PYLOAD.generateAndAddPackages(urls, 0)
+
+ return ""
+
+
+@bottle.route('/flash/addcrypted', method='POST')
+@local_check
+def addcrypted():
+ package = request.forms.get('referer', 'ClickNLoad Package')
+ dlc = request.forms['crypted'].replace(" ", "+")
+
+ dlc_path = os.path.join(DL_ROOT, package.replace("/", "").replace("\\", "").replace(":", "") + ".dlc")
+ with open(dlc_path, "wb") as dlc_file:
+ dlc_file.write(dlc)
+
+ try:
+ PYLOAD.addPackage(package, [dlc_path], 0)
+ except Exception:
+ return bottle.HTTPError()
+ else:
+ return "success\r\n"
+
+
+@bottle.route('/flash/addcrypted2', method='POST')
+@local_check
+def addcrypted2():
+ package = request.forms.get("source", None)
+ crypted = request.forms['crypted']
+ jk = request.forms['jk']
+
+ crypted = base64.standard_b64decode(urllib.unquote(crypted.replace(" ", "+")))
+ if JS:
+ jk = "%s f()" % jk
+ jk = JS.eval(jk)
+
+ else:
+ try:
+ jk = re.findall(r"return ('|\")(.+)('|\")", jk)[0][1]
+ except Exception:
+ # Test for some known js functions to decode
+ if jk.find("dec") > -1 and jk.find("org") > -1:
+ org = re.findall(r"var org = ('|\")([^\"']+)", jk)[0][1]
+ jk = list(org)
+ jk.reverse()
+ jk = "".join(jk)
+ else:
+ print "Could not decrypt key, please install py-spidermonkey or ossp-js"
+
+ try:
+ Key = binascii.unhexlify(jk)
+ except Exception:
+ print "Could not decrypt key, please install py-spidermonkey or ossp-js"
+ return "failed"
+
+ IV = Key
+
+ obj = AES.new(Key, AES.MODE_CBC, IV)
+ result = obj.decrypt(crypted).replace("\x00", "").replace("\r", "").split("\n")
+
+ result = filter(lambda x: x != "", result)
+
+ try:
+ if package:
+ PYLOAD.addPackage(package, result, 0)
+ else:
+ PYLOAD.generateAndAddPackages(result, 0)
+ except Exception:
+ return "failed can't add"
+ else:
+ return "success\r\n"
+
+
+@bottle.route('/flashgot_pyload')
+@bottle.route('/flashgot_pyload', method='POST')
+@bottle.route('/flashgot')
+@bottle.route('/flashgot', method='POST')
+@local_check
+def flashgot():
+ if request.environ['HTTP_REFERER'] not in ("http://localhost:9666/flashgot", "http://127.0.0.1:9666/flashgot"):
+ return bottle.HTTPError()
+
+ autostart = int(request.forms.get('autostart', 0))
+ package = request.forms.get('package', None)
+ urls = filter(lambda x: x != "", request.forms['urls'].split("\n"))
+ folder = request.forms.get('dir', None)
+
+ if package:
+ PYLOAD.addPackage(package, urls, autostart)
+ else:
+ PYLOAD.generateAndAddPackages(urls, autostart)
+
+ return ""
+
+
+@bottle.route('/crossdomain.xml')
+@local_check
+def crossdomain():
+ rep = "<?xml version=\"1.0\"?>\n"
+ rep += "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
+ rep += "<cross-domain-policy>\n"
+ rep += "<allow-access-from domain=\"*\" />\n"
+ rep += "</cross-domain-policy>"
+ return rep
+
+
+@bottle.route('/flash/checkSupportForUrl')
+@local_check
+def checksupport():
+ url = request.GET.get("url")
+ res = PYLOAD.checkURLs([url])
+ supported = (not res[0][1] is None)
+
+ return str(supported).lower()
+
+
+@bottle.route('/jdcheck.js')
+@local_check
+def jdcheck():
+ rep = "jdownloader=true;\n"
+ rep += "var version='9.581;'"
+ return rep
diff --git a/pyload/webui/app/json.py b/pyload/webui/app/json.py
new file mode 100644
index 000000000..a7fe8dcfb
--- /dev/null
+++ b/pyload/webui/app/json.py
@@ -0,0 +1,314 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+import os
+import shutil
+import traceback
+
+import bottle
+
+from pyload.utils import decode, formatSize
+from pyload.webui import PYLOAD
+from pyload.webui.app.utils import login_required, render_to_response, toDict
+
+
+def format_time(seconds):
+ seconds = int(seconds)
+
+ hours, seconds = divmod(seconds, 3600)
+ minutes, seconds = divmod(seconds, 60)
+ return "%.2i:%.2i:%.2i" % (hours, minutes, seconds)
+
+
+def get_sort_key(item):
+ return item['order']
+
+
+@bottle.route('/json/status')
+@bottle.route('/json/status', method='POST')
+@login_required('LIST')
+def status():
+ try:
+ status = toDict(PYLOAD.statusServer())
+ status['captcha'] = PYLOAD.isCaptchaWaiting()
+ return status
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/links')
+@bottle.route('/json/links', method='POST')
+@login_required('LIST')
+def links():
+ try:
+ links = [toDict(x) for x in PYLOAD.statusDownloads()]
+ ids = []
+ for link in links:
+ ids.append(link['fid'])
+
+ if link['status'] == 12:
+ link['info'] = "%s @ %s/s" % (link['format_eta'], formatSize(link['speed']))
+ elif link['status'] == 5:
+ link['percent'] = 0
+ link['size'] = 0
+ link['bleft'] = 0
+ link['info'] = _("waiting %s") % link['format_wait']
+ else:
+ link['info'] = ""
+
+ data = {'links': links, 'ids': ids}
+ return data
+ except Exception, e:
+ traceback.print_exc()
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/packages')
+@login_required('LIST')
+def packages():
+ print "/json/packages"
+ try:
+ data = PYLOAD.getQueue()
+
+ for package in data:
+ package['links'] = []
+ for file in PYLOAD.get_package_files(package['id']):
+ package['links'].append(PYLOAD.get_file_info(file))
+
+ return data
+
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/package/<id:int>')
+@login_required('LIST')
+def package(id):
+ try:
+ data = toDict(PYLOAD.getPackageData(id))
+ data['links'] = [toDict(x) for x in data['links']]
+
+ for pyfile in data['links']:
+ if pyfile['status'] == 0:
+ pyfile['icon'] = "status_finished.png"
+ elif pyfile['status'] in (2, 3):
+ pyfile['icon'] = "status_queue.png"
+ elif pyfile['status'] in (9, 1):
+ pyfile['icon'] = "status_offline.png"
+ elif pyfile['status'] == 5:
+ pyfile['icon'] = "status_waiting.png"
+ elif pyfile['status'] == 8:
+ pyfile['icon'] = "status_failed.png"
+ elif pyfile['status'] == 4:
+ pyfile['icon'] = "arrow_right.png"
+ elif pyfile['status'] in (11, 13):
+ pyfile['icon'] = "status_proc.png"
+ else:
+ pyfile['icon'] = "status_downloading.png"
+
+ tmp = data['links']
+ tmp.sort(key=get_sort_key)
+ data['links'] = tmp
+ return data
+
+ except Exception:
+ traceback.print_exc()
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/package_order/<ids>')
+@login_required('ADD')
+def package_order(ids):
+ try:
+ pid, pos = ids.split("|")
+ PYLOAD.orderPackage(int(pid), int(pos))
+ return {"response": "success"}
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/abort_link/<id:int>')
+@login_required('DELETE')
+def abort_link(id):
+ try:
+ PYLOAD.stopDownloads([id])
+ return {"response": "success"}
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/link_order/<ids>')
+@login_required('ADD')
+def link_order(ids):
+ try:
+ pid, pos = ids.split("|")
+ PYLOAD.orderFile(int(pid), int(pos))
+ return {"response": "success"}
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/add_package')
+@bottle.route('/json/add_package', method='POST')
+@login_required('ADD')
+def add_package():
+ name = request.forms.get("add_name", "New Package").strip()
+ queue = int(request.forms['add_dest'])
+ links = decode(request.forms['add_links'])
+ links = links.split("\n")
+ pw = request.forms.get("add_password", "").strip("\n\r")
+
+ try:
+ f = request.files['add_file']
+
+ if not name or name == "New Package":
+ name = f.name
+
+ fpath = os.path.join(PYLOAD.getConfigValue("general", "download_folder"), "tmp_" + f.filename)
+ with open(fpath, 'wb') as destination:
+ shutil.copyfileobj(f.file, destination)
+ links.insert(0, fpath)
+ except Exception:
+ pass
+
+ name = name.decode("utf8", "ignore")
+
+ links = map(lambda x: x.strip(), links)
+ links = filter(lambda x: x != "", links)
+
+ pack = PYLOAD.addPackage(name, links, queue)
+ if pw:
+ pw = pw.decode("utf8", "ignore")
+ data = {"password": pw}
+ PYLOAD.setPackageData(pack, data)
+
+
+@bottle.route('/json/move_package/<dest:int>/<id:int>')
+@login_required('MODIFY')
+def move_package(dest, id):
+ try:
+ PYLOAD.movePackage(dest, id)
+ return {"response": "success"}
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/edit_package', method='POST')
+@login_required('MODIFY')
+def edit_package():
+ try:
+ id = int(request.forms.get("pack_id"))
+ data = {"name": request.forms.get("pack_name").decode("utf8", "ignore"),
+ "folder": request.forms.get("pack_folder").decode("utf8", "ignore"),
+ "password": request.forms.get("pack_pws").decode("utf8", "ignore")}
+
+ PYLOAD.setPackageData(id, data)
+ return {"response": "success"}
+
+ except Exception:
+ return bottle.HTTPError()
+
+
+@bottle.route('/json/set_captcha')
+@bottle.route('/json/set_captcha', method='POST')
+@login_required('ADD')
+def set_captcha():
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ try:
+ PYLOAD.setCaptchaResult(request.forms['cap_id'], request.forms['cap_result'])
+ except Exception:
+ pass
+
+ task = PYLOAD.getCaptchaTask()
+
+ if task.tid >= 0:
+ src = "data:image/%s;base64,%s" % (task.type, task.data)
+
+ return {'captcha': True, 'id': task.tid, 'src': src, 'result_type': task.resultType}
+ else:
+ return {'captcha': False}
+
+
+@bottle.route('/json/load_config/<category>/<section>')
+@login_required("SETTINGS")
+def load_config(category, section):
+ conf = None
+ if category == "general":
+ conf = PYLOAD.getConfigDict()
+ elif category == "plugin":
+ conf = PYLOAD.getPluginConfigDict()
+
+ for key, option in conf[section].iteritems():
+ if key in ("desc", "outline"):
+ continue
+
+ if ";" in option['type']:
+ option['list'] = option['type'].split(";")
+
+ option['value'] = decode(option['value'])
+
+ return render_to_response("settings_item.html", {"sorted_conf": lambda c: sorted(c.items(), key=lambda i: i[1]['idx'] if i[0] not in ("desc", "outline") else 0),
+ "skey": section, "section": conf[section]})
+
+
+@bottle.route('/json/save_config/<category>', method='POST')
+@login_required("SETTINGS")
+def save_config(category):
+ for key, value in request.POST.iteritems():
+ try:
+ section, option = key.split("|")
+ except Exception:
+ continue
+
+ if category == "general": category = "core"
+
+ PYLOAD.setConfigValue(section, option, decode(value), category)
+
+
+@bottle.route('/json/add_account', method='POST')
+@login_required("ACCOUNTS")
+def add_account():
+ login = request.POST['account_login']
+ password = request.POST['account_password']
+ type = request.POST['account_type']
+
+ PYLOAD.updateAccount(type, login, password)
+
+
+@bottle.route('/json/update_accounts', method='POST')
+@login_required("ACCOUNTS")
+def update_accounts():
+ deleted = [] #: dont update deleted accs or they will be created again
+
+ for name, value in request.POST.iteritems():
+ value = value.strip()
+ if not value:
+ continue
+
+ tmp, user = name.split(";")
+ plugin, action = tmp.split("|")
+
+ if (plugin, user) in deleted:
+ continue
+
+ if action == "password":
+ PYLOAD.updateAccount(plugin, user, value)
+ elif action == "time" and "-" in value:
+ PYLOAD.updateAccount(plugin, user, options={"time": [value]})
+ elif action == "limitdl" and value.isdigit():
+ PYLOAD.updateAccount(plugin, user, options={"limitDL": [value]})
+ elif action == "delete":
+ deleted.append((plugin, user))
+ PYLOAD.removeAccount(plugin, user)
+
+
+@bottle.route('/json/change_password', method='POST')
+def change_password():
+ user = request.POST['user_login']
+ oldpw = request.POST['login_current_password']
+ newpw = request.POST['login_new_password']
+
+ if not PYLOAD.changePassword(user, oldpw, newpw):
+ print "Wrong password"
+ return bottle.HTTPError()
diff --git a/pyload/webui/app/pyload.py b/pyload/webui/app/pyload.py
new file mode 100644
index 000000000..58acdf12c
--- /dev/null
+++ b/pyload/webui/app/pyload.py
@@ -0,0 +1,526 @@
+# -*- coding: utf-8 -*-
+# @author: RaNaN
+
+import datetime
+import operator
+import os
+import sys
+import time
+import urllib
+
+import bottle
+
+from pyload.webui import PYLOAD, PYLOAD_DIR, THEME_DIR, THEME, SETUP, env
+
+from pyload.webui.app.utils import render_to_response, parse_permissions, parse_userdata, \
+ login_required, get_permission, set_permission, permlist, toDict, set_session
+
+from pyload.utils.filters import relpath, unquotepath
+
+from pyload.utils import formatSize, fs_join, fs_encode, fs_decode
+
+# Helper
+
+
+def pre_processor():
+ s = request.environ.get('beaker.session')
+ user = parse_userdata(s)
+ perms = parse_permissions(s)
+ status = {}
+ captcha = False
+ update = False
+ plugins = False
+ if user['is_authenticated']:
+ status = PYLOAD.statusServer()
+ info = PYLOAD.getInfoByPlugin("UpdateManager")
+ captcha = PYLOAD.isCaptchaWaiting()
+
+ # check if update check is available
+ if info:
+ if info['pyload'] == "True":
+ update = info['version']
+ if info['plugins'] == "True":
+ plugins = True
+
+ return {"user": user,
+ 'status': status,
+ 'captcha': captcha,
+ 'perms': perms,
+ 'url': request.url,
+ 'update': update,
+ 'plugins': plugins}
+
+
+def base(messages):
+ return render_to_response('base.html', {'messages': messages}, [pre_processor])
+
+
+# Views
+@bottle.error(403)
+def error403(code):
+ return "The parameter you passed has the wrong format"
+
+
+@bottle.error(404)
+def error404(code):
+ return "Sorry, this page does not exist"
+
+
+@bottle.error(500)
+def error500(error):
+ traceback = error.traceback
+ if traceback:
+ print traceback
+ return base(["An Error occured, please enable debug mode to get more details.", error,
+ traceback.replace("\n", "<br>") if traceback else "No Traceback"])
+
+
+@bottle.route('/<theme>/<file:re:(.+/)?[^/]+\.min\.[^/]+>')
+def server_min(theme, file):
+ filename = os.path.join(THEME_DIR, THEME, theme, file)
+ if not os.path.isfile(filename):
+ file = file.replace(".min.", ".")
+ if file.endswith(".js"):
+ return server_js(theme, file)
+ else:
+ return server_static(theme, file)
+
+
+@bottle.route('/<theme>/<file:re:.+\.js>')
+def server_js(theme, file):
+ response.headers['Content-Type'] = "text/javascript; charset=UTF-8"
+
+ if "/render/" in file or ".render." in file or True:
+ response.headers['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+ time.gmtime(time.time() + 24 * 7 * 60 * 60))
+ response.headers['Cache-control'] = "public"
+
+ path = "/".join((theme, file))
+ return env.get_template(path).render()
+ else:
+ return server_static(theme, file)
+
+
+@bottle.route('/<theme>/<file:path>')
+def server_static(theme, file):
+ response.headers['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+ time.gmtime(time.time() + 24 * 7 * 60 * 60))
+ response.headers['Cache-control'] = "public"
+
+ return bottle.static_file(file, root=join(THEME_DIR, THEME, theme))
+
+
+@bottle.route('/favicon.ico')
+def favicon():
+ return bottle.static_file("icon.ico", root=join(PYLOAD_DIR, "docs", "resources"))
+
+
+@bottle.route('/login', method="GET")
+def login():
+ if not PYLOAD and SETUP:
+ bottle.redirect("/setup")
+ else:
+ return render_to_response("login.html", proc=[pre_processor])
+
+
+@bottle.route('/nopermission')
+def nopermission():
+ return base([_("You dont have permission to access this page.")])
+
+
+@bottle.route('/login', method='POST')
+def login_post():
+ user = request.forms.get("username")
+ password = request.forms.get("password")
+
+ info = PYLOAD.checkAuth(user, password)
+
+ if not info:
+ return render_to_response("login.html", {"errors": True}, [pre_processor])
+
+ set_session(request, info)
+ return bottle.redirect("/")
+
+
+@bottle.route('/logout')
+def logout():
+ s = request.environ.get('beaker.session')
+ s.delete()
+ return render_to_response("logout.html", proc=[pre_processor])
+
+
+@bottle.route('/')
+@bottle.route('/home')
+@login_required("LIST")
+def home():
+ try:
+ res = [toDict(x) for x in PYLOAD.statusDownloads()]
+ except Exception:
+ s = request.environ.get('beaker.session')
+ s.delete()
+ return bottle.redirect("/login")
+
+ for link in res:
+ if link['status'] == 12:
+ link['information'] = "%s kB @ %s kB/s" % (link['size'] - link['bleft'], link['speed'])
+
+ return render_to_response("home.html", {"res": res}, [pre_processor])
+
+
+@bottle.route('/queue')
+@login_required("LIST")
+def queue():
+ queue = PYLOAD.getQueue()
+
+ queue.sort(key=operator.attrgetter("order"))
+
+ return render_to_response('queue.html', {'content': queue, 'target': 1}, [pre_processor])
+
+
+@bottle.route('/collector')
+@login_required('LIST')
+def collector():
+ queue = PYLOAD.getCollector()
+
+ queue.sort(key=operator.attrgetter("order"))
+
+ return render_to_response('queue.html', {'content': queue, 'target': 0}, [pre_processor])
+
+
+@bottle.route('/downloads')
+@login_required('DOWNLOAD')
+def downloads():
+ root = PYLOAD.getConfigValue("general", "download_folder")
+
+ if not os.path.isdir(root):
+ return base([_('Download directory not found.')])
+ data = {
+ 'folder': [],
+ 'files': []
+ }
+
+ items = os.listdir(fs_encode(root))
+
+ for item in sorted([fs_decode(x) for x in items]):
+ if os.path.isdir(fs_join(root, item)):
+ folder = {
+ 'name': item,
+ 'path': item,
+ 'files': []
+ }
+ files = os.listdir(fs_join(root, item))
+ for file in sorted([fs_decode(x) for x in files]):
+ try:
+ if os.path.isfile(fs_join(root, item, file)):
+ folder['files'].append(file)
+ except Exception:
+ pass
+
+ data['folder'].append(folder)
+ elif os.path.isfile(os.path.join(root, item)):
+ data['files'].append(item)
+
+ return render_to_response('downloads.html', {'files': data}, [pre_processor])
+
+
+@bottle.route('/downloads/get/<path:path>')
+@login_required("DOWNLOAD")
+def get_download(path):
+ path = urllib.unquote(path).decode("utf8")
+ #@TODO some files can not be downloaded
+
+ root = PYLOAD.getConfigValue("general", "download_folder")
+
+ path = path.replace("..", "")
+ return bottle.static_file(fs_encode(path), fs_encode(root))
+
+
+@bottle.route('/settings')
+@login_required('SETTINGS')
+def config():
+ conf = PYLOAD.getConfig()
+ plugin = PYLOAD.getPluginConfig()
+ conf_menu = []
+ plugin_menu = []
+
+ for entry in sorted(conf.keys()):
+ conf_menu.append((entry, conf[entry].description))
+
+ last_name = None
+ for entry in sorted(plugin.keys()):
+ desc = plugin[entry].description
+ name, none, type = desc.partition("_")
+
+ if type in PYLOAD.core.pluginManager.TYPES:
+ if name == last_name or len([a for a, b in plugin.iteritems() if b.description.startswith(name + "_")]) > 1:
+ desc = name + " (" + type.title() + ")"
+ else:
+ desc = name
+ last_name = name
+ plugin_menu.append((entry, desc))
+
+ accs = PYLOAD.getAccounts(False)
+
+ for data in accs:
+ if data.trafficleft == -1:
+ data.trafficleft = _("unlimited")
+ elif not data.trafficleft:
+ data.trafficleft = _("not available")
+ else:
+ data.trafficleft = formatSize(data.trafficleft)
+
+ if data.validuntil == -1:
+ data.validuntil = _("unlimited")
+ elif not data.validuntil:
+ data.validuntil = _("not available")
+ else:
+ t = time.localtime(data.validuntil)
+ data.validuntil = time.strftime("%d.%m.%Y - %H:%M:%S", t)
+
+ try:
+ data.options['time'] = data.options['time'][0]
+ except Exception:
+ data.options['time'] = "0:00-0:00"
+
+ if "limitDL" in data.options:
+ data.options['limitdl'] = data.options['limitDL'][0]
+ else:
+ data.options['limitdl'] = "0"
+
+ return render_to_response('settings.html',
+ {'conf': {'plugin': plugin_menu, 'general': conf_menu, 'accs': accs},
+ 'types': PYLOAD.getAccountTypes()},
+ [pre_processor])
+
+
+@bottle.route('/filechooser')
+@bottle.route('/pathchooser')
+@bottle.route('/filechooser/<file:path>')
+@bottle.route('/pathchooser/<path:path>')
+@login_required('STATUS')
+def os.path(file="", path=""):
+ type = "file" if file else "folder"
+
+ path = os.path.normpath(unquotepath(path))
+
+ if os.path.isfile(path):
+ oldfile = path
+ path = os.path.dirname(path)
+ else:
+ oldfile = ''
+
+ abs = False
+
+ if os.path.isdir(path):
+ if os.path.isabs(path):
+ cwd = os.path.abspath(path)
+ abs = True
+ else:
+ cwd = os.relpath(path)
+ else:
+ cwd = os.getcwd()
+
+ try:
+ cwd = cwd.encode("utf8")
+ except Exception:
+ pass
+
+ cwd = os.path.normpath(os.path.abspath(cwd))
+ parentdir = os.path.dirname(cwd)
+ if not abs:
+ if os.path.abspath(cwd) == "/":
+ cwd = os.relpath(cwd)
+ else:
+ cwd = os.relpath(cwd) + os.path.sep
+ parentdir = os.relpath(parentdir) + os.path.sep
+
+ if os.path.abspath(cwd) == "/":
+ parentdir = ""
+
+ try:
+ folders = os.listdir(cwd)
+ except Exception:
+ folders = []
+
+ files = []
+
+ for f in folders:
+ try:
+ f = f.decode(sys.getfilesystemencoding())
+ data = {'name': f, 'fullpath': os.path.join(cwd, f)}
+ data['sort'] = data['fullpath'].lower()
+ data['modified'] = datetime.datetime.fromtimestamp(int(os.path.getmtime(os.path.join(cwd, f))))
+ data['ext'] = os.path.splitext(f)[1]
+ except Exception:
+ continue
+
+ data['type'] = 'dir' if os.path.isdir(os.path.join(cwd, f)) else 'file'
+
+ if os.path.isfile(os.path.join(cwd, f)):
+ data['size'] = os.path.getsize(os.path.join(cwd, f))
+
+ power = 0
+ while (data['size'] / 1024) > 0.3:
+ power += 1
+ data['size'] /= 1024.
+ units = ('', 'K', 'M', 'G', 'T')
+ data['unit'] = units[power] + 'Byte'
+ else:
+ data['size'] = ''
+
+ files.append(data)
+
+ files = sorted(files, key=operator.itemgetter('type', 'sort'))
+
+ return render_to_response('pathchooser.html',
+ {'cwd': cwd, 'files': files, 'parentdir': parentdir, 'type': type, 'oldfile': oldfile,
+ 'absolute': abs}, [])
+
+
+@bottle.route('/logs')
+@bottle.route('/logs', method='POST')
+@bottle.route('/logs/<item>')
+@bottle.route('/logs/<item>', method='POST')
+@login_required('LOGS')
+def logs(item=-1):
+ s = request.environ.get('beaker.session')
+
+ perpage = s.get('perpage', 34)
+ reversed = s.get('reversed', False)
+
+ warning = ""
+ conf = PYLOAD.getConfigValue("log", "file_log")
+ color_template = PYLOAD.getConfigValue("log", "color_template") if PYLOAD.getConfigValue("log", "color_console") else ""
+ if not conf:
+ warning = "Warning: File log is disabled, see settings page."
+
+ perpage_p = ((20, 20), (34, 34), (40, 40), (100, 100), (0, 'all'))
+ fro = None
+
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ try:
+ fro = datetime.datetime.strptime(request.forms['from'], '%d.%m.%Y %H:%M:%S')
+ except Exception:
+ pass
+ try:
+ perpage = int(request.forms['perpage'])
+ s['perpage'] = perpage
+
+ reversed = bool(request.forms.get('reversed', False))
+ s['reversed'] = reversed
+ except Exception:
+ pass
+
+ s.save()
+
+ try:
+ item = int(item)
+ except Exception:
+ pass
+
+ log = PYLOAD.getLog()
+ if not perpage:
+ item = 1
+
+ if item < 1 or type(item) is not int:
+ item = 1 if len(log) - perpage + 1 < 1 else len(log) - perpage + 1
+
+ if type(fro) is datetime.datetime: #: we will search for datetime.datetime
+ item = -1
+
+ data = []
+ counter = 0
+ perpagecheck = 0
+ for l in log:
+ counter += 1
+
+ if counter >= item:
+ try:
+ date, time, level, message = l.decode("utf8", "ignore").split(" ", 3)
+ dtime = datetime.datetime.strptime(date + ' ' + time, '%Y-%m-%d %H:%M:%S')
+ except Exception:
+ dtime = None
+ date = '?'
+ time = ' '
+ level = '?'
+ message = l
+ if item == -1 and dtime is not None and fro <= dtime:
+ item = counter #: found our datetime.datetime
+ if item >= 0:
+ data.append({'line': counter, 'date': date + " " + time, 'level': level, 'message': message})
+ perpagecheck += 1
+ if fro is None and dtime is not None: #: if fro not set set it to first showed line
+ fro = dtime
+ if perpagecheck >= perpage > 0:
+ break
+
+ if fro is None: #: still not set, empty log?
+ fro = datetime.datetime.now()
+ if reversed:
+ data.reverse()
+ return render_to_response('logs.html', {'warning': warning, 'log': data, 'from': fro.strftime('%d.%m.%Y %H:%M:%S'),
+ 'reversed': reversed, 'perpage': perpage, 'perpage_p': sorted(perpage_p),
+ 'iprev': 1 if item - perpage < 1 else item - perpage,
+ 'inext': (item + perpage) if item + perpage < len(log) else item,
+ 'color_template': color_template.title()},
+ [pre_processor])
+
+
+@bottle.route('/admin')
+@bottle.route('/admin', method='POST')
+@login_required("ADMIN")
+def admin():
+ # convert to dict
+ user = dict((name, toDict(y)) for name, y in PYLOAD.getAllUserData().iteritems())
+ perms = permlist()
+
+ for data in user.itervalues():
+ data['perms'] = {}
+ get_permission(data['perms'], data['permission'])
+ data['perms']['admin'] = data['role'] is 0
+
+ s = request.environ.get('beaker.session')
+ if request.environ.get('REQUEST_METHOD', "GET") == "POST":
+ for name in user:
+ if request.POST.get("%s|admin" % name, False):
+ user[name]['role'] = 0
+ user[name]['perms']['admin'] = True
+ elif name != s['name']:
+ user[name]['role'] = 1
+ user[name]['perms']['admin'] = False
+
+ # set all perms to false
+ for perm in perms:
+ user[name]['perms'][perm] = False
+
+ for perm in request.POST.getall("%s|perms" % name):
+ user[name]['perms'][perm] = True
+
+ user[name]['permission'] = set_permission(user[name]['perms'])
+
+ PYLOAD.setUserPermission(name, user[name]['permission'], user[name]['role'])
+
+ return render_to_response("admin.html", {"users": user, "permlist": perms}, [pre_processor])
+
+
+@bottle.route('/setup')
+def setup():
+ return base([_("Run pyload.py -s to access the setup.")])
+
+
+@bottle.route('/info')
+def info():
+ conf = PYLOAD.getConfigDict()
+ extra = os.uname() if hasattr(os, "uname") else tuple()
+
+ data = {"python" : sys.version,
+ "os" : " ".join((os.name, sys.platform) + extra),
+ "version" : PYLOAD.getServerVersion(),
+ "folder" : os.path.abspath(PYLOAD_DIR), "config": os.path.abspath(""),
+ "download" : os.path.abspath(conf['general']['download_folder']['value']),
+ "freespace": formatSize(PYLOAD.freeSpace()),
+ "remote" : conf['remote']['port']['value'],
+ "webif" : conf['webui']['port']['value'],
+ "language" : conf['general']['language']['value']}
+
+ return render_to_response("info.html", data, [pre_processor])
diff --git a/pyload/webui/app/utils.py b/pyload/webui/app/utils.py
new file mode 100644
index 000000000..2e7cf76c5
--- /dev/null
+++ b/pyload/webui/app/utils.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# @author: RaNaN, vuolter
+
+import os
+
+import bottle
+
+from pyload.Api import has_permission, PERMS, ROLE
+from pyload.webui import env, THEME
+
+
+def render_to_response(file, args={}, proc=[]):
+ for p in proc:
+ args.update(p())
+ path = "tml/" + file
+ return env.get_template(path).render(**args)
+
+
+def parse_permissions(session):
+ perms = dict((x, False) for x in dir(PERMS) if not x.startswith("_"))
+ perms['ADMIN'] = False
+ perms['is_admin'] = False
+
+ if not session.get("authenticated", False):
+ return perms
+
+ if session.get("role") == ROLE.ADMIN:
+ for k in perms.iterkeys():
+ perms[k] = True
+
+ elif session.get("perms"):
+ p = session.get("perms")
+ get_permission(perms, p)
+
+ return perms
+
+
+def permlist():
+ return [x for x in dir(PERMS) if not x.startswith("_") and x != "ALL"]
+
+
+def get_permission(perms, p):
+ """Returns a dict with permission key
+
+ :param perms: dictionary
+ :param p: bits
+ """
+ for name in permlist():
+ perms[name] = has_permission(p, getattr(PERMS, name))
+
+
+def set_permission(perms):
+ """generates permission bits from dictionary
+
+ :param perms: dict
+ """
+ permission = 0
+ for name in dir(PERMS):
+ if name.startswith("_"):
+ continue
+
+ if name in perms and perms[name]:
+ permission |= getattr(PERMS, name)
+
+ return permission
+
+
+def set_session(request, info):
+ s = request.environ.get('beaker.session')
+ s['authenticated'] = True
+ s['user_id'] = info['id']
+ s['name'] = info['name']
+ s['role'] = info['role']
+ s['perms'] = info['permission']
+ s['template'] = info['template']
+ s.save()
+
+ return s
+
+
+def parse_userdata(session):
+ return {"name" : session.get("name", "Anonymous"),
+ "is_admin" : session.get("role", 1) == 0,
+ "is_authenticated": session.get("authenticated", False)}
+
+
+def login_required(perm=None):
+
+ def _dec(func):
+
+ def _view(*args, **kwargs):
+ s = request.environ.get('beaker.session')
+ if s.get("name", None) and s.get("authenticated", False):
+ if perm:
+ perms = parse_permissions(s)
+ if perm not in perms or not perms[perm]:
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return bottle.HTTPError(403, "Forbidden")
+ else:
+ return bottle.redirect("/nopermission")
+
+ return func(*args, **kwargs)
+ else:
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ return bottle.HTTPError(403, "Forbidden")
+ else:
+ return bottle.redirect("/login")
+
+ return _view
+
+ return _dec
+
+
+def toDict(obj):
+ return {att: getattr(obj, att) for att in obj.__slots__}
+
+
+class CherryPyWSGI(ServerAdapter):
+
+ def run(self, handler):
+ from wsgiserver import CherryPyWSGIServer
+
+ server = CherryPyWSGIServer((self.host, self.port), handler)
+ server.start()