diff options
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 + + + |