artabro/wire/core/ModulesInfo.php
2024-08-27 11:35:37 +02:00

1393 lines
45 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Modules: Info
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @property-read array $moduleInfoCache
* @property-read array $moduleInfoCacheVerbose
* @property-read array $moduleInfoCacheUninstalled
* @property-read array $moduleInfoVerboseKeys
* @property-read array $modulesLastVersions
*
*/
class ModulesInfo extends ModulesClass {
/**
* Filename for module info cache file
*
*/
const moduleInfoCacheName = 'Modules.info';
/**
* Filename for verbose module info cache file
*
*/
const moduleInfoCacheVerboseName = 'ModulesVerbose.info';
/**
* Filename for uninstalled module info cache file
*
*/
const moduleInfoCacheUninstalledName = 'ModulesUninstalled.info';
/**
* Cache name for module version change cache
*
*/
const moduleLastVersionsCacheName = 'ModulesVersions.info';
/**
* Default namespace
*
*/
const defaultNamespace = "\\ProcessWire\\";
protected $debug = false;
/**
* @var Modules
*
*/
protected $modules;
/**
* Cache of module information
*
*/
public $moduleInfoCache = array();
/**
* Cache of module information (verbose text) including: summary, author, href, file, core
*
*/
public $moduleInfoCacheVerbose = array();
/**
* Cache of uninstalled module information (verbose for uninstalled) including: summary, author, href, file, core
*
* Note that this one is indexed by class name rather than by ID (since uninstalled modules have no ID)
*
*/
public $moduleInfoCacheUninstalled = array();
/**
* Last known versions of modules, for version change tracking
*
* @var array of ModuleName (string) => 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 versionsmodule 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 whats 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] = "<a class='pw-modal' target='_blank' href='$editUrl'>" .
$sanitizer->entities1($this->_('Apply')) . "</a>";
}
}
}
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] = "<a class='pw-modal' target='_blank' href='$editUrl'>" .
$sanitizer->entities1($this->_('Edit')) . "</a>";
$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))) .
"<pre>$itemsStr</pre>",
'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
);
}
}