diff options
Diffstat (limited to 'pyload/PluginManager.py')
-rw-r--r-- | pyload/PluginManager.py | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/pyload/PluginManager.py b/pyload/PluginManager.py new file mode 100644 index 000000000..182768689 --- /dev/null +++ b/pyload/PluginManager.py @@ -0,0 +1,438 @@ +# -*- coding: utf-8 -*- + +############################################################################### +# Copyright(c) 2008-2013 pyLoad Team +# http://www.pyload.org +# +# This file is part of pyLoad. +# pyLoad is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Subjected to the terms and conditions in LICENSE +# +# @author: RaNaN, mkaay +############################################################################### + +import re +import sys + +from os import listdir, makedirs +from os.path import isfile, join, exists, abspath, basename +from sys import version_info +from time import time +from collections import defaultdict + +from pyload.lib.SafeEval import const_eval as literal_eval +from pyload.plugins.Base import Base + +from new_collections import namedtuple + +#TODO: ignores not updatable + +# ignore these plugin configs, mainly because plugins were wiped out +IGNORE = ( + "FreakshareNet", "SpeedManager", "ArchiveTo", "ShareCx", ('addons', 'UnRar'), + 'EasyShareCom', 'FlyshareCz' +) + +PluginTuple = namedtuple("PluginTuple", "version re deps category user path") + +class BaseAttributes(defaultdict): + """ Dictionary that loads defaults values from Base object """ + def __missing__(self, key): + attr = "__%s__" % key + if not hasattr(Base, attr): + return defaultdict.__missing__(self, key) + + return getattr(Base, attr) + + +class PluginManager: + ROOT = "pyload.plugins." + LOCALROOT = "localplugins." + TYPES = ("crypter", "hoster", "accounts", "addons", "network", "internal") + + BUILTIN = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s?(True|False|None|[0-9x.]+)', re.I) + SINGLE = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*(?:r|u|_)?((?:(?<!")"(?!")|\'|\().*(?:(?<!")"(?!")|\'|\)))', + re.I) + # note the nongreedy character: that means we can not embed list and dicts + MULTI = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*((?:\{|\[|"{3}).*?(?:"""|\}|\]))', re.DOTALL | re.M | re.I) + + NO_MATCH = re.compile(r'^no_match$') + + def __init__(self, core): + self.core = core + + #self.config = self.core.config + self.log = core.log + + self.plugins = {} + self.modules = {} # cached modules + self.history = [] # match history to speedup parsing (type, name) + self.user_context = {} # plugins working with user context + self.createIndex() + + #register for import addon + sys.meta_path.append(self) + + def logDebug(self, type, plugin, msg): + self.log.debug("Plugin %s | %s: %s" % (type, plugin, msg)) + + def createIndex(self): + """create information for all plugins available""" + # add to path, so we can import from userplugins + sys.path.append(abspath("")) + + if not exists("userplugins"): + makedirs("userplugins") + if not exists(join("userplugins", "__init__.py")): + f = open(join("userplugins", "__init__.py"), "wb") + f.close() + + a = time() + for type in self.TYPES: + self.plugins[type] = self.parse(type) + + self.log.debug("Created index of plugins in %.2f ms", (time() - a) * 1000) + + def parse(self, folder, home=None): + """ Analyze and parses all plugins in folder """ + plugins = {} + if home: + pfolder = join("userplugins", folder) + if not exists(pfolder): + makedirs(pfolder) + if not exists(join(pfolder, "__init__.py")): + f = open(join(pfolder, "__init__.py"), "wb") + f.close() + + else: + pfolder = join(pypath, "pyload", "plugins", folder) + + for f in listdir(pfolder): + if (isfile(join(pfolder, f)) and f.endswith(".py") or f.endswith("_25.pyc") or f.endswith( + "_26.pyc") or f.endswith("_27.pyc")) and not f.startswith("_"): + if f.endswith("_25.pyc") and version_info[0:2] != (2, 5): + continue + elif f.endswith("_26.pyc") and version_info[0:2] != (2, 6): + continue + elif f.endswith("_27.pyc") and version_info[0:2] != (2, 7): + continue + + # replace suffix and version tag + name = f[:-3] + if name[-1] == ".": name = name[:-4] + + plugin = self.parsePlugin(join(pfolder, f), folder, name, home) + if plugin: + plugins[name] = plugin + + if not home: + temp = self.parse(folder, plugins) + plugins.update(temp) + + return plugins + + def parseAttributes(self, filename, name, folder=""): + """ Parse attribute dict from plugin""" + data = open(filename, "rb") + content = data.read() + data.close() + + attrs = BaseAttributes() + for m in self.BUILTIN.findall(content) + self.SINGLE.findall(content) + self.MULTI.findall(content): + #replace gettext function and eval result + try: + attrs[m[0]] = literal_eval(m[-1].replace("_(", "(")) + except: + self.logDebug(folder, name, "Error when parsing: %s" % m[-1]) + self.core.print_exc() + + if not hasattr(Base, "__%s__" % m[0]): + if m[0] != "type": #TODO remove type from all plugins, its not needed + self.logDebug(folder, name, "Unknown attribute '%s'" % m[0]) + + return attrs + + def parsePlugin(self, filename, folder, name, home=None): + """ Parses a plugin from disk, folder means plugin type in this context. Also sets config. + + :arg home: dict with plugins, of which the found one will be matched against (according version) + :returns PluginTuple""" + + attrs = self.parseAttributes(filename, name, folder) + if not attrs: return + + version = 0 + + if "version" in attrs: + try: + version = float(attrs["version"]) + except ValueError: + self.logDebug(folder, name, "Invalid version %s" % attrs["version"]) + version = 9 #TODO remove when plugins are fixed, causing update loops + else: + self.logDebug(folder, name, "No version attribute") + + # home contains plugins from pyload root + if home and name in home: + if home[name].version >= version: + return + + if name in IGNORE or (folder, name) in IGNORE: + return + + if "pattern" in attrs and attrs["pattern"]: + try: + plugin_re = re.compile(attrs["pattern"], re.I) + except: + self.logDebug(folder, name, "Invalid regexp pattern '%s'" % attrs["pattern"]) + plugin_re = self.NO_MATCH + else: + plugin_re = self.NO_MATCH + + deps = attrs["dependencies"] + category = attrs["category"] if folder == "addons" else "" + + # create plugin tuple + plugin = PluginTuple(version, plugin_re, deps, category, bool(home), filename) + + # internals have no config + if folder == "internal": + return plugin + + if folder == "addons" and "config" not in attrs and not attrs["internal"]: + attrs["config"] = (["activated", "bool", "Activated", False],) + + if "config" in attrs and attrs["config"] is not None: + config = attrs["config"] + desc = attrs["description"] + expl = attrs["explanation"] + + # Convert tuples to list + config = [list(x) for x in config] + + if folder == "addons" and not attrs["internal"]: + for item in config: + if item[0] == "activated": break + else: # activated flag missing + config.insert(0, ("activated", "bool", "Activated", False)) + + # Everything that is no addon and user_context=True, is added to dict + if folder != "addons" or attrs["user_context"]: + self.user_context[name] = True + + try: + self.core.config.addConfigSection(name, name, desc, expl, config) + except: + self.logDebug(folder, name, "Invalid config %s" % config) + + return plugin + + + def parseUrls(self, urls): + """parse plugins for given list of urls, separate to crypter and hoster""" + + res = {"hoster": [], "crypter": []} # tupels of (url, plugin) + + for url in urls: + if type(url) not in (str, unicode, buffer): + self.log.debug("Parsing invalid type %s" % type(url)) + continue + + found = False + + for ptype, name in self.history: + if self.plugins[ptype][name].re.match(url): + res[ptype].append((url, name)) + found = (ptype, name) + break # need to exit this loop first + + if found: # found match + if self.history[0] != found: #update history + self.history.remove(found) + self.history.insert(0, found) + continue + + for ptype in ("crypter", "hoster"): + for name, plugin in self.plugins[ptype].iteritems(): + if plugin.re.match(url): + res[ptype].append((url, name)) + self.history.insert(0, (ptype, name)) + del self.history[10:] # cut down to size of 10 + found = True + break + + if not found: + res["hoster"].append((url, "BasePlugin")) + + return res["hoster"], res["crypter"] + + def getPlugins(self, type): + return self.plugins.get(type, None) + + def findPlugin(self, name, pluginlist=("hoster", "crypter")): + for ptype in pluginlist: + if name in self.plugins[ptype]: + return ptype, self.plugins[ptype][name] + return None, None + + def getPluginModule(self, name): + """ Decprecated: return plugin module from hoster|crypter""" + self.log.debug("Deprecated method: .getPluginModule()") + type, plugin = self.findPlugin(name) + return self.loadModule(type, name) + + def getPluginClass(self, name): + """ return plugin class from hoster|crypter, always the not overwritten one """ + type, plugin = self.findPlugin(name) + return self.loadClass(type, name) + + # MultiHoster will overwrite this + getPlugin = getPluginClass + + def loadAttributes(self, type, name): + plugin = self.plugins[type][name] + return self.parseAttributes(plugin.path, name, type) + + def loadModule(self, type, name): + """ Returns loaded module for plugin + + :param type: plugin type, subfolder of module.plugins + :param name: + """ + plugins = self.plugins[type] + if name in plugins: + if (type, name) in self.modules: return self.modules[(type, name)] + try: + # convert path to python recognizable import + path = basename(plugins[name].path).replace(".pyc", "").replace(".py", "") + module = __import__(self.ROOT + "%s.%s" % (type, path), globals(), locals(), path) + self.modules[(type, name)] = module # cache import, maybe unneeded + return module + except Exception, e: + self.log.error(_("Error importing %(name)s: %(msg)s") % {"name": name, "msg": str(e)}) + self.core.print_exc() + + def loadClass(self, type, name): + """Returns the class of a plugin with the same name""" + module = self.loadModule(type, name) + if module: return getattr(module, name) + + def find_module(self, fullname, path=None): + #redirecting imports if necesarry + if fullname.startswith(self.ROOT) or fullname.startswith(self.LOCALROOT): #separate pyload plugins + if fullname.startswith(self.LOCALROOT): + user = 1 + else: + user = 0 #used as bool and int + + split = fullname.split(".") + if len(split) != 4 - user: return + type, name = split[2 - user:4 - user] + + if type in self.plugins and name in self.plugins[type]: + #userplugin is a newer version + if not user and self.plugins[type][name].user: + return self + #imported from userdir, but pyloads is newer + if user and not self.plugins[type][name].user: + return self + + # TODO: Remove when all plugin imports are adapted + if "module" in fullname: + return self + + + def load_module(self, name, replace=True): + if name not in sys.modules: #could be already in modules + + # TODO: only temporary + if name.endswith("module"): + # name = "pyload." + name = name.replace(".module", "") + self.log.debug("Old import reference detected, use %s" % name) + replace = False + return __import__("pyload") + if name.startswith("module"): + name = name.replace("module", "pyload") + self.log.debug("Old import reference detected, use %s" % name) + replace = False + + if replace: + if self.ROOT in name: + newname = name.replace(self.ROOT, self.LOCALROOT) + else: + newname = name.replace(self.LOCALROOT, self.ROOT) + else: + newname = name + + base, plugin = newname.rsplit(".", 1) + + self.log.debug("Redirected import %s -> %s" % (name, newname)) + + module = __import__(newname, globals(), locals(), [plugin]) + #inject under new an old name + sys.modules[name] = module + sys.modules[newname] = module + + return sys.modules[name] + + def reloadPlugins(self, type_plugins): + """ reloads and reindexes plugins """ + if not type_plugins: return False + + self.log.debug("Request reload of plugins: %s" % type_plugins) + + as_dict = {} + for t, n in type_plugins: + if t in as_dict: + as_dict[t].append(n) + else: + as_dict[t] = [n] + + # we do not reload addons or internals, would cause to much side effects + if "addons" in as_dict or "internal" in as_dict: + return False + + for type in as_dict.iterkeys(): + for plugin in as_dict[type]: + if plugin in self.plugins[type]: + if (type, plugin) in self.modules: + self.log.debug("Reloading %s" % plugin) + reload(self.modules[(type, plugin)]) + + # index re-creation + for type in ("crypter", "container", "hoster", "captcha", "accounts"): + self.plugins[type] = self.parse(type) + + if "accounts" in as_dict: #accounts needs to be reloaded + self.core.accountManager.initPlugins() + self.core.scheduler.addJob(0, self.core.accountManager.getAccountInfos) + + return True + + def isUserPlugin(self, plugin): + """ A plugin suitable for multiple user """ + return plugin in self.user_context + + def isPluginType(self, plugin, type): + return plugin in self.plugins[type] + + def getCategory(self, plugin): + if plugin in self.plugins["addons"]: + return self.plugins["addons"][plugin].category or "addon" + + def loadIcon(self, name): + """ load icon for single plugin, base64 encoded""" + pass + + def checkDependencies(self, type, name): + """ Check deps for given plugin + + :return: List of unfullfilled dependencies + """ + pass + |