1393 lines
45 KiB
PHP
1393 lines
45 KiB
PHP
<?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 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] = "<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
|
||
);
|
||
}
|
||
}
|