summaryrefslogtreecommitdiffstats
path: root/pyload/PluginManager.py
diff options
context:
space:
mode:
Diffstat (limited to 'pyload/PluginManager.py')
-rw-r--r--pyload/PluginManager.py438
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
+