last known version (integer|string) * */ protected $modulesLastVersions = array(); /** * Cache of namespace => path for unique module namespaces (memory cache only) * * @var array|null Becomes an array once populated * */ protected $moduleNamespaceCache = null; /** * Properties that only appear in 'verbose' moduleInfo * * @var array * */ protected $moduleInfoVerboseKeys = array( 'summary', 'author', 'href', 'file', 'core', 'versionStr', 'permissions', 'searchable', 'page', 'license', // 'languages', ); /** * Template for individual module info * * @var array * */ protected $infoTemplate = array( // module database ID 'id' => 0, // module class name 'name' => '', // module title 'title' => '', // module version 'version' => 0, // module version (always formatted string) 'versionStr' => '0.0.0', // who authored the module? (included in 'verbose' mode only) 'author' => '', // summary of what this module does (included in 'verbose' mode only) 'summary' => '', // URL to module details (included in 'verbose' mode only) 'href' => '', // Optional name of icon representing this module (currently font-awesome icon names, excluding the "fa-" portion) 'icon' => '', // this method converts this to array of module names, regardless of how the module specifies it 'requires' => array(), // module name is key, value is array($operator, $version). Note 'requiresVersions' index is created by this function. 'requiresVersions' => array(), // array of module class names 'installs' => array(), // permission required to execute this module 'permission' => '', // permissions automatically installed/uninstalled with this module. array of ('permission-name' => 'Description') 'permissions' => array(), // true if module is autoload, false if not. null=unknown 'autoload' => null, // true if module is singular, false if not. null=unknown 'singular' => null, // unix-timestamp date/time module added to system (for uninstalled modules, it is the file date) 'created' => 0, // is the module currently installed? (boolean, or null when not determined) 'installed' => null, // this is set to true when the module is configurable, false when it's not, and null when it's not determined 'configurable' => null, // verbose mode only: true when module implements SearchableModule interface, or null|false when not 'searchable' => null, // namespace that module lives in (string) 'namespace' => null, // verbose mode only: this is set to the module filename (from PW installation root), false when it can't be found, null when it hasn't been determined 'file' => null, // verbose mode only: this is set to true when the module is a core module, false when it's not, and null when it's not determined 'core' => null, // verbose mode only: any translations supplied with the module // 'languages' => null, // other properties that may be present, but are optional, for Process modules: // 'nav' => array(), // navigation definition: see Process.php // 'useNavJSON' => bool, // whether the Process module provides JSON navigation // 'page' => array(), // page to create for Process module: see Process.php // 'permissionMethod' => string or callable // method to call to determine permission: see Process.php ); /** * Replacement/default values to use when property is null * * @var array * */ protected $infoNullReplacements = array( 'autoload' => false, 'singular' => false, 'configurable' => false, 'core' => false, 'installed' => true, 'namespace' => "\\ProcessWire\\", ); /** * Is the module info cache empty? * * @return bool * */ public function moduleInfoCacheEmpty() { return empty($this->moduleInfoCache); } /** * Does the module info cache have an entry for given module ID? * * @param int $moduleID * @return bool * */ public function moduleInfoCacheHas($moduleID) { return isset($this->moduleInfoCache[$moduleID]); } /** * Get data from the module info cache * * Returns array of module info if given a module ID or name. * If module does not exist or info is not available, returns a blank array. * If not given a module ID or name, it returns an array of all modules info. * Returns value of property if given a property name, or null if not available. * * #pw-internal * * @param string|int|null $moduleID Module ID or name or omit to get info for all modules * @param string $property * @param bool $verbose * @return array|mixed|null * @since 3.0.218 * */ public function moduleInfoCache($moduleID = null, $property = '', $verbose = false) { if($verbose) { if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose(); $infos = &$this->moduleInfoCacheVerbose; } else { if(empty($this->moduleInfoCache)) $this->loadModuleInfoCache(); $infos = &$this->moduleInfoCache; } if($moduleID === null) { // get all foreach($infos as $moduleID => $info) { if(empty($info)) { $info = array(); } else if(is_array($info)) { continue; } else { $info = json_decode($info, true); } $infos[$moduleID] = $info; } return $infos; } else if($moduleID === 0) { return $property ? null : array(); } if(!ctype_digit("$moduleID")) { // convert module name to module id $moduleID = $this->moduleID($moduleID); if(!$moduleID) return ($property ? null : array()); } $moduleID = (int) $moduleID; if(!isset($infos[$moduleID])) return ($property ? null : array()); $info = $infos[$moduleID]; if(empty($info)) return ($property ? null : array()); if(is_string($info)) { $info = json_decode($info, true); if(!is_array($info)) $info = array(); $infos[$moduleID] = $info; } if($property) return isset($info[$property]) ? $info[$property] : null; return $info; } /** * Get data from the verbose module info cache * * #pw-internal * * @param int|string|null $moduleID * @param string $property * @return array|mixed|null * */ public function moduleInfoCacheVerbose($moduleID = null, $property = '') { return $this->moduleInfoCache($moduleID, $property, true); } /** * Retrieve module info from ModuleName.info.json or ModuleName.info.php * * @param string $moduleName * @return array * */ public function getModuleInfoExternal($moduleName) { // ...attempt to load info by info file (Module.info.php or Module.info.json) $path = $this->modules->installableFile($moduleName); if(!empty($path)) { $path = dirname($path) . '/'; } else { $path = $this->wire()->config->paths($moduleName); } if(empty($path)) return array(); // module exists and has a dedicated path on the file system // we will try to get info from a PHP or JSON info file $filePHP = $path . "$moduleName.info.php"; $fileJSON = $path . "$moduleName.info.json"; $info = array(); if(file_exists($filePHP)) { /** @noinspection PhpIncludeInspection */ include($filePHP); // will populate $info automatically if(!is_array($info) || !count($info)) $this->error("Invalid PHP module info file for $moduleName"); } else if(file_exists($fileJSON)) { $info = file_get_contents($fileJSON); $info = json_decode($info, true); if(!$info) { $info = array(); $this->error("Invalid JSON module info file for $moduleName"); } } return $info; } /** * Retrieve module info from internal getModuleInfo function in the class * * @param Module|string $module * @param string $namespace * @return array * */ public function getModuleInfoInternal($module, $namespace = '') { $info = array(); if($module instanceof ModulePlaceholder) { $this->modules->includeModule($module); $module = $module->className(); } if($module instanceof Module) { if(method_exists($module, 'getModuleInfo')) { $info = $module::getModuleInfo(); } } else if($module) { if(empty($namespace)) $namespace = $this->getModuleNamespace($module); $className = wireClassName($namespace . $module, true); if(!class_exists($className)) $this->modules->includeModule($module); if(is_callable("$className::getModuleInfo")) { $info = call_user_func(array($className, 'getModuleInfo')); } } return $info; } /** * Retrieve module info for system properties: PHP or ProcessWire * * @param string $moduleName * @param array $options * @return array * */ public function getModuleInfoSystem($moduleName, array $options = array()) { $info = array(); if($moduleName === 'PHP') { $info['id'] = 0; $info['name'] = $moduleName; $info['title'] = $moduleName; $info['version'] = PHP_VERSION; } else if($moduleName === 'ProcessWire') { $info['id'] = 0; $info['name'] = $moduleName; $info['title'] = $moduleName; $info['version'] = $this->wire()->config->version; $info['namespace'] = self::defaultNamespace; $info['requiresVersions'] = array( 'PHP' => array('>=', '5.3.8'), 'PHP_modules' => array('=', 'PDO,mysqli'), 'Apache_modules' => array('=', 'mod_rewrite'), 'MySQL' => array('>=', '5.0.15'), ); $info['requires'] = array_keys($info['requiresVersions']); } else { return array(); } $info['versionStr'] = $info['version']; if(empty($options['minify'])) $info = array_merge($this->infoTemplate, $info); return $info; } /** * Returns an associative array of information for a Module * * The array returned by this method includes the following: * * - `id` (int): module database ID. * - `name` (string): module class name. * - `title` (string): module title. * - `version` (int): module version. * - `icon` (string): Optional icon name (excluding the "fa-") part. * - `requires` (array): module names required by this module. * - `requiresVersions` (array): required module versions–module name is key, value is array($operator, $version). * - `installs` (array): module names that this module installs. * - `permission` (string): permission name required to execute this module. * - `autoload` (bool): true if module is autoload, false if not. * - `singular` (bool): true if module is singular, false if not. * - `created` (int): unix-timestamp of date/time module added to system (for uninstalled modules, it is the file date). * - `installed` (bool): is the module currently installed? (boolean, or null when not determined) * - `configurable` (bool|int): true or positive number when the module is configurable. * - `namespace` (string): PHP namespace that module lives in. * * The following properties are also included when "verbose" mode is requested. When not in verbose mode, these * properties may be present but with empty values: * * - `versionStr` (string): formatted module version string. * - `file` (string): module filename from PW installation root, or false when it can't be found. * - `core` (bool): true when module is a core module, false when not. * - `author` (string): module author, when specified. * - `summary` (string): summary of what this module does. * - `href` (string): URL to module details (when specified). * - `permissions` (array): permissions installed by this module, associative array ('permission-name' => 'Description'). * - `page` (array): definition of page to create for Process module (see Process class) * * The following properties appear only for "Process" modules, and only if specified by module. * See the Process class for more details: * * - `nav` (array): navigation definition * - `useNavJSON` (bool): whether the Process module provides JSON navigation * - `permissionMethod` (string|callable): method to call to determine permission * - `page` (array): definition of page to create for Process module * * On error, an `error` index in returned array contains error message. You can also identify errors * such as a non-existing module by the returned module info having an `id` index of `0` * * ~~~~~ * // example of getting module info * $moduleInfo = $modules->getModuleInfo('InputfieldCKEditor'); * * // example of getting verbose module info * $moduleInfo = $modules->getModuleInfoVerbose('MarkupAdminDataTable'); * ~~~~~ * * @param string|Module|int $class Specify one of the following: * - Module object instance * - Module class name (string) * - Module ID (int) * - To get info for ALL modules, specify `*` or `all`. * - To get system information, specify `ProcessWire` or `PHP`. * - To get a blank module info template, specify `info`. * @param array $options Optional options to modify behavior of what gets returned * - `verbose` (bool): Makes the info also include verbose properties, which are otherwise blank. (default=false) * - `minify` (bool): Remove non-applicable and properties that match defaults? (default=false, or true when getting `all`) * - `noCache` (bool): prevents use of cache to retrieve the module info. (default=false) * @return array Associative array of module information. * - On error, an `error` index is also populated with an error message. * - When requesting a module that does not exist its `id` value will be `0` and its `name` will be blank. * @see self::getModuleInfoVerbose() * @todo move all getModuleInfo methods to their own ModuleInfo class and break this method down further. * */ public function getModuleInfo($class, array $options = array()) { if($class === 'info') return $this->infoTemplate; if($class === '*' || $class === 'all') return $this->getModuleInfoAll($options); if($class === 'ProcessWire' || $class === 'PHP') return $this->getModuleInfoSystem($class, $options); $defaults = array( 'verbose' => false, 'minify' => false, 'noCache' => false, 'noInclude' => false, ); $options = array_merge($defaults, $options); $info = array(); $module = $class; $fromCache = false; // was the data loaded from cache? $moduleName = $this->moduleName($module); $moduleID = (string) $this->moduleID($moduleName); // typecast to string for cache if($module instanceof Module) { // module is an instance // return from cache if available if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) { $info = $this->moduleInfoCache[$moduleID]; $fromCache = true; } else { $info = $this->getModuleInfoExternal($moduleName); if(!count($info)) $info = $this->getModuleInfoInternal($module); } } else { // module is a class name or ID if(ctype_digit("$module")) $module = $moduleName; // return from cache if available (as it almost always should be) if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) { $info = $this->moduleInfoCache($moduleID); $fromCache = true; } else if(empty($options['noCache']) && $moduleID == 0) { // uninstalled module if(empty($this->moduleInfoCacheUninstalled)) { $this->loadModuleInfoCacheVerbose(true); } if(isset($this->moduleInfoCacheUninstalled[$moduleName])) { $info = $this->moduleInfoCacheUninstalled[$moduleName]; $fromCache = true; } } if(!$fromCache) { $namespace = $this->getModuleNamespace($moduleName); if(class_exists($namespace . $moduleName, false)) { // module is already in memory, check external first, then internal $info = $this->getModuleInfoExternal($moduleName); if(empty($info)) $info = $this->getModuleInfoInternal($moduleName, $namespace); } else { // module is not in memory, check external first, then internal $info = $this->getModuleInfoExternal($moduleName); if(empty($info)) { $installableFile = $this->modules->installableFile($moduleName); if($installableFile) { $this->modules->files->includeModuleFile($installableFile, $moduleName); } // info not available externally, attempt to locate it interally $info = $this->getModuleInfoInternal($moduleName, $namespace); } } } } if(!$fromCache && empty($info)) { return array_merge($this->infoTemplate, array( 'title' => $module, 'summary' => 'Inactive', 'error' => 'Unable to locate module', )); } $info['id'] = (int) $moduleID; if(!$options['minify']) $info = array_merge($this->infoTemplate, $info); if($fromCache) { // since cache is loaded at init(), this is the most common scenario if($options['verbose']) { if(empty($this->moduleInfoCacheVerbose)) { $this->loadModuleInfoCacheVerbose(); } if(!empty($this->moduleInfoCacheVerbose[$moduleID])) { $info = array_merge($info, $this->moduleInfoCacheVerbose($moduleID)); } } // populate defaults for properties omitted from cache foreach($this->infoNullReplacements as $key => $value) { if($info[$key] === null) $info[$key] = $value; } if(!empty($info['requiresVersions'])) $info['requires'] = array_keys($info['requiresVersions']); if($moduleName === 'SystemUpdater') $info['configurable'] = 1; // fallback, just in case // we skip everything else when module comes from cache since we can safely assume the checks below // are already accounted for in the cached module info } else { // not from cache, only likely to occur when refreshing modules info caches // if $info[requires] isn't already an array, make it one if(!is_array($info['requires'])) { $info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespace if(strpos($info['requires'], ',') !== false) { $info['requires'] = explode(',', $info['requires']); } else { $info['requires'] = array($info['requires']); } } // populate requiresVersions foreach($info['requires'] as $key => $class) { if(!ctype_alnum($class)) { // has a version string list($class, $operator, $version) = $this->extractModuleOperatorVersion($class); $info['requires'][$key] = $class; // convert to just class } else { // no version string $operator = '>='; $version = 0; } $info['requiresVersions'][$class] = array($operator, $version); } // what does it install? // if $info[installs] isn't already an array, make it one if(!is_array($info['installs'])) { $info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespace if(strpos($info['installs'], ',') !== false) { $info['installs'] = explode(',', $info['installs']); } else { $info['installs'] = array($info['installs']); } } // misc if($options['verbose']) { $info['versionStr'] = $this->modules->formatVersion($info['version']); // versionStr } $info['name'] = $moduleName; // module name // module configurable? $configurable = $this->modules->isConfigurable($moduleName, false); if($configurable === true || is_int($configurable) && $configurable > 1) { // configurable via ConfigurableModule interface // true=static, 2=non-static, 3=non-static $data, 4=non-static wrap, // 19=non-static getModuleConfigArray, 20=static getModuleConfigArray $info['configurable'] = $configurable; } else if($configurable) { // configurable via external file: ModuleName.config.php or ModuleNameConfig.php file $info['configurable'] = basename($configurable); } else { // not configurable $info['configurable'] = false; } // created date $createdDate = $this->modules->loader->createdDate($moduleID); if($createdDate) $info['created'] = strtotime($createdDate); $installableFile = $this->modules->installableFile($moduleName); $info['installed'] = $installableFile ? false : true; if(!$info['installed'] && !$info['created'] && $installableFile) { // uninstalled modules get their created date from the file or dir that they are in (whichever is newer) $pathname = $installableFile; $filemtime = @filemtime($pathname); if($filemtime === false) { $info['created'] = 0; } else { $dirname = dirname($pathname); $coreModulesPath = $this->modules->coreModulesPath; $dirmtime = substr($dirname, -7) == 'modules' || strpos($dirname, $coreModulesPath) !== false ? 0 : (int) filemtime($dirname); $info['created'] = $dirmtime > $filemtime ? $dirmtime : $filemtime; } } // namespace if($info['core']) { // default namespace, assumed since all core modules are in default namespace $info['namespace'] = self::defaultNamespace; } else { $info['namespace'] = $this->getModuleNamespace($moduleName, array( 'file' => $info['file'], 'noCache' => $options['noCache'] )); } if(!$options['verbose']) { foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]); } } if($info['namespace'] === null) $info['namespace'] = self::defaultNamespace; if(empty($info['created'])) { $createdDate = $this->modules->loader->createdDate($moduleID); if($createdDate) { $info['created'] = strtotime($createdDate); } } if($options['verbose']) { // the file property is not stored in the verbose cache, but provided as a verbose key $info['file'] = $this->modules->getModuleFile($moduleName); if($info['file']) $info['core'] = strpos($info['file'], $this->modules->coreModulesDir) !== false; // is it core? } else { // module info may still contain verbose keys with undefined values } if($options['minify']) { // when minify, any values that match defaults from infoTemplate are removed if(!$options['verbose']) { foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]); foreach($info as $key => $value) { if(!array_key_exists($key, $this->infoTemplate)) continue; if($value !== $this->infoTemplate[$key]) continue; unset($info[$key]); } } } return $info; } /** * Get info arrays for all modules indexed by module name * * @param array $options See options for getModuleInfo() method * @return array * */ public function getModuleInfoAll(array $options = array()) { $defaults = array( 'verbose' => false, 'noCache' => false, 'minify' => true, ); $options = array_merge($defaults, $options); if(!count($this->moduleInfoCache)) $this->loadModuleInfoCache(); $modulesInfo = $this->moduleInfoCache(); if($options['verbose']) { foreach($this->moduleInfoCacheVerbose() as $moduleID => $moduleInfoVerbose) { if($options['noCache']) { $modulesInfo[$moduleID] = $this->getModuleInfo($moduleID, $options); } else { $modulesInfo[$moduleID] = array_merge($modulesInfo[$moduleID], $moduleInfoVerbose); } } } else if($options['noCache']) { foreach(array_keys($modulesInfo) as $moduleID) { $modulesInfo[$moduleID] = $this->getModuleInfo($moduleID, $options); } } if(!$options['minify']) { foreach($modulesInfo as $moduleID => $info) { $modulesInfo[$moduleID] = array_merge($this->infoTemplate, $info); } } return $modulesInfo; } /** * Returns a verbose array of information for a Module * * This is the same as what’s returned by `Modules::getModuleInfo()` except that it has the following additional properties: * * - `versionStr` (string): formatted module version string. * - `file` (string): module filename from PW installation root, or false when it can't be found. * - `core` (bool): true when module is a core module, false when not. * - `author` (string): module author, when specified. * - `summary` (string): summary of what this module does. * - `href` (string): URL to module details (when specified). * - `permissions` (array): permissions installed by this module, associative array ('permission - name' => 'Description'). * - `page` (array): definition of page to create for Process module (see Process class) * * @param string|Module|int $class May be class name, module instance, or module ID * @param array $options Optional options to modify behavior of what gets returned: * - `noCache` (bool): prevents use of cache to retrieve the module info * - `noInclude` (bool): prevents include() of the module file, applicable only if it hasn't already been included * @return array Associative array of module information * @see Modules::getModuleInfo() * */ public function getModuleInfoVerbose($class, array $options = array()) { $options['verbose'] = true; $info = $this->getModuleInfo($class, $options); return $info; } /** * Get just a single property of module info * * @param Module|string $class Module instance or module name * @param string $property Name of property to get * @param array $options Additional options (see getModuleInfo method for options) * @return mixed|null Returns value of property or null if not found * @since 3.0.107 * */ public function getModuleInfoProperty($class, $property, array $options = array()) { if(empty($options['noCache'])) { // shortcuts where possible switch($property) { case 'namespace': return $this->getModuleNamespace($class); case 'requires': $v = $this->moduleInfoCache($class, 'requiresVersions'); // must be 'requiredVersions' here if(empty($v)) return array(); // early exit when known not to exist break; // fallback to calling getModuleInfo } } if(in_array($property, $this->moduleInfoVerboseKeys)) { $info = $this->getModuleInfoVerbose($class, $options); $info['verbose'] = true; } else { $info = $this->getModuleInfo($class, $options); } if(!isset($info[$property]) && empty($info['verbose'])) { // try again, just in case we can find it in verbose data $info = $this->getModuleInfoVerbose($class, $options); } return isset($info[$property]) ? $info[$property] : null; } /** * Return array of ($module, $operator, $requiredVersion) * * $version will be 0 and $operator blank if there are no requirements. * * @param string $require Module class name with operator and version string * @return array of array($moduleClass, $operator, $version) * */ protected function extractModuleOperatorVersion($require) { if(ctype_alnum($require)) { // no version is specified return array($require, '', 0); } $operators = array('<=', '>=', '<', '>', '!=', '='); $operator = ''; foreach($operators as $o) { if(strpos($require, $o)) { $operator = $o; break; } } // if no operator found, then no version is being specified if(!$operator) return array($require, '', 0); // extract class and version list($class, $version) = explode($operator, $require); // make version an integer if possible if(ctype_digit("$version")) $version = (int) $version; return array($class, $operator, $version); } /** * Load the module information cache * * #pw-internal * * @return bool * */ public function loadModuleInfoCache() { if(empty($this->modulesLastVersions)) { $name = self::moduleLastVersionsCacheName; $data = $this->modules->getCache($name); if(is_array($data)) $this->modulesLastVersions = $data; } if(empty($this->moduleInfoCache)) { $name = self::moduleInfoCacheName; $data = $this->modules->getCache($name); // if module class name keys in use (i.e. ProcessModule) it's an older version of // module info cache, so we skip over it to force its re-creation if(is_array($data) && !isset($data['ProcessModule'])) { $this->moduleInfoCache = $data; return true; } return false; } return true; } /** * Load the module information cache (verbose info: summary, author, href, file, core) * * #pw-internal * * @param bool $uninstalled If true, it will load the uninstalled verbose cache. * @return bool * */ public function loadModuleInfoCacheVerbose($uninstalled = false) { $name = $uninstalled ? self::moduleInfoCacheUninstalledName : self::moduleInfoCacheVerboseName; $data = $this->modules->getCache($name); if($data) { if(is_array($data)) { if($uninstalled) { $this->moduleInfoCacheUninstalled = $data; } else { $this->moduleInfoCacheVerbose = $data; } } return true; } return false; } /** * Save the module information cache * */ public function saveModuleInfoCache() { if($this->debug) { static $n = 0; $this->message("saveModuleInfoCache (" . (++$n) . ")"); } $this->moduleInfoCache = array(); $this->moduleInfoCacheVerbose = array(); $this->moduleInfoCacheUninstalled = array(); $user = $this->wire()->user; $languages = $this->wire()->languages; $language = null; if($languages) { // switch to default language to prevent caching of translated title/summary data $language = $user->language; try { if($language && $language->id && !$language->isDefault()) $user->language = $languages->getDefault(); // save } catch(\Exception $e) { $this->trackException($e, false, true); } } $installableFiles = $this->modules->installableFiles; foreach(array(true, false) as $installed) { $items = $installed ? $this->modules : array_keys($installableFiles); foreach($items as $module) { $class = is_object($module) ? $module->className() : $module; $class = wireClassName($class, false); $info = $this->getModuleInfo($class, array('noCache' => true, 'verbose' => true)); $moduleID = (int) $info['id']; // note ID is always 0 for uninstalled modules if(!empty($info['error'])) { if($this->debug) $this->warning("$class reported error: $info[error]"); continue; } if(!$moduleID && $installed) { if($this->debug) $this->warning("No module ID for $class"); continue; } if(!$this->debug) unset($info['id']); // no need to double store this property since it is already the array key if(is_null($info['autoload'])) { // module info does not indicate an autoload state $info['autoload'] = $this->modules->isAutoload($module); } else if(!is_bool($info['autoload']) && !is_string($info['autoload']) && wireIsCallable($info['autoload'])) { // runtime function, identify it only with 'function' so that it can be recognized later as one that // needs to be dynamically loaded $info['autoload'] = 'function'; } if(is_null($info['singular'])) { $info['singular'] = $this->modules->isSingular($module); } if(is_null($info['configurable'])) { $info['configurable'] = $this->modules->isConfigurable($module, false); } if($moduleID) $this->modules->flags->updateModuleFlags($moduleID, $info); if($installed) { $verboseKeys = $this->moduleInfoVerboseKeys; $verboseInfo = array(); foreach($verboseKeys as $key) { if(!empty($info[$key])) $verboseInfo[$key] = $info[$key]; unset($info[$key]); // remove from regular moduleInfo } $this->moduleInfoCache[$moduleID] = $info; $this->moduleInfoCacheVerbose[$moduleID] = $verboseInfo; } else { $this->moduleInfoCacheUninstalled[$class] = $info; } } } $caches = array( self::moduleInfoCacheName => 'moduleInfoCache', self::moduleInfoCacheVerboseName => 'moduleInfoCacheVerbose', self::moduleInfoCacheUninstalledName => 'moduleInfoCacheUninstalled', ); $defaultTrimNS = trim(self::defaultNamespace, "\\"); foreach($caches as $cacheName => $varName) { $data = $this->$varName; foreach($data as $moduleID => $moduleInfo) { foreach($moduleInfo as $key => $value) { // remove unpopulated properties if($key == 'installed') { // no need to store an installed==true property if($value) unset($data[$moduleID][$key]); } else if($key == 'requires' && !empty($value) && !empty($data[$moduleID]['requiresVersions'])) { // requiresVersions has enough info to re-construct requires, so no need to store it unset($data[$moduleID][$key]); } else if(($key == 'created' && empty($value)) || ($value === 0 && ($key == 'singular' || $key == 'autoload' || $key == 'configurable')) || ($value === null || $value === "" || $value === false) || (is_array($value) && !count($value))) { // no need to store these false, null, 0, or blank array properties unset($data[$moduleID][$key]); } else if($key === 'namespace' && (empty($value) || trim($value, "\\") === $defaultTrimNS)) { // no need to cache default namespace in module info unset($data[$moduleID][$key]); } else if($key === 'file') { // file property is cached elsewhere so doesn't need to be included in this cache unset($data[$moduleID][$key]); } } } $this->modules->saveCache($cacheName, $data); } // $this->log('Saved module info caches'); if($languages && $language) $user->language = $language; // restore } /** * Clear the module information cache * * @param bool|null $showMessages Specify true to show message notifications * */ public function clearModuleInfoCache($showMessages = false) { $sanitizer = $this->wire()->sanitizer; $config = $this->wire()->config; $versionChanges = array(); $editLinks = array(); $newModules = array(); $moveModules = array(); $missModules = array(); // record current module versions currently in moduleInfo $moduleVersions = array(); foreach($this->moduleInfoCache() as $id => $moduleInfo) { if(isset($this->modulesLastVersions[$id])) { $moduleVersions[$id] = $this->modulesLastVersions[$id]; } else { $moduleVersions[$id] = $moduleInfo['version']; } } // delete the caches $this->modules->deleteCache(self::moduleInfoCacheName); $this->modules->deleteCache(self::moduleInfoCacheVerboseName); $this->modules->deleteCache(self::moduleInfoCacheUninstalledName); $this->moduleInfoCache = array(); $this->moduleInfoCacheVerbose = array(); $this->moduleInfoCacheUninstalled = array(); // save new moduleInfo cache $this->saveModuleInfoCache(); // compare new moduleInfo versions with the previous ones, looking for changes foreach($this->moduleInfoCache() as $id => $moduleInfo) { $moduleName = $moduleInfo['name']; if(!isset($moduleVersions[$id])) { if($this->modules->moduleID($moduleName)) { $moveModules[] = $moduleName; } else { $newModules[] = $moduleName; } continue; } if($moduleVersions[$id] != $moduleInfo['version']) { $fromVersion = $this->modules->formatVersion($moduleVersions[$id]); $toVersion = $this->modules->formatVersion($moduleInfo['version']); $versionChanges[$moduleName] = "$fromVersion => $toVersion: $moduleName"; $editUrl = $this->modules->configs->getModuleEditUrl($moduleName, false) . '&upgrade=1'; $this->modulesLastVersions[$id] = $moduleVersions[$id]; if(strpos($moduleName, 'Fieldtype') === 0) { // apply update now, to Fieldtype modules only (since they are loaded differently) $this->modules->getModule($moduleName); } else { $editLinks[$moduleName] = "" . $sanitizer->entities1($this->_('Apply')) . ""; } } } foreach($this->modules->moduleIDs as $moduleName => $moduleID) { if(isset($this->moduleInfoCache[$moduleID])) { // module is present in moduleInfo if($this->modules->flags->hasFlag($moduleID, Modules::flagsNoFile)) { $file = $this->modules->getModuleFile($moduleName, array('fast' => false)); if($file) { // remove flagsNoFile if file is found $this->modules->flags->setFlag($moduleID, Modules::flagsNoFile, false); } } } else { // module is missing moduleInfo $file = $this->modules->getModuleFile($moduleName, array('fast' => false)); if(!$file) { $file = $this->modules->getModuleFile($moduleName, array('fast' => true, 'guess' => true)); // add flagsNoFile if file cannot be located $missModules[$moduleName] = "$moduleName => $file"; $editUrl = $this->modules->configs->getModuleEditUrl($moduleName, false) . '&missing=1'; $editLinks[$moduleName] = "" . $sanitizer->entities1($this->_('Edit')) . ""; $this->modules->flags->setFlag($moduleID, Modules::flagsNoFile, true); } } } $this->updateModuleVersionsCache(); // report detected changes $reports = array( array( 'label' => $this->_('Found %d new module(s):'), 'items' => $newModules, ), /* array( 'label' => $this->_('Found %d moved module(s):'), 'items' => $moveModules, ), */ array( 'label' => $this->_('Found %d module(s) missing file:'), 'items' => $missModules, ), array( 'label' => $this->_('Found %d module version changes (applied when each module is loaded):'), 'items' => $versionChanges, ), ); $qty = 0; foreach($reports as $report) { if(!count($report['items'])) continue; if($showMessages) { $items = array(); foreach($report['items'] as $moduleName => $item) { $item = $sanitizer->entities($item); if(isset($editLinks[$moduleName])) $item .= " - " . $editLinks[$moduleName]; $items[] = $item; } $itemsStr = implode("\n", $items); $itemsStr = str_replace($config->paths->root, $config->urls->root, $itemsStr); $this->message( $sanitizer->entities1(sprintf($report['label'], count($items))) . "
$itemsStr", 'icon-plug markup nogroup' ); $qty++; } $this->log( sprintf($report['label'], count($report['items'])) . ' ' . implode(', ', $report['items']) ); } if($qty) { /** @var JqueryUI $jQueryUI */ $jQueryUI = $this->modules->getModule('JqueryUI'); if($jQueryUI) $jQueryUI->use('modal'); } } /** * Update the cache of queued module version changes * */ protected function updateModuleVersionsCache() { $moduleIDs = $this->modules->moduleIDs; foreach($this->modulesLastVersions as $id => $version) { // clear out stale data, if present if(!in_array($id, $moduleIDs)) unset($this->modulesLastVersions[$id]); } $this->modules->saveCache(self::moduleLastVersionsCacheName, $this->modulesLastVersions); } /** * Check the module version to make sure it is consistent with our moduleInfo * * When not consistent, this triggers the moduleVersionChanged hook, which in turn * triggers the $module->___upgrade($fromVersion, $toVersion) method. * * @param Module $module * */ public function checkModuleVersion(Module $module) { $id = (string) $this->modules->getModuleID($module); $moduleInfo = $this->getModuleInfo($module); if(!isset($this->modulesLastVersions[$id])) return; $lastVersion = $this->modulesLastVersions[$id]; if($lastVersion === $moduleInfo['version']) return; // calling the one from $modules rather than $this is intentional $this->modules->moduleVersionChanged($module, $lastVersion, $moduleInfo['version']); } /** * @param int|null $id * @return string|null|array * */ public function modulesLastVersions($id = null) { if($id === null) return $this->modulesLastVersions; return isset($this->modulesLastVersions[$id]) ? $this->modulesLastVersions[$id] : null; } /** * Module version changed * * This calls the module's ___upgrade($fromVersion, $toVersion) method. * * @param Module|_Module $module * @param int|string $fromVersion * @param int|string $toVersion * */ public function moduleVersionChanged(Module $module, $fromVersion, $toVersion) { $moduleName = wireClassName($module, false); $moduleID = $this->modules->getModuleID($module); $fromVersionStr = $this->modules->formatVersion($fromVersion); $toVersionStr = $this->modules->formatVersion($toVersion); $this->message($this->_('Upgrading module') . " ($moduleName: $fromVersionStr => $toVersionStr)"); try { if(method_exists($module, '___upgrade') || method_exists($module, 'upgrade')) { $module->upgrade($fromVersion, $toVersion); } unset($this->modulesLastVersions[$moduleID]); $this->updateModuleVersionsCache(); } catch(\Exception $e) { $this->error("Error upgrading module ($moduleName): " . $e->getMessage()); } } /** * Get an array of all unique, non-default, non-root module namespaces mapped to directory names * * @return array * */ public function getNamespaces() { $config = $this->wire()->config; if(!is_null($this->moduleNamespaceCache)) return $this->moduleNamespaceCache; $defaultNamespace = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : ""; $namespaces = array(); foreach($this->moduleInfoCache() as /* $moduleID => */ $info) { if(!isset($info['namespace']) || $info['namespace'] === $defaultNamespace || $info['namespace'] === "\\") continue; $moduleName = $info['name']; $namespaces[$info['namespace']] = $config->paths($moduleName); } $this->moduleNamespaceCache = $namespaces; return $namespaces; } /** * Get the namespace for the given module * * #pw-internal * * @param string|Module $moduleName * @param array $options * - `file` (string): Known module path/file, as an optimization. * - `noCache` (bool): Specify true to force reload namespace info directly from module file. (default=false) * - `noLoad` (bool): Specify true to prevent loading of file for namespace discovery. (default=false) Added 3.0.170 * @return null|string Returns namespace, or NULL if unable to determine. Namespace is ready to use in a string (i.e. has trailing slashes) * */ public function getModuleNamespace($moduleName, $options = array()) { $defaults = array( 'file' => null, 'noLoad' => false, 'noCache' => false, ); $namespace = null; if(is_object($moduleName) || strpos($moduleName, "\\") !== false) { $className = is_object($moduleName) ? get_class($moduleName) : $moduleName; if(strpos($className, "ProcessWire\\") === 0) return "ProcessWire\\"; if(strpos($className, "\\") === false) return "\\"; $parts = explode("\\", $className); array_pop($parts); $namespace = count($parts) ? implode("\\", $parts) : ""; $namespace = $namespace == "" ? "\\" : "\\$namespace\\"; return $namespace; } if(empty($options['noCache'])) { $moduleID = $this->modules->getModuleID($moduleName); $info = isset($this->moduleInfoCache[$moduleID]) ? $this->moduleInfoCache($moduleID) : null; if($info) { if(isset($info['namespace'])) { if("$info[namespace]" === "1") return __NAMESPACE__ . "\\"; return $info['namespace']; } else { // if namespace not present in info then use default namespace return __NAMESPACE__ . "\\"; } } } $options = array_merge($defaults, $options); if(empty($options['file'])) { $options['file'] = $this->modules->getModuleFile($moduleName); } if(strpos($options['file'], $this->modules->coreModulesDir) !== false) { // all core modules use \ProcessWire\ namespace $namespace = strlen(__NAMESPACE__) ? __NAMESPACE__ . "\\" : ""; return $namespace; } if(!$options['file'] || !file_exists($options['file'])) { return null; } if(empty($options['noLoad'])) { $namespace = $this->modules->files->getFileNamespace($options['file']); } return $namespace; } /** * Is the given namespace a unique recognized module namespace? If yes, returns the path to it. If not, returns boolean false. * * #pw-internal * * @param string $namespace * @return bool|string * */ public function getNamespacePath($namespace) { if($namespace === 'ProcessWire') return "ProcessWire\\"; if(is_null($this->moduleNamespaceCache)) $this->getNamespaces(); $namespace = "\\" . trim($namespace, "\\") . "\\"; return isset($this->moduleNamespaceCache[$namespace]) ? $this->moduleNamespaceCache[$namespace] : false; } public function __get($name) { switch($name) { case 'moduleInfoCache': return $this->moduleInfoCache; case 'moduleInfoCacheVerbose': return $this->moduleInfoCacheVerbose; case 'moduleInfoCacheUninstalled': return $this->moduleInfoCacheUninstalled; case 'moduleInfoVerboseKeys': return $this->moduleInfoVerboseKeys; case 'modulesLastVersions': return $this->modulesLastVersions; } return parent::__get($name); } public function getDebugData() { return array( 'moduleInfoCache' => $this->moduleInfoCache, 'moduleInfoCacheVerbose' => $this->moduleInfoCacheVerbose, 'moduleInfoCacheUninstalled' => $this->moduleInfoCacheUninstalled, 'modulesLastVersions' => $this->modulesLastVersions, 'moduleNamespaceCache' => $this->moduleNamespaceCache ); } }