praiadeseselle/wire/core/Modules.php

5507 lines
178 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php namespace ProcessWire;
/**
* ProcessWire Modules
*
* Loads and manages all runtime modules for ProcessWire
*
* Note that when iterating, find(), or calling any other method that returns module(s), excepting get(), a ModulePlaceholder may be
* returned rather than a real Module. ModulePlaceholders are used in instances when the module may or may not be needed at runtime
* in order to save resources. As a result, anything iterating through these Modules should check to make sure it's not a ModulePlaceholder
* before using it. If it's a ModulePlaceholder, then the real Module can be instantiated/retrieved by $modules->get($className).
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Loads and manages all modules in ProcessWire.
* #pw-body =
* The `$modules` API variable is most commonly used for getting individual modules to use their API.
* ~~~~~
* // Getting a module by name
* $m = $modules->get('MarkupPagerNav');
*
* // Getting a module by name (alternate)
* $m = $modules->MarkupPagerNav;
* ~~~~~
*
* #pw-body
*
* @todo Move all module information methods to a ModulesInfo class
* @todo Move all module loading methods to a ModulesLoad class
*
* @method void refresh($showMessages = false) Refresh the cache that stores module files by recreating it
* @method null|Module install($class, $options = array())
* @method bool|int delete($class)
* @method bool uninstall($class)
* @method bool saveModuleConfigData($className, array $configData) Alias of saveConfig() method #pw-internal
* @method bool saveConfig($class, $data, $value = null)
* @method InputfieldWrapper|null getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) #pw-internal
* @method void moduleVersionChanged(Module $module, $fromVersion, $toVersion) #pw-internal
* @method bool|string isUninstallable($class, $returnReason = false) hookable in 3.0.181+ #pw-internal
*
*/
class Modules extends WireArray {
/**
* Whether or not module debug mode is active
*
*/
protected $debug = false;
/**
* Flag indicating the module may have only one instance at runtime.
*
*/
const flagsSingular = 1;
/**
* Flag indicating that the module should be instantiated at runtime, rather than when called upon.
*
*/
const flagsAutoload = 2;
/**
* Flag indicating the module has more than one copy of it on the file system.
*
*/
const flagsDuplicate = 4;
/**
* When combined with flagsAutoload, indicates that the autoload is conditional
*
*/
const flagsConditional = 8;
/**
* When combined with flagsAutoload, indicates that the module's autoload state is temporarily disabled
*
*/
const flagsDisabled = 16;
/**
* Indicates module that maintains a configurable interface but with no interactive Inputfields
*
*/
const flagsNoUserConfig = 32;
/**
* Module where no file could be located
*
*/
const flagsNoFile = 64;
/**
* Filename for module info cache file
*
*/
const moduleInfoCacheName = 'Modules.info';
/**
* Filename for verbose module info cache file
*
*/
const moduleInfoCacheVerboseName = 'ModulesVerbose.info';
/**
* Filename for uninstalled module info cache file
*
*/
const moduleInfoCacheUninstalledName = 'ModulesUninstalled.info';
/**
* Cache name for module version change cache
*
*/
const moduleLastVersionsCacheName = 'ModulesVersions.info';
/**
* Array of modules that are not currently installed, indexed by className => filename
*
*/
protected $installable = array();
/**
* An array of module database IDs indexed by: class => id
*
* Used internally for database operations
*
*/
protected $moduleIDs = array();
/**
* Full system paths where modules are stored
*
* index 0 must be the core modules path (/i.e. /wire/modules/)
*
*/
protected $paths = array();
/**
* Cached module configuration data indexed by module ID
*
* Values are integer 1 for modules that have config data but data is not yet loaded.
* Values are an array for modules have have config data and has been loaded.
*
*/
protected $configData = array();
/**
* Module created dates indexed by module ID
*
*/
protected $createdDates = array();
/**
* Have the modules been init'd() ?
*
*/
protected $initialized = false;
/**
* Becomes an array if debug mode is on
*
*/
protected $debugLog = array();
/**
* Array of moduleName => condition
*
* Condition can be either an anonymous function or a selector string to be evaluated at ready().
*
*/
protected $conditionalAutoloadModules = array();
/**
* Cache of module information
*
*/
protected $moduleInfoCache = array();
/**
* Cache of module information (verbose text) including: summary, author, href, file, core
*
*/
protected $moduleInfoCacheVerbose = array();
/**
* Cache of uninstalled module information (verbose for uninstalled) including: summary, author, href, file, core
*
* Note that this one is indexed by class name rather than by ID (since uninstalled modules have no ID)
*
*/
protected $moduleInfoCacheUninstalled = array();
/**
* Cache of module information from DB used across multiple calls temporarily by load() method
*
*/
protected $modulesTableCache = array();
/**
* Cache of namespace => path for unique module namespaces
*
* @var array|null Becomes an array once populated
*
*/
protected $moduleNamespaceCache = null;
/**
* Last known versions of modules, for version change tracking
*
* @var array of ModuleName (string) => last known version (integer|string)
*
*/
protected $modulesLastVersions = array();
/**
* Array of module ID => flags (int)
*
* @var array
*
*/
protected $moduleFlags = array();
/**
* Array of moduleName => substituteModuleName to be used when moduleName doesn't exist
*
* Primarily for providing backwards compatiblity with modules assumed installed that
* may no longer be in core.
*
* see setSubstitutes() method
*
*/
protected $substitutes = array();
/**
* Instance of ModulesDuplicates
*
* @var ModulesDuplicates
*
*/
protected $duplicates;
/**
* Module file extensions indexed by module name where value 1=.module, and 2=.module.php
*
* @var array
*
*/
protected $moduleFileExts = array();
/**
* Dir for core modules relative to root path, i.e. '/wire/modules/'
*
* @var string
*
*/
protected $coreModulesDir = '';
/**
* Array of moduleName => order to indicate autoload order when necessary
*
* @var array
*
*/
protected $autoloadOrders = array();
/**
* Are we currently refreshing?
*
* @var bool
*
*/
protected $refreshing = false;
/**
* Properties that only appear in 'verbose' moduleInfo
*
* @var array
*
*/
protected $moduleInfoVerboseKeys = array(
'summary',
'author',
'href',
'file',
'core',
'versionStr',
'permissions',
'searchable',
'page',
// 'languages',
);
/**
* Core module types that are isolated by directory
*
* @var array
*
*/
protected $coreTypes = array(
'AdminTheme',
'Fieldtype',
'Inputfield',
'Jquery',
'LanguageSupport',
'Markup',
'Process',
'Session',
'System',
'Textformatter',
);
/**
* Construct the Modules
*
* @param string $path Core modules path (you may add other paths with addPath method)
*
*/
public function __construct($path) {
parent::__construct();
$this->addPath($path);
}
/**
* Wired to API
*
* #pw-internal
*
*/
public function wired() {
$this->coreModulesDir = '/' . $this->wire('config')->urls->data('modules');
parent::wired();
}
/**
* Get the ModulesDuplicates instance
*
* #pw-internal
*
* @return ModulesDuplicates
*
*/
public function duplicates() {
if(is_null($this->duplicates)) $this->duplicates = $this->wire(new ModulesDuplicates());
return $this->duplicates;
}
/**
* Add another modules path, must be called before init()
*
* #pw-internal
*
* @param string $path
*
*/
public function addPath($path) {
$this->paths[] = $path;
}
/**
* Return all assigned module root paths
*
* #pw-internal
*
* @return array of modules paths, with index 0 always being the core modules path.
*
*/
public function getPaths() {
return $this->paths;
}
/**
* Initialize modules
*
* Must be called after construct before this class is ready to use
*
* #pw-internal
*
* @see load()
*
*/
public function init() {
$this->setTrackChanges(false);
$this->loadModuleInfoCache();
$this->loadModulesTable();
if(!empty($this->autoloadOrders)) $this->preloadModules();
foreach($this->paths as $path) {
$this->load($path);
}
$this->modulesTableCache = array(); // clear out data no longer needed
}
/**
* Include site preload modules
*
* Preload modules load before all other modules, including core modules. In order
* for a module to be a preload module, it must meet the following conditions:
*
* - Module info `autoload` value is integer of 10000 or greater, i.e. `[ 'autoload' => 10000 ]`
* - Module info `singular` value must be non-empty, i.e. `[ 'singular' => true ]`
* - Module file is located in: /site/modules/ModuleName/ModuleName.module.php
* - Module cannot load any other modules at least until ready() method called.
* - Module cannot have any `requires` dependencies to any other modules.
*
* Please note the above is specifically stating that the module must be in its
* own “site/ModuleName/” directory and have the “.module.php” extension. Using
* just the “.module” extension is not supported for preload modules.
*
* @since 3.0.173
*
*/
protected function preloadModules() {
if(!isset($this->paths[1])) return;
arsort($this->autoloadOrders);
foreach($this->autoloadOrders as $moduleName => $order) {
if($order < 10000) break;
if(!isset($this->moduleIDs[$moduleName])) continue;
$moduleID = $this->moduleIDs[$moduleName];
if(!isset($this->moduleInfoCache[$moduleID])) continue;
$info = $this->moduleInfoCache[$moduleID];
if(empty($info['singular'])) continue;
$file = $this->paths[1] . "$moduleName/$moduleName.module.php";
if(!file_exists($file) || !$this->includeModuleFile($file, $moduleName)) continue;
if(!isset($info['namespace'])) $info['namespace'] = '';
$className = $info['namespace'] . $moduleName;
$module = $this->newModule($className, $moduleName);
if($module) parent::set($moduleName, $module);
}
}
/**
* Modules class accepts only Module instances, per the WireArray interface
*
* #pw-internal
*
* @param Wire $item
* @return bool
*
*/
public function isValidItem($item) {
return $item instanceof Module;
}
/**
* The key/index used for each module in the array is it's class name, per the WireArray interface
*
* #pw-internal
*
* @param Wire $item
* @return int|string
*
*/
public function getItemKey($item) {
return $this->getModuleClass($item);
}
/**
* There is no blank/generic module type, so makeBlankItem returns null
*
* #pw-internal
*
*/
public function makeBlankItem() {
return null;
}
/**
* Make a new/blank WireArray
*
* #pw-internal
*
*/
public function makeNew() {
// ensures that find(), etc. operations don't initalize a new Modules() 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 modules 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((string) $this->wire()->config->paths->$basename, '/');
if($module && $dir && $dirname != $dir) {
$duplicates->recordDuplicate($basename, $pathname, "$dir/$filename", $installed);
return '';
}
if($module) return $basename;
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the 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);
if(method_exists($className, $method)) return $className::$method($data);
return false;
}
return true;
}
/**
* Get the requested module and reset cache + install it if necessary.
*
* This is exactly the same as get() except that this one will rebuild the modules cache if
* it doesn't find the module at first. If the module is on the file system, this
* one will return it in some instances that a regular get() can't.
*
* #pw-internal
*
* @param string|int $key Module className or database ID
* @return Module|null
*
*/
public function getInstall($key) {
$module = $this->get($key);
if(!$module) {
$this->refresh();
$module = $this->getModule($key);
}
return $module;
}
/**
* Include the file for a given module, but don't instantiate it
*
* #pw-internal
*
* @param ModulePlaceholder|Module|string Expects a ModulePlaceholder or className
* @param string $file Optionally specify the module filename if you already know it
* @return bool true on success, false on fail or unknown
*
*/
public function includeModule($module, $file = '') {
$className = '';
$moduleName = '';
if(is_string($module)) {
$moduleName = ctype_alnum($module) ? $module : wireClassName($module);
$className = wireClassName($module, true);
} else if(is_object($module)) {
if($module instanceof ModulePlaceholder) {
$moduleName = $module->className();
$className = $module->className(true);
} else if($module instanceof Module) {
return true; // already included
}
} else {
$moduleName = $this->getModuleClass($module, false);
$className = $this->getModuleClass($module, true);
}
if(!$className) return false;
// already 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 arent 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 versionsmodule name is key, value is array($operator, $version).
* - `installs` (array): module names that this module installs.
* - `permission` (string): permission name required to execute this module.
* - `autoload` (bool): true if module is autoload, false if not.
* - `singular` (bool): true if module is singular, false if not.
* - `created` (int): unix-timestamp of date/time module added to system (for uninstalled modules, it is the file date).
* - `installed` (bool): is the module currently installed? (boolean, or null when not determined)
* - `configurable` (bool|int): true or positive number when the module is configurable.
* - `namespace` (string): PHP namespace that module lives in.
*
* The following properties are also included when "verbose" mode is requested. When not in verbose mode, these
* properties 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 whats returned by `Modules::getModuleInfo()` except that it has the following additional properties:
*
* - `versionStr` (string): formatted module version string.
* - `file` (string): module filename from PW installation root, or false when it can't be found.
* - `core` (bool): true when module is a core module, false when not.
* - `author` (string): module author, when specified.
* - `summary` (string): summary of what this module does.
* - `href` (string): URL to module details (when specified).
* - `permissions` (array): permissions installed by this module, associative array ('permission - name' => 'Description').
* - `page` (array): definition of page to create for Process module (see Process class)
*
* @param string|Module|int $class May be class name, module instance, or module ID
* @param array $options Optional options to modify behavior of what gets returned:
* - `noCache` (bool): prevents use of cache to retrieve the module info
* - `noInclude` (bool): prevents include() of the module file, applicable only if it hasn't already been included
* @return array Associative array of module information
* @see Modules::getModuleInfo()
*
*/
public function getModuleInfoVerbose($class, array $options = array()) {
$options['verbose'] = true;
$info = $this->getModuleInfo($class, $options);
return $info;
}
/**
* Get just a single property of module info
*
* @param Module|string $class Module instance or module name
* @param string $property Name of property to get
* @param array $options Additional options (see getModuleInfo method for options)
* @return mixed|null Returns value of property or null if not found
* @since 3.0.107
*
*/
public function getModuleInfoProperty($class, $property, array $options = array()) {
if(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']))) .
'<pre>' . $sanitizer->entities(implode("\n", $report['items'])) . '</pre>',
Notice::allowMarkup | Notice::noGroup
);
$this->log(
sprintf($report['label'], count($report['items'])) . ' ' .
implode(', ', $report['items'])
);
}
}
/**
* Update the cache of queued module version changes
*
*/
protected function updateModuleVersionsCache() {
foreach($this->modulesLastVersions as $id => $version) {
// clear out stale data, if 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;
}
}