/wp-content/plugins/broken-link-checker/includes/module-manager.php
PHP | 855 lines | 465 code | 109 blank | 281 comment | 98 complexity | 761a497ef6cbf84f8256184695c5b14c MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, AGPL-1.0
- <?php
- class blcModuleManager {
- /* @var blcConfigurationManager */
- var $plugin_conf;
- var $module_dir = '';
-
- var $_module_cache;
- var $_category_cache;
- var $_category_cache_active;
- var $_virtual_modules = array();
-
- var $loaded;
- var $instances;
- var $default_active_modules;
-
-
- /**
- * Class "constructor".
- *
- * @param array $default_active_modules An array of module ids specifying which modules are active by default.
- * @return void
- */
- function init($default_active_modules = null){
- $this->module_dir = realpath(dirname(__FILE__) . '/../modules');
-
- $this->plugin_conf = blc_get_configuration();
- $this->default_active_modules = $default_active_modules;
-
- $this->loaded = array();
- $this->instances = array();
-
- add_filter('extra_plugin_headers', array(&$this, 'inject_module_headers'));
- }
-
- /**
- * Get an instance of the module manager.
- *
- * @param array|null $default_active_modules
- * @return blcModuleManager
- */
- static function getInstance($default_active_modules = null){
- static $instance = null;
- if ( is_null($instance) ){
- $instance = new blcModuleManager();
- $instance->init($default_active_modules);
- }
- return $instance;
- }
-
- /**
- * Retrieve a list of all installed BLC modules.
- *
- * This is essentially a slightly modified copy of get_plugins().
- *
- * @return array An associative array of module headers indexed by module ID.
- */
- function get_modules(){
- if ( !isset($this->_module_cache) ){
- //Refresh the module cache.
-
- $relative_path = '/' . plugin_basename($this->module_dir);
- if ( !function_exists('get_plugins') ){
- //BUG: Potentional security flaw/bug. plugin.php is not meant to be loaded outside admin panel.
- require_once(ABSPATH . 'wp-admin/includes/plugin.php');
- }
- $modules = get_plugins( $relative_path );
-
- $this->_module_cache = array();
-
- foreach($modules as $module_filename => $module_header){
- //Figure out the module ID. If not specified, it is equal to module's filename (sans the .php)
- if ( !empty($module_header['ModuleID']) ){
- $module_id = strtolower(trim($module_header['ModuleID']));
- } else {
- $module_id = strtolower(basename($module_filename, '.php'));
- }
-
- $module_header = $this->normalize_module_header($module_header, $module_id, $module_filename);
- $this->_module_cache[$module_id] = $module_header;
- }
-
- $this->_category_cache = null;
- }
-
- return array_merge($this->_module_cache, $this->_virtual_modules);
- }
-
- /**
- * Retrieve modules that match a specific category, or all modules sorted by categories.
- *
- * If a category ID is specified, this method returns the modules that have the "ModuleCategory:"
- * file header set to that value, or an empty array if no modules match that category. The
- * return array is indexed by module id :
- * [module_id1 => module1_data, module_id1 => module2_data, ...]
- *
- * If category is omitted, this method returns a list of all categories plus the modules
- * they contain. Only categories that have at least one module will be included. The return
- * value is an array of arrays, indexed by category ID :
- * [category1 => [module1_id => module1_data, module2_id => module2_data, ...], ...]
- *
- *
- * @param string $category Category id, e.g. "parser" or "container". Optional.
- * @param bool $markup Apply markup to module headers. Not implemented.
- * @param bool $translate Translate module headers. Defaults to false.
- * @return array An array of categories or module data.
- */
- function get_modules_by_category($category = '', $markup = false, $translate = false){
- if ( !isset($this->_category_cache) ){
- $this->_category_cache = $this->sort_into_categories($this->get_modules());
- }
-
- if ( empty($category) ){
- if ( $markup || $translate ){
-
- //Translate/apply markup to module headers
- $processed = array();
- foreach($this->_category_cache as $category_id => $modules){
- $processed[$category_id] = array();
- foreach($modules as $module_id => $module_data){
- if ( $translate ){
- $module_data['Name'] = _x($module_data['Name'], 'module name', 'broken-link-checker');
- }
- $processed[$category_id][$module_id] = $module_data;
- }
- }
-
- return $processed;
- } else {
- return $this->_category_cache;
- }
- } else {
- if ( isset($this->_category_cache[$category]) ){
- if ( $markup || $translate ){
- //Translate/apply markup to module headers
- $processed = array();
- foreach($this->_category_cache[$category] as $module_id => $module_data){
- if ( $translate ){
- $module_data['Name'] = _x($module_data['Name'], 'module name', 'broken-link-checker');
- }
- $processed[$module_id] = $module_data;
- }
- return $processed;
- } else {
- return $this->_category_cache[$category];
- }
- } else {
- return array();
- }
- }
- }
-
- /**
- * Retrieve active modules that match a specific category, or all active modules sorted by categories.
- *
- * @see blcModuleManager::get_modules_by_category()
- *
- * @param string $category Category id. Optional.
- * @return array An associative array of categories or module data.
- */
- function get_active_by_category($category = ''){
- if ( !isset($this->_category_cache_active) ){
- $this->_category_cache_active = $this->sort_into_categories($this->get_active_modules());
- }
-
- if ( empty($category) ){
- return $this->_category_cache_active;
- } else {
- if ( isset($this->_category_cache_active[$category]) ){
- return $this->_category_cache_active[$category];
- } else {
- return array();
- }
- }
- }
-
- /**
- * Get the module ids of all active modules that belong to a specific category,
- * quoted and ready for use in SQL.
- *
- * @param string $category Category ID. If not specified, a list of all active modules will be returned.
- * @return string A comma separated list of single-quoted module ids, e.g. 'module-foo','module-bar','modbaz'
- */
- function get_escaped_ids($category = ''){
- global $wpdb;
-
- if ( empty($category) ){
- $modules = $this->get_active_modules();
- } else {
- $modules = $this->get_active_by_category($category);
- }
-
- $modules = array_map(array(&$wpdb, 'escape'), array_keys($modules));
- $modules = "'" . implode("', '", $modules) . "'";
-
- return $modules;
- }
-
- /**
- * Sort a list of modules into categories. Inside each category, modules are sorted by priority (descending).
- *
- * @access private
- *
- * @param array $modules
- * @return array
- */
- function sort_into_categories($modules){
- $categories = array();
-
- foreach($modules as $module_id => $module_data){
- $cat = $module_data['ModuleCategory'];
- if ( isset($categories[$cat]) ){
- $categories[$cat][$module_id] = $module_data;
- } else {
- $categories[$cat] = array($module_id => $module_data);
- }
- }
-
- foreach($categories as $cat => $cat_modules){
- uasort($categories[$cat], array(&$this, 'compare_priorities'));
- }
-
- return $categories;
- }
-
- /**
- * Callback for sorting modules by priority.
- *
- * @access private
- *
- * @param array $a First module header.
- * @param array $b Second module header.
- * @return int
- */
- function compare_priorities($a, $b){
- return $b['ModulePriority'] - $a['ModulePriority'];
- }
-
- /**
- * Retrieve a reference to an active module.
- *
- * Each module is instantiated only once, so if the module was already loaded you'll get back
- * a reference to the existing module object. If the module isn't loaded or instantiated yet,
- * the function will do it automatically (but only for active modules).
- *
- * @param string $module_id Module ID.
- * @param bool $autoload Optional. Whether to load the module file if it's not loaded yet. Defaults to TRUE.
- * @param string $category Optional. Return the module only if it's in a specific category. Categories are ignored by default.
- * @return blcModule A reference to a module object, or NULL on error.
- */
- function get_module($module_id, $autoload = true, $category=''){
- $no_result = null;
- if ( !is_string($module_id) ){
- //$backtrace = debug_backtrace();
- //FB::error($backtrace, "get_module called with a non-string argument");
- return $no_result;
- }
-
- if ( empty($this->loaded[$module_id]) ){
- if ( $autoload && $this->is_active($module_id) ){
- if ( !$this->load_module($module_id) ){
- return $no_result;
- }
- } else {
- return $no_result;
- }
- }
-
- if ( !empty($category) ){
- $data = $this->get_module_data($module_id);
- if ( $data['ModuleCategory'] != $category ){
- return $no_result;
- }
- }
-
- $module = $this->init_module($module_id);
- return $module;
- }
-
- /**
- * Retrieve the header data of a specific module.
- * Uses cached module info if available.
- *
- * @param string $module_id
- * @param bool $use_active_cache Check the active module cache before the general one. Defaults to true.
- * @return array Associative array of module data, or FALSE if the specified module was not found.
- */
- function get_module_data($module_id, $use_active_cache = true){
- //Check virtual modules
- if ( isset($this->_virtual_modules[$module_id]) ){
- return $this->_virtual_modules[$module_id];
- }
-
- //Check active modules
- if ( $use_active_cache && isset($this->plugin_conf->options['active_modules'][$module_id]) ){
- return $this->plugin_conf->options['active_modules'][$module_id];
- }
-
- //Otherwise, use the general module cache
- if ( !isset($this->_module_cache) ){
- $this->get_modules(); //Populates the cache
- }
-
- if ( isset($this->_module_cache[$module_id]) ){
- return $this->_module_cache[$module_id];
- } else {
- return false;
- }
- }
-
- /**
- * Retrieve a list of active modules.
- *
- * The list of active modules is stored in the 'active_modules' key of the
- * plugin configuration object. If this key is not set, this function will
- * create it and populate it using the list of default active modules passed
- * to the module manager's constructor.
- *
- * @return array Associative array of module data indexed by module ID.
- */
- function get_active_modules(){
- if ( isset($this->plugin_conf->options['active_modules']) ){
- return $this->plugin_conf->options['active_modules'];
- }
-
- $active = array();
- $modules = $this->get_modules();
-
- if ( is_array($this->default_active_modules) ){
- foreach($this->default_active_modules as $module_id){
- if ( array_key_exists($module_id, $modules) ){
- $active[$module_id] = $modules[$module_id];
- }
- }
- }
-
- $this->plugin_conf->options['active_modules'] = $active;
- $this->plugin_conf->save_options();
-
- return $this->plugin_conf->options['active_modules'];
- }
-
- /**
- * Determine if module is active.
- *
- * @param string $module_id
- * @return bool
- */
- function is_active($module_id){
- return array_key_exists($module_id, $this->get_active_modules());
- }
-
- /**
- * Activate a module.
- * Does nothing if the module is already active.
- *
- * @param string $module_id
- * @return bool True if module was activated successfully, false otherwise.
- */
- function activate($module_id){
- if ( $this->is_active($module_id) ){
- return true;
- }
-
- //Retrieve the module header
- $module_data = $this->get_module_data($module_id, false);
- if ( !$module_data ){
- return false;
- }
-
- //Attempt to load the module
- if ( $this->load_module($module_id, $module_data) ){
- //Okay, if it loads, we can assume it works.
- $this->plugin_conf->options['active_modules'][$module_id] = $module_data;
- $this->plugin_conf->save_options();
- //Invalidate the per-category active module cache
- $this->_category_cache_active = null;
-
- //Notify the module that it's been activated
- $module = $this->get_module($module_id);
- if ( $module ){
- $module->activated();
- }
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Deactivate a module.
- * Does nothing if the module is already inactive.
- *
- * @param string $module_id
- * @return bool
- */
- function deactivate($module_id){
- if ( !$this->is_active($module_id) ){
- return true;
- }
-
- //Some modules are supposed to be always active and thus can't be deactivated
- $module_data = $this->get_module_data($module_id, false);
- if ( isset($module_data['ModuleAlwaysActive']) && $module_data['ModuleAlwaysActive'] ){
- return false;
- }
-
- //Notify the module that it's being deactivated
- $module = $this->get_module($module_id);
- if ( $module ){
- $module->deactivated();
- }
-
- unset($this->plugin_conf->options['active_modules'][$module_id]);
-
- //Keep track of when each module was last deactivated. Used for parser resynchronization.
- if ( isset($this->plugin_conf->options['module_deactivated_when']) ){
- $this->plugin_conf->options['module_deactivated_when'][$module_id] = current_time('timestamp');
- } else {
- $this->plugin_conf->options['module_deactivated_when'] = array(
- $module_id => current_time('timestamp'),
- );
- }
- $this->plugin_conf->save_options();
-
- $this->_category_cache_active = null; //Invalidate the by-category cache since we just changed something
- return true;
- }
-
- /**
- * Determine when a module was last deactivated.
- *
- * @param string $module_id Module ID.
- * @return int Timestamp of last deactivation, or 0 if the module has never been deactivated.
- */
- function get_last_deactivation_time($module_id){
- if ( isset($this->plugin_conf->options['module_deactivated_when'][$module_id]) ){
- return $this->plugin_conf->options['module_deactivated_when'][$module_id];
- } else {
- return 0;
- }
- }
-
- /**
- * Set the current list of active modules. If any of the modules are not currently active,
- * they will be activated. Any currently active modules that are not on the new list will
- * be deactivated.
- *
- * @param array $ids An array of module IDs.
- * @return void
- */
- function set_active_modules($ids){
- $current_active = array_keys($this->get_active_modules());
-
- $activate = array_diff($ids, $current_active);
- $deactivate = array_diff($current_active, $ids);
-
- //Deactivate any modules not present in the new list
- foreach($deactivate as $module_id){
- $this->deactivate($module_id);
- }
-
- //Activate modules present in the new list but not in the old list
- foreach($activate as $module_id){
- $this->activate($module_id);
- }
-
- //Ensure all active modules have the latest headers
- $this->refresh_active_module_cache();
-
- //Invalidate the per-category active module cache
- $this->_category_cache_active = null;
- }
-
- /**
- * Send the activation message to all currently active plugins when the plugin is activated.
- *
- * @return void
- */
- function plugin_activated(){
- global $blclog;
-
- //Ensure all active modules have the latest headers
- $blclog->log('... Updating module cache');
- $this->refresh_active_module_cache();
-
- //Notify them that we've been activated
- $active = $this->get_active_modules();
- foreach($active as $module_id => $module_data){
- $blclog->log( sprintf('... Notifying module "%s"', $module_id) );
- $module = $this->get_module($module_id);
- if ( $module ){
- $module->activated();
- } else {
- $blclog->warn(sprintf('... Module "%s" failed to load!', $module_id));
- }
- }
- }
-
- /**
- * Refresh the cached data of all active modules.
- *
- * @return array An updated list of active modules.
- */
- function refresh_active_module_cache(){
- $modules = $this->get_modules();
- foreach($this->plugin_conf->options['active_modules'] as $module_id => $module_header){
- if ( array_key_exists($module_id, $modules) ){
- $this->plugin_conf->options['active_modules'][$module_id] = $modules[$module_id];
- }
- }
- $this->plugin_conf->save_options();
- $this->_category_cache_active = null; //Invalidate the by-category active module cache
- return $this->plugin_conf->options['active_modules'];
- }
-
- /**
- * Load active modules.
- *
- * @param string $context Optional. If specified, only the modules that match this context (or the "all" context) will be loaded.
- * @return void
- */
- function load_modules($context = ''){
- $active = $this->get_active_modules();
- //Avoid trying to load a virtual module before the module that registered it has been loaded.
- $active = $this->put_virtual_last($active);
-
- foreach($active as $module_id => $module_data){
- //Load the module
- $should_load = ($module_data['ModuleContext'] == 'all') || (!empty($context) && $module_data['ModuleContext'] == $context);
- if ( $should_load ){
- $this->load_module($module_id, $module_data);
- }
- }
- }
-
- /**
- * Load and possibly instantiate a specific module.
- *
- * @access private
- *
- * @param string $module_id
- * @param array $module_data
- * @return bool True if the module was successfully loaded, false otherwise.
- */
- function load_module($module_id, $module_data = null){
-
- //Only load each module once.
- if ( !empty($this->loaded[$module_id]) ){
- return true;
- }
-
- if ( !isset($module_data) ){
- $module_data = $this->get_module_data($module_id);
- if ( empty($module_data) ){
- return false;
- }
- }
-
- //Load a normal module
- if ( empty($module_data['virtual']) ){
-
- //Skip invalid and missing modules
- if ( empty($module_data['file']) ){
- return false;
- }
-
- //Get the full path to the module file
- $filename = $this->module_dir . '/' . $module_data['file'];
- if ( !file_exists($filename) ){
- return false;
- }
-
- //Load it
- include $filename;
- $this->loaded[$module_id] = true;
-
- } else {
-
- //Virtual modules don't need to be explicitly loaded, but they must
- //be registered.
- if ( !array_key_exists($module_id, $this->_virtual_modules) ) {
- return false;
- }
- $this->loaded[$module_id] = true;
-
- }
-
- //Instantiate the main module class unless lazy init is on (default is off)
- if ( !array_key_exists($module_id, $this->instances) ){ //Only try to instantiate once
- if ( !$module_data['ModuleLazyInit'] ){
- $this->init_module($module_id, $module_data);
- }
- }
-
- return true;
- }
-
- /**
- * Instantiate a certain module.
- *
- * @param string $module_id
- * @param array $module_data Optional. The header data of the module that needs to be instantiated. If not specified, it will be retrieved automatically.
- * @return object The newly instantiated module object (extends blcModule), or NULL on error.
- */
- function init_module($module_id, $module_data = null){
- //Each module is only instantiated once.
- if ( isset($this->instances[$module_id]) ){
- return $this->instances[$module_id];
- }
-
- if ( !isset($module_data) ){
- $module_data = $this->get_module_data($module_id);
- if ( empty($module_data) ){
- return null;
- }
- }
-
- if ( !empty($module_data['ModuleClassName']) && class_exists($module_data['ModuleClassName']) ){
- $className = $module_data['ModuleClassName'];
- $this->instances[$module_id] = new $className(
- $module_id,
- $module_data,
- $this->plugin_conf,
- $this
- );
- return $this->instances[$module_id];
- };
-
- return null;
- }
-
- function is_instantiated($module_id){
- return !empty($this->instances[$module_id]);
- }
-
- /**
- * Register a virtual module.
- *
- * Virtual modules are the same as normal modules, except that they can be added
- * on the fly, e.g. by other modules.
- *
- * @param string $module_id Module Id.
- * @param string $module_data Associative array of module data. All module header fields are allowed, except ModuleID.
- * @return void
- */
- function register_virtual_module($module_id, $module_data){
- $module_data = $this->normalize_module_header($module_data, $module_id);
- $module_data['virtual'] = true;
- $this->_virtual_modules[$module_id] = $module_data;
- }
-
- /**
- * Sort an array of modules so that all virtual modules are placed at its end.
- *
- * @param array $modules Input array, [module_id => module_data, ...].
- * @return array Sorted array.
- */
- function put_virtual_last($modules){
- uasort($modules, array(&$this, 'compare_virtual_flags'));
- return $modules;
- }
-
- /**
- * Callback function for sorting modules by the state of their 'virtual' flag.
- *
- * @param array $a Associative array of module A data
- * @param array $b Associative array of module B data
- * @return int
- */
- function compare_virtual_flags($a, $b){
- if ( empty($a['virtual']) ){
- return empty($b['virtual'])?0:-1;
- } else {
- return empty($b['virtual'])?1:0;
- }
- }
-
- /**
- * Validate active modules.
- *
- * Validates all active modules, deactivates invalid ones and returns
- * an array of deactivated modules.
- *
- * @return array
- */
- function validate_active_modules(){
- $active = $this->get_active_modules();
- if ( empty($active) ){
- return array();
- }
-
- $invalid = array();
- foreach($active as $module_id => $module_data){
- $rez = $this->validate_module($module_data);
- if ( is_wp_error($rez) ){
- $invalid[$module_id] = $rez;
- $this->deactivate($module_id);
- }
- }
-
- return $invalid;
- }
-
- /**
- * Validate module data.
- *
- * Checks that the module file exists or that the module
- * is a currently registered virtual module.
- *
- * @param array $module_data Associative array of module data.
- * @return bool|WP_Error True on success, an error object if the module fails validation
- */
- function validate_module($module_data){
- if ( empty($module_data['ModuleID']) ){
- return new WP_Error('invalid_cached_header', 'The cached module header is invalid');
- }
-
- if ( empty($module_data['virtual']) ){
- //Normal modules must have a valid filename
- if ( empty($module_data['file']) ){
- return new WP_Error('module_not_found', 'Invalid module file');
- }
-
- $filename = $this->module_dir . '/' . $module_data['file'];
- if ( !file_exists($filename) ){
- return new WP_Error('module_not_found', 'Module file not found');
- }
-
- //The module file header must be in the proper format. While $module_data comes
- //from cache and can be assumed to be correct, get_modules() will attempt to load
- //the current headers and only return modules with semi-valid headers.
- $installed = $this->get_modules();
- if ( !array_key_exists($module_data['ModuleID'], $installed) ){
- return new WP_Error('invalid_module_header', 'Invalid module header');
- }
- } else {
- //Virtual modules need to be currently registered
- if ( !array_key_exists($module_data['ModuleID'], $this->_virtual_modules) ){
- return new WP_Error('module_not_registered', 'The virtual module is not registered');
- }
- }
-
- return true;
- }
-
- /**
- * Add BLC-module specific headers to the list of allowed plugin headers. This
- * lets us use get_plugins() to retrieve the list of BLC modules.
- *
- * @param array $headers Currently known plugin headers.
- * @return array New plugin headers.
- */
- function inject_module_headers($headers){
- $module_headers = array(
- 'ModuleID',
- 'ModuleCategory',
- 'ModuleContext',
- 'ModuleLazyInit',
- 'ModuleClassName',
- 'ModulePriority',
- 'ModuleCheckerUrlPattern',
- 'ModuleHidden', //Don't show the module in the Settings page
- 'ModuleAlwaysActive', //Module can't be deactivated.
- 'ModuleRequiresPro', //Can only be activated in the Pro version
- );
-
- return array_merge($headers, $module_headers);
- }
-
- /**
- * Normalize a module header, using defaults where necessary.
- *
- * @param array $module_header Module header, as read from the module's .php file.
- * @param string $module_id Module ID.
- * @param string $module_filename Module filename. Optional.
- * @return array Normalized module header.
- */
- function normalize_module_header($module_header, $module_id, $module_filename = ''){
- //Default values for optional module header fields
- $defaults = array(
- 'ModuleContext' => 'all',
- 'ModuleCategory' => 'other',
- 'ModuleLazyInit' => 'false',
- 'ModulePriority' => '0',
- 'ModuleHidden' => 'false',
- 'ModuleAlwaysActive' => 'false',
- 'ModuleRequiresPro' => 'false',
- 'TextDomain' => 'broken-link-checker', //For translating module headers
- );
-
- $module_header['ModuleID'] = $module_id; //Just for consistency
- $module_header['file'] = $module_filename; //Used later to load the module
-
- //Apply defaults
- foreach($defaults as $field => $default_value){
- if ( empty($module_header[$field]) ){
- $module_header[$field] = $default_value;
- }
- }
-
- //Convert bool/int fields from strings to native datatypes
- $module_header['ModuleLazyInit'] = $this->str_to_bool($module_header['ModuleLazyInit']);
- $module_header['ModuleHidden'] = $this->str_to_bool($module_header['ModuleHidden']);
- $module_header['ModuleAlwaysActive'] = $this->str_to_bool($module_header['ModuleAlwaysActive']);
- $module_header['ModuleRequiresPro'] = $this->str_to_bool($module_header['ModuleRequiresPro']);
- $module_header['ModulePriority'] = intval($module_header['ModulePriority']);
-
- return $module_header;
- }
-
- /**
- * Converts the strings "true" and "false" to boolean TRUE and FALSE, respectively.
- * Any other string will yield FALSE.
- *
- * @param string $value "true" or "false", case-insensitive.
- * @return bool
- */
- function str_to_bool($value){
- $value = trim(strtolower($value));
- return $value == 'true';
- }
-
- /**
- * Generates a PHP script that calls the __() i18n function with
- * the name and description of each available module. The generated
- * script is used to make module headers show up in the .POT file.
- *
- * @access private
- *
- * @return string
- */
- function _build_header_translation_code(){
- $this->_module_cache = null; //Clear the cache
- $modules = $this->get_modules();
-
- $strings = array();
- foreach($modules as $module_id => $module_header){
- if ( $module_header['ModuleHidden'] || ($module_id == 'write-module-placeholders')) {
- continue;
- }
- if ( !empty($module_header['Name']) ){
- $strings[] = sprintf(
- '_x("%s", "module name", "broken-link-checker");',
- str_replace('"', '\"', $module_header['Name'])
- );
- }
- }
-
- return "<?php\n" . implode("\n", $strings) . "\n?>";
- }
- }
- ?>