get('MarkupPagerNav'); * * // Getting a module by name (alternate) * $m = $modules->MarkupPagerNav; * ~~~~~ * * #pw-body * * 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). * * @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 * * @property-read array $installableFiles * @property-read string $coreModulesDir * @property-read string $coreModulesPath * @property-read string $siteModulesPath * @property-read array $moduleIDs * @property-read array $moduleNames * @property-read ModulesInfo $info * @property-read ModulesLoader $loader * @property-read ModulesFlags $flags * @property-read ModulesFiles $files * @property-read ModulesInstaller $installer * @property-read ModulesConfigs $configs * @property-read bool $refreshing * */ 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; /** * Indicates row is for Modules system cache use and not an actual module * * @since 3.0.218 * */ const flagsSystemCache = 8192; /** * Array of modules that are not currently installed, indexed by className => filename * */ protected $installableFiles = array(); /** * An array of module database IDs indexed by module name/class * */ protected $moduleIDs = array(); /** * Array of module names/classes indexed by module ID * */ protected $moduleNames = array(); /** * Full system paths where modules are stored * * index 0 must be the core modules path (/i.e. /wire/modules/) * */ protected $paths = array(); /** * Have the modules been init'd() ? * */ protected $initialized = false; /** * Becomes an array if debug mode is on * */ protected $debugLog = 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 = null; /** * Dir for core modules relative to root path, i.e. '/wire/modules/' * * @var string * */ protected $coreModulesDir = ''; /** * Are we currently refreshing? * * @var bool * */ protected $refreshing = false; /** * Runtime caches * * @var array * @since 3.0.218 * */ protected $caches = array(); /** * @var ModulesInfo * */ protected $info; /** * @var ModulesFlags * */ protected $flags; /** * @var ModulesFiles * */ protected $files; /** * @var ModulesConfigs * */ protected $configs; /** * @var ModulesInstaller|null * */ protected $installer = null; /** * @var ModulesLoader * */ protected $loader = null; /** * 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); // paths[0] is always core modules path } /** * Wired to API * * #pw-internal * */ public function wired() { $this->coreModulesDir = '/' . $this->wire()->config->urls->data('modules'); parent::wired(); $this->info = new ModulesInfo($this); $this->flags = new ModulesFlags($this); $this->files = new ModulesFiles($this); $this->configs = new ModulesConfigs($this); $this->loader = new ModulesLoader($this); } /** * Get the ModulesDuplicates instance * * #pw-internal * * @return ModulesDuplicates * */ public function duplicates() { if($this->duplicates === null) $this->duplicates = $this->wire(new ModulesDuplicates()); return $this->duplicates; } /** * Get the ModulesInstaller instance * * #pw-internal * * @return ModulesInstaller * */ public function installer() { if($this->installer === null) $this->installer = new ModulesInstaller($this); return $this->installer; } /** * 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->loader->loadModulesTable(); $this->info->loadModuleInfoCache(); if(isset($this->paths[1])) { $this->loader->preloadModules($this->paths[1]); // site modules path } foreach($this->paths as $path) { $this->loader->loadPath($path); } $this->loader->loaded(); } /** * Trigger 'init' on all autoload modules, which calls their init() methods * * #pw-internal * */ public function triggerInit() { $this->loader->triggerInit(); } /** * Trigger 'ready' on all autoload modules, which calls their ready() methods * * This is to indicate to them that the API environment is fully ready. * * #pw-internal * */ public function triggerReady() { $this->loader->triggerReady(); } /********************************************************************************************** * WIREARRAY OVERRIDES * */ /** * 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 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() class return $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; } /********************************************************************************************** * GETTING/LOADING MODULES * */ /** * 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); } /** * 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()) { $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 found if(ctype_digit("$key")) { $moduleID = (int) $key; if(!isset($this->moduleNames[$moduleID])) { return empty($options['returnError']) ? null : "Unable to find module ID $moduleID"; } $key = $this->moduleNames[$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($this->moduleNames 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 created if($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 calls if($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 it if(array_key_exists($key, $this->getInstallable())) { // check if the request is for an uninstalled module // if so, install it and return it try { $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 module $page = $this->wire()->page; $user = $this->wire()->user; if(!$this->hasPermission($module, $user, $page)) { $error = $this->_('You do not have permission to execute this module') . ' - ' . wireClassName($module) . " (page=$page)"; 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 now if($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->loader->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; } /** * Get the requested module and reset cache + install it if necessary. * * This is exactly the same as getModule() 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->getModule($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 = '') { return $this->loader->includeModule($module, $file); } /** * Create a new Module instance * * Given a class name, return the constructed module or null * * #pw-internal * * @param string $className Module class name * @param string $moduleName Optional module name only (no namespace) * @return Module|null * */ public 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)) { $result = $this->includeModule($moduleName); if(!$result) return null; } if(!class_exists($className, false)) { // attempt 2.x module in dedicated namespace or root namespace $className = $this->info->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; } 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; } /** * 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) { return $this->loader->hasPermission($moduleName, $user, $page, $strict); } /********************************************************************************************** * FINDER METHODS * */ /** * 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; $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); } else 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 find if(!$verbose) { $moduleInfoVerboseKeys = $this->info->moduleInfoVerboseKeys; foreach($properties as $property) { if(!in_array($property, $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-empty if(!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 value if(!empty($info[$key])) $n++; } else { // match exact value if(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; } /** * 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; } /********************************************************************************************** * INSTALLER METHODS * */ /** * Get an associative array [name => filename] for all modules that aren’t currently installed. * * #pw-internal * * @return array Array of elements with $moduleName => $pathName * */ public function getInstallable() { return $this->installableFiles; } /** * 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); $class = (string) $class; if(empty($class)) return false; $operator = null; $requiredVersion = null; $currentVersion = null; if(!ctype_alnum($class)) { // class has something other than just a classname, likely operator + version if(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($requiredVersion !== null) { $currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire()->config->version; } } else { $installed = parent::get($class) !== null; if($installed && $requiredVersion !== null) { $info = $this->info->getModuleInfo($class); $currentVersion = $info['version']; } } if($installed && $currentVersion !== null) { $installed = $this->installer()->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) { return $this->installer()->isInstallable($class, $now); } /** * Return installable file pathname for given class or null if not present * * #pw-internal * * @param string|null $class Module class name or omit to get all in array * @param null|string|bool Specify string to set, boolean false to unset, otherwise omit * @return string|null|array * @since 3.0.219 * */ public function installableFile($class = null, $setPath = null) { if($class === null) return $this->installableFiles; if($setPath !== null) { if($setPath === false) { unset($this->installableFiles[$class]); } else if(is_string($setPath)) { $this->installableFiles[$class] = $setPath; } return $setPath; } return isset($this->installableFiles[$class]) ? $this->installableFiles[$class] : null; } /** * 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()) { return $this->installer()->install($class, $options); } /** * 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) { return $this->installer()->isUninstallable($class, $returnReason); } /** * Returns whether the module can be deleted (have its 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) { return $this->installer()->isDeleteable($class, $returnReason); } /** * Delete the given module, physically removing its files * * #pw-group-manipulation * * @param string $class Module name (class name) * @return bool * @throws WireException If module can't be deleted, exception will be thrown containing reason. * */ public function ___delete($class) { return $this->installer()->delete($class); } /** * Uninstall the given module name * * #pw-group-manipulation * * @param string $class Module name (class name) * @return bool * @throws WireException * */ public function ___uninstall($class) { return $this->installer()->uninstall($class); } /** * 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) { return $this->installer()->getUninstalls($class); } /********************************************************************************************** * MODULE FLAGS * */ /** * 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 * @deprecated * */ public function getFlags($class) { return $this->flags->getFlags($class); } /** * 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 * @deprecated * */ public function hasFlag($class, $flag) { return $this->flags->hasFlag($class, $flag); } /** * Set module flags * * #pw-internal * * @param $class * @param $flags * @return bool * @deprecated * */ public function setFlags($class, $flags) { return $this->flags->setFlags($class, $flags); } /** * 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 * @deprecated * */ public function setFlag($class, $flag, $add = true) { return $this->flags->setFlag($class, $flag, $add); } /********************************************************************************************** * MODULE INFO * */ /** * Get data from the module info cache * * Returns array of module info if given a module ID or name. * If module does not exist or info is not available, returns a blank array. * If not given a module ID or name, it returns an array of all modules info. * Returns value of property if given a property name, or null if not available. * * #pw-internal * * @param string|int|null $moduleID Module ID or name or omit to get info for all modules * @param string $property * @param bool $verbose * @return array|mixed|null * @since 3.0.218 * */ public function moduleInfoCache($moduleID = null, $property = '', $verbose = false) { return $this->info->moduleInfoCache($moduleID, $property, $verbose); } /** * Get data from the verbose module info cache * * #pw-internal * * @param int|string|null $moduleID * @param string $property * @return array|mixed|null * */ public function moduleInfoCacheVerbose($moduleID = null, $property = '') { return $this->info->moduleInfoCacheVerbose($moduleID, $property); } /** * Get or set module ID from name/class * * #pw-internal * * @param string|Module $name * @param int|null|false $setID Optionally set module ID or false to unset * @return int * */ public function moduleID($name, $setID = null) { if($name instanceof Module) $name = $name->className(); if(strpos("$name", '\\') !== false) $name = wireClassName($name, false); if($setID !== null) { if($setID === false) { unset($this->moduleIDs[$name]); } else { $setID = (int) $setID; $this->moduleIDs[$name] = $setID; } return $setID; } if(ctype_digit("$name")) return (int) $name; if(!is_string($name)) return $this->getModuleID($name); return isset($this->moduleIDs[$name]) ? $this->moduleIDs[$name] : 0; } /** * Get or set module name from ID * * #pw-internal * * @param int|string|Module $id * @param string|null $setName * @return string * */ public function moduleName($id, $setName = null) { if($id instanceof Module) { $name = $id->className(); if($setName === null) return $name; $id = $this->getModuleID($name); } else if(!ctype_digit("$id")) { if(strpos("$id", '\\') !== false) $id = wireClassName($id, false); if($setName === null && is_string($id)) return $id; $id = $this->getModuleID($id); } $id = (int) $id; if($setName) { $this->moduleNames[$id] = (string) $setName; return $setName; } return isset($this->moduleNames[$id]) ? $this->moduleNames[$id] : ''; } /** * Returns the database ID of a given module class, or 0 if not found * * @param string|int|Module $class Module, module name or ID * @return int * */ public function getModuleID($class) { $id = 0; if(ctype_digit("$class")) return (int) $class; if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"]; if(is_object($class)) { if(!$class instanceof Module) return 0; // class is not a module $class = $this->getModuleClass($class); } else if(strpos("$class", '\\') !== false) { $class = wireClassName($class, false); } if(isset($this->moduleIDs["$class"])) return (int) $this->moduleIDs["$class"]; foreach($this->info->moduleInfoCache as $key => $info) { if(is_string($info)) { $info = $this->info->moduleInfoCache($key); // json to array } if($info['name'] === $class) { $id = (int) $key; $this->moduleIDs[$class] = $id; break; } } return $id; } /** * Returns the module's class/name, optionally with namespace * * - 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(ctype_digit("$module")) { $className = $this->moduleName((int) $module); } 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 name if(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->installableFiles)) { $className = $module; } } if($className) { if($withNamespace) { if($namespace) { $className = "$namespace\\$className"; } else { $className = $this->info->getModuleNamespace($className) . $className; } } return $className; } return false; } /** * Returns an associative array of information for a Module * * The array returned by this method includes the following: * * - `id` (int): module database ID. * - `name` (string): module class name. * - `title` (string): module title. * - `version` (int): module version. * - `icon` (string): Optional icon name (excluding the "fa-") part. * - `requires` (array): module names required by this module. * - `requiresVersions` (array): required module versions–module name is key, value is array($operator, $version). * - `installs` (array): module names that this module installs. * - `permission` (string): permission name required to execute this module. * - `autoload` (bool): true if module is autoload, false if not. * - `singular` (bool): true if module is singular, false if not. * - `created` (int): unix-timestamp of date/time module added to system (for uninstalled modules, it is the file date). * - `installed` (bool): is the module currently installed? (boolean, or null when not determined) * - `configurable` (bool|int): true or positive number when the module is configurable. * - `namespace` (string): PHP namespace that module lives in. * * The following properties are also included when "verbose" mode is requested. When not in verbose mode, these * properties may be present but with empty values: * * - `versionStr` (string): formatted module version string. * - `file` (string): module filename from PW installation root, or false when it can't be found. * - `core` (bool): true when module is a core module, false when not. * - `author` (string): module author, when specified. * - `summary` (string): summary of what this module does. * - `href` (string): URL to module details (when specified). * - `permissions` (array): permissions installed by this module, associative array ('permission-name' => 'Description'). * - `page` (array): definition of page to create for Process module (see Process class) * * The following properties appear only for "Process" modules, and only if specified by module. * See the Process class for more details: * * - `nav` (array): navigation definition * - `useNavJSON` (bool): whether the Process module provides JSON navigation * - `permissionMethod` (string|callable): method to call to determine permission * - `page` (array): definition of page to create for Process module * * On error, an `error` index in returned array contains error message. You can also identify errors * such as a non-existing module by the returned module info having an `id` index of `0` * * ~~~~~ * // example of getting module info * $moduleInfo = $modules->getModuleInfo('InputfieldCKEditor'); * * // example of getting verbose module info * $moduleInfo = $modules->getModuleInfoVerbose('MarkupAdminDataTable'); * ~~~~~ * * @param string|Module|int $class Specify one of the following: * - Module object instance * - Module class name (string) * - Module ID (int) * - To get info for ALL modules, specify `*` or `all`. * - To get system information, specify `ProcessWire` or `PHP`. * - To get a blank module info template, specify `info`. * @param array $options Optional options to modify behavior of what gets returned * - `verbose` (bool): Makes the info also include verbose properties, which are otherwise blank. (default=false) * - `minify` (bool): Remove non-applicable and properties that match defaults? (default=false, or true when getting `all`) * - `noCache` (bool): prevents use of cache to retrieve the module info. (default=false) * @return array Associative array of module information. * - On error, an `error` index is also populated with an error message. * - When requesting a module that does not exist its `id` value will be `0` and its `name` will be blank. * @see 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()) { return $this->info->getModuleInfo($class, $options); } /** * 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()) { return $this->info->getModuleInfoVerbose($class, $options); } /** * 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()) { return $this->info->getModuleInfoProperty($class, $property, $options); } /** * Get an array of all unique, non-default, non-root module namespaces mapped to directory names * * #pw-internal * * @return array * */ public function getNamespaces() { return $this->info->getNamespaces(); } /** * 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()) { return $this->info->getModuleNamespace($moduleName, $options); } /** * 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) { return $this->info->getNamespacePath($namespace); } /********************************************************************************************** * MODULE CONFIG * */ /** * 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 = '') { return $this->configs->getConfig($class, $property); } /** * 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) { return $this->configs->saveConfig($class, $data, $value); } /** * 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 * */ public function isConfigurable($class, $useCache = true) { return $this->configs->isConfigurable($class, $useCache); } /** * 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. * * #pw-internal * * @param string|Module $class * @param bool $useCache * @return bool|int|string * @since 3.0.179 * */ public function isConfigable($class, $useCache = true) { return $this->configs->isConfigable($class, $useCache); } /** * Alias of isConfigurable() for backwards compatibility * * #pw-internal * * @param $className * @param bool $useCache * @return bool|string|int * @deprecated Please use isConfigurable() method instead * */ public function isConfigurableModule($className, $useCache = true) { return $this->configs->isConfigurable($className, $useCache); } /** * Alias of saveConfig() for backwards compatibility * * * #pw-internal * * @param $className * @param array $configData * @return bool * @deprecated Please use saveConfig() method instead * */ public function ___saveModuleConfigData($className, array $configData) { return $this->saveConfig($className, $configData); } /** * 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) { return $this->configs->getModuleConfigInputfields($moduleName, $form); } /** * Alias of getConfig() for backwards compatibility * * #pw-internal * * @param string|Module $className * @return array * @deprecated Please use getConfig() instead * */ public function getModuleConfigData($className) { return $this->configs->getConfig($className); } /** * Return the URL where the module can be edited, configured or uninstalled * * If module is not installed, it returns URL to install the module. * * #pw-group-configuration * * @param string|Module $className * @param bool $collapseInfo * @return string * */ public function getModuleEditUrl($className, $collapseInfo = true) { return $this->configs->getModuleEditUrl($className, $collapseInfo); } /** * Get URL where an administrator can install given module name * * If module is already installed, it returns the URL to edit the module. * * #pw-group-configuration * * @param string $className * @return string * @since 3.0.216 * */ public function getModuleInstallUrl($className) { return $this->installer()->getModuleInstallUrl($className); } /************************************************************************************************ * TOOLS * */ /** * 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->info->getModuleInfo($module); if(isset($info['singular'])) 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 moduleInfo if(isset($this->installableFiles[$module])) return null; $this->loader->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->info->getModuleInfo($module); $autoload = null; if(isset($info['autoload'])) { // if autoload is a string (selector) or callable, then we flag it as autoload if(is_string($info['autoload']) || wireIsCallable($info['autoload'])) return "conditional"; $autoload = $info['autoload']; } else if(!is_object($module)) { if(isset($this->installableFiles[$module])) { // module is not installed // we are not going to be able to determine if this is autoload or not $flags = $this->flags->getFlags($module); if($flags !== null) { $autoload = $flags & Modules::flagsAutoload; } else { // unable to determine return null; } } else { // include for method exists call $this->loader->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($set = null) { if($set !== null) $this->initialized = $set ? true : false; 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->installableFiles[$moduleName])) { $isModule = true; } else { $isModule = false; } if($isModule && $namespace) { $actualNamespace = $this->info->getModuleNamespace($moduleName); if(trim("$namespace", '\\') != trim("$actualNamespace", '\\')) { $isModule = false; } } return $isModule; } /** * 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->info->clearModuleInfoCache($showMessages); $this->loader->loadModulesTable(); foreach($this->paths as $path) $this->files->findModuleFiles($path, false); foreach($this->paths as $path) $this->loader->loadPath($path); if($this->duplicates()->numNewDuplicates() > 0) $this->duplicates()->updateDuplicates(); // PR#1020 $this->loader->loaded(); $this->refreshing = false; } /** * Alias of refresh() method for backwards compatibility * * #pw-internal * * @deprecated Use refresh() method instead * */ public function resetCache() { $this->refresh(); } /************************************************************************************* * DEPENDENCIES * */ /** * 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) { return $this->installer()->getRequiredBy($class, $uninstalled, $installs); } /** * 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) { return $this->installer()->getRequires($class, $onlyMissing, $versions); } /** * 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) { return $this->installer()->versionCompare($currentVersion, $requiredVersion, $operator); } /** * 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->installer()->getRequiresForInstall($class); } /** * 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->installer()->getRequiresForInstall($class); } /** * Return array of dependency errors for given module name * * #pw-internal * * @param string $moduleName * @return array If no errors, array will be blank. If errors, array will be of strings (error messages) * */ public function getDependencyErrors($moduleName) { return $this->installer()->getDependencyErrors($moduleName); } /** * 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() { return $this->files->findMissingModules(); } /** * 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 = substr($version, 0, 1) . '.' . substr($version, 1, 1) . '.' . substr($version, 2); } else if(strpos($version, '.') !== false) { // version is a formatted string if(strpos($version, '.') == strrpos($version, '.')) { // only 1 period, like: 2.0, convert that to 2.0.0 if(preg_match('/^\d\.\d$/', $version)) $version .= ".0"; } } else { // invalid version? } if(!strlen($version)) $version = '0.0.0'; return $version; } /** * Hook called when a module's version changes * * This calls the module's ___upgrade($fromVersion, $toVersion) method. * * #pw-internal * * @param Module|_Module $module * @param int|string $fromVersion * @param int|string $toVersion * */ public function ___moduleVersionChanged(Module $module, $fromVersion, $toVersion) { $this->info->moduleVersionChanged($module, $fromVersion, $toVersion); } /************************************************************************************* * SUBSTITUTES * */ /** * 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); } /** * Attempt to find a substitute for moduleName and return module if found or null if not * * #pw-internal * * @param $moduleName * @param array $options See getModule() options * @return Module|null * */ protected function getSubstituteModule($moduleName, array $options = array()) { $module = null; $options['noSubstitute'] = true; // prevent recursion while(isset($this->substitutes[$moduleName]) && !$module) { $substituteName = $this->substitutes[$moduleName]; $module = $this->getModule($substituteName, $options); if(!$module) $moduleName = $substituteName; } return $module; } /************************************************************************************* * FILES * */ /** * 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()) { return $this->files->getModuleFile($class, $options); } /** * 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) { return $this->files->loadModuleFileAssets($module); } /** * 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) { return $this->files->getModuleLanguageFiles($module); } /** * 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 * @deprecated * */ public function getFileNamespace($file) { return $this->files->getFileNamespace($file); } /************************************************************************************* * DEBUG AND CLASS SUPPORT * */ /** * Enables use of $modules('ModuleName') * * @param string $key * @return Module|null * */ public function __invoke($key) { return $this->get($key); } /** * Save to the modules log * * #pw-internal * * @param string $str Message to log * @param array|string $options Specify module name (string) or options array * @return WireLog * */ public function log($str, $options = array()) { $moduleName = is_string($options) ? $options : ''; if(!is_array($options)) $options = array(); 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)"; $options['name'] = 'modules'; return $this->___log($str, $options); } /** * Record and log error message * * #pw-internal * * @param array|Wire|string $text * @param int $flags * @return Modules|WireArray * */ public function error($text, $flags = 0) { if(is_string($text)) $this->log($text); return parent::error($text, $flags); } /** * Start a debug timer, only works when module debug mode is on ($this->debug) * * #pw-internal * * @param $note * @return int|null Returns a key for the debug timer * */ public 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) * * #pw-internal * * @param int $key The key returned by debugTimerStart * */ public 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; } public function getDebugData() { return array( 'installableFiles' => $this->installableFiles, 'moduleIDs' => $this->moduleIDs, 'moduleNames' => $this->moduleNames, 'paths' => $this->paths, 'substitutes' => $this->substitutes, 'caches' => $this->caches, 'loader' => $this->loader->getDebugData(), 'configs' => $this->configs->getDebugData(), 'installer' => $this->installer()->getDebugData(), 'files' => $this->files->getDebugData(), 'flags' => $this->flags->getDebugData(), 'info' => $this->info->getDebugData(), 'duplicates' => $this->duplicates()->getDebugData() ); } /************************************************************************************* * CACHES * */ /** * Set a runtime memory cache * * @param string $name * @param mixed $setValue * @return bool|array|mixed|null * */ public function memcache($name, $setValue = null) { if($setValue) { $this->caches[$name] = $setValue; return true; } return isset($this->caches[$name]) ? $this->caches[$name] : null; } /** * Save cache * * #pw-internal * * @param string $cacheName * @param string|array $data * @return bool * @since 3.0.218 * */ public function saveCache($cacheName, $data) { $database = $this->wire()->database; if(!$this->saveCacheReady) { $this->saveCacheReady = true; $col = $database->getColumns('modules', 'data'); if(strtolower($col['type']) === 'text') { try { // increase size of data column for cache storage in 3.0.218 $database->exec("ALTER TABLE modules MODIFY `data` MEDIUMTEXT NOT NULL"); $this->message("Updated modules.data to mediumtext", Notice::debug); } catch(\Exception $e) { $this->error($e->getMessage()); } } } $cache = $this->wire()->cache; if($cache) $cache->save($cacheName, $data, WireCache::expireReserved); if(is_array($data)) $data = json_encode($data); $sql = "INSERT INTO modules SET class=:name, data=:data, flags=:flags ON DUPLICATE KEY UPDATE data=VALUES(data)"; $query = $database->prepare($sql); $query->bindValue(':name', ".$cacheName"); $query->bindValue(':data', $data); $query->bindValue(':flags', Modules::flagsSystemCache); return $query->execute(); } protected $saveCacheReady = false; /** * Get cache * * #pw-internal * * @param string $cacheName * @return string|array|bool * @since 3.0.218 * */ public function getCache($cacheName) { $data = null; if(isset($this->caches[$cacheName])) { $data = $this->caches[$cacheName]; unset($this->caches[$cacheName]); } if(empty($data)) { $sql = "SELECT data FROM modules WHERE class=:name"; $query = $this->wire()->database->prepare($sql); $query->bindValue(':name', ".$cacheName"); $query->execute(); $data = $query->fetchColumn(); $query->closeCursor(); } if(empty($data)) { // fallback to $cache API var, necessary only temporarily $data = $this->wire()->cache->get($cacheName); if($data) return $data; } if(is_string($data) && (strpos($data, '{') === 0 || strpos($data, '[') === 0)) { $data = json_decode($data, true); } return $data; } /** * Delete cache by name * * #pw-internal * * @param string $cacheName * @since 3.0.218 * */ public function deleteCache($cacheName) { $this->wire()->cache->delete($cacheName); unset($this->caches[$cacheName]); } /** * Direct read-only properties * * @param string $name * @return mixed * */ public function __get($name) { switch($name) { case 'loader': return $this->loader; case 'info': return $this->info; case 'configs': return $this->configs; case 'flags': return $this->flags; case 'files': return $this->files; case 'installableFiles': return $this->installableFiles; case 'coreModulesDir': return $this->coreModulesDir; case 'coreModulesPath': return $this->paths[0]; case 'siteModulesPath': return isset($this->paths[1]) ? $this->paths[1] : ''; case 'moduleIDs': return $this->moduleIDs; case 'moduleNames': return $this->moduleNames; case 'refreshing': return $this->refreshing; } return parent::__get($name); } }