summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pyload/AccountManager.py142
-rw-r--r--pyload/PluginManager.py4
-rw-r--r--pyload/api/AccountApi.py58
-rw-r--r--pyload/config/ConfigParser.py36
-rw-r--r--pyload/config/convert.py29
-rw-r--r--pyload/database/AccountDatabase.py30
-rw-r--r--pyload/database/DatabaseBackend.py4
-rw-r--r--pyload/datatypes/User.py5
-rw-r--r--pyload/plugins/Account.py118
-rw-r--r--pyload/plugins/Base.py2
-rw-r--r--pyload/plugins/Hoster.py8
-rw-r--r--pyload/plugins/ReCaptcha.py22
-rw-r--r--pyload/plugins/accounts/Http.py7
-rw-r--r--pyload/plugins/addons/MultiHoster.py8
-rw-r--r--pyload/plugins/internal/CaptchaService.py2
-rw-r--r--pyload/plugins/network/CurlRequest.py2
-rw-r--r--pyload/plugins/network/XDCCRequest.py (renamed from pyload/network/XDCCRequest.py)2
-rw-r--r--pyload/remote/apitypes.py15
-rw-r--r--pyload/remote/apitypes_debug.py5
-rw-r--r--pyload/remote/pyload.thrift12
-rw-r--r--pyload/remote/wsbackend/AbstractHandler.py13
-rw-r--r--pyload/web/app/scripts/collections/AccountList.js3
-rw-r--r--pyload/web/app/scripts/models/Account.js2
-rw-r--r--pyload/web/app/scripts/views/accounts/accountModal.js2
-rw-r--r--pyload/web/app/scripts/views/headerView.js3
-rw-r--r--tests/helper/Stubs.py1
-rw-r--r--tests/manager/test_accountManager.py70
27 files changed, 375 insertions, 230 deletions
diff --git a/pyload/AccountManager.py b/pyload/AccountManager.py
index ab753c2e4..1c409b754 100644
--- a/pyload/AccountManager.py
+++ b/pyload/AccountManager.py
@@ -19,8 +19,10 @@
from threading import Lock
from random import choice
+from pyload.Api import AccountInfo
from pyload.utils import lock, json
+
class AccountManager:
"""manages all accounts"""
@@ -30,111 +32,123 @@ class AccountManager:
self.core = core
self.lock = Lock()
+ # PluginName mapped to list of account instances
+ self.accounts = {}
+
self.loadAccounts()
- def loadAccounts(self):
- """loads all accounts available"""
+ def _createAccount(self, info, password, options):
+ plugin = info.plugin
+ loginname = info.loginname
+ # Owner != None must be enforced
+ if info.owner is None:
+ raise ValueError("Owner must not be null")
- self.accounts = {}
+ klass = self.core.pluginManager.loadClass("accounts", plugin)
+ if not klass:
+ self.core.log.warning(_("Unknown account plugin %s") % plugin)
+ return
- for plugin, loginname, activated, password, options in self.core.db.loadAccounts():
- # put into options as used in other context
- options = json.loads(options) if options else {}
- options["activated"] = activated
+ if plugin not in self.accounts:
+ self.accounts[plugin] = []
- self.createAccount(plugin, loginname, password, options)
+ self.core.log.debug("Create account %s:%s" % (plugin, loginname))
+ # New account instance
+ account = klass.fromInfoData(self, info, password, options)
+ self.accounts[plugin].append(account)
+ return account
+
+ def loadAccounts(self):
+ """loads all accounts available from db"""
+
+ for info, password, options in self.core.db.loadAccounts():
+ # put into options as used in other context
+ options = json.loads(options) if options else {}
+ try:
+ self._createAccount(info, password, options)
+ except:
+ self.core.log.error(_("Could not load account %s") % info)
+ self.core.print_exc()
def iterAccounts(self):
""" yields login, account for all accounts"""
- for name, data in self.accounts.iteritems():
- for login, account in data.iteritems():
- yield login, account
+ for plugin, accounts in self.accounts.iteritems():
+ for account in accounts:
+ yield plugin, account
def saveAccounts(self):
"""save all account information"""
- # TODO: multi user
- # TODO: activated
-
data = []
- for name, plugin in self.accounts.iteritems():
+ for plugin, accounts in self.accounts.iteritems():
data.extend(
- [(name, acc.loginname, 1 if acc.activated else 0, acc.password, json.dumps(acc.options)) for acc in
- plugin.itervalues()])
+ [(plugin, acc.loginname, acc.owner, 1 if acc.activated else 0, 1 if acc.shared else 0, acc.password,
+ json.dumps(acc.options)) for acc in
+ accounts])
self.core.db.saveAccounts(data)
- def createAccount(self, plugin, loginname, password, options):
- klass = self.core.pluginManager.loadClass("accounts", plugin)
- if not klass:
- self.core.log.warning(_("Unknown account plugin %s") % plugin)
- return
-
- if plugin not in self.accounts:
- self.accounts[plugin] = {}
-
- self.core.log.debug("Create account %s:%s" % (plugin, loginname))
-
- self.accounts[plugin][loginname] = klass(self, loginname, password, options)
-
-
- def getAccount(self, plugin, user):
- return self.accounts[plugin].get(user, None)
+ def getAccount(self, plugin, loginname, user=None):
+ """ Find a account by specific user (if given) """
+ if plugin in self.accounts:
+ for acc in self.accounts[plugin]:
+ if acc.loginname == loginname and (not user or acc.owner == user.true_primary):
+ return acc
@lock
- def updateAccount(self, plugin, user, password=None, options={}):
+ def updateAccount(self, plugin, loginname, password, user):
"""add or update account"""
- if plugin in self.accounts and user in self.accounts[plugin]:
- acc = self.accounts[plugin][user]
- updated = acc.update(password, options)
-
- self.saveAccounts()
- if updated: acc.scheduleRefresh(force=True)
+ account = self.getAccount(plugin, loginname, user)
+ if account:
+ if account.setPassword(password):
+ self.saveAccounts()
+ account.scheduleRefresh(force=True)
else:
- self.createAccount(plugin, user, password, options)
+ info = AccountInfo(plugin, loginname, user.true_primary, activated=True)
+ account = self._createAccount(info, password, {})
+ account.scheduleRefresh()
self.saveAccounts()
- self.sendChange(plugin, user)
+ self.sendChange(plugin, loginname)
+ return account
@lock
- def removeAccount(self, plugin, user):
+ def removeAccount(self, plugin, loginname, uid):
"""remove account"""
- if plugin in self.accounts and user in self.accounts[plugin]:
- del self.accounts[plugin][user]
- self.core.db.removeAccount(plugin, user)
- self.core.eventManager.dispatchEvent("account:deleted", plugin, user)
- else:
- self.core.log.debug("Remove non existent account %s %s" % (plugin, user))
-
+ if plugin in self.accounts:
+ for acc in self.accounts[plugin]:
+ # admins may delete accounts
+ if acc.loginname == loginname and (not uid or acc.owner == uid):
+ self.accounts[plugin].remove(acc)
+ self.core.db.removeAccount(plugin, loginname)
+ self.core.evm.dispatchEvent("account:deleted", plugin, loginname)
+ break
@lock
- def getAccountForPlugin(self, plugin):
+ def selectAccount(self, plugin, user):
+ """ Determines suitable plugins and select one """
if plugin in self.accounts:
- accs = [x for x in self.accounts[plugin].values() if x.isUsable()]
+ uid = user.true_primary if user else None
+ accs = [x for x in self.accounts[plugin] if x.isUsable() and (x.shared or x.owner == uid)]
if accs: return choice(accs)
- return None
-
@lock
- def getAllAccounts(self, refresh=False):
+ def getAllAccounts(self, uid):
""" Return account info, refresh afterwards if needed
:param refresh:
:return:
"""
- if refresh:
- self.core.scheduler.addJob(0, self.core.accountManager.getAllAccounts)
-
- # load unavailable account info
- for p_dict in self.accounts.itervalues():
- for acc in p_dict.itervalues():
- acc.getAccountInfo()
+ # filter by owner / shared, but admins see all accounts
+ accounts = []
+ for plugin, accs in self.accounts.iteritems():
+ accounts.extend([acc for acc in accs if acc.shared or not uid or acc.owner == uid])
- return self.accounts
+ return accounts
def refreshAllAccounts(self):
""" Force a refresh of every account """
for p in self.accounts.itervalues():
- for acc in p.itervalues():
+ for acc in p:
acc.getAccountInfo(True)
def sendChange(self, plugin, name):
diff --git a/pyload/PluginManager.py b/pyload/PluginManager.py
index 182768689..6886903cc 100644
--- a/pyload/PluginManager.py
+++ b/pyload/PluginManager.py
@@ -199,8 +199,8 @@ class PluginManager:
# create plugin tuple
plugin = PluginTuple(version, plugin_re, deps, category, bool(home), filename)
- # internals have no config
- if folder == "internal":
+ # These have none or their own config
+ if folder in ("internal", "accounts", "network"):
return plugin
if folder == "addons" and "config" not in attrs and not attrs["internal"]:
diff --git a/pyload/api/AccountApi.py b/pyload/api/AccountApi.py
index 999484974..144074d3c 100644
--- a/pyload/api/AccountApi.py
+++ b/pyload/api/AccountApi.py
@@ -1,44 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from pyload.Api import Api, RequirePerm, Permission
-
+from pyload.Api import Api, RequirePerm, Permission, Conflict
from ApiComponent import ApiComponent
class AccountApi(ApiComponent):
""" All methods to control accounts """
+ @RequirePerm(Permission.All)
+ def getAccountTypes(self):
+ """All available account types.
+
+ :return: string list
+ """
+ return self.core.pluginManager.getPlugins("accounts").keys()
+
@RequirePerm(Permission.Accounts)
- def getAccounts(self, refresh):
+ def getAccounts(self):
"""Get information about all entered accounts.
- :param refresh: reload account info
:return: list of `AccountInfo`
"""
- accs = self.core.accountManager.getAllAccounts(refresh)
- accounts = []
- for plugin in accs.itervalues():
- accounts.extend([acc.toInfoData() for acc in plugin.values()])
+ accounts = self.core.accountManager.getAllAccounts(self.primaryUID)
+ return [acc.toInfoData() for acc in accounts]
- return accounts
-
- @RequirePerm(Permission.All)
- def getAccountTypes(self):
- """All available account types.
+ @RequirePerm(Permission.Accounts)
+ def getAccountInfo(self, plugin, loginname, refresh=False):
+ """ Returns :class:`AccountInfo` for a specific account
- :return: string list
+ :param refresh: reload account info
"""
- return self.core.pluginManager.getPlugins("accounts").keys()
+ account = self.core.accountManager.getAccount(plugin, loginname)
+
+ # Admins can see and refresh accounts
+ if not account or (self.primaryUID and self.primaryUID != account.owner):
+ return None
+
+ if refresh:
+ # reload account in place
+ account.getAccountInfo(True)
+
+ return account.toInfoData()
@RequirePerm(Permission.Accounts)
- def updateAccount(self, plugin, login, password):
- """Changes pw/options for specific account."""
- # TODO: options
- self.core.accountManager.updateAccount(plugin, login, password, {})
+ def updateAccount(self, plugin, loginname, password):
+ """Creates an account if not existent or updates the password
+
+ :return: newly created or updated account info
+ """
+ return self.core.accountManager.updateAccount(plugin, loginname, password, self.user).toInfoData()
+
+ @RequirePerm(Permission.Accounts)
def updateAccountInfo(self, account):
- """ Update account from :class:`AccountInfo` """
+ """ Update account settings from :class:`AccountInfo` """
#TODO
@RequirePerm(Permission.Accounts)
@@ -47,7 +63,7 @@ class AccountApi(ApiComponent):
:param account: :class:`ÀccountInfo` instance
"""
- self.core.accountManager.removeAccount(account.plugin, account.loginname)
+ self.core.accountManager.removeAccount(account.plugin, account.loginname, self.primaryUID)
if Api.extend(AccountApi):
diff --git a/pyload/config/ConfigParser.py b/pyload/config/ConfigParser.py
index bda3f7bd4..0f96fd8b9 100644
--- a/pyload/config/ConfigParser.py
+++ b/pyload/config/ConfigParser.py
@@ -1,21 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
-from time import sleep
from os.path import exists
from gettext import gettext
from new_collections import namedtuple, OrderedDict
-
from pyload.Api import Input, InputType
from pyload.utils.fs import chmod
from default import make_config
-from convert import to_input, from_string
+from convert import to_configdata, from_string
CONF_VERSION = 2
SectionTuple = namedtuple("SectionTuple", "label description explanation config")
-ConfigData = namedtuple("ConfigData", "label description input")
+
class ConfigParser:
"""
@@ -109,8 +107,10 @@ class ConfigParser:
for option, data in data.config.iteritems():
value = self.get(section, option)
- if type(value) == unicode: value = value.encode("utf8")
- else: value = str(value)
+ if type(value) == unicode:
+ value = value.encode("utf8")
+ else:
+ value = str(value)
f.write('%s = %s\n' % (option, value))
@@ -165,35 +165,19 @@ class ConfigParser:
return self.config[section], self.values[section] if section in self.values else {}
def addConfigSection(self, section, label, desc, expl, config):
- """Adds a section to the config. `config` is a list of config tuples as used in plugin api defined as:
+ """Adds a section to the config. `config` is a list of config tuple as used in plugin api defined as:
The order of the config elements is preserved with OrderedDict
"""
d = OrderedDict()
for entry in config:
- if len(entry) != 4:
- raise ValueError("Config entry must be of length 4")
-
- # Values can have different roles depending on the two config formats
- conf_name, type_label, label_desc, default_input = entry
-
- # name, label, desc, input
- if isinstance(default_input, Input):
- input = default_input
- conf_label = type_label
- conf_desc = label_desc
- # name, type, label, default
- else:
- input = Input(to_input(type_label))
- input.default_value = from_string(default_input, input.type)
- conf_label = label_desc
- conf_desc = ""
-
- d[conf_name] = ConfigData(gettext(conf_label), gettext(conf_desc), input)
+ name, data = to_configdata(entry)
+ d[name] = data
data = SectionTuple(gettext(label), gettext(desc), gettext(expl), d)
self.config[section] = data
+
class Section:
"""provides dictionary like access for configparser"""
diff --git a/pyload/config/convert.py b/pyload/config/convert.py
index 7a110e0f3..59f814020 100644
--- a/pyload/config/convert.py
+++ b/pyload/config/convert.py
@@ -1,7 +1,14 @@
+# -*- coding: utf-8 -*-
+
+from gettext import gettext
+
+from new_collections import namedtuple
from pyload.Api import Input, InputType
from pyload.utils import decode, to_bool
+ConfigData = namedtuple("ConfigData", "label description input")
+
# Maps old config formats to new values
input_dict = {
"int": InputType.Int,
@@ -18,6 +25,28 @@ def to_input(typ):
return input_dict.get(typ, InputType.Text)
+def to_configdata(entry):
+ if len(entry) != 4:
+ raise ValueError("Config entry must be of length 4")
+
+ # Values can have different roles depending on the two config formats
+ conf_name, type_label, label_desc, default_input = entry
+
+ # name, label, desc, input
+ if isinstance(default_input, Input):
+ _input = default_input
+ conf_label = type_label
+ conf_desc = label_desc
+ # name, type, label, default
+ else:
+ _input = Input(to_input(type_label))
+ _input.default_value = from_string(default_input, _input.type)
+ conf_label = label_desc
+ conf_desc = ""
+
+ return conf_name, ConfigData(gettext(conf_label), gettext(conf_desc), _input)
+
+
def from_string(value, typ=None):
""" cast value to given type, unicode for strings """
diff --git a/pyload/database/AccountDatabase.py b/pyload/database/AccountDatabase.py
index eaa1a3203..3ca841fbc 100644
--- a/pyload/database/AccountDatabase.py
+++ b/pyload/database/AccountDatabase.py
@@ -1,25 +1,29 @@
# -*- coding: utf-8 -*-
-from pyload.database import queue, async
-from pyload.database import DatabaseBackend
+from pyload.Api import AccountInfo
+from pyload.database import DatabaseMethods, queue, async
-class AccountMethods:
+class AccountMethods(DatabaseMethods):
@queue
- def loadAccounts(db):
- db.c.execute('SELECT plugin, loginname, activated, password, options FROM accounts;')
- return db.c.fetchall()
+ def loadAccounts(self):
+ self.c.execute('SELECT plugin, loginname, owner, activated, shared, password, options FROM accounts')
+
+ return [(AccountInfo(r[0], r[1], r[2], activated=r[3] is 1, shared=r[4] is 1), r[5], r[6]) for r in self.c]
@async
- def saveAccounts(db, data):
- # TODO: owner, shared
+ def saveAccounts(self, data):
- db.c.executemany(
- 'INSERT INTO accounts(plugin, loginname, activated, password, options) VALUES(?,?,?,?,?)', data)
+ self.c.executemany(
+ 'INSERT INTO accounts(plugin, loginname, owner, activated, shared, password, options) VALUES(?,?,?,?,?,?,?)',
+ data)
@async
- def removeAccount(db, plugin, loginname):
- db.c.execute('DELETE FROM accounts WHERE plugin=? AND loginname=?', (plugin, loginname))
+ def removeAccount(self, plugin, loginname):
+ self.c.execute('DELETE FROM accounts WHERE plugin=? AND loginname=?', (plugin, loginname))
+ @queue
+ def purgeAccounts(self):
+ self.c.execute('DELETE FROM accounts')
-DatabaseBackend.registerSub(AccountMethods) \ No newline at end of file
+AccountMethods.register() \ No newline at end of file
diff --git a/pyload/database/DatabaseBackend.py b/pyload/database/DatabaseBackend.py
index 2244a3026..df8c6e704 100644
--- a/pyload/database/DatabaseBackend.py
+++ b/pyload/database/DatabaseBackend.py
@@ -369,9 +369,9 @@ class DatabaseBackend(Thread):
'"plugin" TEXT NOT NULL, '
'"loginname" TEXT NOT NULL, '
'"owner" INTEGER NOT NULL DEFAULT -1, '
- '"activated" INTEGER DEFAULT 1, '
+ '"activated" INTEGER NOT NULL DEFAULT 1, '
'"password" TEXT DEFAULT "", '
- '"shared" INTEGER DEFAULT 0, '
+ '"shared" INTEGER NOT NULL DEFAULT 0, '
'"options" TEXT DEFAULT "", '
'FOREIGN KEY(owner) REFERENCES users(uid), '
'PRIMARY KEY (plugin, loginname, owner) ON CONFLICT REPLACE'
diff --git a/pyload/datatypes/User.py b/pyload/datatypes/User.py
index 31c9a55cc..645fd0983 100644
--- a/pyload/datatypes/User.py
+++ b/pyload/datatypes/User.py
@@ -60,4 +60,9 @@ class User(UserData):
Secondary user account share id with primary user. Only Admins have no primary id. """
if self.hasRole(Role.Admin):
return None
+ return self.true_primary
+
+ @property
+ def true_primary(self):
+ """ Primary handle that does not distinguish admin accounts """
return self.user if self.user else self.uid \ No newline at end of file
diff --git a/pyload/plugins/Account.py b/pyload/plugins/Account.py
index 4492dfa18..b3e26ce58 100644
--- a/pyload/plugins/Account.py
+++ b/pyload/plugins/Account.py
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
from time import time
-from traceback import print_exc
from threading import RLock
-from pyload.utils import compare_time, format_size, parseFileSize, lock, to_bool
-from pyload.Api import AccountInfo
+from pyload.Api import AccountInfo, ConfigItem
from pyload.network.CookieJar import CookieJar
+from pyload.config.convert import from_string, to_configdata
+from pyload.utils import to_string, compare_time, format_size, parseFileSize, lock
from Base import Base
@@ -29,40 +29,30 @@ class Account(Base):
UNLIMITED = -2
# Default values
- owner = None
valid = True
validuntil = -1
trafficleft = -1
maxtraffic = -1
premium = True
- activated = True
- shared = False
#: after that time [in minutes] pyload will relogin the account
login_timeout = 600
#: account data will be reloaded after this time
info_threshold = 600
- # known options
- known_opt = ("time", "limitDL")
+ @classmethod
+ def fromInfoData(cls, m, info, password, options):
+ return cls(m, info.loginname, info.owner,
+ True if info.activated else False, True if info.shared else False, password, options)
- def __init__(self, manager, loginname, password, options):
- Base.__init__(self, manager.core)
-
- if "activated" in options:
- self.activated = to_bool(options["activated"])
- else:
- self.activated = Account.activated
-
- for opt in self.known_opt:
- if opt not in options:
- options[opt] = ""
-
- for opt in options.keys():
- if opt not in self.known_opt:
- del options[opt]
+ def __init__(self, manager, loginname, owner, activated, shared, password, options):
+ Base.__init__(self, manager.core, owner)
self.loginname = loginname
+ self.owner = owner
+ self.activated = activated
+ self.shared = shared
+ self.password = password
self.options = options
self.manager = manager
@@ -71,25 +61,58 @@ class Account(Base):
self.timestamp = 0
self.login_ts = 0 # timestamp for login
self.cj = CookieJar()
- self.password = password
self.error = None
+ try:
+ self.config_data = dict(to_configdata(x) for x in self.__config__)
+ except Exception, e:
+ self.logError("Invalid config: %s" % e)
+ self.config_data = {}
+
self.init()
def toInfoData(self):
- return AccountInfo(self.__name__, self.loginname, self.owner, self.valid, self.validuntil, self.trafficleft,
- self.maxtraffic,
- self.premium, self.activated, self.shared, self.options)
+ info = AccountInfo(self.__name__, self.loginname, self.owner, self.valid, self.validuntil, self.trafficleft,
+ self.maxtraffic, self.premium, self.activated, self.shared, self.options)
+
+ info.config = [ConfigItem(name, item.label, item.description, item.input,
+ to_string(self.getConfig(name))) for name, item in
+ self.config_data.iteritems()]
+ return info
def init(self):
pass
+ def getConfig(self, option):
+ """ Gets an option that was configured via the account options dialog and
+ is only valid for this specific instance."""
+ if option not in self.config_data:
+ return Base.getConfig(self, option)
+
+ if option in self.options:
+ return self.options[option]
+
+ return self.config_data[option].input.default_value
+
+ def setConfig(self, option, value):
+ """ Sets a config value for this account instance. Fallsback """
+ if option not in self.config_data:
+ return Base.setConfig(self, option, value)
+
+ value = from_string(value, self.config_data[option].input.type)
+ # given value is the default value and does not need to be saved at all
+ if value == self.config_data[option].input.default_value:
+ if option in self.options:
+ del self.options[option]
+ else:
+ self.options[option] = from_string(value, self.config_data[option].input.type)
+
def login(self, req):
"""login into account, the cookies will be saved so the user can be recognized
:param req: `Request` instance
"""
- raise NotImplemented
+ raise NotImplementedError
def relogin(self):
""" Force a login. """
@@ -123,8 +146,7 @@ class Account(Base):
_("Could not login with account %(user)s | %(msg)s") % {"user": self.loginname
, "msg": e})
self.valid = False
- if self.core.debug:
- print_exc()
+ self.core.print_exc()
return self.valid
@@ -134,28 +156,24 @@ class Account(Base):
self.maxtraffic = Account.maxtraffic
self.premium = Account.premium
- def update(self, password=None, options=None):
- """ updates the account and returns true if anything changed """
-
- self.login_ts = 0
- self.valid = True #set valid, so the login will be retried
+ def setPassword(self, password):
+ """ updates the password and returns true if anything changed """
- if "activated" in options:
- self.activated = True if options["activated"] == "True" else False
+ if password != self.password:
+ self.login_ts = 0
+ self.valid = True #set valid, so the login will be retried
- if password:
self.password = password
- self.relogin()
return True
- if options:
- # remove unknown options
- for opt in options.keys():
- if opt not in self.known_opt:
- del options[opt]
- before = self.options
- self.options.update(options)
- return self.options != before
+ return False
+
+ def updateConfig(self, items):
+ """ Updates the accounts options from config items """
+ for item in items:
+ # Check if a valid option
+ if item.name in self.config_data:
+ self.setConfig(item.name, item.value)
def getAccountRequest(self):
return self.core.requestFactory.getRequest(self.cj)
@@ -163,7 +181,7 @@ class Account(Base):
def getDownloadSettings(self):
""" Can be overwritten to change download settings. Default is no chunkLimit, max dl limit, resumeDownload
- :return: (chunkLimit, limitDL, resumeDownload) / (int, int ,bool)
+ :return: (chunkLimit, limitDL, resumeDownload) / (int, int, bool)
"""
return -1, 0, True
@@ -229,9 +247,11 @@ class Account(Base):
def isUsable(self):
"""Check several constraints to determine if account should be used"""
+
if not self.valid or not self.activated: return False
- if self.options["time"]:
+ # TODO: not in ui currently
+ if "time" in self.options and self.options["time"]:
time_data = ""
try:
time_data = self.options["time"]
diff --git a/pyload/plugins/Base.py b/pyload/plugins/Base.py
index cd4831d82..3ca8abdd0 100644
--- a/pyload/plugins/Base.py
+++ b/pyload/plugins/Base.py
@@ -92,7 +92,7 @@ class Base(object):
self.evm = core.eventManager
#: :class:`InteractionManager`
self.im = core.interactionManager
- if user:
+ if user is not None:
#: :class:`Api`, user api when user is set
self.api = self.core.api.withUserContext(user)
if not self.api:
diff --git a/pyload/plugins/Hoster.py b/pyload/plugins/Hoster.py
index 44b10899d..b3be7a9e9 100644
--- a/pyload/plugins/Hoster.py
+++ b/pyload/plugins/Hoster.py
@@ -82,17 +82,13 @@ class Hoster(Base):
self.ocr = None #captcha reader instance
#: account handler instance, see :py:class:`Account`
- self.account = self.core.accountManager.getAccountForPlugin(self.__name__)
+ self.account = self.core.accountManager.selectAccount(self.__name__, self.user)
#: premium status
self.premium = False
- #: username/login
- self.user = None
- if self.account and not self.account.isUsable(): self.account = None
if self.account:
- self.user = self.account.loginname
- #: Browser instance, see `network.Browser`
+ #: Request instance bound to account
self.req = self.account.getAccountRequest()
# Default: -1, True, True
self.chunkLimit, self.limitDL, self.resumeDownload = self.account.getDownloadSettings()
diff --git a/pyload/plugins/ReCaptcha.py b/pyload/plugins/ReCaptcha.py
deleted file mode 100644
index e47522b4a..000000000
--- a/pyload/plugins/ReCaptcha.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import re
-
-class ReCaptcha():
- def __init__(self, plugin):
- self.plugin = plugin
- self.plugin.logDebug("Deprecated usage of ReCaptcha: Use CaptchaService instead")
-
- def challenge(self, id):
- js = self.plugin.req.load("http://www.google.com/recaptcha/api/challenge", get={"k":id}, cookies=True)
-
- try:
- challenge = re.search("challenge : '(.*?)',", js).group(1)
- server = re.search("server : '(.*?)',", js).group(1)
- except:
- self.plugin.fail("recaptcha error")
- result = self.result(server,challenge)
-
- return challenge, result
-
- def result(self, server, challenge):
- return self.plugin.decryptCaptcha("%simage"%server, get={"c":challenge}, cookies=True, imgtype="jpg")
-
diff --git a/pyload/plugins/accounts/Http.py b/pyload/plugins/accounts/Http.py
index 5701d1f03..de9490b2c 100644
--- a/pyload/plugins/accounts/Http.py
+++ b/pyload/plugins/accounts/Http.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-from module.plugins.Account import Account
+from pyload.plugins.Account import Account
class Http(Account):
@@ -11,4 +11,9 @@ class Http(Account):
__author_name__ = ("zoidberg")
__author_mail__ = ("zoidberg@mujmail.cz")
+ __config__ = [("domain", "str", "Domain", "")]
+
login_timeout = info_threshold = 1000000
+
+ def login(self, req):
+ pass \ No newline at end of file
diff --git a/pyload/plugins/addons/MultiHoster.py b/pyload/plugins/addons/MultiHoster.py
index 329a87e4a..446dfe922 100644
--- a/pyload/plugins/addons/MultiHoster.py
+++ b/pyload/plugins/addons/MultiHoster.py
@@ -72,18 +72,18 @@ class MultiHoster(Addon):
@AddEventListener("account:deleted")
- def refreshAccounts(self, plugin=None, user=None):
+ def refreshAccounts(self, plugin=None, loginname=None):
self.logDebug("Re-checking accounts")
self.plugins = {}
- for name, account in self.core.accountManager.iterAccounts():
+ for plugin, account in self.core.accountManager.iterAccounts():
if isinstance(account, MultiHosterAccount) and account.isUsable():
self.addHoster(account)
@AddEventListener("account:updated")
- def refreshAccount(self, plugin, user):
+ def refreshAccount(self, plugin, loginname):
- account = self.core.accountManager.getAccount(plugin, user)
+ account = self.core.accountManager.getAccount(plugin, loginname)
if isinstance(account, MultiHosterAccount) and account.isUsable():
self.addHoster(account)
diff --git a/pyload/plugins/internal/CaptchaService.py b/pyload/plugins/internal/CaptchaService.py
index b912436a7..4f903e3e6 100644
--- a/pyload/plugins/internal/CaptchaService.py
+++ b/pyload/plugins/internal/CaptchaService.py
@@ -60,8 +60,6 @@ class AdsCaptcha(CaptchaService):
return self.plugin.decryptCaptcha("%sChallenge.aspx" % server, get={"cid": challenge, "dummy": random()}, cookies=True, imgtype="jpg")
class SolveMedia(CaptchaService):
- def __init__(self,plugin):
- self.plugin = plugin
def challenge(self, src):
html = self.plugin.req.load("http://api.solvemedia.com/papi/challenge.noscript?k=%s" % src, cookies=True)
diff --git a/pyload/plugins/network/CurlRequest.py b/pyload/plugins/network/CurlRequest.py
index 775c98522..b7e37900b 100644
--- a/pyload/plugins/network/CurlRequest.py
+++ b/pyload/plugins/network/CurlRequest.py
@@ -152,7 +152,6 @@ class CurlRequest(Request):
url = "%s?%s" % (url, get)
self.c.setopt(pycurl.URL, url)
- self.lastURL = url
if post:
self.c.setopt(pycurl.POST, 1)
@@ -222,6 +221,7 @@ class CurlRequest(Request):
rep = self.getResponse()
self.c.setopt(pycurl.POSTFIELDS, "")
+ self.lastURL = url
self.lastEffectiveURL = self.c.getinfo(pycurl.EFFECTIVE_URL)
self.code = self.verifyHeader()
diff --git a/pyload/network/XDCCRequest.py b/pyload/plugins/network/XDCCRequest.py
index 89c4f3b73..6b692ab38 100644
--- a/pyload/network/XDCCRequest.py
+++ b/pyload/plugins/network/XDCCRequest.py
@@ -30,7 +30,7 @@ from select import select
from pyload.plugins.Plugin import Abort
-
+# TODO: This must be adapted to the new request interfaces
class XDCCRequest():
def __init__(self, timeout=30, proxies={}):
diff --git a/pyload/remote/apitypes.py b/pyload/remote/apitypes.py
index 0d9e35963..385f4ca07 100644
--- a/pyload/remote/apitypes.py
+++ b/pyload/remote/apitypes.py
@@ -97,9 +97,9 @@ class Role:
User = 1
class AccountInfo(BaseObject):
- __slots__ = ['plugin', 'loginname', 'owner', 'valid', 'validuntil', 'trafficleft', 'maxtraffic', 'premium', 'activated', 'shared', 'options']
+ __slots__ = ['plugin', 'loginname', 'owner', 'valid', 'validuntil', 'trafficleft', 'maxtraffic', 'premium', 'activated', 'shared', 'config']
- def __init__(self, plugin=None, loginname=None, owner=None, valid=None, validuntil=None, trafficleft=None, maxtraffic=None, premium=None, activated=None, shared=None, options=None):
+ def __init__(self, plugin=None, loginname=None, owner=None, valid=None, validuntil=None, trafficleft=None, maxtraffic=None, premium=None, activated=None, shared=None, config=None):
self.plugin = plugin
self.loginname = loginname
self.owner = owner
@@ -110,7 +110,7 @@ class AccountInfo(BaseObject):
self.premium = premium
self.activated = activated
self.shared = shared
- self.options = options
+ self.config = config
class AddonInfo(BaseObject):
__slots__ = ['func_name', 'description', 'value']
@@ -161,6 +161,9 @@ class ConfigItem(BaseObject):
self.input = input
self.value = value
+class Conflict(ExceptionObject):
+ pass
+
class DownloadInfo(BaseObject):
__slots__ = ['url', 'plugin', 'hash', 'status', 'statusmsg', 'error']
@@ -415,9 +418,11 @@ class Iface(object):
pass
def generatePackages(self, links):
pass
+ def getAccountInfo(self, plugin, loginname, refresh):
+ pass
def getAccountTypes(self):
pass
- def getAccounts(self, refresh):
+ def getAccounts(self):
pass
def getAddonHandler(self):
pass
@@ -525,7 +530,7 @@ class Iface(object):
pass
def unpauseServer(self):
pass
- def updateAccount(self, plugin, login, password):
+ def updateAccount(self, plugin, loginname, password):
pass
def updateAccountInfo(self, account):
pass
diff --git a/pyload/remote/apitypes_debug.py b/pyload/remote/apitypes_debug.py
index 0d04a8225..177029054 100644
--- a/pyload/remote/apitypes_debug.py
+++ b/pyload/remote/apitypes_debug.py
@@ -18,7 +18,7 @@ enums = [
]
classes = {
- 'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (dict, basestring, basestring)],
+ 'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (list, ConfigItem)],
'AddonInfo' : [basestring, basestring, basestring],
'AddonService' : [basestring, basestring, (list, basestring), (None, int)],
'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo))],
@@ -72,6 +72,7 @@ methods = {
'generateAndAddPackages': (list, int),
'generateDownloadLink': basestring,
'generatePackages': (dict, basestring, list),
+ 'getAccountInfo': AccountInfo,
'getAccountTypes': (list, basestring),
'getAccounts': (list, AccountInfo),
'getAddonHandler': (dict, basestring, list),
@@ -127,7 +128,7 @@ methods = {
'togglePause': bool,
'toggleReconnect': bool,
'unpauseServer': None,
- 'updateAccount': None,
+ 'updateAccount': AccountInfo,
'updateAccountInfo': None,
'updatePackage': None,
'updateUserData': None,
diff --git a/pyload/remote/pyload.thrift b/pyload/remote/pyload.thrift
index 9f2cfc8ee..702bd9b94 100644
--- a/pyload/remote/pyload.thrift
+++ b/pyload/remote/pyload.thrift
@@ -294,7 +294,7 @@ struct AccountInfo {
8: bool premium,
9: bool activated,
10: bool shared,
- 11: map<string, string> options,
+ 11: list <ConfigItem> config,
}
struct OnlineCheck {
@@ -335,6 +335,9 @@ exception Unauthorized {
exception Forbidden {
}
+exception Conflict {
+}
+
service Pyload {
@@ -489,9 +492,12 @@ service Pyload {
// Account Methods
///////////////////////
- list<AccountInfo> getAccounts(1: bool refresh),
list<string> getAccountTypes(),
- void updateAccount(1: PluginName plugin, 2: string login, 3: string password),
+
+ list<AccountInfo> getAccounts(),
+ AccountInfo getAccountInfo(1: PluginName plugin, 2: string loginname, 3: bool refresh),
+
+ AccountInfo updateAccount(1: PluginName plugin, 2: string loginname, 3: string password),
void updateAccountInfo(1: AccountInfo account),
void removeAccount(1: AccountInfo account),
diff --git a/pyload/remote/wsbackend/AbstractHandler.py b/pyload/remote/wsbackend/AbstractHandler.py
index 8012d6cd8..f540435c4 100644
--- a/pyload/remote/wsbackend/AbstractHandler.py
+++ b/pyload/remote/wsbackend/AbstractHandler.py
@@ -18,6 +18,8 @@
from mod_pywebsocket.msgutil import send_message
from mod_pywebsocket.util import get_class_logger
+
+from pyload.Api import User
from pyload.remote.json_converter import loads, dumps
@@ -115,7 +117,16 @@ class AbstractHandler:
return tuple(o)
def do_login(self, req, args, kwargs):
- user = self.api.checkAuth(*args, **kwargs)
+ user = None
+ # Cookies login when one argument is given
+ if len(args) == 1:
+ s = self.load_session(args)
+ else:
+ s = self.api.checkAuth(*args, **kwargs)
+ if s:
+ uid = s.get('uid', None)
+ user = User(uid=uid)
+
if user:
req.api = self.api.withUserContext(user.uid)
return self.send_result(req, self.OK, True)
diff --git a/pyload/web/app/scripts/collections/AccountList.js b/pyload/web/app/scripts/collections/AccountList.js
index bfc2af5a3..f6a8eda65 100644
--- a/pyload/web/app/scripts/collections/AccountList.js
+++ b/pyload/web/app/scripts/collections/AccountList.js
@@ -14,8 +14,7 @@ define(['jquery', 'backbone', 'underscore', 'app', 'models/Account'], function($
},
fetch: function(options) {
- // TODO: refresh options?
- options = App.apiRequest('getAccounts/false', null, options);
+ options = App.apiRequest('getAccounts', null, options);
return Backbone.Collection.prototype.fetch.call(this, options);
}
diff --git a/pyload/web/app/scripts/models/Account.js b/pyload/web/app/scripts/models/Account.js
index a2e24b056..9cfc1c0c1 100644
--- a/pyload/web/app/scripts/models/Account.js
+++ b/pyload/web/app/scripts/models/Account.js
@@ -18,7 +18,7 @@ define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], function($
premium: false,
activated: false,
shared: false,
- options: null
+ config: null
},
// Model Constructor
diff --git a/pyload/web/app/scripts/views/accounts/accountModal.js b/pyload/web/app/scripts/views/accounts/accountModal.js
index 6c2b226df..85db96b2b 100644
--- a/pyload/web/app/scripts/views/accounts/accountModal.js
+++ b/pyload/web/app/scripts/views/accounts/accountModal.js
@@ -60,7 +60,7 @@ define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dial
self = this;
$.ajax(App.apiRequest('updateAccount', {
- plugin: plugin, login: login, password: password
+ plugin: plugin, loginname: login, password: password
}, { success: function() {
App.vent.trigger('accounts:updated');
self.hide();
diff --git a/pyload/web/app/scripts/views/headerView.js b/pyload/web/app/scripts/views/headerView.js
index e6e763b26..3fdfe32ba 100644
--- a/pyload/web/app/scripts/views/headerView.js
+++ b/pyload/web/app/scripts/views/headerView.js
@@ -67,6 +67,9 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
console.log(error);
alert('WebSocket error' + error);
};
+ ws.onclose = function() {
+ alert('WebSocket was closed');
+ };
this.ws = ws;
},
diff --git a/tests/helper/Stubs.py b/tests/helper/Stubs.py
index 551778828..81b7d8a09 100644
--- a/tests/helper/Stubs.py
+++ b/tests/helper/Stubs.py
@@ -75,6 +75,7 @@ class Core:
self.addonManager = AddonManager()
self.eventManager = self.evm = NoopClass()
self.interactionManager = self.im = NoopClass()
+ self.scheduler = NoopClass()
self.js = JsEngine()
self.cache = {}
self.packageCache = {}
diff --git a/tests/manager/test_accountManager.py b/tests/manager/test_accountManager.py
new file mode 100644
index 000000000..1b328f892
--- /dev/null
+++ b/tests/manager/test_accountManager.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+
+from unittest import TestCase
+
+from tests.helper.Stubs import Core, adminUser, normalUser
+
+from pyload.database import DatabaseBackend
+from pyload.AccountManager import AccountManager
+
+
+class TestAccountManager(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.core = Core()
+ cls.db = DatabaseBackend(cls.core)
+ cls.core.db = cls.db
+ cls.db.setup()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.db.shutdown()
+
+ def setUp(self):
+ self.db.purgeAccounts()
+ self.manager = AccountManager(self.core)
+
+ def test_access(self):
+ account = self.manager.updateAccount("Http", "User", "somepw", adminUser)
+
+ assert account is self.manager.updateAccount("Http", "User", "newpw", adminUser)
+ self.assertEqual(account.password, "newpw")
+
+ assert self.manager.getAccount("Http", "User") is account
+ assert self.manager.getAccount("Http", "User", normalUser) is None
+
+ def test_config(self):
+ account = self.manager.updateAccount("Http", "User", "somepw", adminUser)
+ info = account.toInfoData()
+
+ self.assertEqual(info.config[0].name, "domain")
+ self.assertEqual(info.config[0].value, "")
+ self.assertEqual(account.getConfig("domain"), "")
+
+ account.setConfig("domain", "df")
+
+ info = account.toInfoData()
+ self.assertEqual(info.config[0].value, "df")
+
+ info.config[0].value = "new"
+
+ account.updateConfig(info.config)
+ self.assertEqual(account.getConfig("domain"), "new")
+
+
+ def test_shared(self):
+ account = self.manager.updateAccount("Http", "User", "somepw", adminUser)
+
+ assert self.manager.selectAccount("Http", adminUser) is account
+ assert account.loginname == "User"
+
+ assert self.manager.selectAccount("Something", adminUser) is None
+ assert self.manager.selectAccount("Http", normalUser) is None
+
+ account.shared = True
+
+ assert self.manager.selectAccount("Http", normalUser) is account
+ assert self.manager.selectAccount("sdf", normalUser) is None
+
+
+