Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Modules** Loads and manages all runtime modules for ProcessWire** Note that when iterating, find(), or calling any other method that returns module(s), excepting get(), a ModulePlaceholder may be* returned rather than a real Module. ModulePlaceholders are used in instances when the module may or may not be needed at runtime* in order to save resources. As a result, anything iterating through these Modules should check to make sure it's not a ModulePlaceholder* before using it. If it's a ModulePlaceholder, then the real Module can be instantiated/retrieved by $modules->get($className).** ProcessWire 3.x, Copyright 2021 by Ryan Cramer* https://processwire.com** #pw-summary Loads and manages all modules in ProcessWire.* #pw-body =* The `$modules` API variable is most commonly used for getting individual modules to use their API.* ~~~~~* // Getting a module by name* $m = $modules->get('MarkupPagerNav');** // Getting a module by name (alternate)* $m = $modules->MarkupPagerNav;* ~~~~~** #pw-body** @todo Move all module information methods to a ModulesInfo class* @todo Move all module loading methods to a ModulesLoad class** @method void refresh($showMessages = false) Refresh the cache that stores module files by recreating it* @method null|Module install($class, $options = array())* @method bool|int delete($class)* @method bool uninstall($class)* @method bool saveModuleConfigData($className, array $configData) Alias of saveConfig() method #pw-internal* @method bool saveConfig($class, $data, $value = null)* @method InputfieldWrapper|null getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) #pw-internal* @method void moduleVersionChanged(Module $module, $fromVersion, $toVersion) #pw-internal* @method bool|string isUninstallable($class, $returnReason = false) hookable in 3.0.181+ #pw-internal**/class Modules extends WireArray {/*** Whether or not module debug mode is active**/protected $debug = false;/*** Flag indicating the module may have only one instance at runtime.**/const flagsSingular = 1;/*** Flag indicating that the module should be instantiated at runtime, rather than when called upon.**/const flagsAutoload = 2;/*** Flag indicating the module has more than one copy of it on the file system.**/const flagsDuplicate = 4;/*** When combined with flagsAutoload, indicates that the autoload is conditional**/const flagsConditional = 8;/*** When combined with flagsAutoload, indicates that the module's autoload state is temporarily disabled**/const flagsDisabled = 16;/*** Indicates module that maintains a configurable interface but with no interactive Inputfields**/const flagsNoUserConfig = 32;/*** Module where no file could be located**/const flagsNoFile = 64;/*** 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';/*** Array of modules that are not currently installed, indexed by className => filename**/protected $installable = array();/*** An array of module database IDs indexed by: class => id** Used internally for database operations**/protected $moduleIDs = array();/*** Full system paths where modules are stored** index 0 must be the core modules path (/i.e. /wire/modules/)**/protected $paths = array();/*** Cached module configuration data indexed by module ID** Values are integer 1 for modules that have config data but data is not yet loaded.* Values are an array for modules have have config data and has been loaded.**/protected $configData = array();/*** Module created dates indexed by module ID**/protected $createdDates = array();/*** Have the modules been init'd() ?**/protected $initialized = false;/*** Becomes an array if debug mode is on**/protected $debugLog = array();/*** Array of moduleName => condition** Condition can be either an anonymous function or a selector string to be evaluated at ready().**/protected $conditionalAutoloadModules = array();/*** Cache of module information**/protected $moduleInfoCache = array();/*** Cache of module information (verbose text) including: summary, author, href, file, core**/protected $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)**/protected $moduleInfoCacheUninstalled = array();/*** Cache of module information from DB used across multiple calls temporarily by load() method**/protected $modulesTableCache = array();/*** Cache of namespace => path for unique module namespaces** @var array|null Becomes an array once populated**/protected $moduleNamespaceCache = null;/*** Last known versions of modules, for version change tracking** @var array of ModuleName (string) => last known version (integer|string)**/protected $modulesLastVersions = array();/*** Array of module ID => flags (int)** @var array**/protected $moduleFlags = array();/*** Array of moduleName => substituteModuleName to be used when moduleName doesn't exist** Primarily for providing backwards compatiblity with modules assumed installed that* may no longer be in core.** see setSubstitutes() method**/protected $substitutes = array();/*** Instance of ModulesDuplicates** @var ModulesDuplicates**/protected $duplicates;/*** Module file extensions indexed by module name where value 1=.module, and 2=.module.php** @var array**/protected $moduleFileExts = array();/*** Dir for core modules relative to root path, i.e. '/wire/modules/'** @var string**/protected $coreModulesDir = '';/*** Array of moduleName => order to indicate autoload order when necessary** @var array**/protected $autoloadOrders = array();/*** Are we currently refreshing?** @var bool**/protected $refreshing = false;/*** Properties that only appear in 'verbose' moduleInfo** @var array**/protected $moduleInfoVerboseKeys = array('summary','author','href','file','core','versionStr','permissions','searchable','page',// 'languages',);/*** Core module types that are isolated by directory** @var array**/protected $coreTypes = array('AdminTheme','Fieldtype','Inputfield','Jquery','LanguageSupport','Markup','Process','Session','System','Textformatter',);/*** Construct the Modules** @param string $path Core modules path (you may add other paths with addPath method)**/public function __construct($path) {parent::__construct();$this->addPath($path);}/*** Wired to API** #pw-internal**/public function wired() {$this->coreModulesDir = '/' . $this->wire('config')->urls->data('modules');parent::wired();}/*** Get the ModulesDuplicates instance** #pw-internal** @return ModulesDuplicates**/public function duplicates() {if(is_null($this->duplicates)) $this->duplicates = $this->wire(new ModulesDuplicates());return $this->duplicates;}/*** Add another modules path, must be called before init()** #pw-internal** @param string $path**/public function addPath($path) {$this->paths[] = $path;}/*** Return all assigned module root paths** #pw-internal** @return array of modules paths, with index 0 always being the core modules path.**/public function getPaths() {return $this->paths;}/*** Initialize modules** Must be called after construct before this class is ready to use** #pw-internal** @see load()**/public function init() {$this->setTrackChanges(false);$this->loadModuleInfoCache();$this->loadModulesTable();if(!empty($this->autoloadOrders)) $this->preloadModules();foreach($this->paths as $path) {$this->load($path);}$this->modulesTableCache = array(); // clear out data no longer needed}/*** Include site preload modules** Preload modules load before all other modules, including core modules. In order* for a module to be a preload module, it must meet the following conditions:** - Module info `autoload` value is integer of 10000 or greater, i.e. `[ 'autoload' => 10000 ]`* - Module info `singular` value must be non-empty, i.e. `[ 'singular' => true ]`* - Module file is located in: /site/modules/ModuleName/ModuleName.module.php* - Module cannot load any other modules at least until ready() method called.* - Module cannot have any `requires` dependencies to any other modules.** Please note the above is specifically stating that the module must be in its* own “site/ModuleName/” directory and have the “.module.php” extension. Using* just the “.module” extension is not supported for preload modules.** @since 3.0.173**/protected function preloadModules() {if(!isset($this->paths[1])) return;arsort($this->autoloadOrders);foreach($this->autoloadOrders as $moduleName => $order) {if($order < 10000) break;if(!isset($this->moduleIDs[$moduleName])) continue;$moduleID = $this->moduleIDs[$moduleName];if(!isset($this->moduleInfoCache[$moduleID])) continue;$info = $this->moduleInfoCache[$moduleID];if(empty($info['singular'])) continue;$file = $this->paths[1] . "$moduleName/$moduleName.module.php";if(!file_exists($file) || !$this->includeModuleFile($file, $moduleName)) continue;if(!isset($info['namespace'])) $info['namespace'] = '';$className = $info['namespace'] . $moduleName;$module = $this->newModule($className, $moduleName);if($module) parent::set($moduleName, $module);}}/*** Modules class accepts only Module instances, per the WireArray interface** #pw-internal** @param Wire $item* @return bool**/public function isValidItem($item) {return $item instanceof Module;}/*** The key/index used for each module in the array is it's class name, per the WireArray interface** #pw-internal** @param Wire $item* @return int|string**/public function getItemKey($item) {return $this->getModuleClass($item);}/*** There is no blank/generic module type, so makeBlankItem returns null** #pw-internal**/public function makeBlankItem() {return null;}/*** Make a new/blank WireArray** #pw-internal**/public function makeNew() {// ensures that find(), etc. operations don't initalize a new Modules() classreturn $this->wire(new WireArray());}/*** Make a new populated copy of a WireArray containing all the modules** #pw-internal** @return WireArray**/public function makeCopy() {// ensures that find(), etc. operations don't initalize a new Modules() class$copy = $this->makeNew();foreach($this->data as $key => $value) $copy[$key] = $value;$copy->resetTrackChanges($this->trackChanges());return $copy;}/*** Initialize all the modules that are loaded at boot** #pw-internal** @param null|array|Modules $modules* @param array $completed* @param int $level**/public function triggerInit($modules = null, $completed = array(), $level = 0) {$debugKey = null;$debugKey2 = null;if($this->debug) {$debugKey = $this->debugTimerStart("triggerInit$level");$this->message("triggerInit(level=$level)");}$queue = array();if(is_null($modules)) $modules = $this;foreach($modules as $class => $module) {if($module instanceof ModulePlaceholder) {// skip modules that aren't autoload and those that are conditional autoloadif(!$module->autoload) continue;if(isset($this->conditionalAutoloadModules[$class])) continue;}if($this->debug) $debugKey2 = $this->debugTimerStart("triggerInit$level($class)");$info = $this->getModuleInfo($module);$skip = false;// module requires other modulesforeach($info['requires'] as $requiresClass) {if(in_array($requiresClass, $completed)) continue;$dependencyInfo = $this->getModuleInfo($requiresClass);if(empty($dependencyInfo['autoload'])) {// if dependency isn't an autoload one, there's no point in waiting for itif($this->debug) $this->warning("Autoload module '$module' requires a non-autoload module '$requiresClass'");continue;} else if(isset($this->conditionalAutoloadModules[$requiresClass])) {// autoload module requires another autoload module that may or may not loadif($this->debug) $this->warning("Autoload module '$module' requires a conditionally autoloaded module '$requiresClass'");continue;}// dependency is autoload and required by this module, so queue this module to init later$queue[$class] = $module;$skip = true;break;}if(!$skip) {if($info['autoload'] !== false) {if($info['autoload'] === true || $this->isAutoload($module)) {$this->initModule($module);}}$completed[] = $class;}if($this->debug) $this->debugTimerStop($debugKey2);}// if there is a dependency queue, go recursive till the queue is completedif(count($queue) && $level < 3) {$this->triggerInit($queue, $completed, $level + 1);}$this->initialized = true;if($this->debug) if($debugKey) $this->debugTimerStop($debugKey);if(!$level && (empty($this->moduleInfoCache))) { // || empty($this->moduleInfoCacheVerbose))) {if($this->debug) $this->message("saveModuleInfoCache from triggerInit");$this->saveModuleInfoCache();}}/*** Given a class name, return the constructed module** @param string $className Module class name* @param string $moduleName Optional module name only (no namespace)* @return Module|null**/protected function newModule($className, $moduleName = '') {if(!$moduleName) {$moduleName = wireClassName($className, false);$className = wireClassName($className, true);}$debugKey = $this->debug ? $this->debugTimerStart("newModule($moduleName)") : null;if(!class_exists($className, false)) $this->includeModule($moduleName);if(!class_exists($className, false)) {// attempt 2.x module in dedicated namespace or root namespace$className = $this->getModuleNamespace($moduleName) . $moduleName;}if(ProcessWire::getNumInstances() > 1) {// in a multi-instance environment, ensures that anything happening during// the module __construct is using the right instance. necessary because the// construct method runs before the wire instance is set to the module$wire1 = ProcessWire::getCurrentInstance();$wire2 = $this->wire();if($wire1 !== $wire2) {ProcessWire::setCurrentInstance($wire2);} else {$wire1 = null;}} else {$wire1 = null;$wire2 = null;}try {$module = $this->wire(new $className());} catch(\Exception $e) {$this->error(sprintf($this->_('Failed to construct module: %s'), $className) . " - " . $e->getMessage());$module = null;}if($this->debug) $this->debugTimerStop($debugKey);if($wire1) ProcessWire::setCurrentInstance($wire1);return $module;}/*** Return a new ModulePlaceholder for the given className** @param string $className Module class this placeholder will stand in for* @param string $ns Module namespace* @param string $file Full path and filename of $className* @param bool $singular Is the module a singular module?* @param bool $autoload Is the module an autoload module?* @return ModulePlaceholder**/protected function newModulePlaceholder($className, $ns, $file, $singular, $autoload) {$module = $this->wire(new ModulePlaceholder());$module->setClass($className);$module->setNamespace($ns);$module->singular = $singular;$module->autoload = $autoload;$module->file = $file;return $module;}/*** Initialize a single module** @param Module $module* @param array $options* - `clearSettings` (bool): When true, module settings will be cleared when appropriate to save space. (default=true)* - `configOnly` (bool): When true, module init() method NOT called, but config data still set (default=false) 3.0.169+* - `configData` (array): Extra config data merge with module’s config data (default=[]) 3.0.169+* - `throw` (bool): When true, exceptions will be allowed to pass through. (default=false)* @return bool True on success, false on fail* @throws \Exception Only if the `throw` option is true.**/protected function initModule(Module $module, array $options = array()) {$result = true;$debugKey = null;$clearSettings = isset($options['clearSettings']) ? (bool) $options['clearSettings'] : true;$throw = isset($options['throw']) ? (bool) $options['throw'] : false;if($this->debug) {static $n = 0;$this->message("initModule (" . (++$n) . "): " . wireClassName($module));}// if the module is configurable, then load its config data// and set values for each before initializing the module$extraConfigData = isset($options['configData']) ? $options['configData'] : null;$this->setModuleConfigData($module, null, $extraConfigData);$moduleName = wireClassName($module, false);$moduleID = isset($this->moduleIDs[$moduleName]) ? $this->moduleIDs[$moduleName] : 0;if($moduleID && isset($this->modulesLastVersions[$moduleID])) {$this->checkModuleVersion($module);}if(method_exists($module, 'init') && empty($options['configOnly'])) {if($this->debug) {$debugKey = $this->debugTimerStart("initModule($moduleName)");}try {$module->init();} catch(\Exception $e) {if($throw) throw($e);$this->error(sprintf($this->_('Failed to init module: %s'), $moduleName) . " - " . $e->getMessage());$result = false;}if($this->debug) {$this->debugTimerStop($debugKey);}}// if module is autoload (assumed here) and singular, then// we no longer need the module's config data, so remove itif($clearSettings && $this->isSingular($module)) {if(!$moduleID) $moduleID = $this->getModuleID($module);if(isset($this->configData[$moduleID])) $this->configData[$moduleID] = 1;}return $result;}/*** Call ready for a single module** @param Module $module* @return bool**/protected function readyModule(Module $module) {$result = true;if(method_exists($module, 'ready')) {$debugKey = $this->debug ? $this->debugTimerStart("readyModule(" . $module->className() . ")") : null;try {$module->ready();} catch(\Exception $e) {$this->error(sprintf($this->_('Failed to ready module: %s'), $module->className()) . " - " . $e->getMessage());$result = false;}if($this->debug) {$this->debugTimerStop($debugKey);static $n = 0;$this->message("readyModule (" . (++$n) . "): " . wireClassName($module));}}return $result;}/*** Init conditional autoload modules, if conditions allow** @return array of skipped module names**/protected function triggerConditionalAutoload() {// conditional autoload modules that are skipped (className => 1)$skipped = array();// init conditional autoload modules, now that $page is knownforeach($this->conditionalAutoloadModules as $className => $func) {if($this->debug) {$moduleID = $this->getModuleID($className);$flags = $this->moduleFlags[$moduleID];$this->message("Conditional autoload: $className (flags=$flags, condition=" . (is_string($func) ? $func : 'func') . ")");}$load = true;if(is_string($func)) {// selector stringif(!$this->wire('page')->is($func)) $load = false;} else {// anonymous functionif(!is_callable($func)) $load = false;else if(!$func()) $load = false;}if($load) {$module = $this->newModule($className);if($module) {$this->set($className, $module);if($this->initModule($module)) {if($this->debug) $this->message("Conditional autoload: $className LOADED");} else {if($this->debug) $this->warning("Failed conditional autoload: $className");}}} else {$skipped[$className] = $className;if($this->debug) $this->message("Conditional autoload: $className SKIPPED");}}// clear this out since we don't need it anymore$this->conditionalAutoloadModules = array();return $skipped;}/*** Trigger all modules 'ready' method, if they have it.** This is to indicate to them that the API environment is fully ready and $page is in fuel.** This is triggered by ProcessPageView::ready** #pw-internal**/public function triggerReady() {$debugKey = $this->debug ? $this->debugTimerStart("triggerReady") : null;$skipped = $this->triggerConditionalAutoload();// trigger ready method on all applicable modulesforeach($this as $module) {/** @var Module $module */if($module instanceof ModulePlaceholder) continue;// $info = $this->getModuleInfo($module);// if($info['autoload'] === false) continue;// if(!$this->isAutoload($module)) continue;$class = $this->getModuleClass($module);if(isset($skipped[$class])) continue;$id = $this->moduleIDs[$class];if(!($this->moduleFlags[$id] & self::flagsAutoload)) continue;if(!method_exists($module, 'ready')) continue;$this->readyModule($module);}if($this->debug) $this->debugTimerStop($debugKey);}/*** Retrieve the installed module info as stored in the database**/protected function loadModulesTable() {$this->autoloadOrders = array();$database = $this->wire()->database;// we use SELECT * so that this select won't be broken by future DB schema additions// Currently: id, class, flags, data, with created added at sysupdate 7$query = $database->prepare("SELECT * FROM modules ORDER BY class", "modules.loadModulesTable()"); // QA$query->execute();/** @noinspection PhpAssignmentInConditionInspection */while($row = $query->fetch(\PDO::FETCH_ASSOC)) {$moduleID = (int) $row['id'];$flags = (int) $row['flags'];$class = $row['class'];$this->moduleIDs[$class] = $moduleID;$this->moduleFlags[$moduleID] = $flags;$autoload = $flags & self::flagsAutoload;$loadSettings = $autoload || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');if($loadSettings) {// preload config data for autoload modules since we'll need it again very soon$data = strlen($row['data']) ? wireDecodeJSON($row['data']) : array();$this->configData[$moduleID] = $data;// populate information about duplicates, if applicableif($flags & self::flagsDuplicate) $this->duplicates()->addFromConfigData($class, $data);} else if(!empty($row['data'])) {// indicate that it has config data, but not yet loaded$this->configData[$moduleID] = 1;}if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {$this->createdDates[$moduleID] = $row['created'];}if($autoload && !empty($this->moduleInfoCache[$moduleID]['autoload'])) {$autoload = $this->moduleInfoCache[$moduleID]['autoload'];$disabled = $flags & self::flagsDisabled;if(is_int($autoload) && $autoload > 1 && !$disabled) {// autoload specifies an order > 1, indicating it should load before others$this->autoloadOrders[$class] = $autoload;}}unset($row['data'], $row['created']); // info we don't want stored in modulesTableCache$this->modulesTableCache[$class] = $row;}$query->closeCursor();}/*** Given a disk path to the modules, determine all installed modules and keep track of all uninstalled (installable) modules.** @param string $path**/protected function load($path) {$config = $this->wire('config');$debugKey = $this->debug ? $this->debugTimerStart("load($path)") : null;$installed =& $this->modulesTableCache;$modulesLoaded = array();$modulesDelayed = array();$modulesRequired = array();$rootPath = $config->paths->root;$basePath = substr($path, strlen($rootPath));foreach($this->findModuleFiles($path, true) as $pathname) {$pathname = trim($pathname);if(empty($pathname)) continue;$basename = basename($pathname);list($moduleName, $ext) = explode('.', $basename, 2); // i.e. "module.php" or "module"$this->moduleFileExts[$moduleName] = $ext === 'module' ? 1 : 2;// @todo next, remove the 'file' property from verbose module info since it is redundant$requires = array();$name = $moduleName;$moduleName = $this->loadModule($path, $pathname, $requires, $installed);if(!$config->paths->get($name)) $this->setConfigPaths($name, dirname($basePath . $pathname));if(!$moduleName) continue;if(count($requires)) {// module not loaded because it required other module(s) not yet loadedforeach($requires as $requiresModuleName) {if(!isset($modulesRequired[$requiresModuleName])) $modulesRequired[$requiresModuleName] = array();if(!isset($modulesDelayed[$moduleName])) $modulesDelayed[$moduleName] = array();// queue module for later load$modulesRequired[$requiresModuleName][$moduleName] = $pathname;$modulesDelayed[$moduleName][] = $requiresModuleName;}continue;}// module was successfully loaded$modulesLoaded[$moduleName] = 1;$loadedNames = array($moduleName);// now determine if this module had any other modules waiting on it as a dependency/** @noinspection PhpAssignmentInConditionInspection */while($moduleName = array_shift($loadedNames)) {// iternate through delayed modules that require this oneif(empty($modulesRequired[$moduleName])) continue;foreach($modulesRequired[$moduleName] as $delayedName => $delayedPathName) {$loadNow = true;if(isset($modulesDelayed[$delayedName])) {foreach($modulesDelayed[$delayedName] as $requiresModuleName) {if(!isset($modulesLoaded[$requiresModuleName])) {$loadNow = false;}}}if(!$loadNow) continue;// all conditions satisified to load delayed moduleunset($modulesDelayed[$delayedName], $modulesRequired[$moduleName][$delayedName]);$unused = array();$loadedName = $this->loadModule($path, $delayedPathName, $unused, $installed);if(!$loadedName) continue;$modulesLoaded[$loadedName] = 1;$loadedNames[] = $loadedName;}}}if(count($modulesDelayed)) {foreach($modulesDelayed as $moduleName => $requiredNames) {$this->error("Module '$moduleName' dependency not fulfilled for: " . implode(', ', $requiredNames), Notice::debug);}}if($this->debug) $this->debugTimerStop($debugKey);}/*** Load a module into memory (companion to load bootstrap method)** @param string $basepath Base path of modules being processed (path provided to the load method)* @param string $pathname* @param array $requires This method will populate this array with required dependencies (class names) if present.* @param array $installed Array of installed modules info, indexed by module class name* @return string Returns module name (classname)**/protected function loadModule($basepath, $pathname, array &$requires, array &$installed) {$pathname = $basepath . $pathname;$dirname = dirname($pathname);$filename = basename($pathname);$basename = basename($filename, '.php');$basename = basename($basename, '.module');$requires = array();$duplicates = $this->duplicates();$moduleInfo = null;// check if module has duplicate files, where one to use has already been specified to use first$currentFile = $duplicates->getCurrent($basename); // returns the current file in use, if more than oneif($currentFile) {// there is a duplicate file in use$file = rtrim($this->wire('config')->paths->root, '/') . $currentFile;if(file_exists($file) && $pathname != $file) {// file in use is different from the file we are looking at// check if this is a new/yet unknown duplicateif(!$duplicates->hasDuplicate($basename, $pathname)) {// new duplicate$duplicates->recordDuplicate($basename, $pathname, $file, $installed);}return '';}}// check if module has already been loaded, or maybe we've got duplicatesif(wireClassExists($basename, false)) {$module = parent::get($basename);$dir = rtrim((string) $this->wire()->config->paths->$basename, '/');if($module && $dir && $dirname != $dir) {$duplicates->recordDuplicate($basename, $pathname, "$dir/$filename", $installed);return '';}if($module) return $basename;}// if the filename doesn't end with .module or .module.php, then stop and move onto the nextif(!strpos($filename, '.module') || (substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php')) return false;// if the filename doesn't start with the requested path, then continueif(strpos($pathname, $basepath) !== 0) return '';// if the file isn't there, it was probably uninstalled, so ignore itif(!file_exists($pathname)) return '';// if the module isn't installed, then stop and move on to nextif(!array_key_exists($basename, $installed)) {$this->installable[$basename] = $pathname;return '';}$info = $installed[$basename];$this->setConfigPaths($basename, $dirname);$module = null;$autoload = false;if($info['flags'] & self::flagsAutoload) {// this is an Autoload module.// include the module and instantiate it but don't init() it,// because it will be done by Modules::init()$moduleInfo = $this->getModuleInfo($basename);// determine if module has dependencies that are not yet metif(count($moduleInfo['requires'])) {foreach($moduleInfo['requires'] as $requiresClass) {$nsRequiresClass = $this->getModuleClass($requiresClass, true);if(!wireClassExists($nsRequiresClass, false)) {$requiresInfo = $this->getModuleInfo($requiresClass);if(!empty($requiresInfo['error'])|| $requiresInfo['autoload'] === true|| !$this->isInstalled($requiresClass)) {// we only handle autoload===true since load() only instantiates other autoload===true modules$requires[] = $requiresClass;}}}if(count($requires)) {// module has unmet requirementsreturn $basename;}}// if not defined in getModuleInfo, then we'll accept the database flag as enough proof// since the module may have defined it via an isAutoload() functionif(!isset($moduleInfo['autoload'])) $moduleInfo['autoload'] = true;/** @var bool|string|callable $autoload */$autoload = $moduleInfo['autoload'];if($autoload === 'function') {// function is stored by the moduleInfo cache to indicate we need to call a dynamic function specified with the module itself$i = $this->getModuleInfoExternal($basename);if(empty($i)) {$this->includeModuleFile($pathname, $basename);$className = $moduleInfo['namespace'] . $basename;if(method_exists($className, 'getModuleInfo')) {$i = $className::getModuleInfo();} else {$i = array();}}$autoload = isset($i['autoload']) ? $i['autoload'] : true;unset($i);}// check for conditional autoloadif(!is_bool($autoload) && (is_string($autoload) || is_callable($autoload)) && !($info['flags'] & self::flagsDisabled)) {// anonymous function or selector string$this->conditionalAutoloadModules[$basename] = $autoload;$this->moduleIDs[$basename] = $info['id'];$autoload = true;} else if($autoload) {$this->includeModuleFile($pathname, $basename);if(!($info['flags'] & self::flagsDisabled)) {$module = null;if($this->refreshing) {$module = parent::get($basename);} else if(isset($this->autoloadOrders[$basename]) && $this->autoloadOrders[$basename] >= 10000) {$module = parent::get($basename); // preloaded module}if(!$module) $module = $this->newModule($basename);}}}if(is_null($module)) {// placeholder for a module, which is not yet included and instantiatedif(!$moduleInfo) $moduleInfo = $this->getModuleInfo($basename);$module = $this->newModulePlaceholder($basename, $moduleInfo['namespace'], $pathname, $info['flags'] & self::flagsSingular, $autoload);}$this->moduleIDs[$basename] = $info['id'];$this->set($basename, $module);return $basename;}/*** Find new module files in the given $path** If $readCache is true, this will perform the find from the cache** @param string $path Path to the modules* @param bool $readCache Optional. If set to true, then this method will attempt to read modules from the cache.* @param int $level For internal recursive use.* @return array Array of module files**/protected function findModuleFiles($path, $readCache = false, $level = 0) {static $startPath;static $callNum = 0;static $prependFiles = array();$callNum++;$config = $this->wire('config');$cache = $this->wire('cache');$cacheName = '';if($level == 0) {$startPath = $path;$cacheName = "Modules." . str_replace($config->paths->root, '', $path);if($readCache && $cache) {$cacheContents = $cache->get($cacheName);if($cacheContents !== null) {if(empty($cacheContents) && $callNum === 1) {// don't accept empty cache for first path (/wire/modules/)} else {$cacheContents = explode("\n", trim($cacheContents));return $cacheContents;}}}}$files = array();$autoloadOrders = null;if(count($this->autoloadOrders) && $path !== $config->paths->modules) {$autoloadOrders = &$this->autoloadOrders;}try {$dir = new \DirectoryIterator($path);} catch(\Exception $e) {$this->trackException($e, false, true);$dir = null;}if($dir) foreach($dir as $file) {if($file->isDot()) continue;$filename = $file->getFilename();$pathname = $file->getPathname();if(DIRECTORY_SEPARATOR != '/') {$pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);}if(strpos($pathname, '/.') !== false) {$pos = strrpos(rtrim($pathname, '/'), '/');if($pathname[$pos+1] == '.') continue; // skip hidden files and dirs}// if it's a directory with a .module file in it named the same as the dir, then descend into itif($file->isDir() && ($level < 1 || (is_file("$pathname/$filename.module") || is_file("$pathname/$filename.module.php")))) {$files = array_merge($files, $this->findModuleFiles($pathname, false, $level + 1));}// if the filename doesn't end with .module or .module.php, then stop and move onto the next$extension = $file->getExtension();if($extension !== 'module' && $extension !== 'php') continue;list($moduleName, $extension) = explode('.', $filename, 2);if($extension !== 'module' && $extension !== 'module.php') continue;$pathname = str_replace($startPath, '', $pathname);if($autoloadOrders !== null && isset($autoloadOrders[$moduleName])) {$prependFiles[$pathname] = $autoloadOrders[$moduleName];} else {$files[] = $pathname;}}if($level == 0 && $dir !== null) {if(!empty($prependFiles)) {// one or more non-core modules must be loaded first in a specific orderarsort($prependFiles);$files = array_merge(array_keys($prependFiles), $files);$prependFiles = array();}if($cache && $cacheName) {$cache->save($cacheName, implode("\n", $files), WireCache::expireReserved);}}return $files;}/*** Setup entries in config->urls and config->paths for the given module** @param string $moduleName* @param string $path**/protected function setConfigPaths($moduleName, $path) {$config = $this->wire('config');$rootPath = $config->paths->root;if(strpos($path, $rootPath) === 0) {// if root path included, strip it out$path = substr($path, strlen($config->paths->root));}$path = rtrim($path, '/') . '/';$config->paths->set($moduleName, $path);$config->urls->set($moduleName, $path);}/*** Get the requested Module** - If the module is not installed, but is installable, it will be installed, instantiated, and initialized.* If you don't want that behavior, call `$modules->isInstalled('ModuleName')` as a conditional first.* - You can also get/load a module by accessing it directly, like `$modules->ModuleName`.* - To get a module with additional options, use `$modules->getModule($name, $options)` instead.** ~~~~~* // Get the MarkupAdminDataTable module* $table = $modules->get('MarkupAdminDataTable');** // You can also do this* $table = $modules->MarkupAdminDataTable;* ~~~~~** @param string|int $key Module name (also accepts database ID)* @return Module|_Module|null Returns a Module or null if not found* @throws WirePermissionException If module requires a particular permission the user does not have* @see Modules::getModule(), Modules::isInstalled()**/public function get($key) {// If the module is a ModulePlaceholder, then it will be converted to the real module (included, instantiated, initialized).return $this->getModule($key);}/*** Attempt to find a substitute for moduleName and return module if found or null if not** @param $moduleName* @param array $options See getModule() options* @return Module|null**/protected function getSubstituteModule($moduleName, array $options = array()) {$module = null;$options['noSubstitute'] = true; // prevent recursionwhile(isset($this->substitutes[$moduleName]) && !$module) {$substituteName = $this->substitutes[$moduleName];$module = $this->getModule($substituteName, $options);if(!$module) $moduleName = $substituteName;}return $module;}/*** Get the requested Module (with options)** This is the same as `$modules->get()` except that you can specify additional options to modify default behavior.* These are the options you can specify in the `$options` array argument:** - `noPermissionCheck` (bool): Specify true to disable module permission checks (and resulting exception). (default=false)* - `noInstall` (bool): Specify true to prevent a non-installed module from installing from this request. (default=false)* - `noInit` (bool): Specify true to prevent the module from being initialized or configured. (default=false). See `configOnly` as alternative.* - `noSubstitute` (bool): Specify true to prevent inclusion of a substitute module. (default=false)* - `noCache` (bool): Specify true to prevent module instance from being cached for later getModule() calls. (default=false)* - `noThrow` (bool): Specify true to prevent exceptions from being thrown on permission or fatal error. (default=false)* - `returnError` (bool): Return an error message (string) on error, rather than null. (default=false)* - `configOnly` (bool): Populate module config data but do not call its init() method. (default=false) 3.0.169+. Alternative to `noInit`.* - `configData` (array): Associative array of additional config data to populate to module. (default=[]) 3.0.169+** If the module is not installed, but is installable, it will be installed, instantiated, and initialized.* If you don't want that behavior, call `$modules->isInstalled('ModuleName')` as a condition first, OR specify* true for the `noInstall` option in the `$options` argument.** @param string|int $key Module name or database ID.* @param array $options Optional settings to change load behavior, see method description for details.* @return Module|_Module|null|string Returns ready-to-use module or NULL|string if not found (string if `returnError` option used).* @throws WirePermissionException|\Exception If module requires a particular permission the user does not have* @see Modules::get()**/public function getModule($key, array $options = array()) {$module = null;$needsInit = false;$noInit = !empty($options['noInit']); // force cancel of Module::init() call?$initOptions = array(); // options for initModule() call$find = false; // try to find new location of module file?$error = '';if(empty($key)) {return empty($options['returnError']) ? null : "No module specified";}// check for optional module ID and convert to classname if foundif(ctype_digit("$key")) {$moduleID = (int) $key;if(!$key = array_search($key, $this->moduleIDs)) {return empty($options['returnError']) ? null : "Unable to find module ID $moduleID";}} else {$moduleID = 0;$key = wireClassName($key, false);}$module = parent::get($key);if(!$module && !$moduleID) {// make non case-sensitive for module name ($key)$lowerKey = strtolower($key);foreach(array_keys($this->moduleIDs) as $className) {if(strtolower($className) !== $lowerKey) continue;$module = parent::get($className);break;}}if(!$module) {if(empty($options['noSubstitute'])) {if($this->isInstallable($key) && empty($options['noInstall'])) {// module is on file system and may be installed, no need to substitute} else {$module = $this->getSubstituteModule($key, $options);if($module) return $module; // returned module is ready to use}} else {$error = "Module '$key' not found and substitute not allowed (noSubstitute=true)";}}if($module) {// check if it's a placeholder, and if it is then include/instantiate/init the real module// OR check if it's non-singular, so that a new instance is createdif($module instanceof ModulePlaceholder || !$this->isSingular($module)) {$placeholder = $module;$class = $this->getModuleClass($placeholder);try {if($module instanceof ModulePlaceholder) $this->includeModule($module);$module = $this->newModule($class);} catch(\Exception $e) {if(empty($options['noThrow'])) throw $e;return empty($options['returnError']) ? null : "Module '$key' - " . $e->getMessage();}// if singular, save the instance so it can be used in later callsif($module && $this->isSingular($module) && empty($options['noCache'])) $this->set($key, $module);$needsInit = true;}} else if(empty($options['noInstall'])) {// module was not available to get, see if we can install itif(array_key_exists($key, $this->getInstallable())) {// check if the request is for an uninstalled module// if so, install it and return ittry {$module = $this->install($key);} catch(\Exception $e) {if(empty($options['noThrow'])) throw $e;if(!empty($options['returnError'])) return "Module '$key' install failed: " . $e->getMessage();}$needsInit = true;if(!$module) $error = "Module '$key' not installed and install failed";} else {$error = "Module '$key' is not present or listed as installable";$find = true;}} else {$error = "Module '$key' is not present and not installable (noInstall=true)";$find = true;}if(!$module && $find) {// This is reached if module has moved elsewhere in file system, like from:// site/modules/ModuleName.module to site/modules/ModuleName/ModuleName.module// Code below tries to find the file to keep it working, but modules need Refresh.try {if($this->includeModule($key)) {$module = $this->newModule($key);}} catch(\Exception $e) {if(empty($options['noThrow'])) throw $e;$error .= ($error ? " - " : "Module '$key' - ") . $e->getMessage();return empty($options['returnError']) ? null : $error;}}if(!$module) {if(!$error) $error = "Unable to get module '$key'";return empty($options['returnError']) ? null : $error;}if(empty($options['noPermissionCheck'])) {// check that user has permission required to use moduleif(!$this->hasPermission($module, $this->wire('user'), $this->wire('page'))) {$error = $this->_('You do not have permission to execute this module') . ' - ' . wireClassName($module);if(empty($options['noThrow'])) throw new WirePermissionException($error);return empty($options['returnError']) ? null : $error;}}if($needsInit && $noInit) {// forced cancel of init() call$needsInit = false;}if(!$needsInit && (!empty($options['configData']) || !empty($options['configOnly']))) {// if config data was supplied in options then we have to init()$needsInit = true;if(!empty($options['configData'])) $initOptions['configData'] = $options['configData'];// if forced noInit then tell initModule() to only config and not call Module::init()if($noInit || !empty($options['configOnly'])) $initOptions['configOnly'] = true;}// skip autoload modules because they have already been initialized in the load() method// unless they were just installed, in which case we need do init nowif($needsInit) {// if the module is configurable, then load its config data// and set values for each before initializing the module$initOptions['clearSettings'] = false;$initOptions['throw'] = true;try {if(!$this->initModule($module, $initOptions)) {return empty($options['returnError']) ? null : "Module '$module' failed init";}} catch(\Exception $e) {if(empty($options['noThrow'])) throw $e;return empty($options['returnError']) ? null : "Module '$module' throw Exception on init - " . $e->getMessage();}}return $module;}/*** Check if user has permission for given module** #pw-internal** @param string|object $moduleName Module instance or module name* @param User $user Optionally specify different user to consider than current.* @param Page $page Optionally specify different page to consider than current.* @param bool $strict If module specifies no permission settings, assume no permission.* - Default (false) is to assume permission when module doesn't say anything about it.* - Process modules (for instance) generally assume no permission when it isn't specifically defined* (though this method doesn't get involved in that, leaving you to specify $strict instead).** @return bool**/public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {if(is_object($moduleName)) {$module = $moduleName;$className = $module->className(true);$moduleName = $module->className(false);} else {$module = null;// $className = wireClassName($moduleName, true);$className = $this->getModuleClass($moduleName, true); // ???$moduleName = wireClassName($moduleName, false);}$info = $this->getModuleInfo($module ? $module : $moduleName);if(empty($info['permission']) && empty($info['permissionMethod'])) return ($strict ? false : true);if(is_null($user)) $user = $this->wire('user');if($user && $user->isSuperuser()) return true;if(!empty($info['permission'])) {if(!$user->hasPermission($info['permission'])) return false;}if(!empty($info['permissionMethod'])) {// module specifies a static method to call for permissionif(is_null($page)) $page = $this->wire('page');$data = array('wire' => $this->wire(),'page' => $page,'user' => $user,'info' => $info,);$method = $info['permissionMethod'];$this->includeModule($moduleName);if(method_exists($className, $method)) return $className::$method($data);return false;}return true;}/*** Get the requested module and reset cache + install it if necessary.** This is exactly the same as get() except that this one will rebuild the modules cache if* it doesn't find the module at first. If the module is on the file system, this* one will return it in some instances that a regular get() can't.** #pw-internal** @param string|int $key Module className or database ID* @return Module|null**/public function getInstall($key) {$module = $this->get($key);if(!$module) {$this->refresh();$module = $this->getModule($key);}return $module;}/*** Include the file for a given module, but don't instantiate it** #pw-internal** @param ModulePlaceholder|Module|string Expects a ModulePlaceholder or className* @param string $file Optionally specify the module filename if you already know it* @return bool true on success, false on fail or unknown**/public function includeModule($module, $file = '') {$className = '';$moduleName = '';if(is_string($module)) {$moduleName = ctype_alnum($module) ? $module : wireClassName($module);$className = wireClassName($module, true);} else if(is_object($module)) {if($module instanceof ModulePlaceholder) {$moduleName = $module->className();$className = $module->className(true);} else if($module instanceof Module) {return true; // already included}} else {$moduleName = $this->getModuleClass($module, false);$className = $this->getModuleClass($module, true);}if(!$className) return false;// already includedif(class_exists($className, false)) return true;// attempt to retrieve module$module = parent::get($moduleName);if($module) {// module found, check to make sure it actually points to a moduleif(!$module instanceof Module) $module = false;} else if($moduleName) {// This is reached for any of the following:// 1. an uninstalled module// 2. an installed module that has changed locations// 3. a module outside the \ProcessWire\ namespace// 4. a module that does not exist$fast = true;if(!$file) {// determine module file, if not already provided to the method$file = $this->getModuleFile($moduleName, array('fast' => true));if(!$file) {$fast = false;$file = $this->getModuleFile($moduleName, array('fast' => false));}// still can't figure out what file is? failif(!$file) return false;}if(!$this->includeModuleFile($file, $moduleName)) {// module file failed to include(), try to identify and include file againif($fast) {$filePrev = $file;$file = $this->getModuleFile($moduleName, array('fast' => false));if($file && $file !== $filePrev) {$this->includeModuleFile($file, $moduleName);}} else {// we already tried this earlier, no point in doing it again}}// now check to see if included file resulted in presence of module classif(class_exists($className)) {// module in ProcessWire namespace$module = true;} else {// module in root namespace or some other namespace$namespace = $this->getModuleNamespace($moduleName, array('file' => $file));$className = trim($namespace, "\\") . "\\$moduleName";if(class_exists($className, false)) {// successful include module$module = true;}}}if($module === true) {// greatreturn true;} else if(!$module) {// darnreturn false;} else if($module instanceof ModulePlaceholder) {$this->includeModuleFile($module->file, $moduleName);return true;} else if($module instanceof Module) {// it's already been included, since we have a real modulereturn true;} else {return false;}}/*** Include the given filename** @param string $file* @param string $moduleName* @return bool**/protected function includeModuleFile($file, $moduleName) {$wire1 = ProcessWire::getCurrentInstance();$wire2 = $this->wire();// check if there is more than one PW instance activeif($wire1 !== $wire2) {// multi-instance is active, don't autoload module if class already exists// first do a fast check, which should catch any core modulesif(class_exists(__NAMESPACE__ . "\\$moduleName", false)) return true;// next do a slower check, figuring out namespace$ns = $this->getModuleNamespace($moduleName, array('file' => $file));$className = trim($ns, "\\") . "\\$moduleName";if(class_exists($className, false)) return true;// if this point is reached, module is not yet in memory in either instance// temporarily set the $wire instance to 2nd instance during include()ProcessWire::setCurrentInstance($wire2);}// get compiled version (if it needs compilation)$file = $this->compile($moduleName, $file);if($file) {/** @noinspection PhpIncludeInspection */$success = @include_once($file);} else {$success = false;}// set instance back, if multi-instanceif($wire1 !== $wire2) ProcessWire::setCurrentInstance($wire1);return (bool) $success;}/*** Find modules based on a selector string** #pw-internal Almost always recommend using findByPrefix() instead** @param string $selector Selector string* @return WireArray of found modules, instantiated and ready-to-use**/public function find($selector) {// ensures any ModulePlaceholders are loaded in the returned result.$a = parent::find($selector);if($a) {foreach($a as $key => $value) {$a[$key] = $this->get($value->className());}}return $a;}/*** Find modules matching the given prefix (i.e. “Inputfield”)** By default this method returns module class names matching the given prefix.* To instead retrieve instantiated (ready-to-use) modules, specify boolean true* for the second argument. Regardless of `$load` argument all returned arrays* are indexed by module name.** ~~~~~* // Retrieve array of all Textformatter module names* $items = $modules->findByPrefix('Textformatter');** // Retrieve array of all Textformatter modules (ready to use)* $items = $modules->findByPrefix('Textformatter', true);* ~~~~~** @param string $prefix Specify prefix, i.e. "Process", "Fieldtype", "Inputfield", etc.* @param bool|int $load Specify one of the following (all indexed by module name):* - Boolean true to return array of instantiated modules.* - Boolean false to return array of module names (default).* - Integer 1 to return array of module info for each matching module.* - Integer 2 to return array of verbose module info for each matching module.* - Integer 3 to return array of Module or ModulePlaceholder objects (whatever current state is). Added 3.0.146.* @return array Returns array of module class names, module info arrays, or Module objects. In all cases, array indexes are class names.**/public function findByPrefix($prefix, $load = false) {$results = array();foreach($this as $moduleName => $value) {if(stripos($moduleName, $prefix) !== 0) continue;if($load === false) {$results[$moduleName] = $moduleName;} else if($load === true) {$results[$moduleName] = $this->getModule($moduleName);} else if($load === 1) {$results[$moduleName] = $this->getModuleInfo($moduleName);} else if($load === 2) {$results[$moduleName] = $this->getModuleInfoVerbose($moduleName);} else if($load === 3) {$results[$moduleName] = $value;} else {$results[$moduleName] = $moduleName;}}ksort($results);return $results;}/*** Find modules by matching a property or properties in their module info** @param string|array $selector Specify one of the following:* - Selector string to match module info.* - Array of [ 'property' => 'value' ] to match in module info (this is not a selector array).* - Name of property that will match module if not empty in module info.* @param bool|int $load Specify one of the following:* - Boolean true to return array of instantiated modules.* - Boolean false to return array of module names (default).* - Integer 1 to return array of module info for each matching module.* - Integer 2 to return array of verbose module info for each matching module.* @return array Array of modules, module names or module info arrays, indexed by module name.**/public function findByInfo($selector, $load = false) {$selectors = null;$infos = null;$keys = null;$results = array();$verbose = $load === 2;$properties = array();$has = '';if(is_array($selector)) {// find matching all values in array$keys = $selector;$properties = array_keys($keys);} if(!ctype_alnum($selector) && Selectors::stringHasOperator($selector)) {// find by selectors$selectors = new Selectors($selector);if(!$verbose) foreach($selectors as $s) {$properties = array_merge($properties, $s->fields());}} else {// find non-empty property$has = $selector;$properties[] = $has;}// check if any verbose properties are part of the findif(!$verbose) foreach($properties as $property) {if(!in_array($property, $this->moduleInfoVerboseKeys)) continue;$verbose = true;break;}$moduleInfoOptions = array('verbose' => $verbose,'minify' => false);foreach($this->getModuleInfo('*', $moduleInfoOptions) as $info) {$isMatch = false;if($has) {// simply check if property is non-emptyif(!empty($info[$has])) $isMatch = true;} else if($selectors) {// match selector$total = 0;$n = 0;foreach($selectors as $selector) {$total++;$values = array();foreach($selector->fields() as $property) {if(isset($info[$property])) $values[] = $info[$property];}if($selector->matches($values)) $n++;}if($n && $n === $total) $isMatch = true;} else if($keys) {// match all values in $keys array$n = 0;foreach($keys as $key => $value) {if($value === '*') {// match any non-empty valueif(!empty($info[$key])) $n++;} else {// match exact valueif(isset($info[$key]) && $value == $info[$key]) $n++;}}if($n && $n === count($keys)) $isMatch = true;}if($isMatch) {$moduleName = $info['name'];if(is_int($load)) {$results[$moduleName] = $info;} else if($load === true) {$results[$moduleName] = $this->getModule($moduleName);} else {$results[$moduleName] = $moduleName;}}}return $results;}/*** Get an associative array [name => path] for all modules that aren’t currently installed.** #pw-internal** @return array Array of elements with $moduleName => $pathName**/public function getInstallable() {return $this->installable;}/*** Is the given module name installed?** @param string $class Just a module class name, or optionally: `ModuleClassName>=1.2.3` (operator and version)* @return bool True if installed, false if not**/public function isInstalled($class) {if(is_object($class)) $class = $this->getModuleClass($class);$operator = null;$requiredVersion = null;$currentVersion = null;if(!ctype_alnum($class)) {// class has something other than just a classname, likely operator + versionif(preg_match('/^([a-zA-Z0-9_]+)\s*([<>=!]+)\s*([\d.]+)$/', $class, $matches)) {$class = $matches[1];$operator = $matches[2];$requiredVersion = $matches[3];}}if($class === 'PHP' || $class === 'ProcessWire') {$installed = true;if(!is_null($requiredVersion)) {$currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire('config')->version;}} else {$installed = parent::get($class) !== null;if($installed && !is_null($requiredVersion)) {$info = $this->getModuleInfo($class);$currentVersion = $info['version'];}}if($installed && !is_null($currentVersion)) {$installed = $this->versionCompare($currentVersion, $requiredVersion, $operator);}return $installed;}/*** Is the given module name installable? (i.e. not already installed)** #pw-internal** @param string $class Module class name* @param bool $now Is module installable RIGHT NOW? This makes it check that all dependencies are already fulfilled (default=false)* @return bool True if module is installable, false if not**/public function isInstallable($class, $now = false) {$installable = array_key_exists($class, $this->installable);if(!$installable) return false;if(!wireInstanceOf($class, 'Module')) {$nsClass = $this->getModuleClass($class, true);if(!wireInstanceOf($nsClass, 'ProcessWire\\Module')) return false;}if($now) {$requires = $this->getRequiresForInstall($class);if(count($requires)) return false;}return $installable;}/*** Install the given module name** #pw-group-manipulation** @param string $class Module name (class name)* @param array|bool $options Optional associative array that can contain any of the following:* - `dependencies` (boolean): When true, dependencies will also be installed where possible. Specify false to prevent installation of uninstalled modules. (default=true)* - `resetCache` (boolean): When true, module caches will be reset after installation. (default=true)* - `force` (boolean): Force installation, even if dependencies can't be met.* @return null|Module Returns null if unable to install, or ready-to-use Module object if successfully installed.* @throws WireException**/public function ___install($class, $options = array()) {$defaults = array('dependencies' => true,'resetCache' => true,'force' => false,);if(is_bool($options)) {// dependencies argument allowed instead of $options, for backwards compatibility$dependencies = $options;$options = array('dependencies' => $dependencies);}$options = array_merge($defaults, $options);$dependencyOptions = $options;$dependencyOptions['resetCache'] = false;if(!$this->isInstallable($class)) return null;$requires = $this->getRequiresForInstall($class);if(count($requires)) {$error = '';$installable = false;if($options['dependencies']) {$installable = true;foreach($requires as $requiresModule) {if(!$this->isInstallable($requiresModule)) $installable = false;}if($installable) {foreach($requires as $requiresModule) {if(!$this->install($requiresModule, $dependencyOptions)) {$error = $this->_('Unable to install required module') . " - $requiresModule. ";$installable = false;break;}}}}if(!$installable) {$error = sprintf($this->_('Module %s requires: %s'), $class, implode(', ', $requires)) . ' ' . $error;if($options['force']) {$this->warning($this->_('Warning!') . ' ' . $error);} else {throw new WireException($error);}}}$languages = $this->wire('languages');if($languages) $languages->setDefault();$pathname = $this->installable[$class];$this->includeModuleFile($pathname, $class);$this->setConfigPaths($class, dirname($pathname));$module = $this->newModule($class);if(!$module) return null;$flags = 0;$database = $this->wire('database');$moduleID = 0;if($this->isSingular($module)) $flags = $flags | self::flagsSingular;if($this->isAutoload($module)) $flags = $flags | self::flagsAutoload;$sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";if($this->wire('config')->systemVersion >=7) $sql .= ", created=NOW()";$query = $database->prepare($sql, "modules.install($class)");$query->bindValue(":class", $class, \PDO::PARAM_STR);$query->bindValue(":flags", $flags, \PDO::PARAM_INT);try {if($query->execute()) $moduleID = (int) $database->lastInsertId();} catch(\Exception $e) {if($languages) $languages->unsetDefault();$this->trackException($e, false, true);return null;}$this->moduleIDs[$class] = $moduleID;$this->add($module);unset($this->installable[$class]);// note: the module's install is called here because it may need to know its module ID for installation of permissions, etc.if(method_exists($module, '___install') || method_exists($module, 'install')) {try {/** @var _Module $module */$module->install();} catch(\PDOException $e) {$error = $this->_('Module reported error during install') . " ($class): " . $e->getMessage();$this->error($error);$this->trackException($e, false, $error);} catch(\Exception $e) {// remove the module from the modules table if the install failed$moduleID = (int) $moduleID;$error = $this->_('Unable to install module') . " ($class): " . $e->getMessage();$ee = null;try {$query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1'); // QA$query->bindValue(":id", $moduleID, \PDO::PARAM_INT);$query->execute();} catch(\Exception $ee) {$this->trackException($e, false, $error)->trackException($ee, true);}if($languages) $languages->unsetDefault();if(is_null($ee)) $this->trackException($e, false, $error);return null;}}$info = $this->getModuleInfoVerbose($class, array('noCache' => true));// if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install themforeach($info['permissions'] as $name => $title) {$name = $this->wire('sanitizer')->pageName($name);if(ctype_digit("$name") || empty($name)) continue; // permission name not valid$permission = $this->wire('permissions')->get($name);if($permission->id) continue; // permision already theretry {$permission = $this->wire('permissions')->add($name);$permission->title = $title;$this->wire('permissions')->save($permission);if($languages) $languages->unsetDefault();$this->message(sprintf($this->_('Added Permission: %s'), $permission->name));} catch(\Exception $e) {if($languages) $languages->unsetDefault();$error = sprintf($this->_('Error adding permission: %s'), $name);$this->trackException($e, false, $error);}}// check if there are any modules in 'installs' that this module didn't handle installation of, and install them$label = $this->_('Module Auto Install');foreach($info['installs'] as $name) {if(!$this->isInstalled($name)) {try {$this->install($name, $dependencyOptions);$this->message("$label: $name");} catch(\Exception $e) {$error = "$label: $name - " . $e->getMessage();$this->trackException($e, false, $error);}}}$this->log("Installed module '$module'");if($languages) $languages->unsetDefault();if($options['resetCache']) $this->clearModuleInfoCache();return $module;}/*** Returns whether the module can be uninstalled** #pw-internal** @param string|Module $class* @param bool $returnReason If true, the reason why it can't be uninstalled with be returned rather than boolean false.* @return bool|string**/public function ___isUninstallable($class, $returnReason = false) {$reason = '';$reason1 = $this->_("Module is not already installed");$namespace = $this->getModuleNamespace($class);$class = $this->getModuleClass($class);if(!$this->isInstalled($class)) {$reason = $reason1 . ' (a)';} else {$this->includeModule($class);if(!wireClassExists($namespace . $class, false)) {$reason = $reason1 . " (b: $namespace$class)";}}if(!$reason) {// if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable$info = $this->getModuleInfo($class);if(!empty($info['permanent'])) {$reason = $this->_("Module is permanent");} else {$dependents = $this->getRequiresForUninstall($class);if(count($dependents)) $reason = $this->_("Module is required by other modules that must be removed first");}if(!$reason && in_array('Fieldtype', wireClassParents($namespace . $class))) {foreach($this->wire('fields') as $field) {$fieldtype = wireClassName($field->type, false);if($fieldtype == $class) {$reason = $this->_("This module is a Fieldtype currently in use by one or more fields");break;}}}}if($returnReason && $reason) return $reason;return $reason ? false : true;}/*** Returns whether the module can be deleted (have it's files physically removed)** #pw-internal** @param string|Module $class* @param bool $returnReason If true, the reason why it can't be removed will be returned rather than boolean false.* @return bool|string**/public function isDeleteable($class, $returnReason = false) {$reason = '';$class = $this->getModuleClass($class);$filename = isset($this->installable[$class]) ? $this->installable[$class] : null;$dirname = dirname($filename);if(empty($filename) || $this->isInstalled($class)) {$reason = "Module must be uninstalled before it can be deleted.";} else if(is_link($filename) || is_link($dirname) || is_link(dirname($dirname))) {$reason = "Module is linked to another location";} else if(!is_file($filename)) {$reason = "Module file does not exist";} else if(strpos($filename, $this->paths[0]) === 0) {$reason = "Core modules may not be deleted.";} else if(!is_writable($filename)) {$reason = "We have no write access to the module file, it must be removed manually.";}if($returnReason && $reason) return $reason;return $reason ? false : true;}/*** Delete the given module, physically removing its files** #pw-group-manipulation** @param string $class Module name (class name)* @return bool|int* @throws WireException If module can't be deleted, exception will be thrown containing reason.**/public function ___delete($class) {$class = $this->getModuleClass($class);$success = true;$reason = $this->isDeleteable($class, true);if($reason !== true) throw new WireException($reason);$siteModulesPath = $this->wire('config')->paths->siteModules;$filename = $this->installable[$class];$basename = basename($filename);/** @var WireFileTools $fileTools */$fileTools = $this->wire('files');// double check that $class is consistent with the actual $basenameif($basename === "$class.module" || $basename === "$class.module.php") {// good, this is consistent with the format we require} else {throw new WireException("Unrecognized module filename format");}// now determine if module is the owner of the directory it exists in// this is the case if the module class name is the same as the directory name$path = dirname($filename); // full path to directory, i.e. .../site/modules/ProcessHello$name = basename($path); // just name of directory that module is, i.e. ProcessHello$parentPath = dirname($path); // full path to parent directory, i.e. ../site/modules$backupPath = $parentPath . "/.$name"; // backup path, in case module is backed up// first check that we are still in the /site/modules/ (or another non core modules path)$inPath = false; // is module somewhere beneath /site/modules/ ?$inRoot = false; // is module in /site/modules/ root? i.e. /site/modules/ModuleName.moduleforeach($this->paths as $key => $modulesPath) {if($key === 0) continue; // skip core modules pathif(strpos("$parentPath/", $modulesPath) === 0) $inPath = true;if($modulesPath === $path) $inRoot = true;}$basename = basename($basename, '.php');$basename = basename($basename, '.module');$files = array("$basename.module","$basename.module.php","$basename.info.php","$basename.info.json","$basename.config.php","{$basename}Config.php",);if($inPath) {// module is in /site/modules/[ModuleName]/$numOtherModules = 0; // num modules in dir other than this one$numLinks = 0; // number of symbolic links$dirs = array("$path/");do {$dir = array_shift($dirs);$this->message("Scanning: $dir", Notice::debug);foreach(new \DirectoryIterator($dir) as $file) {if($file->isDot()) continue;if($file->isLink()) {$numLinks++;continue;}if($file->isDir()) {$dirs[] = $fileTools->unixDirName($file->getPathname());continue;}if(in_array($file->getBasename(), $files)) continue; // skip known filesif(strpos($file->getBasename(), '.module') && preg_match('{(\.module|\.module\.php)$}', $file->getBasename())) {// another module exists in this dir, so we don't want to delete that$numOtherModules++;}if(preg_match('{^(' . $basename . '\.[-_.a-zA-Z0-9]+)$}', $file->getBasename(), $matches)) {// keep track of potentially related files in case we have to delete them individually$files[] = $matches[1];}}} while(count($dirs));if(!$inRoot && !$numOtherModules && !$numLinks) {// the modulePath had no other modules or directories in it, so we can delete it entirely$success = $fileTools->rmdir($path, true);if($success) {$this->message("Removed directory: $path", Notice::debug);if(is_dir($backupPath)) {if($fileTools->rmdir($backupPath, true)) $this->message("Removed directory: $backupPath", Notice::debug);}$files = array();} else {$this->error("Failed to remove directory: $path", Notice::debug);}}}// remove module files individuallyforeach($files as $file) {$file = "$path/$file";if(!file_exists($file)) continue;if($fileTools->unlink($file, $siteModulesPath)) {$this->message("Removed file: $file", Notice::debug);} else {$this->error("Unable to remove file: $file", Notice::debug);}}$this->log("Deleted module '$class'");return $success;}/*** Uninstall the given module name** #pw-group-manipulation** @param string $class Module name (class name)* @return bool* @throws WireException**/public function ___uninstall($class) {$class = $this->getModuleClass($class);$reason = $this->isUninstallable($class, true);if($reason !== true) {// throw new WireException("$class - Can't Uninstall - $reason");return false;}// check if there are any modules still installed that this one says it is responsible for installingforeach($this->getUninstalls($class) as $name) {// catch uninstall exceptions at this point since original module has already been uninstalled$label = $this->_('Module Auto Uninstall');try {$this->uninstall($name);$this->message("$label: $name");} catch(\Exception $e) {$error = "$label: $name - " . $e->getMessage();$this->trackException($e, false, $error);}}$info = $this->getModuleInfoVerbose($class);$module = $this->getModule($class, array('noPermissionCheck' => true,'noInstall' => true,// 'noInit' => true));if(!$module) return false;// remove all hooks attached to this module$hooks = $module instanceof Wire ? $module->getHooks() : array();foreach($hooks as $hook) {if($hook['method'] == 'uninstall') continue;$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);$module->removeHook($hook['id']);}// remove all hooks attached to other ProcessWire objects$hooks = array_merge($this->getHooks('*'), $this->wire('hooks')->getAllLocalHooks());foreach($hooks as $hook) {/** @var Wire $toObject */$toObject = $hook['toObject'];$toClass = wireClassName($toObject, false);$toMethod = $hook['toMethod'];if($class === $toClass && $toMethod != 'uninstall') {$toObject->removeHook($hook['id']);$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);}}if(method_exists($module, '___uninstall') || method_exists($module, 'uninstall')) {// note module's uninstall method may throw an exception to abort the uninstall/** @var _Module $module */$module->uninstall();}$database = $this->wire('database');$query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1'); // QA$query->bindValue(":class", $class, \PDO::PARAM_STR);$query->execute();// add back to the installable listif(class_exists("ReflectionClass")) {$reflector = new \ReflectionClass($this->getModuleClass($module, true));$this->installable[$class] = $reflector->getFileName();}unset($this->moduleIDs[$class]);$this->remove($module);// delete permissions installed by this moduleif(isset($info['permissions']) && is_array($info['permissions'])) {foreach($info['permissions'] as $name => $title) {$name = $this->wire('sanitizer')->pageName($name);if(ctype_digit("$name") || empty($name)) continue;$permission = $this->wire('permissions')->get($name);if(!$permission->id) continue;try {$this->wire('permissions')->delete($permission);$this->message(sprintf($this->_('Deleted Permission: %s'), $name));} catch(\Exception $e) {$error = sprintf($this->_('Error deleting permission: %s'), $name);$this->trackException($e, false, $error);}}}$this->log("Uninstalled module '$class'");$this->refresh();return true;}/*** Get flags for the given module** #pw-internal** @param int|string|Module $class Module to add flag to* @return int|false Returns integer flags on success, or boolean false on fail**/public function getFlags($class) {$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);if(isset($this->moduleFlags[$id])) return $this->moduleFlags[$id];if(!$id) return false;$query = $this->wire('database')->prepare('SELECT flags FROM modules WHERE id=:id');$query->bindValue(':id', $id, \PDO::PARAM_INT);$query->execute();if(!$query->rowCount()) return false;list($flags) = $query->fetch(\PDO::FETCH_NUM);$flags = (int) $flags;$this->moduleFlags[$id] = $flags;return $flags;}/*** Does module have flag?** #pw-internal** @param int|string|Module $class Module ID, class name or instance* @param int $flag* @return bool* @since 3.0.170**/public function hasFlag($class, $flag) {$flags = $this->getFlags($class);return $flags === false ? false : ($flags & $flag);}/*** Set module flags** #pw-internal** @param $class* @param $flags* @return bool**/public function setFlags($class, $flags) {$flags = (int) $flags;$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);if(!$id) return false;if($this->moduleFlags[$id] === $flags) return true;$query = $this->wire('database')->prepare('UPDATE modules SET flags=:flags WHERE id=:id');$query->bindValue(':flags', $flags);$query->bindValue(':id', $id);if($this->debug) $this->message("setFlags(" . $this->getModuleClass($class) . ", " . $this->moduleFlags[$id] . " => $flags)");$this->moduleFlags[$id] = $flags;return $query->execute();}/*** Add or remove a flag from a module** #pw-internal** @param $class int|string|Module $class Module to add flag to* @param $flag int Flag to add (see flags* constants)* @param $add bool $add Specify true to add the flag or false to remove it* @return bool True on success, false on fail**/public function setFlag($class, $flag, $add = true) {$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);if(!$id) return false;$flag = (int) $flag;if(!$flag) return false;$flags = $this->getFlags($id);if($add) {if($flags & $flag) return true; // already has the flag$flags = $flags | $flag;} else {if(!($flags & $flag)) return true; // doesn't already have the flag$flags = $flags & ~$flag;}$this->setFlags($id, $flags);return true;}/*** Return an array of other module class names that are uninstalled when the given one is** #pw-internal** The opposite of this function is found in the getModuleInfo array property 'installs'.* Note that 'installs' and uninstalls may be different, as only modules in the 'installs' list* that indicate 'requires' for the installer module will be uninstalled.** @param $class* @return array**/public function getUninstalls($class) {$uninstalls = array();$class = $this->getModuleClass($class);if(!$class) return $uninstalls;$info = $this->getModuleInfoVerbose($class);// check if there are any modules still installed that this one says it is responsible for installingforeach($info['installs'] as $name) {// if module isn't installed, then greatif(!$this->isInstalled($name)) continue;// if an 'installs' module doesn't indicate that it requires this one, then leave it installed$i = $this->getModuleInfo($name);if(!in_array($class, $i['requires'])) continue;// add it to the uninstalls array$uninstalls[] = $name;}return $uninstalls;}/*** Returns the database ID of a given module class, or 0 if not found** #pw-internal** @param string|Module $class Module or module name* @return int**/public function getModuleID($class) {$id = 0;if(is_object($class)) {if($class instanceof Module) {$class = $this->getModuleClass($class);} else {// Class is not a modulereturn $id;}}if(isset($this->moduleIDs[$class])) {$id = (int) $this->moduleIDs[$class];} else foreach($this->moduleInfoCache as $key => $info) {if($info['name'] == $class) {$id = (int) $key;break;}}return $id;}/*** Returns the module's class name.** - Given a numeric database ID, returns the associated module class name or false if it doesn't exist* - Given a Module or ModulePlaceholder instance, returns the Module's class name.** If the module has a className() method then it uses that rather than PHP's get_class().* This is important because of placeholder modules. For example, get_class would return* 'ModulePlaceholder' rather than the correct className for a Module.** #pw-internal** @param string|int|Module* @param bool $withNamespace Specify true to include the namespace in the class* @return string|bool The Module's class name or false if not found.* Note that 'false' is only possible if you give this method a non-Module, or an integer ID* that doesn't correspond to a module ID.**/public function getModuleClass($module, $withNamespace = false) {$className = '';$namespace = '';if($module instanceof Module) {if(wireMethodExists($module, 'className')) {if($withNamespace) return $module->className(true);return $module->className();} else {return wireClassName($module, $withNamespace);}} else if(is_int($module) || ctype_digit("$module")) {$className = array_search((int) $module, $this->moduleIDs);} else if(is_string($module)) {if(strpos($module, "\\") !== false) {$namespace = wireClassName($module, 1);$className = wireClassName($module, false);}// remove extensions if they were included in the module nameif(strpos($module, '.') !== false) {$module = basename(basename($module, '.php'), '.module');}if(array_key_exists($module, $this->moduleIDs)) {$className = $module;} else if(array_key_exists($module, $this->installable)) {$className = $module;}}if($className) {if($withNamespace) {if($namespace) {$className = "$namespace\\$className";} else {$className = $this->getModuleNamespace($className) . $className;}}return $className;}return false;}/*** Retrieve module info from ModuleName.info.json or ModuleName.info.php** @param string $moduleName* @return array**/protected function getModuleInfoExternal($moduleName) {// if($this->debug) $this->message("getModuleInfoExternal($moduleName)");// ...attempt to load info by info file (Module.info.php or Module.info.json)if(!empty($this->installable[$moduleName])) {$path = dirname($this->installable[$moduleName]) . '/';} 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 automaticallyif(!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**/protected function getModuleInfoInternal($module, $namespace = '') {// if($this->debug) $this->message("getModuleInfoInternal($module)");$info = array();if($module instanceof ModulePlaceholder) {$this->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->includeModule($module);if(is_callable("$className::getModuleInfo")) {$info = call_user_func(array($className, 'getModuleInfo'));}}return $info;}/*** Pull module info directly from the module file's getModuleInfo without letting PHP parse it** Useful for getting module info from modules that extend another module not already on the file system.** @param $className* @return array Only includes module info specified in the module file itself.*protected function getModuleInfoInternalSafe($className) {// future addition// load file, preg_split by /^\s*(public|private|protected)[^;{]+function\s*([^)]*)[^{]*{/// isolate the one that starts has getModuleInfo in matches[1]// parse data from matches[2]return array();}*//*** Retrieve module info for system properties: PHP or ProcessWire** @param $moduleName* @return array**/protected function getModuleInfoSystem($moduleName) {$info = array();if($moduleName === 'PHP') {$info['id'] = 0;$info['name'] = $moduleName;$info['title'] = $moduleName;$info['version'] = PHP_VERSION;return $info;} else if($moduleName === 'ProcessWire') {$info['id'] = 0;$info['name'] = $moduleName;$info['title'] = $moduleName;$info['version'] = $this->wire('config')->version;$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";$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'];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 are present but blank:** - `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 Modules::getModuleInfoVerbose()* @todo move all getModuleInfo methods to their own ModuleInfo class and break this method down further.**/public function getModuleInfo($class, array $options = array()) {$getAll = $class === '*' || $class === 'all';$getSystem = $class === 'ProcessWire' || $class === 'PHP' || $class === 'info';$defaults = array('verbose' => false,'minify' => $getAll,'noCache' => false,'noInclude' => false,);$options = array_merge($defaults, $options);$info = array();$module = $class;$moduleName = '';$moduleID = 0;$fromCache = false; // was the data loaded from cache?if(!$getAll && !$getSystem) {$moduleName = $this->getModuleClass($module);$moduleID = (string) $this->getModuleID($module); // typecast to string for cache}static $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);if($getAll) {if(empty($this->moduleInfoCache)) $this->loadModuleInfoCache();$modulesInfo = $this->moduleInfoCache;if($options['verbose']) {if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();foreach($this->moduleInfoCacheVerbose as $moduleID => $moduleInfoVerbose) {$modulesInfo[$moduleID] = array_merge($modulesInfo[$moduleID], $moduleInfoVerbose);}}if(!$options['minify']) {foreach($modulesInfo as $moduleID => $info) {$modulesInfo[$moduleID] = array_merge($infoTemplate, $info);}}return $modulesInfo;} else if($getSystem) {// module is a systemif($class === 'info') return $infoTemplate;$info = $this->getModuleInfoSystem($module);return $options['minify'] ? $info : array_merge($infoTemplate, $info);} else if($module instanceof Module) {// module is an instance// $moduleName = method_exists($module, 'className') ? $module->className() : get_class($module);// return from cache if availableif(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 IDif(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 moduleif(!count($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(!count($info)) $info = $this->getModuleInfoInternal($moduleName, $namespace);} else {// module is not in memory, check external first, then internal$info = $this->getModuleInfoExternal($moduleName);if(!count($info)) {if(isset($this->installable[$moduleName])) $this->includeModuleFile($this->installable[$moduleName], $moduleName);// info not available externally, attempt to locate it interally$info = $this->getModuleInfoInternal($moduleName, $namespace);}}}}if(!$fromCache && !count($info)) {$info = $infoTemplate;$info['title'] = $module;$info['summary'] = 'Inactive';$info['error'] = 'Unable to locate module';return $info;}if(!$options['minify']) $info = array_merge($infoTemplate, $info);$info['id'] = (int) $moduleID;if($fromCache) {// since cache is loaded at init(), this is the most common scenarioif($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 cacheif(is_null($info['autoload'])) $info['autoload'] = false;if(is_null($info['singular'])) $info['singular'] = false;if(is_null($info['configurable'])) $info['configurable'] = false;if(is_null($info['core'])) $info['core'] = false;if(is_null($info['installed'])) $info['installed'] = true;if(is_null($info['namespace'])) $info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";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 oneif(!is_array($info['requires'])) {$info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespaceif(strpos($info['requires'], ',') !== false) {$info['requires'] = explode(',', $info['requires']);} else {$info['requires'] = array($info['requires']);}}// populate requiresVersionsforeach($info['requires'] as $key => $class) {if(!ctype_alnum($class)) {// has a version stringlist($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 oneif(!is_array($info['installs'])) {$info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespaceif(strpos($info['installs'], ',') !== false) {$info['installs'] = explode(',', $info['installs']);} else {$info['installs'] = array($info['installs']);}}// miscif($options['verbose']) $info['versionStr'] = $this->formatVersion($info['version']); // versionStr$info['name'] = $moduleName; // module name// module configurable?$configurable = $this->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 dateif(isset($this->createdDates[$moduleID])) $info['created'] = strtotime($this->createdDates[$moduleID]);$info['installed'] = isset($this->installable[$moduleName]) ? false : true;if(!$info['installed'] && !$info['created'] && isset($this->installable[$moduleName])) {// uninstalled modules get their created date from the file or dir that they are in (whichever is newer)$pathname = $this->installable[$moduleName];$filemtime = (int) filemtime($pathname);$dirname = dirname($pathname);$dirmtime = substr($dirname, -7) == 'modules' || strpos($dirname, $this->paths[0]) !== false ? 0 : (int) filemtime($dirname);$info['created'] = $dirmtime > $filemtime ? $dirmtime : $filemtime;}// namespaceif($info['core']) {// default namespace, assumed since all core modules are in default namespace$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";} 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(is_null($info['namespace'])) {$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";}if(empty($info['created']) && isset($this->createdDates[$moduleID])) {$info['created'] = strtotime($this->createdDates[$moduleID]);}if($options['verbose']) {// the file property is not stored in the verbose cache, but provided as a verbose key$info['file'] = $this->getModuleFile($moduleName);if($info['file']) $info['core'] = strpos($info['file'], $this->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 removedif(!$options['verbose']) foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]);foreach($info as $key => $value) {if(!array_key_exists($key, $infoTemplate)) continue;if($value !== $infoTemplate[$key]) continue;unset($info[$key]);}}// if($this->debug) $this->message("getModuleInfo($moduleName) " . ($fromCache ? "CACHE" : "NO-CACHE"));return $info;}/*** 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(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;}/*** Get an array of all unique, non-default, non-root module namespaces mapped to directory names** #pw-internal** @return array**/public function getNamespaces() {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']] = $this->wire('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;$options = array_merge($defaults, $options);if(is_object($moduleName) || strpos($moduleName, "\\") !== false) {$className = is_object($moduleName) ? get_class($moduleName) : $moduleName;$parts = explode("\\", $className);array_pop($parts);$namespace = count($parts) ? implode("\\", $parts) : "";$namespace = $namespace == "" ? "\\" : "\\$namespace\\";return $namespace;}if(empty($options['noCache'])) {$moduleID = $this->getModuleID($moduleName);$info = isset($this->moduleInfoCache[$moduleID]) ? $this->moduleInfoCache[$moduleID] : null;if($info && isset($info['namespace'])) {return $info['namespace'];}}if(empty($options['file'])) {$options['file'] = $this->getModuleFile($moduleName);}if(strpos($options['file'], $this->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->getFileNamespace($options['file']);}return $namespace;}/*** Get the namespace used in the given .php or .module file** #pw-internal** @param string $file* @return string Includes leading and trailing backslashes where applicable**/public function getFileNamespace($file) {$namespace = $this->wire()->files->getNamespace($file);if($namespace !== "\\") $namespace = "\\" . trim($namespace, "\\") . "\\";return $namespace;}/*** Get the class defined in the file (or optionally the 'extends' or 'implements')** #pw-internal** @param string $file* @return array Returns array with these indexes:* 'class' => string (class without namespace)* 'className' => string (class with namespace)* 'extends' => string* 'namespace' => string* 'implements' => array**/public function getFileClassInfo($file) {$value = array('class' => '','className' => '','extends' => '','namespace' => '','implements' => array());if(!is_file($file)) return $value;$data = file_get_contents($file);if(!strpos($data, 'class')) return $value;if(!preg_match('/^\s*class\s+(.+)$/m', $data, $matches)) return $value;if(strpos($matches[1], "\t") !== false) $matches[1] = str_replace("\t", " ", $matches[1]);$parts = explode(' ', trim($matches[1]));foreach($parts as $key => $part) {if(empty($part)) unset($parts[$key]);}$className = array_shift($parts);if(strpos($className, '\\') !== false) {$className = trim($className, '\\');$a = explode('\\', $className);$value['className'] = "\\$className\\";$value['class'] = array_pop($a);$value['namespace'] = '\\' . implode('\\', $a) . '\\';} else {$value['className'] = '\\' . $className;$value['class'] = $className;$value['namespace'] = '\\';}while(count($parts)) {$next = array_shift($parts);if($next == 'extends') {$value['extends'] = array_shift($parts);} else if($next == 'implements') {$implements = array_shift($parts);if(strlen($implements)) {$implements = str_replace(' ', '', $implements);$value['implements'] = explode(',', $implements);}}}return $value;}/*** Alias of getConfig() for backwards compatibility** #pw-internal** @param string|Module $className* @return array**/public function getModuleConfigData($className) {return $this->getConfig($className);}/*** Return the URL where the module can be edited, configured or uninstalled** If module is not installed, it just returns the URL to ProcessModule.** #pw-group-configuration** @param string|Module $className* @param bool $collapseInfo* @return string**/public function getModuleEditUrl($className, $collapseInfo = true) {if(!is_string($className)) $className = $this->getModuleClass($className);$url = $this->wire('config')->urls->admin . 'module/';if(empty($className) || !$this->isInstalled($className)) return $url;$url .= "edit/?name=$className";if($collapseInfo) $url .= "&collapse_info=1";return $url;}/*** Given a module name, return an associative array of configuration data for it** - Applicable only for modules that support configuration.* - Configuration data is stored encoded in the database "modules" table "data" field.** ~~~~~~* // Getting, modifying and saving module config data* $data = $modules->getConfig('HelloWorld');* $data['greeting'] = 'Hello World! How are you today?';* $modules->saveConfig('HelloWorld', $data);** // Getting just one property 'apiKey' from module config data* @apiKey = $modules->getConfig('HelloWorld', 'apiKey');* ~~~~~~** #pw-group-configuration* #pw-changelog 3.0.16 Changed from more verbose name `getModuleConfigData()`, which can still be used.** @param string|Module $class* @param string $property Optionally just get value for a specific property (omit to get all config)* @return array|string|int|float Module configuration data, returns array unless a specific $property was requested* @see Modules::saveConfig()* @since 3.0.16 Use method getModuleConfigData() with same arguments for prior versions (can also be used on any version).**/public function getConfig($class, $property = '') {$emptyReturn = $property ? null : array();$className = $class;if(is_object($className)) $className = wireClassName($className->className(), false);if(!isset($this->moduleIDs[$className])) return $emptyReturn;$id = $this->moduleIDs[$className];if(!$id) return $emptyReturn;if(!isset($this->configData[$id])) return $emptyReturn; // module has no config dataif(is_array($this->configData[$id])) {$data = $this->configData[$id];} else {$configable = $this->isConfigable($className);if(!$configable) return $emptyReturn;$database = $this->wire()->database;$query = $database->prepare("SELECT data FROM modules WHERE id=:id", "modules.getConfig($className)"); // QA$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);$query->execute();$data = $query->fetchColumn();$query->closeCursor();if(strlen($data)) $data = wireDecodeJSON($data);if(empty($data)) $data = array();$this->configData[$id] = $data;}if($property) return isset($data[$property]) ? $data[$property] : null;return $data;}/*** Get the path + filename (or optionally URL) for this module** @param string|Module $class Module class name or object instance* @param array|bool $options Options to modify default behavior:* - `getURL` (bool): Specify true if you want to get the URL rather than file path (default=false).* - `fast` (bool): Specify true to omit file_exists() checks (default=false).* - `guess` (bool): Manufacture/guess a module location if one cannot be found (default=false) 3.0.170+* - Note: If you specify a boolean for the $options argument, it is assumed to be the $getURL property.* @return bool|string Returns string of module file, or false on failure.**/public function getModuleFile($class, $options = array()) {$config = $this->wire()->config;$className = $class;if(is_bool($options)) $options = array('getURL' => $options);if(!isset($options['getURL'])) $options['getURL'] = false;if(!isset($options['fast'])) $options['fast'] = false;$file = false;// first see it's an object, and if we can get the file from the objectif(is_object($className)) {$module = $className;if($module instanceof ModulePlaceholder) $file = $module->file;$moduleName = $module->className();$className = $module->className(true);} else {$moduleName = wireClassName($className, false);}$hasDuplicate = $this->duplicates()->hasDuplicate($moduleName);if(!$hasDuplicate) {// see if we can determine it from already stored paths$path = $config->paths->$moduleName;if($path) {$file = $path . $moduleName . ($this->moduleFileExts[$moduleName] === 2 ? '.module.php' : '.module');if(!$options['fast'] && !file_exists($file)) $file = false;}}// next see if we've already got the module filename cached locallyif(!$file && isset($this->installable[$moduleName]) && !$hasDuplicate) {$file = $this->installable[$moduleName];if(!$options['fast'] && !file_exists($file)) $file = false;}if(!$file) {$dupFile = $this->duplicates()->getCurrent($moduleName);if($dupFile) {$rootPath = $config->paths->root;$file = rtrim($rootPath, '/') . $dupFile;if(!file_exists($file)) {// module in use may have been deleted, find the next available one that exists$file = '';$dups = $this->duplicates()->getDuplicates($moduleName);foreach($dups['files'] as $pathname) {$pathname = rtrim($rootPath, '/') . $pathname;if(file_exists($pathname)) $file = $pathname;if($file) break;}}}}if(!$file) {// see if it's a predefined core type that can be determined from the type// this should only come into play if module has moved or had a load errorforeach($this->coreTypes as $typeName) {if(strpos($moduleName, $typeName) !== 0) continue;$checkFiles = array("$typeName/$moduleName/$moduleName.module","$typeName/$moduleName/$moduleName.module.php","$typeName/$moduleName.module","$typeName/$moduleName.module.php",);$path1 = $config->paths->modules;foreach($checkFiles as $checkFile) {$file1 = $path1 . $checkFile;if(file_exists($file1)) $file = $file1;if($file) break;}if($file) break;}if(!$file) {// check site modules$checkFiles = array("$moduleName/$moduleName.module","$moduleName/$moduleName.module.php","$moduleName.module","$moduleName.module.php",);$path1 = $config->paths->siteModules;foreach($checkFiles as $checkFile) {$file1 = $path1 . $checkFile;if(file_exists($file1)) $file = $file1;if($file) break;}}}if(!$file) {// if all the above failed, try to get it from Reflectiontry {// note we don't call getModuleClass() here because it may result in a circular referenceif(strpos($className, "\\") === false) {$moduleID = $this->getModuleID($moduleName);if(!empty($this->moduleInfoCache[$moduleID]['namespace'])) {$className = rtrim($this->moduleInfoCache[$moduleID]['namespace'], "\\") . "\\$moduleName";} else {$className = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\$moduleName" : $moduleName;}}$reflector = new \ReflectionClass($className);$file = $reflector->getFileName();} catch(\Exception $e) {$file = false;}}if(!$file && !empty($options['guess'])) {// make a guess about where module would be if we had been able to find it$file = $config->paths->siteModules . "$moduleName/$moduleName.module";}if($file) {if(DIRECTORY_SEPARATOR != '/') $file = str_replace(DIRECTORY_SEPARATOR, '/', $file);if($options['getURL']) $file = str_replace($config->paths->root, '/', $file);}return $file;}/*** Is the given module interactively configurable?** This method can be used to simply determine if a module is configurable (yes or no), or more specifically* how it is configurable.** ~~~~~* // Determine IF a module is configurable* if($modules->isConfigurable('HelloWorld')) {* // Module is configurable* } else {* // Module is NOT configurable* }* ~~~~~* ~~~~~* // Determine HOW a module is configurable* $configurable = $module->isConfigurable('HelloWorld');* if($configurable === true) {* // configurable in a way compatible with all past versions of ProcessWire* } else if(is_string($configurable)) {* // configurable via an external configuration file* // file is identifed in $configurable variable* } else if(is_int($configurable)) {* // configurable via a method in the class* // the $configurable variable contains a number with specifics* } else {* // module is NOT configurable* }* ~~~~~** ### Return value details** #### If module is configurable via external configuration file:** - Returns string of full path/filename to `ModuleName.config.php` file** #### If module is configurable because it implements a configurable module interface:** - Returns boolean `true` if module is configurable via the static `getModuleConfigInputfields()` method.* This particular method is compatible with all past versions of ProcessWire.* - Returns integer `2` if module is configurable via the non-static `getModuleConfigInputfields()` and requires no arguments.* - Returns integer `3` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `$data` array.* - Returns integer `4` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `InputfieldWrapper` argument.* - Returns integer `19` if module is configurable via non-static `getModuleConfigArray()` method.* - Returns integer `20` if module is configurable via static `getModuleConfigArray()` method.** #### If module is not configurable:** - Returns boolean `false` if not configurable** *This method is named isConfigurableModule() in ProcessWire versions prior to to 3.0.16.*** #pw-group-configuration** @param Module|string $class Module name* @param bool $useCache Use caching? This accepts a few options:* - Specify boolean `true` to allow use of cache when available (default behavior).* - Specify boolean `false` to disable retrieval of this property from getModuleInfo (forces a new check).* - Specify string `interface` to check only if module implements ConfigurableModule interface.* - Specify string `file` to check only if module has a separate configuration class/file.* @return bool|string|int See details about return values in method description.* @since 3.0.16** @todo all ConfigurableModule methods need to be split out into their own class (ConfigurableModules?)* @todo this method has two distinct parts (file and interface) that need to be split in two methods.**/public function isConfigurable($class, $useCache = true) {$className = $class;$moduleInstance = null;$namespace = $this->getModuleNamespace($className);if(is_object($className)) {$moduleInstance = $className;$className = $this->getModuleClass($moduleInstance);}$nsClassName = $namespace . $className;if($useCache === true || $useCache === 1 || $useCache === "1") {$info = $this->getModuleInfo($className);// if regular module info doesn't have configurable info, attempt it from verbose module info// should only be necessary for transition period between the 'configurable' property being// moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7)if($info['configurable'] === null) $info = $this->getModuleInfoVerbose($className);if(!$info['configurable']) {if($moduleInstance && $moduleInstance instanceof ConfigurableModule) {// re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format// this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12$this->error("Configurable module check failed for $className. " ."If this error persists, please do a Modules > Refresh.",Notice::debug);$useCache = false;} else {return false;}} else {if($info['configurable'] === true) return $info['configurable'];if($info['configurable'] === 1 || $info['configurable'] === "1") return true;if(is_int($info['configurable']) || ctype_digit("$info[configurable]")) return (int) $info['configurable'];if(strpos($info['configurable'], $className) === 0) {if(empty($info['file'])) $info['file'] = $this->getModuleFile($className);if($info['file']) {return dirname($info['file']) . "/$info[configurable]";}}}}if($useCache !== "interface") {// check for separate module configuration file$dir = dirname($this->getModuleFile($className));if($dir) {$files = array("$dir/{$className}Config.php","$dir/$className.config.php");$found = false;foreach($files as $file) {if(!is_file($file)) continue;$config = null; // include file may override$this->includeModuleFile($file, $className);$classConfig = $nsClassName . 'Config';if(class_exists($classConfig, false)) {$parents = wireClassParents($classConfig, false);if(is_array($parents) && in_array('ModuleConfig', $parents)) {$found = $file;break;}} else {// bypass include_once, because we need to read $config every timeif(is_null($config)) {$classInfo = $this->getFileClassInfo($file);if($classInfo['class']) {// not safe to include because this is not just a file with a $config array} else {$ns = $this->getFileNamespace($file);$file = $this->compile($className, $file, $ns);if($file) {/** @noinspection PhpIncludeInspection */include($file);}}}if(!is_null($config)) {// included file specified a $config array$found = $file;break;}}}if($found) return $found;}}// if file-only check was requested and we reach this point, exit with false nowif($useCache === "file") return false;// ConfigurableModule interface checks$result = false;foreach(array('getModuleConfigArray', 'getModuleConfigInputfields') as $method) {$configurable = false;// if we have a module instance, use that for our checkif($moduleInstance && $moduleInstance instanceof ConfigurableModule) {if(method_exists($moduleInstance, $method)) {$configurable = $method;} else if(method_exists($moduleInstance, "___$method")) {$configurable = "___$method";}}// if we didn't have a module instance, load the file to find what we need to knowif(!$configurable) {if(!wireClassExists($nsClassName, false)) {$this->includeModule($className);}$interfaces = wireClassImplements($nsClassName, false);if(is_array($interfaces) && in_array('ConfigurableModule', $interfaces)) {if(wireMethodExists($nsClassName, $method)) {$configurable = $method;} else if(wireMethodExists($nsClassName, "___$method")) {$configurable = "___$method";}}}// if still not determined to be configurable, move on to next methodif(!$configurable) continue;// now determine if static or non-static$ref = new \ReflectionMethod(wireClassName($nsClassName, true), $configurable);if($ref->isStatic()) {// config method is implemented as a static methodif($method == 'getModuleConfigInputfields') {// static getModuleConfigInputfields$result = true;} else {// static getModuleConfigArray$result = 20;}} else if($method == 'getModuleConfigInputfields') {// non-static getModuleConfigInputfields// we allow for different arguments, so determine what it needs$parameters = $ref->getParameters();if(count($parameters)) {$param0 = reset($parameters);if(strpos($param0, 'array') !== false || strpos($param0, '$data') !== false) {// method requires a $data array (for compatibility with non-static version)$result = 3;} else if(strpos($param0, 'InputfieldWrapper') !== false || strpos($param0, 'inputfields') !== false) {// method requires an empty InputfieldWrapper (as a convenience)$result = 4;}}// method requires no argumentsif(!$result) $result = 2;} else {// non-static getModuleConfigArray$result = 19;}// if we make it here, we know we already have a result so can stop nowbreak;}return $result;}/*** Indicates whether module accepts config settings, whether interactively or API only** - Returns false if module does not accept config settings.* - Returns integer `30` if module accepts config settings but is not interactively configurable.* - Returns true, int or string if module is interactively configurable, see `Modules::isConfigurable()` return values.** @param string|Module $class* @param bool $useCache* @return bool|int|string* @since 3.0.179**/public function isConfigable($class, $useCache = true) {if(is_object($class)) {if($class instanceof ConfigModule) {$result = 30;} else {$result = $this->isConfigurable($class, $useCache);}} else {$result = $this->isConfigurable($class, $useCache);if(!$result && wireInstanceOf($class, 'ConfigModule')) $result = 30;}return $result;}/*** Alias of isConfigurable() for backwards compatibility** #pw-internal** @param $className* @param bool $useCache* @return mixed**/public function isConfigurableModule($className, $useCache = true) {return $this->isConfigurable($className, $useCache);}/*** Populate configuration data to a ConfigurableModule** If the Module has a 'setConfigData' method, it will send the array of data to that.* Otherwise it will populate the properties individually.** @param Module $module* @param array|null $data Configuration data [key=value], or omit/null if you want it to retrieve the config data for you.* @param array|null $extraData Additional runtime configuration data to merge (default=null) 3.0.169+* @return bool True if configured, false if not configurable**/protected function setModuleConfigData(Module $module, $data = null, $extraData = null) {$configurable = $this->isConfigable($module);if(!$configurable) return false;if(!is_array($data)) $data = $this->getConfig($module);if($extraData !== null && is_array($extraData)) $data = array_merge($data, $extraData);$nsClassName = $module->className(true);$moduleName = $module->className(false);if(is_string($configurable) && is_file($configurable) && strpos(basename($configurable), $moduleName) === 0) {// get defaults from ModuleConfig class if available$className = $nsClassName . 'Config';$config = null; // may be overridden by included file// $compile = strrpos($className, '\\') < 1 && $this->wire('config')->moduleCompile;$configFile = '';if(!class_exists($className, false)) {$configFile = $this->compile($className, $configurable);// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;if($configFile) {/** @noinspection PhpIncludeInspection */include_once($configFile);}}if(wireClassExists($className)) {$parents = wireClassParents($className, false);if(is_array($parents) && in_array('ModuleConfig', $parents)) {$moduleConfig = $this->wire(new $className());if($moduleConfig instanceof ModuleConfig) {$defaults = $moduleConfig->getDefaults();$data = array_merge($defaults, $data);}}} else {// the file may have already been include_once before, so $config would not be set// so we try a regular include() next.if(is_null($config)) {if(!$configFile) {$configFile = $this->compile($className, $configurable);// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;}if($configFile) {/** @noinspection PhpIncludeInspection */include($configFile);}}if(is_array($config)) {// alternatively, file may just specify a $config array$moduleConfig = $this->wire(new ModuleConfig());$moduleConfig->add($config);$defaults = $moduleConfig->getDefaults();$data = array_merge($defaults, $data);}}}if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) {/** @var _Module $module */$module->setConfigData($data);return true;}foreach($data as $key => $value) {$module->$key = $value;}return true;}/*** Alias of saveConfig() for backwards compatibility** #pw-internal** @param $className* @param array $configData* @return mixed**/public function ___saveModuleConfigData($className, array $configData) {return $this->saveConfig($className, $configData);}/*** Save provided configuration data for the given module** - Applicable only for modules that support configuration.* - Configuration data is stored encoded in the database "modules" table "data" field.** ~~~~~~* // Getting, modifying and saving module config data* $data = $modules->getConfig('HelloWorld');* $data['greeting'] = 'Hello World! How are you today?';* $modules->saveConfig('HelloWorld', $data);* ~~~~~~** #pw-group-configuration* #pw-group-manipulation* #pw-changelog 3.0.16 Changed name from the more verbose saveModuleConfigData(), which will still work.** @param string|Module $class Module or module name* @param array|string $data Associative array of configuration data, or name of property you want to save.* @param mixed|null $value If you specified a property in previous arg, the value for the property.* @return bool True on success, false on failure* @throws WireException* @see Modules::getConfig()* @since 3.0.16 Use method saveModuleConfigData() with same arguments for prior versions (can also be used on any version).**/public function ___saveConfig($class, $data, $value = null) {$className = $class;if(is_object($className)) $className = $className->className();$moduleName = wireClassName($className, false);if(!$id = $this->moduleIDs[$moduleName]) throw new WireException("Unable to find ID for Module '$moduleName'");if(is_string($data)) {// a property and value have been provided$property = $data;$data = $this->getConfig($class);if(is_null($value)) {// remove the propertyunset($data[$property]);} else {// populate the value for the property$data[$property] = $value;}} else {// data must be an associative array of configuration dataif(!is_array($data)) return false;}// ensure original duplicates info is retained and validate that it is still current$data = $this->duplicates()->getDuplicatesConfigData($moduleName, $data);$this->configData[$id] = $data;$json = count($data) ? wireEncodeJSON($data, true) : '';$database = $this->wire('database');$query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveConfig($moduleName)"); // QA$query->bindValue(":data", $json, \PDO::PARAM_STR);$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);$result = $query->execute();// $this->log("Saved module '$moduleName' config data");return $result;}/*** Get the Inputfields that configure the given module or return null if not configurable** #pw-internal** @param string|Module|int $moduleName* @param InputfieldWrapper|null $form Optionally specify the form you want Inputfields appended to.* @return InputfieldWrapper|null**/public function ___getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {$moduleName = $this->getModuleClass($moduleName);$configurable = $this->isConfigurable($moduleName);if(!$configurable) return null;if(is_null($form)) $form = $this->wire(new InputfieldWrapper());$data = $this->getConfig($moduleName);$fields = null;// check for configurable module interface$configurableInterface = $this->isConfigurable($moduleName, "interface");if($configurableInterface) {if(is_int($configurableInterface) && $configurableInterface > 1 && $configurableInterface < 20) {// non-static/** @var ConfigurableModule|Module|_Module $module */if($configurableInterface === 2) {// requires no arguments$module = $this->getModule($moduleName);$fields = $module->getModuleConfigInputfields();} else if($configurableInterface === 3) {// requires $data array$module = $this->getModule($moduleName, array('noInit' => true, 'noCache' => true));$this->setModuleConfigData($module);$fields = $module->getModuleConfigInputfields($data);} else if($configurableInterface === 4) {// requires InputfieldWrapper// we allow for option of no return statement in the method$module = $this->getModule($moduleName);$fields = $this->wire(new InputfieldWrapper());$fields->setParent($form);$_fields = $module->getModuleConfigInputfields($fields);if($_fields instanceof InputfieldWrapper) $fields = $_fields;unset($_fields);} else if($configurableInterface === 19) {// non-static getModuleConfigArray method$module = $this->getModule($moduleName);$fields = $this->wire(new InputfieldWrapper());$fields->importArray($module->getModuleConfigArray());$fields->populateValues($module);}} else if($configurableInterface === 20) {// static getModuleConfigArray method$fields = $this->wire(new InputfieldWrapper());$fields->importArray(call_user_func(array(wireClassName($moduleName, true), 'getModuleConfigArray')));$fields->populateValues($data);} else if($configurableInterface) {// static getModuleConfigInputfields method$nsClassName = $this->getModuleNamespace($moduleName) . $moduleName;$fields = call_user_func(array($nsClassName, 'getModuleConfigInputfields'), $data);}if($fields instanceof InputfieldWrapper) {foreach($fields as $field) {$form->append($field);}} else if($fields instanceof Inputfield) {$form->append($fields);} else {$this->error("$moduleName.getModuleConfigInputfields() did not return InputfieldWrapper");}}// check for file-based config$file = $this->isConfigurable($moduleName, "file");if(!$file || !is_string($file) || !is_file($file)) {// config is not file-based} else {// file-based config$config = null;$ns = $this->getModuleNamespace($moduleName);$configClass = $ns . $moduleName . "Config";if(!class_exists($configClass)) {$configFile = $this->compile($moduleName, $file, $ns);if($configFile) {/** @noinspection PhpIncludeInspection */include_once($configFile);}}$configModule = null;if(wireClassExists($configClass)) {// file contains a ModuleNameConfig class$configModule = $this->wire(new $configClass());} else {if(is_null($config)) {$configFile = $this->compile($moduleName, $file, $ns);// if(!$configFile) $configFile = $compile ? $this->wire('files')->compile($file) : $file;if($configFile) {/** @noinspection PhpIncludeInspection */include($configFile); // in case of previous include_once}}if(is_array($config)) {// file contains a $config array$configModule = $this->wire(new ModuleConfig());$configModule->add($config);}}if($configModule && $configModule instanceof ModuleConfig) {$defaults = $configModule->getDefaults();$data = array_merge($defaults, $data);$configModule->setArray($data);$fields = $configModule->getInputfields();if($fields instanceof InputfieldWrapper) {foreach($fields as $field) {$form->append($field);}foreach($data as $key => $value) {$f = $form->getChildByName($key);if(!$f) continue;if($f instanceof InputfieldCheckbox && $value) {$f->attr('checked', 'checked');} else {$f->attr('value', $value);}}} else {$this->error("$configModule.getInputfields() did not return InputfieldWrapper");}}} // file-based configif($form) {// determine how many visible Inputfields there are in the module configuration// for assignment or removal of flagsNoUserConfig flag when applicable$numVisible = 0;foreach($form->getAll() as $inputfield) {if($inputfield instanceof InputfieldHidden || $inputfield instanceof InputfieldWrapper) continue;$numVisible++;}$flags = $this->getFlags($moduleName);if($numVisible) {if($flags & self::flagsNoUserConfig) {$info = $this->getModuleInfoVerbose($moduleName);if(empty($info['addFlag']) || !($info['addFlag'] & self::flagsNoUserConfig)) {$this->setFlag($moduleName, self::flagsNoUserConfig, false); // remove flag}}} else {if(!($flags & self::flagsNoUserConfig)) {if(empty($info['removeFlag']) || !($info['removeFlag'] & self::flagsNoUserConfig)) {$this->setFlag($moduleName, self::flagsNoUserConfig, true); // add flag}}}}return $form;}/*** Is the given module Singular (single instance)?** isSingular and isAutoload Module methods have been deprecated. So this method, and isAutoload()* exist in part to enable singular and autoload properties to be set in getModuleInfo, rather than* with methods.** Note that isSingular() and isAutoload() are not deprecated for ModulePlaceholder, so the Modules* class isn't going to stop looking for them.** #pw-internal** @param Module|string $module Module instance or class name* @return bool**/public function isSingular($module) {$info = $this->getModuleInfo($module);if(isset($info['singular']) && $info['singular'] !== null) return $info['singular'];if(is_object($module)) {if(method_exists($module, 'isSingular')) return $module->isSingular();} else {// singular status can't be determined if module not installed and not specified in moduleInfoif(isset($this->installable[$module])) return null;$this->includeModule($module);$module = wireClassName($module, true);if(method_exists($module, 'isSingular')) {/** @var Module|_Module $moduleInstance */$moduleInstance = $this->wire(new $module());return $moduleInstance->isSingular();}}return false;}/*** Is the given module Autoload (automatically loaded at runtime)?** #pw-internal** @param Module|string $module Module instance or class name* @return bool|string|null Returns string "conditional" if conditional autoload, true if autoload, or false if not. Or null if unavailable.**/public function isAutoload($module) {$info = $this->getModuleInfo($module);$autoload = null;if(isset($info['autoload']) && $info['autoload'] !== null) {// if autoload is a string (selector) or callable, then we flag it as autoloadif(is_string($info['autoload']) || wireIsCallable($info['autoload'])) return "conditional";$autoload = $info['autoload'];} else if(!is_object($module)) {if(isset($this->installable[$module])) {// module is not installed// we are not going to be able to determine if this is autoload or not$flags = $this->getFlags($module);if($flags !== null) {$autoload = $flags & self::flagsAutoload;} else {// unable to determinereturn null;}} else {// include for method exists call$this->includeModule($module);$module = wireClassName($module, true);$module = $this->wire(new $module());}}if($autoload === null && is_object($module) && method_exists($module, 'isAutoload')) {/** @var module $module */$autoload = $module->isAutoload();}return $autoload;}/*** Returns whether the modules have been initialized yet** #pw-internal** @return bool**/public function isInitialized() {return $this->initialized;}/*** Does the given module name resolve to a module in the system (installed or uninstalled)** If given module name also includes a namespace, then that namespace will be validated as well.** #pw-internal** @param string|Module $moduleName With or without namespace* @return bool**/public function isModule($moduleName) {if(!is_string($moduleName)) {if(is_object($moduleName)) {if($moduleName instanceof Module) return true;return false;}$moduleName = $this->getModuleClass($moduleName);}/** @var string $moduleName */if(strpos($moduleName, "\\") !== false) {$namespace = wireClassName($moduleName, 1);$moduleName = wireClassName($moduleName, false);} else {$namespace = false;}if(isset($this->moduleIDs[$moduleName])) {$isModule = true;} else if(isset($this->installable[$moduleName])) {$isModule = true;} else {$isModule = false;}if($isModule && $namespace) {$actualNamespace = $this->getModuleNamespace($moduleName);if(trim("$namespace", '\\') != trim("$actualNamespace", '\\')) {$isModule = false;}}return $isModule;}/*** 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(is_null($this->moduleNamespaceCache)) $this->getNamespaces();$namespace = "\\" . trim($namespace, "\\") . "\\";return isset($this->moduleNamespaceCache[$namespace]) ? $this->moduleNamespaceCache[$namespace] : false;}/*** Refresh the modules cache** This forces the modules file and information cache to be re-created.** #pw-group-manipulation** @param bool $showMessages Show notification messages about what was found? (default=false) 3.0.172+**/public function ___refresh($showMessages = false) {if($this->wire()->config->systemVersion < 6) return;$this->refreshing = true;$this->clearModuleInfoCache($showMessages);$this->loadModulesTable();foreach($this->paths as $path) $this->findModuleFiles($path, false);foreach($this->paths as $path) $this->load($path);if($this->duplicates()->numNewDuplicates() > 0) $this->duplicates()->updateDuplicates(); // PR#1020$this->refreshing = false;}/*** Alias of refresh() method for backwards compatibility** #pw-internal**/public function resetCache() {$this->refresh();}/*** Return an array of module class names that require the given one** #pw-internal** @param string $class* @param bool $uninstalled Set to true to include modules dependent upon this one, even if they aren't installed.* @param bool $installs Set to true to exclude modules that indicate their install/uninstall is controlled by $class.* @return array()**/public function getRequiredBy($class, $uninstalled = false, $installs = false) {$class = $this->getModuleClass($class);$info = $this->getModuleInfo($class);$dependents = array();foreach($this as $module) {$c = $this->getModuleClass($module);if(!$uninstalled && !$this->isInstalled($c)) continue;$i = $this->getModuleInfo($c);if(!count($i['requires'])) continue;if($installs && in_array($c, $info['installs'])) continue;if(in_array($class, $i['requires'])) $dependents[] = $c;}return $dependents;}/*** Return an array of module class names required by the given one** Default behavior is to return all listed requirements, whether they are currently met by* the environment or not. Specify TRUE for the 2nd argument to return only requirements* that are not currently met.** #pw-internal** @param string $class* @param bool $onlyMissing Set to true to return only required modules/versions that aren't* yet installed or don't have the right version. It excludes those that the class says it* will install (via 'installs' property of getModuleInfo)* @param null|bool $versions Set to true to always include versions in the returned requirements list.* Set to null to always exclude versions in requirements list (so only module class names will be there).* Set to false (which is the default) to include versions only when version is the dependency issue.* Note versions are already included when the installed version is not adequate.* @return array of strings each with ModuleName Operator Version, i.e. "ModuleName>=1.0.0"**/public function getRequires($class, $onlyMissing = false, $versions = false) {$class = $this->getModuleClass($class);$info = $this->getModuleInfo($class);$requires = $info['requires'];$currentVersion = 0;// quick exit if arguments permit itif(!$onlyMissing) {if($versions) foreach($requires as $key => $value) {list($operator, $version) = $info['requiresVersions'][$value];if(empty($version)) continue;if(ctype_digit("$version")) $version = $this->formatVersion($version);if(!empty($version)) $requires[$key] .= "$operator$version";}return $requires;}foreach($requires as $key => $requiresClass) {if(in_array($requiresClass, $info['installs'])) {// if this module installs the required class, then we can stop now// and we assume it's installing the version it wantsunset($requires[$key]);}list($operator, $requiresVersion) = $info['requiresVersions'][$requiresClass];$installed = true;if($requiresClass == 'PHP') {$currentVersion = PHP_VERSION;} else if($requiresClass == 'ProcessWire') {$currentVersion = $this->wire('config')->version;} else if($this->isInstalled($requiresClass)) {if(!$requiresVersion) {// if no version is specified then requirement is already metunset($requires[$key]);continue;}$i = $this->getModuleInfo($requiresClass, array('noCache' => true));$currentVersion = $i['version'];} else {// module is not installed$installed = false;}if($installed && $this->versionCompare($currentVersion, $requiresVersion, $operator)) {// required version is installedunset($requires[$key]);} else if(empty($requiresVersion)) {// just the class name is finecontinue;} else if(is_null($versions)) {// request is for no versions to be included (just class names)$requires[$key] = $requiresClass;} else {// update the requires string to clarify what version it requiresif(ctype_digit("$requiresVersion")) $requiresVersion = $this->formatVersion($requiresVersion);$requires[$key] = "$requiresClass$operator$requiresVersion";}}return $requires;}/*** Compare one module version to another, returning TRUE if they match the $operator or FALSE otherwise** #pw-internal** @param int|string $currentVersion May be a number like 123 or a formatted version like 1.2.3* @param int|string $requiredVersion May be a number like 123 or a formatted version like 1.2.3* @param string $operator* @return bool**/public function versionCompare($currentVersion, $requiredVersion, $operator) {if(ctype_digit("$currentVersion") && ctype_digit("$requiredVersion")) {// integer comparison is ok$currentVersion = (int) $currentVersion;$requiredVersion = (int) $requiredVersion;$result = false;switch($operator) {case '=': $result = ($currentVersion == $requiredVersion); break;case '>': $result = ($currentVersion > $requiredVersion); break;case '<': $result = ($currentVersion < $requiredVersion); break;case '>=': $result = ($currentVersion >= $requiredVersion); break;case '<=': $result = ($currentVersion <= $requiredVersion); break;case '!=': $result = ($currentVersion != $requiredVersion); break;}return $result;}// if either version has no periods or only one, like "1.2" then format it to stanard: "1.2.0"if(substr_count($currentVersion, '.') < 2) $currentVersion = $this->formatVersion($currentVersion);if(substr_count($requiredVersion, '.') < 2) $requiredVersion = $this->formatVersion($requiredVersion);return version_compare($currentVersion, $requiredVersion, $operator);}/*** 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 specifiedreturn 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 specifiedif(!$operator) return array($require, '', 0);// extract class and versionlist($class, $version) = explode($operator, $require);// make version an integer if possibleif(ctype_digit("$version")) $version = (int) $version;return array($class, $operator, $version);}/*** Return an array of module class names required by the given one to be installed before this one.** Excludes modules that are required but already installed.* Excludes uninstalled modules that $class indicates it handles via it's 'installs' getModuleInfo property.** #pw-internal** @param string $class* @return array()**/public function getRequiresForInstall($class) {return $this->getRequires($class, true);}/*** Return an array of module class names required by the given one to be uninstalled before this one.** Excludes modules that the given one says it handles via it's 'installs' getModuleInfo property.* Module class names in returned array include operator and version in the string.** #pw-internal** @param string $class* @return array()**/public function getRequiresForUninstall($class) {return $this->getRequiredBy($class, false, true);}/*** Return array of dependency errors for given module name** #pw-internal** @param $moduleName* @return array If no errors, array will be blank. If errors, array will be of strings (error messages)**/public function getDependencyErrors($moduleName) {$moduleName = $this->getModuleClass($moduleName);$info = $this->getModuleInfo($moduleName);$errors = array();if(empty($info['requires'])) return $errors;foreach($info['requires'] as $requiresName) {$error = '';if(!$this->isInstalled($requiresName)) {$error = $requiresName;} else if(!empty($info['requiresVersions'][$requiresName])) {list($operator, $version) = $info['requiresVersions'][$requiresName];$info2 = $this->getModuleInfo($requiresName);$requiresVersion = $info2['version'];if(!empty($version) && !$this->versionCompare($requiresVersion, $version, $operator)) {$error = "$requiresName $operator $version";}}if($error) $errors[] = sprintf($this->_('Failed module dependency: %s requires %s'), $moduleName, $error);}return $errors;}/*** Find modules that are missing their module file on the file system** Return value is array:* ~~~~~* [* 'ModuleName' => [* 'id' => 123,* 'name' => 'ModuleName',* 'file' => '/path/to/expected/file.module'* ],* 'ModuleName' => [* ...* ]* ];* ~~~~~** #pw-internal** @return array* @since 3.0.170**/public function findMissingModules() {$missing = array();$unflags = array();$sql = "SELECT id, class FROM modules WHERE flags & :flagsNoFile ORDER BY class";$query = $this->wire()->database->prepare($sql);$query->bindValue(':flagsNoFile', self::flagsNoFile, \PDO::PARAM_INT);$query->execute();while($row = $query->fetch(\PDO::FETCH_ASSOC)) {$class = $row['class'];$file = $this->getModuleFile($class, array('fast' => true));if($file && file_exists($file)) {$unflags[] = $class;continue;}$fileAlt = $this->getModuleFile($class, array('fast' => false));if($fileAlt) {$file = $fileAlt;if(file_exists($file)) continue;}if(!$file) {$file = $this->getModuleFile($class, array('fast' => true, 'guess' => true));}$missing[$class] = array('id' => $row['id'],'name' => $class,'file' => $file,);}foreach($unflags as $name) {$this->setFlag($name, self::flagsNoFile, false);}return $missing;}/*** Remove entry for module from modules table** #pw-internal** @param string|int $class Module class or ID* @return bool* @since 3.0.170**/public function removeModuleEntry($class) {$database = $this->wire()->database;if(ctype_digit("$class")) {$query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1');$query->bindValue(':id', (int) $class, \PDO::PARAM_INT);} else {$query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1');$query->bindValue(':class', $class, \PDO::PARAM_STR);}$result = $query->execute() ? $query->rowCount() > 0 : false;$query->closeCursor();return $result;}/*** Given a module version number, format it in a consistent way as 3 parts: 1.2.3** #pw-internal** @param $version int|string* @return string**/public function formatVersion($version) {$version = trim($version);if(!ctype_digit(str_replace('.', '', $version))) {// if version has some characters other than digits or periods, remove them$version = preg_replace('/[^\d.]/', '', $version);}if(ctype_digit("$version")) {// version contains only digits// make sure version is at least 3 characters in length, left padded with 0s$len = strlen($version);if($len < 3) {$version = str_pad($version, 3, "0", STR_PAD_LEFT);} else if($len > 3) {// they really need to use a string for this type of version,// as we can't really guess, but we'll try, converting 1234 to 1.2.34}// $version = preg_replace('/(\d)(?=\d)/', '$1.', $version);$version =substr($version, 0, 1) . '.' .substr($version, 1, 1) . '.' .substr($version, 2);} else if(strpos($version, '.') !== false) {// version is a formatted stringif(strpos($version, '.') == strrpos($version, '.')) {// only 1 period, like: 2.0, convert that to 2.0.0if(preg_match('/^\d\.\d$/', $version)) $version .= ".0";}} else {// invalid version?}if(!strlen($version)) $version = '0.0.0';return $version;}/*** Load the module information cache** @return bool**/protected function loadModuleInfoCache() {$data = $this->wire('cache')->get(self::moduleInfoCacheName);if($data) {// 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-creationif(is_array($data) && !isset($data['ProcessModule'])) $this->moduleInfoCache = $data;$data = $this->wire('cache')->get(self::moduleLastVersionsCacheName);if(is_array($data)) $this->modulesLastVersions = $data;return true;}return false;}/*** Load the module information cache (verbose info: summary, author, href, file, core)** @param bool $uninstalled If true, it will load the uninstalled verbose cache.* @return bool**/protected function loadModuleInfoCacheVerbose($uninstalled = false) {$name = $uninstalled ? self::moduleInfoCacheUninstalledName : self::moduleInfoCacheVerboseName;$data = $this->wire('cache')->get($name);if($data) {if(is_array($data)) {if($uninstalled) $this->moduleInfoCacheUninstalled = $data;else $this->moduleInfoCacheVerbose = $data;}return true;}return false;}/*** Clear the module information cache** @param bool|null $showMessages Specify true to show message notifications**/protected function clearModuleInfoCache($showMessages = false) {$cache = $this->wire()->cache;$versionChanges = 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$cache->delete(self::moduleInfoCacheName);$cache->delete(self::moduleInfoCacheVerboseName);$cache->delete(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 changesforeach($this->moduleInfoCache as $id => $moduleInfo) {$moduleName = $moduleInfo['name'];if(!isset($moduleVersions[$id])) {if(isset($this->moduleIDs[$moduleName])) {$moveModules[] = $moduleName;} else {$newModules[] = $moduleName;}continue;}if($moduleVersions[$id] != $moduleInfo['version']) {$fromVersion = $this->formatVersion($moduleVersions[$id]);$toVersion = $this->formatVersion($moduleInfo['version']);$versionChanges[] = "$fromVersion => $toVersion: $moduleName";$this->modulesLastVersions[$id] = $moduleVersions[$id];if(strpos($moduleName, 'Fieldtype') === 0) {// apply update now, to Fieldtype modules only (since they are loaded differently)$this->getModule($moduleName);}}}foreach($this->moduleIDs as $moduleName => $moduleID) {if(isset($this->moduleInfoCache[$moduleID])) {// module is present in moduleInfoif($this->hasFlag($moduleID, self::flagsNoFile)) {$file = $this->getModuleFile($moduleName, array('fast' => false));if($file) {// remove flagsNoFile if file is found$this->setFlag($moduleID, self::flagsNoFile, false);}}} else {// module is missing moduleInfo$file = $this->getModuleFile($moduleName, array('fast' => false));if(!$file) {$file = $this->getModuleFile($moduleName, array('fast' => true, 'guess' => true));// add flagsNoFile if file cannot be located$missModules[] = "$moduleName => $file";$this->setFlag($moduleID, self::flagsNoFile, true);}}}$this->updateModuleVersionsCache();// report detected changes$sanitizer = $this->wire()->sanitizer;$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,),);foreach($reports as $report) {if(!count($report['items'])) continue;if($showMessages) $this->message($sanitizer->entities1(sprintf($report['label'], count($report['items']))) .'<pre>' . $sanitizer->entities(implode("\n", $report['items'])) . '</pre>',Notice::allowMarkup | Notice::noGroup);$this->log(sprintf($report['label'], count($report['items'])) . ' ' .implode(', ', $report['items']));}}/*** Update the cache of queued module version changes**/protected function updateModuleVersionsCache() {foreach($this->modulesLastVersions as $id => $version) {// clear out stale data, if presentif(!in_array($id, $this->moduleIDs)) unset($this->modulesLastVersions[$id]);}if(count($this->modulesLastVersions)) {$this->wire()->cache->save(self::moduleLastVersionsCacheName, $this->modulesLastVersions, WireCache::expireReserved);} else {$this->wire()->cache->delete(self::moduleLastVersionsCacheName);}}/*** 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**/protected function checkModuleVersion(Module $module) {$id = $this->getModuleID($module);$moduleInfo = $this->getModuleInfo($module);$lastVersion = isset($this->modulesLastVersions[$id]) ? $this->modulesLastVersions[$id] : null;if(!is_null($lastVersion)) {if($lastVersion != $moduleInfo['version']) {$this->moduleVersionChanged($module, $lastVersion, $moduleInfo['version']);unset($this->modulesLastVersions[$id]);}$this->updateModuleVersionsCache();}}/*** Hook called when a module's version changes** This calls the module's ___upgrade($fromVersion, $toVersion) method.** @param Module|_Module $module* @param int|string $fromVersion* @param int|string $toVersion**/protected function ___moduleVersionChanged(Module $module, $fromVersion, $toVersion) {$moduleName = wireClassName($module, false);$moduleID = $this->getModuleID($module);$fromVersionStr = $this->formatVersion($fromVersion);$toVersionStr = $this->formatVersion($toVersion);$this->message($this->_('Upgrading module') . " ($moduleName: $fromVersionStr => $toVersionStr)");try {if(method_exists($module, '___upgrade')) {$module->upgrade($fromVersion, $toVersion);}unset($this->modulesLastVersions[$moduleID]);} catch(\Exception $e) {$this->error("Error upgrading module ($moduleName): " . $e->getMessage());}}/*** Update module flags if any happen to differ from what's in the given moduleInfo** @param $moduleID* @param array $info**/protected function updateModuleFlags($moduleID, array $info) {$flags = (int) $this->getFlags($moduleID);if($info['autoload']) {// module is autoloadif(!($flags & self::flagsAutoload)) {// add autoload flag$this->setFlag($moduleID, self::flagsAutoload, true);}if(is_string($info['autoload'])) {// requires conditional flag// value is either: "function", or the conditional string (like key=value)if(!($flags & self::flagsConditional)) $this->setFlag($moduleID, self::flagsConditional, true);} else {// should not have conditional flagif($flags & self::flagsConditional) $this->setFlag($moduleID, self::flagsConditional, false);}} else if($info['autoload'] !== null) {// module is not autoloadif($flags & self::flagsAutoload) {// remove autoload flag$this->setFlag($moduleID, self::flagsAutoload, false);}if($flags & self::flagsConditional) {// remove conditional flag$this->setFlag($moduleID, self::flagsConditional, false);}}if($info['singular']) {if(!($flags & self::flagsSingular)) $this->setFlag($moduleID, self::flagsSingular, true);} else {if($flags & self::flagsSingular) $this->setFlag($moduleID, self::flagsSingular, false);}// handle addFlag and removeFlag moduleInfo propertiesforeach(array(0 => 'removeFlag', 1 => 'addFlag') as $add => $flagsType) {if(empty($info[$flagsType])) continue;if($flags & $info[$flagsType]) {// already has the flagsif(!$add) {// remove the flag(s)$this->setFlag($moduleID, $info[$flagsType], false);}} else {// does not have the flagsif($add) {// add the flag(s)$this->setFlag($moduleID, $info[$flagsType], true);}}}}/*** Save the module information cache**/protected 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);}}foreach(array(true, false) as $installed) {$items = $installed ? $this : array_keys($this->installable);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 modulesif(!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 keyif(is_null($info['autoload'])) {// module info does not indicate an autoload state$info['autoload'] = $this->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->isSingular($module);}if(is_null($info['configurable'])) {$info['configurable'] = $this->isConfigurable($module, false);}if($moduleID) $this->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',);foreach($caches as $cacheName => $varName) {$data = $this->$varName;foreach($data as $moduleID => $moduleInfo) {foreach($moduleInfo as $key => $value) {// remove unpopulated propertiesif($key == 'installed') {// no need to store an installed==true propertyif($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 itunset($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 propertiesunset($data[$moduleID][$key]);} else if(($key == 'namespace' && $value == "\\" . __NAMESPACE__ . "\\") || (!strlen(__NAMESPACE__) && empty($value))) {// no need to cache default namespace in module infounset($data[$moduleID][$key]);} else if($key == 'file') {// file property is cached elsewhere so doesn't need to be included in this cacheunset($data[$moduleID][$key]);}}}$this->wire('cache')->save($cacheName, $data, WireCache::expireReserved);}$this->log('Saved module info caches');if($languages && $language) $user->language = $language; // restore}/*** Start a debug timer, only works when module debug mode is on ($this->debug)** @param $note* @return int|null Returns a key for the debug timer**/protected function debugTimerStart($note) {if(!$this->debug) return null;$key = count($this->debugLog);while(isset($this->debugLog[$key])) $key++;$this->debugLog[$key] = array(0 => Debug::timer("Modules$key"),1 => $note);return $key;}/*** Stop a debug timer, only works when module debug mode is on ($this->debug)** @param int $key The key returned by debugTimerStart**/protected function debugTimerStop($key) {if(!$this->debug) return;$log = $this->debugLog[$key];$timerKey = $log[0];$log[0] = Debug::timer($timerKey);$this->debugLog[$key] = $log;Debug::removeTimer($timerKey);}/*** Return a log of module construct, init and ready times, active only when debug mode is on ($this->debug)** #pw-internal** @return array**/public function getDebugLog() {return $this->debugLog;}/*** Substitute one module for another, to be used only when $moduleName doesn't exist.** #pw-internal** @param string $moduleName Module class name that may need a substitute* @param string $substituteName Module class name you want to substitute when $moduleName isn't found.* Specify null to remove substitute.**/public function setSubstitute($moduleName, $substituteName = null) {if(is_null($substituteName)) {unset($this->substitutes[$moduleName]);} else {$this->substitutes[$moduleName] = $substituteName;}}/*** Substitute modules for other modules, to be used only when $moduleName doesn't exist.** This appends existing entries rather than replacing them.** #pw-internal** @param array $substitutes Array of module name => substitute module name**/public function setSubstitutes(array $substitutes) {$this->substitutes = array_merge($this->substitutes, $substitutes);}/*** Load module related CSS and JS files (where applicable)** - Applies only to modules that carry class-named CSS and/or JS files, such as Process, Inputfield and ModuleJS modules.* - Assets are populated to `$config->styles` and `$config->scripts`.** #pw-internal** @param Module|int|string $module Module object or class name* @return int Returns number of files that were added**/public function loadModuleFileAssets($module) {$class = $this->getModuleClass($module);static $classes = array();if(isset($classes[$class])) return 0; // already loaded$info = null;$config = $this->wire('config');$path = $config->paths->$class;$url = $config->urls->$class;$debug = $config->debug;$version = 0;$cnt = 0;foreach(array('styles' => 'css', 'scripts' => 'js') as $type => $ext) {$fileURL = '';$modified = 0;$file = "$path$class.$ext";$minFile = "$path$class.min.$ext";if(!$debug && is_file($minFile)) {$fileURL = "$url$class.min.$ext";$modified = filemtime($minFile);} else if(is_file($file)) {$fileURL = "$url$class.$ext";$modified = filemtime($file);}if($fileURL) {if(!$version) {$info = $this->getModuleInfo($module, array('verbose' => false));$version = (int) isset($info['version']) ? $info['version'] : 0;}$config->$type->add("$fileURL?v=$version-$modified");$cnt++;}}$classes[$class] = true;return $cnt;}/*** Get module language translation files** @param Module|string $module* @return array Array of translation files including full path, indexed by basename without extension* @since 3.0.181**/public function getModuleLanguageFiles($module) {$module = $this->getModuleClass($module);if(empty($module)) return array();$path = $this->wire()->config->paths($module);if(empty($path)) return array();$pathHidden = $path . '.languages/';$pathVisible = $path . 'languages/';if(is_dir($pathVisible)) {$path = $pathVisible;} else if(is_dir($pathHidden)) {$path = $pathHidden;} else {return array();}$items = array();$options = array('extensions' => array('csv'),'recursive' => false,'excludeHidden' => true,);foreach($this->wire()->files->find($path, $options) as $file) {$basename = basename($file, '.csv');$items[$basename] = $file;}return $items;}/*** Enables use of $modules('ModuleName')** @param string $key* @return mixed**/public function __invoke($key) {return $this->get($key);}/*** Save to the modules log** #pw-internal** @param string $str Message to log* @param string $moduleName* @return WireLog**/public function log($str, $moduleName = '') {if(!in_array('modules', $this->wire('config')->logs)) return $this->___log();if(!is_string($moduleName)) $moduleName = (string) $moduleName;if($moduleName && strpos($str, $moduleName) === false) $str .= " (Module: $moduleName)";return $this->___log($str, array('name' => 'modules'));}/*** Record and log error message** #pw-internal** @param array|Wire|string $text* @param int $flags* @return Modules|WireArray**/public function error($text, $flags = 0) {$this->log($text);return parent::error($text, $flags);}/*** Compile and return the given file for module, if allowed to do so** #pw-internal** @param Module|string $moduleName* @param string $file Optionally specify the module filename as an optimization* @param string|null $namespace Optionally specify namespace as an optimization* @return string|bool**/public function compile($moduleName, $file = '', $namespace = null) {static $allowCompile = null;if($allowCompile === null) $allowCompile = $this->wire('config')->moduleCompile;// if not given a file, track it downif(empty($file)) $file = $this->getModuleFile($moduleName);// don't compile when module compilation is disabledif(!$allowCompile) return $file;// don't compile core modulesif(strpos($file, $this->coreModulesDir) !== false) return $file;// if namespace not provided, get itif(is_null($namespace)) {if(is_object($moduleName)) {$className = $moduleName->className(true);$namespace = wireClassName($className, 1);} else if(is_string($moduleName) && strpos($moduleName, "\\") !== false) {$namespace = wireClassName($moduleName, 1);} else {$namespace = $this->getModuleNamespace($moduleName, array('file' => $file));}}// determine if compiler should be usedif(__NAMESPACE__) {$compile = $namespace === '\\' || empty($namespace);} else {$compile = trim($namespace, '\\') === 'ProcessWire';}// compile if necessaryif($compile) {/** @var FileCompiler $compiler */$compiler = $this->wire(new FileCompiler(dirname($file)));$compiledFile = $compiler->compile(basename($file));if($compiledFile) $file = $compiledFile;}return $file;}}