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); } 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; $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() 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; } /** * 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 autoload if(!$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 modules foreach($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 it if($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 load if($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 completed if(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 it if($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 known foreach($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 string if(!$this->wire('page')->is($func)) $load = false; } else { // anonymous function if(!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 modules foreach($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 applicable if($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 loaded foreach($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 one if(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 module unset($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 one if($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 duplicate if(!$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 duplicates if(wireClassExists($basename, false)) { $module = parent::get($basename); $dir = rtrim($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 next if(!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 continue if(strpos($pathname, $basepath) !== 0) return ''; // if the file isn't there, it was probably uninstalled, so ignore it if(!file_exists($pathname)) return ''; // if the module isn't installed, then stop and move on to next if(!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 met if(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 requirements return $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() function if(!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 autoload if(!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 instantiated if(!$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 it if($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 order arsort($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 recursion while(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 found if(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 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 if(!$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 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->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 permission if(is_null($page)) $page = $this->wire('page'); $data = array( 'wire' => $this->wire(), 'page' => $page, 'user' => $user, 'info' => $info, ); $method = $info['permissionMethod']; $this->includeModule($moduleName); return $className::$method($data); } 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 included if(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 module if(!$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? fail if(!$file) return false; } if(!$this->includeModuleFile($file, $moduleName)) { // module file failed to include(), try to identify and include file again if($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 class if(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) { // great return true; } else if(!$module) { // darn return 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 module return 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 active if($wire1 !== $wire2) { // multi-instance is active, don't autoload module if class already exists // first do a fast check, which should catch any core modules if(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-instance if($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 find if(!$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-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; } /** * 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 + 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(!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 them foreach($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 there try { $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 $basename if($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.module foreach($this->paths as $key => $modulesPath) { if($key === 0) continue; // skip core modules path if(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 files if(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 individually foreach($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 installing foreach($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 list if(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 module if(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 installing foreach($info['installs'] as $name) { // if module isn't installed, then great if(!$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 module return $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 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->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 automatically if(!is_array($info) || !count($info)) $this->error("Invalid PHP module info file for $moduleName"); } else if(file_exists($fileJSON)) { $info = file_get_contents($fileJSON); $info = json_decode($info, true); if(!$info) { $info = array(); $this->error("Invalid JSON module info file for $moduleName"); } } return $info; } /** * Retrieve module info from internal getModuleInfo function in the class * * @param Module|string $module * @param string $namespace * @return array * */ 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 system if($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 available if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) { $info = $this->moduleInfoCache[$moduleID]; $fromCache = true; } else { $info = $this->getModuleInfoExternal($moduleName); if(!count($info)) $info = $this->getModuleInfoInternal($module); } } else { // module is a class name or ID if(ctype_digit("$module")) $module = $moduleName; // return from cache if available (as it almost always should be) if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) { $info = $this->moduleInfoCache[$moduleID]; $fromCache = true; } else if(empty($options['noCache']) && $moduleID == 0) { // uninstalled module if(!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 scenario if($options['verbose']) { if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose(); if(!empty($this->moduleInfoCacheVerbose[$moduleID])) { $info = array_merge($info, $this->moduleInfoCacheVerbose[$moduleID]); } } // populate defaults for properties omitted from cache if(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 one if(!is_array($info['requires'])) { $info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespace if(strpos($info['requires'], ',') !== false) { $info['requires'] = explode(',', $info['requires']); } else { $info['requires'] = array($info['requires']); } } // populate requiresVersions foreach($info['requires'] as $key => $class) { if(!ctype_alnum($class)) { // has a version string list($class, $operator, $version) = $this->extractModuleOperatorVersion($class); $info['requires'][$key] = $class; // convert to just class } else { // no version string $operator = '>='; $version = 0; } $info['requiresVersions'][$class] = array($operator, $version); } // what does it install? // if $info[installs] isn't already an array, make it one if(!is_array($info['installs'])) { $info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespace if(strpos($info['installs'], ',') !== false) { $info['installs'] = explode(',', $info['installs']); } else { $info['installs'] = array($info['installs']); } } // misc if($options['verbose']) $info['versionStr'] = $this->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 date if(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; } // namespace if($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 removed if(!$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 data if(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 object if(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 locally if(!$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 error foreach($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 Reflection try { // note we don't call getModuleClass() here because it may result in a circular reference if(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 time if(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 now if($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 check if($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 know if(!$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 method if(!$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 method if($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 arguments if(!$result) $result = 2; } else { // non-static getModuleConfigArray $result = 19; } // if we make it here, we know we already have a result so can stop now break; } 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 property unset($data[$property]); } else { // populate the value for the property $data[$property] = $value; } } else { // data must be an associative array of configuration data if(!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 config if($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 moduleInfo if(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 autoload if(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 determine return 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 it if(!$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 wants unset($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 met unset($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 installed unset($requires[$key]); } else if(empty($requiresVersion)) { // just the class name is fine continue; } 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 requires if(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 specified return array($require, '', 0); } $operators = array('<=', '>=', '<', '>', '!=', '='); $operator = ''; foreach($operators as $o) { if(strpos($require, $o)) { $operator = $o; break; } } // if no operator found, then no version is being specified if(!$operator) return array($require, '', 0); // extract class and version list($class, $version) = explode($operator, $require); // make version an integer if possible if(ctype_digit("$version")) $version = (int) $version; return array($class, $operator, $version); } /** * 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 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; } /** * 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-creation if(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 changes foreach($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 moduleInfo if($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']))) . '
' . $sanitizer->entities(implode("\n", $report['items'])) . '
', 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 present if(!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 autoload if(!($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 flag if($flags & self::flagsConditional) $this->setFlag($moduleID, self::flagsConditional, false); } } else if($info['autoload'] !== null) { // module is not autoload if($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 properties foreach(array(0 => 'removeFlag', 1 => 'addFlag') as $add => $flagsType) { if(empty($info[$flagsType])) continue; if($flags & $info[$flagsType]) { // already has the flags if(!$add) { // remove the flag(s) $this->setFlag($moduleID, $info[$flagsType], false); } } else { // does not have the flags if($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 modules if(!empty($info['error'])) { if($this->debug) $this->warning("$class reported error: $info[error]"); continue; } if(!$moduleID && $installed) { if($this->debug) $this->warning("No module ID for $class"); continue; } if(!$this->debug) unset($info['id']); // no need to double store this property since it is already the array key if(is_null($info['autoload'])) { // module info does not indicate an autoload state $info['autoload'] = $this->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 properties if($key == 'installed') { // no need to store an installed==true property if($value) unset($data[$moduleID][$key]); } else if($key == 'requires' && !empty($value) && !empty($data[$moduleID]['requiresVersions'])) { // requiresVersions has enough info to re-construct requires, so no need to store it unset($data[$moduleID][$key]); } else if(($key == 'created' && empty($value)) || ($value === 0 && ($key == 'singular' || $key == 'autoload' || $key == 'configurable')) || ($value === null || $value === "" || $value === false) || (is_array($value) && !count($value))) { // no need to store these false, null, 0, or blank array properties unset($data[$moduleID][$key]); } else if(($key == 'namespace' && $value == "\\" . __NAMESPACE__ . "\\") || (!strlen(__NAMESPACE__) && empty($value))) { // no need to cache default namespace in module info unset($data[$moduleID][$key]); } else if($key == 'file') { // file property is cached elsewhere so doesn't need to be included in this cache unset($data[$moduleID][$key]); } } } $this->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 down if(empty($file)) $file = $this->getModuleFile($moduleName); // don't compile when module compilation is disabled if(!$allowCompile) return $file; // don't compile core modules if(strpos($file, $this->coreModulesDir) !== false) return $file; // if namespace not provided, get it if(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 used if(__NAMESPACE__) { $compile = $namespace === '\\' || empty($namespace); } else { $compile = trim($namespace, '\\') === 'ProcessWire'; } // compile if necessary if($compile) { /** @var FileCompiler $compiler */ $compiler = $this->wire(new FileCompiler(dirname($file))); $compiledFile = $compiler->compile(basename($file)); if($compiledFile) $file = $compiledFile; } return $file; } }