5507 lines
178 KiB
PHP
5507 lines
178 KiB
PHP
<?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 module’s config data (default=[]) 3.0.169+
|
||
* - `throw` (bool): When true, exceptions will be allowed to pass through. (default=false)
|
||
* @return bool True on success, false on fail
|
||
* @throws \Exception Only if the `throw` option is true.
|
||
*
|
||
*/
|
||
protected function initModule(Module $module, array $options = array()) {
|
||
|
||
$result = true;
|
||
$debugKey = null;
|
||
$clearSettings = isset($options['clearSettings']) ? (bool) $options['clearSettings'] : true;
|
||
$throw = isset($options['throw']) ? (bool) $options['throw'] : false;
|
||
|
||
if($this->debug) {
|
||
static $n = 0;
|
||
$this->message("initModule (" . (++$n) . "): " . wireClassName($module));
|
||
}
|
||
|
||
// if the module is configurable, then load its config data
|
||
// and set values for each before initializing the module
|
||
$extraConfigData = isset($options['configData']) ? $options['configData'] : null;
|
||
$this->setModuleConfigData($module, null, $extraConfigData);
|
||
|
||
$moduleName = wireClassName($module, false);
|
||
$moduleID = isset($this->moduleIDs[$moduleName]) ? $this->moduleIDs[$moduleName] : 0;
|
||
|
||
if($moduleID && isset($this->modulesLastVersions[$moduleID])) {
|
||
$this->checkModuleVersion($module);
|
||
}
|
||
|
||
if(method_exists($module, 'init') && empty($options['configOnly'])) {
|
||
|
||
if($this->debug) {
|
||
$debugKey = $this->debugTimerStart("initModule($moduleName)");
|
||
}
|
||
|
||
try {
|
||
$module->init();
|
||
} catch(\Exception $e) {
|
||
if($throw) throw($e);
|
||
$this->error(sprintf($this->_('Failed to init module: %s'), $moduleName) . " - " . $e->getMessage());
|
||
$result = false;
|
||
}
|
||
|
||
if($this->debug) {
|
||
$this->debugTimerStop($debugKey);
|
||
}
|
||
}
|
||
|
||
// if module is autoload (assumed here) and singular, then
|
||
// we no longer need the module's config data, so remove it
|
||
if($clearSettings && $this->isSingular($module)) {
|
||
if(!$moduleID) $moduleID = $this->getModuleID($module);
|
||
if(isset($this->configData[$moduleID])) $this->configData[$moduleID] = 1;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Call ready for a single module
|
||
*
|
||
* @param Module $module
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function readyModule(Module $module) {
|
||
$result = true;
|
||
if(method_exists($module, 'ready')) {
|
||
$debugKey = $this->debug ? $this->debugTimerStart("readyModule(" . $module->className() . ")") : null;
|
||
try {
|
||
$module->ready();
|
||
} catch(\Exception $e) {
|
||
$this->error(sprintf($this->_('Failed to ready module: %s'), $module->className()) . " - " . $e->getMessage());
|
||
$result = false;
|
||
}
|
||
if($this->debug) {
|
||
$this->debugTimerStop($debugKey);
|
||
static $n = 0;
|
||
$this->message("readyModule (" . (++$n) . "): " . wireClassName($module));
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Init conditional autoload modules, if conditions allow
|
||
*
|
||
* @return array of skipped module names
|
||
*
|
||
*/
|
||
protected function triggerConditionalAutoload() {
|
||
|
||
// conditional autoload modules that are skipped (className => 1)
|
||
$skipped = array();
|
||
|
||
// init conditional autoload modules, now that $page is known
|
||
foreach($this->conditionalAutoloadModules as $className => $func) {
|
||
|
||
if($this->debug) {
|
||
$moduleID = $this->getModuleID($className);
|
||
$flags = $this->moduleFlags[$moduleID];
|
||
$this->message("Conditional autoload: $className (flags=$flags, condition=" . (is_string($func) ? $func : 'func') . ")");
|
||
}
|
||
|
||
$load = true;
|
||
|
||
if(is_string($func)) {
|
||
// selector string
|
||
if(!$this->wire('page')->is($func)) $load = false;
|
||
} else {
|
||
// anonymous function
|
||
if(!is_callable($func)) $load = false;
|
||
else if(!$func()) $load = false;
|
||
}
|
||
|
||
if($load) {
|
||
$module = $this->newModule($className);
|
||
if($module) {
|
||
$this->set($className, $module);
|
||
if($this->initModule($module)) {
|
||
if($this->debug) $this->message("Conditional autoload: $className LOADED");
|
||
} else {
|
||
if($this->debug) $this->warning("Failed conditional autoload: $className");
|
||
}
|
||
}
|
||
|
||
} else {
|
||
$skipped[$className] = $className;
|
||
if($this->debug) $this->message("Conditional autoload: $className SKIPPED");
|
||
}
|
||
}
|
||
|
||
// clear this out since we don't need it anymore
|
||
$this->conditionalAutoloadModules = array();
|
||
|
||
return $skipped;
|
||
}
|
||
|
||
/**
|
||
* Trigger all modules 'ready' method, if they have it.
|
||
*
|
||
* This is to indicate to them that the API environment is fully ready and $page is in fuel.
|
||
*
|
||
* This is triggered by ProcessPageView::ready
|
||
*
|
||
* #pw-internal
|
||
*
|
||
*/
|
||
public function triggerReady() {
|
||
|
||
$debugKey = $this->debug ? $this->debugTimerStart("triggerReady") : null;
|
||
|
||
$skipped = $this->triggerConditionalAutoload();
|
||
|
||
// trigger ready method on all applicable modules
|
||
foreach($this as $module) {
|
||
/** @var Module $module */
|
||
if($module instanceof ModulePlaceholder) continue;
|
||
|
||
// $info = $this->getModuleInfo($module);
|
||
// if($info['autoload'] === false) continue;
|
||
// if(!$this->isAutoload($module)) continue;
|
||
|
||
$class = $this->getModuleClass($module);
|
||
if(isset($skipped[$class])) continue;
|
||
|
||
$id = $this->moduleIDs[$class];
|
||
if(!($this->moduleFlags[$id] & self::flagsAutoload)) continue;
|
||
|
||
if(!method_exists($module, 'ready')) continue;
|
||
|
||
$this->readyModule($module);
|
||
}
|
||
|
||
if($this->debug) $this->debugTimerStop($debugKey);
|
||
}
|
||
|
||
/**
|
||
* Retrieve the installed module info as stored in the database
|
||
*
|
||
*/
|
||
protected function loadModulesTable() {
|
||
$this->autoloadOrders = array();
|
||
$database = $this->wire()->database;
|
||
// we use SELECT * so that this select won't be broken by future DB schema additions
|
||
// Currently: id, class, flags, data, with created added at sysupdate 7
|
||
$query = $database->prepare("SELECT * FROM modules ORDER BY class", "modules.loadModulesTable()"); // QA
|
||
$query->execute();
|
||
|
||
/** @noinspection PhpAssignmentInConditionInspection */
|
||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||
|
||
$moduleID = (int) $row['id'];
|
||
$flags = (int) $row['flags'];
|
||
$class = $row['class'];
|
||
$this->moduleIDs[$class] = $moduleID;
|
||
$this->moduleFlags[$moduleID] = $flags;
|
||
$autoload = $flags & self::flagsAutoload;
|
||
$loadSettings = $autoload || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');
|
||
|
||
if($loadSettings) {
|
||
// preload config data for autoload modules since we'll need it again very soon
|
||
$data = strlen($row['data']) ? wireDecodeJSON($row['data']) : array();
|
||
$this->configData[$moduleID] = $data;
|
||
// populate information about duplicates, if applicable
|
||
if($flags & self::flagsDuplicate) $this->duplicates()->addFromConfigData($class, $data);
|
||
|
||
} else if(!empty($row['data'])) {
|
||
// indicate that it has config data, but not yet loaded
|
||
$this->configData[$moduleID] = 1;
|
||
}
|
||
|
||
if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {
|
||
$this->createdDates[$moduleID] = $row['created'];
|
||
}
|
||
|
||
if($autoload && !empty($this->moduleInfoCache[$moduleID]['autoload'])) {
|
||
$autoload = $this->moduleInfoCache[$moduleID]['autoload'];
|
||
$disabled = $flags & self::flagsDisabled;
|
||
if(is_int($autoload) && $autoload > 1 && !$disabled) {
|
||
// autoload specifies an order > 1, indicating it should load before others
|
||
$this->autoloadOrders[$class] = $autoload;
|
||
}
|
||
}
|
||
|
||
unset($row['data'], $row['created']); // info we don't want stored in modulesTableCache
|
||
$this->modulesTableCache[$class] = $row;
|
||
}
|
||
|
||
$query->closeCursor();
|
||
}
|
||
|
||
/**
|
||
* Given a disk path to the modules, determine all installed modules and keep track of all uninstalled (installable) modules.
|
||
*
|
||
* @param string $path
|
||
*
|
||
*/
|
||
protected function load($path) {
|
||
|
||
$config = $this->wire('config');
|
||
$debugKey = $this->debug ? $this->debugTimerStart("load($path)") : null;
|
||
$installed =& $this->modulesTableCache;
|
||
$modulesLoaded = array();
|
||
$modulesDelayed = array();
|
||
$modulesRequired = array();
|
||
$rootPath = $config->paths->root;
|
||
$basePath = substr($path, strlen($rootPath));
|
||
|
||
foreach($this->findModuleFiles($path, true) as $pathname) {
|
||
|
||
$pathname = trim($pathname);
|
||
if(empty($pathname)) continue;
|
||
$basename = basename($pathname);
|
||
list($moduleName, $ext) = explode('.', $basename, 2); // i.e. "module.php" or "module"
|
||
|
||
$this->moduleFileExts[$moduleName] = $ext === 'module' ? 1 : 2;
|
||
// @todo next, remove the 'file' property from verbose module info since it is redundant
|
||
|
||
$requires = array();
|
||
$name = $moduleName;
|
||
$moduleName = $this->loadModule($path, $pathname, $requires, $installed);
|
||
if(!$config->paths->get($name)) $this->setConfigPaths($name, dirname($basePath . $pathname));
|
||
if(!$moduleName) continue;
|
||
|
||
if(count($requires)) {
|
||
// module not loaded because it required other module(s) not yet loaded
|
||
foreach($requires as $requiresModuleName) {
|
||
if(!isset($modulesRequired[$requiresModuleName])) $modulesRequired[$requiresModuleName] = array();
|
||
if(!isset($modulesDelayed[$moduleName])) $modulesDelayed[$moduleName] = array();
|
||
// queue module for later load
|
||
$modulesRequired[$requiresModuleName][$moduleName] = $pathname;
|
||
$modulesDelayed[$moduleName][] = $requiresModuleName;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// module was successfully loaded
|
||
$modulesLoaded[$moduleName] = 1;
|
||
$loadedNames = array($moduleName);
|
||
|
||
// now determine if this module had any other modules waiting on it as a dependency
|
||
/** @noinspection PhpAssignmentInConditionInspection */
|
||
while($moduleName = array_shift($loadedNames)) {
|
||
// iternate through delayed modules that require this one
|
||
if(empty($modulesRequired[$moduleName])) continue;
|
||
|
||
foreach($modulesRequired[$moduleName] as $delayedName => $delayedPathName) {
|
||
$loadNow = true;
|
||
if(isset($modulesDelayed[$delayedName])) {
|
||
foreach($modulesDelayed[$delayedName] as $requiresModuleName) {
|
||
if(!isset($modulesLoaded[$requiresModuleName])) {
|
||
$loadNow = false;
|
||
}
|
||
}
|
||
}
|
||
if(!$loadNow) continue;
|
||
// all conditions satisified to load delayed module
|
||
unset($modulesDelayed[$delayedName], $modulesRequired[$moduleName][$delayedName]);
|
||
$unused = array();
|
||
$loadedName = $this->loadModule($path, $delayedPathName, $unused, $installed);
|
||
if(!$loadedName) continue;
|
||
$modulesLoaded[$loadedName] = 1;
|
||
$loadedNames[] = $loadedName;
|
||
}
|
||
}
|
||
}
|
||
|
||
if(count($modulesDelayed)) {
|
||
foreach($modulesDelayed as $moduleName => $requiredNames) {
|
||
$this->error("Module '$moduleName' dependency not fulfilled for: " . implode(', ', $requiredNames), Notice::debug);
|
||
}
|
||
}
|
||
|
||
if($this->debug) $this->debugTimerStop($debugKey);
|
||
}
|
||
|
||
/**
|
||
* Load a module into memory (companion to load bootstrap method)
|
||
*
|
||
* @param string $basepath Base path of modules being processed (path provided to the load method)
|
||
* @param string $pathname
|
||
* @param array $requires This method will populate this array with required dependencies (class names) if present.
|
||
* @param array $installed Array of installed modules info, indexed by module class name
|
||
* @return string Returns module name (classname)
|
||
*
|
||
*/
|
||
protected function loadModule($basepath, $pathname, array &$requires, array &$installed) {
|
||
|
||
$pathname = $basepath . $pathname;
|
||
$dirname = dirname($pathname);
|
||
$filename = basename($pathname);
|
||
$basename = basename($filename, '.php');
|
||
$basename = basename($basename, '.module');
|
||
$requires = array();
|
||
$duplicates = $this->duplicates();
|
||
$moduleInfo = null;
|
||
|
||
// check if module has duplicate files, where one to use has already been specified to use first
|
||
$currentFile = $duplicates->getCurrent($basename); // returns the current file in use, if more than one
|
||
if($currentFile) {
|
||
// there is a duplicate file in use
|
||
$file = rtrim($this->wire('config')->paths->root, '/') . $currentFile;
|
||
if(file_exists($file) && $pathname != $file) {
|
||
// file in use is different from the file we are looking at
|
||
// check if this is a new/yet unknown duplicate
|
||
if(!$duplicates->hasDuplicate($basename, $pathname)) {
|
||
// new duplicate
|
||
$duplicates->recordDuplicate($basename, $pathname, $file, $installed);
|
||
}
|
||
return '';
|
||
}
|
||
}
|
||
|
||
// check if module has already been loaded, or maybe we've got duplicates
|
||
if(wireClassExists($basename, false)) {
|
||
$module = parent::get($basename);
|
||
$dir = rtrim((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 aren’t currently installed.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return array Array of elements with $moduleName => $pathName
|
||
*
|
||
*/
|
||
public function getInstallable() {
|
||
return $this->installable;
|
||
}
|
||
|
||
/**
|
||
* Is the given module name installed?
|
||
*
|
||
* @param string $class Just a module class name, or optionally: `ModuleClassName>=1.2.3` (operator and version)
|
||
* @return bool True if installed, false if not
|
||
*
|
||
*/
|
||
public function isInstalled($class) {
|
||
|
||
if(is_object($class)) $class = $this->getModuleClass($class);
|
||
|
||
$operator = null;
|
||
$requiredVersion = null;
|
||
$currentVersion = null;
|
||
|
||
if(!ctype_alnum($class)) {
|
||
// class has something other than just a classname, likely operator + version
|
||
if(preg_match('/^([a-zA-Z0-9_]+)\s*([<>=!]+)\s*([\d.]+)$/', $class, $matches)) {
|
||
$class = $matches[1];
|
||
$operator = $matches[2];
|
||
$requiredVersion = $matches[3];
|
||
}
|
||
}
|
||
|
||
if($class === 'PHP' || $class === 'ProcessWire') {
|
||
$installed = true;
|
||
if(!is_null($requiredVersion)) {
|
||
$currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire('config')->version;
|
||
}
|
||
} else {
|
||
$installed = parent::get($class) !== null;
|
||
if($installed && !is_null($requiredVersion)) {
|
||
$info = $this->getModuleInfo($class);
|
||
$currentVersion = $info['version'];
|
||
}
|
||
}
|
||
|
||
if($installed && !is_null($currentVersion)) {
|
||
$installed = $this->versionCompare($currentVersion, $requiredVersion, $operator);
|
||
}
|
||
|
||
return $installed;
|
||
}
|
||
|
||
|
||
/**
|
||
* Is the given module name installable? (i.e. not already installed)
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $class Module class name
|
||
* @param bool $now Is module installable RIGHT NOW? This makes it check that all dependencies are already fulfilled (default=false)
|
||
* @return bool True if module is installable, false if not
|
||
*
|
||
*/
|
||
public function isInstallable($class, $now = false) {
|
||
$installable = array_key_exists($class, $this->installable);
|
||
if(!$installable) return false;
|
||
if(!wireInstanceOf($class, 'Module')) {
|
||
$nsClass = $this->getModuleClass($class, true);
|
||
if(!wireInstanceOf($nsClass, 'ProcessWire\\Module')) return false;
|
||
}
|
||
if($now) {
|
||
$requires = $this->getRequiresForInstall($class);
|
||
if(count($requires)) return false;
|
||
}
|
||
return $installable;
|
||
}
|
||
|
||
/**
|
||
* Install the given module name
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $class Module name (class name)
|
||
* @param array|bool $options Optional associative array that can contain any of the following:
|
||
* - `dependencies` (boolean): When true, dependencies will also be installed where possible. Specify false to prevent installation of uninstalled modules. (default=true)
|
||
* - `resetCache` (boolean): When true, module caches will be reset after installation. (default=true)
|
||
* - `force` (boolean): Force installation, even if dependencies can't be met.
|
||
* @return null|Module Returns null if unable to install, or ready-to-use Module object if successfully installed.
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function ___install($class, $options = array()) {
|
||
|
||
$defaults = array(
|
||
'dependencies' => true,
|
||
'resetCache' => true,
|
||
'force' => false,
|
||
);
|
||
if(is_bool($options)) {
|
||
// dependencies argument allowed instead of $options, for backwards compatibility
|
||
$dependencies = $options;
|
||
$options = array('dependencies' => $dependencies);
|
||
}
|
||
$options = array_merge($defaults, $options);
|
||
$dependencyOptions = $options;
|
||
$dependencyOptions['resetCache'] = false;
|
||
|
||
if(!$this->isInstallable($class)) return null;
|
||
|
||
$requires = $this->getRequiresForInstall($class);
|
||
if(count($requires)) {
|
||
$error = '';
|
||
$installable = false;
|
||
if($options['dependencies']) {
|
||
$installable = true;
|
||
foreach($requires as $requiresModule) {
|
||
if(!$this->isInstallable($requiresModule)) $installable = false;
|
||
}
|
||
if($installable) {
|
||
foreach($requires as $requiresModule) {
|
||
if(!$this->install($requiresModule, $dependencyOptions)) {
|
||
$error = $this->_('Unable to install required module') . " - $requiresModule. ";
|
||
$installable = false;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if(!$installable) {
|
||
$error = sprintf($this->_('Module %s requires: %s'), $class, implode(', ', $requires)) . ' ' . $error;
|
||
if($options['force']) {
|
||
$this->warning($this->_('Warning!') . ' ' . $error);
|
||
} else {
|
||
throw new WireException($error);
|
||
}
|
||
}
|
||
}
|
||
|
||
$languages = $this->wire('languages');
|
||
if($languages) $languages->setDefault();
|
||
|
||
$pathname = $this->installable[$class];
|
||
$this->includeModuleFile($pathname, $class);
|
||
$this->setConfigPaths($class, dirname($pathname));
|
||
|
||
$module = $this->newModule($class);
|
||
if(!$module) return null;
|
||
$flags = 0;
|
||
$database = $this->wire('database');
|
||
$moduleID = 0;
|
||
|
||
if($this->isSingular($module)) $flags = $flags | self::flagsSingular;
|
||
if($this->isAutoload($module)) $flags = $flags | self::flagsAutoload;
|
||
|
||
$sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";
|
||
if($this->wire('config')->systemVersion >=7) $sql .= ", created=NOW()";
|
||
$query = $database->prepare($sql, "modules.install($class)");
|
||
$query->bindValue(":class", $class, \PDO::PARAM_STR);
|
||
$query->bindValue(":flags", $flags, \PDO::PARAM_INT);
|
||
|
||
try {
|
||
if($query->execute()) $moduleID = (int) $database->lastInsertId();
|
||
} catch(\Exception $e) {
|
||
if($languages) $languages->unsetDefault();
|
||
$this->trackException($e, false, true);
|
||
return null;
|
||
}
|
||
|
||
$this->moduleIDs[$class] = $moduleID;
|
||
|
||
$this->add($module);
|
||
unset($this->installable[$class]);
|
||
|
||
// note: the module's install is called here because it may need to know its module ID for installation of permissions, etc.
|
||
if(method_exists($module, '___install') || method_exists($module, 'install')) {
|
||
try {
|
||
/** @var _Module $module */
|
||
$module->install();
|
||
|
||
} catch(\PDOException $e) {
|
||
$error = $this->_('Module reported error during install') . " ($class): " . $e->getMessage();
|
||
$this->error($error);
|
||
$this->trackException($e, false, $error);
|
||
|
||
} catch(\Exception $e) {
|
||
// remove the module from the modules table if the install failed
|
||
$moduleID = (int) $moduleID;
|
||
$error = $this->_('Unable to install module') . " ($class): " . $e->getMessage();
|
||
$ee = null;
|
||
try {
|
||
$query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1'); // QA
|
||
$query->bindValue(":id", $moduleID, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
} catch(\Exception $ee) {
|
||
$this->trackException($e, false, $error)->trackException($ee, true);
|
||
}
|
||
if($languages) $languages->unsetDefault();
|
||
if(is_null($ee)) $this->trackException($e, false, $error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
$info = $this->getModuleInfoVerbose($class, array('noCache' => true));
|
||
|
||
// if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
|
||
foreach($info['permissions'] as $name => $title) {
|
||
$name = $this->wire('sanitizer')->pageName($name);
|
||
if(ctype_digit("$name") || empty($name)) continue; // permission name not valid
|
||
$permission = $this->wire('permissions')->get($name);
|
||
if($permission->id) continue; // permision already there
|
||
try {
|
||
$permission = $this->wire('permissions')->add($name);
|
||
$permission->title = $title;
|
||
$this->wire('permissions')->save($permission);
|
||
if($languages) $languages->unsetDefault();
|
||
$this->message(sprintf($this->_('Added Permission: %s'), $permission->name));
|
||
} catch(\Exception $e) {
|
||
if($languages) $languages->unsetDefault();
|
||
$error = sprintf($this->_('Error adding permission: %s'), $name);
|
||
$this->trackException($e, false, $error);
|
||
}
|
||
}
|
||
|
||
// check if there are any modules in 'installs' that this module didn't handle installation of, and install them
|
||
$label = $this->_('Module Auto Install');
|
||
|
||
foreach($info['installs'] as $name) {
|
||
if(!$this->isInstalled($name)) {
|
||
try {
|
||
$this->install($name, $dependencyOptions);
|
||
$this->message("$label: $name");
|
||
} catch(\Exception $e) {
|
||
$error = "$label: $name - " . $e->getMessage();
|
||
$this->trackException($e, false, $error);
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->log("Installed module '$module'");
|
||
if($languages) $languages->unsetDefault();
|
||
if($options['resetCache']) $this->clearModuleInfoCache();
|
||
|
||
return $module;
|
||
}
|
||
|
||
/**
|
||
* Returns whether the module can be uninstalled
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $class
|
||
* @param bool $returnReason If true, the reason why it can't be uninstalled with be returned rather than boolean false.
|
||
* @return bool|string
|
||
*
|
||
*/
|
||
public function ___isUninstallable($class, $returnReason = false) {
|
||
|
||
$reason = '';
|
||
$reason1 = $this->_("Module is not already installed");
|
||
$namespace = $this->getModuleNamespace($class);
|
||
$class = $this->getModuleClass($class);
|
||
|
||
if(!$this->isInstalled($class)) {
|
||
$reason = $reason1 . ' (a)';
|
||
|
||
} else {
|
||
$this->includeModule($class);
|
||
if(!wireClassExists($namespace . $class, false)) {
|
||
$reason = $reason1 . " (b: $namespace$class)";
|
||
}
|
||
}
|
||
|
||
if(!$reason) {
|
||
// if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable
|
||
$info = $this->getModuleInfo($class);
|
||
if(!empty($info['permanent'])) {
|
||
$reason = $this->_("Module is permanent");
|
||
} else {
|
||
$dependents = $this->getRequiresForUninstall($class);
|
||
if(count($dependents)) $reason = $this->_("Module is required by other modules that must be removed first");
|
||
}
|
||
|
||
if(!$reason && in_array('Fieldtype', wireClassParents($namespace . $class))) {
|
||
foreach($this->wire('fields') as $field) {
|
||
$fieldtype = wireClassName($field->type, false);
|
||
if($fieldtype == $class) {
|
||
$reason = $this->_("This module is a Fieldtype currently in use by one or more fields");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if($returnReason && $reason) return $reason;
|
||
|
||
return $reason ? false : true;
|
||
}
|
||
|
||
/**
|
||
* Returns whether the module can be deleted (have it's files physically removed)
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $class
|
||
* @param bool $returnReason If true, the reason why it can't be removed will be returned rather than boolean false.
|
||
* @return bool|string
|
||
*
|
||
*/
|
||
public function isDeleteable($class, $returnReason = false) {
|
||
|
||
$reason = '';
|
||
$class = $this->getModuleClass($class);
|
||
|
||
$filename = isset($this->installable[$class]) ? $this->installable[$class] : null;
|
||
$dirname = dirname($filename);
|
||
|
||
if(empty($filename) || $this->isInstalled($class)) {
|
||
$reason = "Module must be uninstalled before it can be deleted.";
|
||
|
||
} else if(is_link($filename) || is_link($dirname) || is_link(dirname($dirname))) {
|
||
$reason = "Module is linked to another location";
|
||
|
||
} else if(!is_file($filename)) {
|
||
$reason = "Module file does not exist";
|
||
|
||
} else if(strpos($filename, $this->paths[0]) === 0) {
|
||
$reason = "Core modules may not be deleted.";
|
||
|
||
} else if(!is_writable($filename)) {
|
||
$reason = "We have no write access to the module file, it must be removed manually.";
|
||
}
|
||
|
||
if($returnReason && $reason) return $reason;
|
||
|
||
return $reason ? false : true;
|
||
}
|
||
|
||
/**
|
||
* Delete the given module, physically removing its files
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $class Module name (class name)
|
||
* @return bool|int
|
||
* @throws WireException If module can't be deleted, exception will be thrown containing reason.
|
||
*
|
||
*/
|
||
public function ___delete($class) {
|
||
|
||
$class = $this->getModuleClass($class);
|
||
$success = true;
|
||
$reason = $this->isDeleteable($class, true);
|
||
if($reason !== true) throw new WireException($reason);
|
||
$siteModulesPath = $this->wire('config')->paths->siteModules;
|
||
|
||
$filename = $this->installable[$class];
|
||
$basename = basename($filename);
|
||
|
||
/** @var WireFileTools $fileTools */
|
||
$fileTools = $this->wire('files');
|
||
|
||
// double check that $class is consistent with the actual $basename
|
||
if($basename === "$class.module" || $basename === "$class.module.php") {
|
||
// good, this is consistent with the format we require
|
||
} else {
|
||
throw new WireException("Unrecognized module filename format");
|
||
}
|
||
|
||
// now determine if module is the owner of the directory it exists in
|
||
// this is the case if the module class name is the same as the directory name
|
||
|
||
$path = dirname($filename); // full path to directory, i.e. .../site/modules/ProcessHello
|
||
$name = basename($path); // just name of directory that module is, i.e. ProcessHello
|
||
$parentPath = dirname($path); // full path to parent directory, i.e. ../site/modules
|
||
$backupPath = $parentPath . "/.$name"; // backup path, in case module is backed up
|
||
|
||
// first check that we are still in the /site/modules/ (or another non core modules path)
|
||
$inPath = false; // is module somewhere beneath /site/modules/ ?
|
||
$inRoot = false; // is module in /site/modules/ root? i.e. /site/modules/ModuleName.module
|
||
|
||
foreach($this->paths as $key => $modulesPath) {
|
||
if($key === 0) continue; // skip core modules path
|
||
if(strpos("$parentPath/", $modulesPath) === 0) $inPath = true;
|
||
if($modulesPath === $path) $inRoot = true;
|
||
}
|
||
|
||
$basename = basename($basename, '.php');
|
||
$basename = basename($basename, '.module');
|
||
|
||
$files = array(
|
||
"$basename.module",
|
||
"$basename.module.php",
|
||
"$basename.info.php",
|
||
"$basename.info.json",
|
||
"$basename.config.php",
|
||
"{$basename}Config.php",
|
||
);
|
||
|
||
if($inPath) {
|
||
// module is in /site/modules/[ModuleName]/
|
||
|
||
$numOtherModules = 0; // num modules in dir other than this one
|
||
$numLinks = 0; // number of symbolic links
|
||
$dirs = array("$path/");
|
||
|
||
do {
|
||
$dir = array_shift($dirs);
|
||
$this->message("Scanning: $dir", Notice::debug);
|
||
|
||
foreach(new \DirectoryIterator($dir) as $file) {
|
||
if($file->isDot()) continue;
|
||
if($file->isLink()) {
|
||
$numLinks++;
|
||
continue;
|
||
}
|
||
if($file->isDir()) {
|
||
$dirs[] = $fileTools->unixDirName($file->getPathname());
|
||
continue;
|
||
}
|
||
if(in_array($file->getBasename(), $files)) continue; // skip known files
|
||
if(strpos($file->getBasename(), '.module') && preg_match('{(\.module|\.module\.php)$}', $file->getBasename())) {
|
||
// another module exists in this dir, so we don't want to delete that
|
||
$numOtherModules++;
|
||
}
|
||
if(preg_match('{^(' . $basename . '\.[-_.a-zA-Z0-9]+)$}', $file->getBasename(), $matches)) {
|
||
// keep track of potentially related files in case we have to delete them individually
|
||
$files[] = $matches[1];
|
||
}
|
||
}
|
||
} while(count($dirs));
|
||
|
||
if(!$inRoot && !$numOtherModules && !$numLinks) {
|
||
// the modulePath had no other modules or directories in it, so we can delete it entirely
|
||
$success = $fileTools->rmdir($path, true);
|
||
if($success) {
|
||
$this->message("Removed directory: $path", Notice::debug);
|
||
if(is_dir($backupPath)) {
|
||
if($fileTools->rmdir($backupPath, true)) $this->message("Removed directory: $backupPath", Notice::debug);
|
||
}
|
||
$files = array();
|
||
} else {
|
||
$this->error("Failed to remove directory: $path", Notice::debug);
|
||
}
|
||
}
|
||
}
|
||
|
||
// remove module files individually
|
||
foreach($files as $file) {
|
||
$file = "$path/$file";
|
||
if(!file_exists($file)) continue;
|
||
if($fileTools->unlink($file, $siteModulesPath)) {
|
||
$this->message("Removed file: $file", Notice::debug);
|
||
} else {
|
||
$this->error("Unable to remove file: $file", Notice::debug);
|
||
}
|
||
}
|
||
|
||
$this->log("Deleted module '$class'");
|
||
|
||
return $success;
|
||
}
|
||
|
||
|
||
/**
|
||
* Uninstall the given module name
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param string $class Module name (class name)
|
||
* @return bool
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function ___uninstall($class) {
|
||
|
||
$class = $this->getModuleClass($class);
|
||
$reason = $this->isUninstallable($class, true);
|
||
if($reason !== true) {
|
||
// throw new WireException("$class - Can't Uninstall - $reason");
|
||
return false;
|
||
}
|
||
|
||
// check if there are any modules still installed that this one says it is responsible for installing
|
||
foreach($this->getUninstalls($class) as $name) {
|
||
|
||
// catch uninstall exceptions at this point since original module has already been uninstalled
|
||
$label = $this->_('Module Auto Uninstall');
|
||
try {
|
||
$this->uninstall($name);
|
||
$this->message("$label: $name");
|
||
|
||
} catch(\Exception $e) {
|
||
$error = "$label: $name - " . $e->getMessage();
|
||
$this->trackException($e, false, $error);
|
||
}
|
||
}
|
||
|
||
$info = $this->getModuleInfoVerbose($class);
|
||
$module = $this->getModule($class, array(
|
||
'noPermissionCheck' => true,
|
||
'noInstall' => true,
|
||
// 'noInit' => true
|
||
));
|
||
if(!$module) return false;
|
||
|
||
// remove all hooks attached to this module
|
||
$hooks = $module instanceof Wire ? $module->getHooks() : array();
|
||
foreach($hooks as $hook) {
|
||
if($hook['method'] == 'uninstall') continue;
|
||
$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
|
||
$module->removeHook($hook['id']);
|
||
}
|
||
|
||
// remove all hooks attached to other ProcessWire objects
|
||
$hooks = array_merge($this->getHooks('*'), $this->wire('hooks')->getAllLocalHooks());
|
||
foreach($hooks as $hook) {
|
||
/** @var Wire $toObject */
|
||
$toObject = $hook['toObject'];
|
||
$toClass = wireClassName($toObject, false);
|
||
$toMethod = $hook['toMethod'];
|
||
if($class === $toClass && $toMethod != 'uninstall') {
|
||
$toObject->removeHook($hook['id']);
|
||
$this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
|
||
}
|
||
}
|
||
|
||
if(method_exists($module, '___uninstall') || method_exists($module, 'uninstall')) {
|
||
// note module's uninstall method may throw an exception to abort the uninstall
|
||
/** @var _Module $module */
|
||
$module->uninstall();
|
||
}
|
||
$database = $this->wire('database');
|
||
$query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1'); // QA
|
||
$query->bindValue(":class", $class, \PDO::PARAM_STR);
|
||
$query->execute();
|
||
|
||
// add back to the installable list
|
||
if(class_exists("ReflectionClass")) {
|
||
$reflector = new \ReflectionClass($this->getModuleClass($module, true));
|
||
$this->installable[$class] = $reflector->getFileName();
|
||
}
|
||
|
||
unset($this->moduleIDs[$class]);
|
||
$this->remove($module);
|
||
|
||
// delete permissions installed by this module
|
||
if(isset($info['permissions']) && is_array($info['permissions'])) {
|
||
foreach($info['permissions'] as $name => $title) {
|
||
$name = $this->wire('sanitizer')->pageName($name);
|
||
if(ctype_digit("$name") || empty($name)) continue;
|
||
$permission = $this->wire('permissions')->get($name);
|
||
if(!$permission->id) continue;
|
||
try {
|
||
$this->wire('permissions')->delete($permission);
|
||
$this->message(sprintf($this->_('Deleted Permission: %s'), $name));
|
||
} catch(\Exception $e) {
|
||
$error = sprintf($this->_('Error deleting permission: %s'), $name);
|
||
$this->trackException($e, false, $error);
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->log("Uninstalled module '$class'");
|
||
$this->refresh();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Get flags for the given module
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|string|Module $class Module to add flag to
|
||
* @return int|false Returns integer flags on success, or boolean false on fail
|
||
*
|
||
*/
|
||
public function getFlags($class) {
|
||
$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
|
||
if(isset($this->moduleFlags[$id])) return $this->moduleFlags[$id];
|
||
if(!$id) return false;
|
||
$query = $this->wire('database')->prepare('SELECT flags FROM modules WHERE id=:id');
|
||
$query->bindValue(':id', $id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
if(!$query->rowCount()) return false;
|
||
list($flags) = $query->fetch(\PDO::FETCH_NUM);
|
||
$flags = (int) $flags;
|
||
$this->moduleFlags[$id] = $flags;
|
||
return $flags;
|
||
}
|
||
|
||
/**
|
||
* Does module have flag?
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|string|Module $class Module ID, class name or instance
|
||
* @param int $flag
|
||
* @return bool
|
||
* @since 3.0.170
|
||
*
|
||
*/
|
||
public function hasFlag($class, $flag) {
|
||
$flags = $this->getFlags($class);
|
||
return $flags === false ? false : ($flags & $flag);
|
||
}
|
||
|
||
/**
|
||
* Set module flags
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $class
|
||
* @param $flags
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function setFlags($class, $flags) {
|
||
$flags = (int) $flags;
|
||
$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
|
||
if(!$id) return false;
|
||
if($this->moduleFlags[$id] === $flags) return true;
|
||
$query = $this->wire('database')->prepare('UPDATE modules SET flags=:flags WHERE id=:id');
|
||
$query->bindValue(':flags', $flags);
|
||
$query->bindValue(':id', $id);
|
||
if($this->debug) $this->message("setFlags(" . $this->getModuleClass($class) . ", " . $this->moduleFlags[$id] . " => $flags)");
|
||
$this->moduleFlags[$id] = $flags;
|
||
return $query->execute();
|
||
}
|
||
|
||
/**
|
||
* Add or remove a flag from a module
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $class int|string|Module $class Module to add flag to
|
||
* @param $flag int Flag to add (see flags* constants)
|
||
* @param $add bool $add Specify true to add the flag or false to remove it
|
||
* @return bool True on success, false on fail
|
||
*
|
||
*/
|
||
public function setFlag($class, $flag, $add = true) {
|
||
$id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
|
||
if(!$id) return false;
|
||
$flag = (int) $flag;
|
||
if(!$flag) return false;
|
||
$flags = $this->getFlags($id);
|
||
if($add) {
|
||
if($flags & $flag) return true; // already has the flag
|
||
$flags = $flags | $flag;
|
||
} else {
|
||
if(!($flags & $flag)) return true; // doesn't already have the flag
|
||
$flags = $flags & ~$flag;
|
||
}
|
||
$this->setFlags($id, $flags);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Return an array of other module class names that are uninstalled when the given one is
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* The opposite of this function is found in the getModuleInfo array property 'installs'.
|
||
* Note that 'installs' and uninstalls may be different, as only modules in the 'installs' list
|
||
* that indicate 'requires' for the installer module will be uninstalled.
|
||
*
|
||
* @param $class
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getUninstalls($class) {
|
||
|
||
$uninstalls = array();
|
||
$class = $this->getModuleClass($class);
|
||
if(!$class) return $uninstalls;
|
||
$info = $this->getModuleInfoVerbose($class);
|
||
|
||
// check if there are any modules still installed that this one says it is responsible for installing
|
||
foreach($info['installs'] as $name) {
|
||
|
||
// if module isn't installed, then great
|
||
if(!$this->isInstalled($name)) continue;
|
||
|
||
// if an 'installs' module doesn't indicate that it requires this one, then leave it installed
|
||
$i = $this->getModuleInfo($name);
|
||
if(!in_array($class, $i['requires'])) continue;
|
||
|
||
// add it to the uninstalls array
|
||
$uninstalls[] = $name;
|
||
}
|
||
|
||
return $uninstalls;
|
||
}
|
||
|
||
/**
|
||
* Returns the database ID of a given module class, or 0 if not found
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $class Module or module name
|
||
* @return int
|
||
*
|
||
*/
|
||
public function getModuleID($class) {
|
||
|
||
$id = 0;
|
||
|
||
if(is_object($class)) {
|
||
if($class instanceof Module) {
|
||
$class = $this->getModuleClass($class);
|
||
} else {
|
||
// Class is not a module
|
||
return $id;
|
||
}
|
||
}
|
||
|
||
if(isset($this->moduleIDs[$class])) {
|
||
$id = (int) $this->moduleIDs[$class];
|
||
|
||
} else foreach($this->moduleInfoCache as $key => $info) {
|
||
if($info['name'] == $class) {
|
||
$id = (int) $key;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* Returns the module's class name.
|
||
*
|
||
* - Given a numeric database ID, returns the associated module class name or false if it doesn't exist
|
||
* - Given a Module or ModulePlaceholder instance, returns the Module's class name.
|
||
*
|
||
* If the module has a className() method then it uses that rather than PHP's get_class().
|
||
* This is important because of placeholder modules. For example, get_class would return
|
||
* 'ModulePlaceholder' rather than the correct className for a Module.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|int|Module
|
||
* @param bool $withNamespace Specify true to include the namespace in the class
|
||
* @return string|bool The Module's class name or false if not found.
|
||
* Note that 'false' is only possible if you give this method a non-Module, or an integer ID
|
||
* that doesn't correspond to a module ID.
|
||
*
|
||
*/
|
||
public function getModuleClass($module, $withNamespace = false) {
|
||
|
||
$className = '';
|
||
$namespace = '';
|
||
|
||
if($module instanceof Module) {
|
||
if(wireMethodExists($module, 'className')) {
|
||
if($withNamespace) return $module->className(true);
|
||
return $module->className();
|
||
} else {
|
||
return wireClassName($module, $withNamespace);
|
||
}
|
||
|
||
} else if(is_int($module) || ctype_digit("$module")) {
|
||
$className = array_search((int) $module, $this->moduleIDs);
|
||
|
||
} else if(is_string($module)) {
|
||
|
||
if(strpos($module, "\\") !== false) {
|
||
$namespace = wireClassName($module, 1);
|
||
$className = wireClassName($module, false);
|
||
}
|
||
|
||
// remove extensions if they were included in the module name
|
||
if(strpos($module, '.') !== false) {
|
||
$module = basename(basename($module, '.php'), '.module');
|
||
}
|
||
|
||
if(array_key_exists($module, $this->moduleIDs)) {
|
||
$className = $module;
|
||
} else if(array_key_exists($module, $this->installable)) {
|
||
$className = $module;
|
||
}
|
||
}
|
||
|
||
if($className) {
|
||
if($withNamespace) {
|
||
if($namespace) {
|
||
$className = "$namespace\\$className";
|
||
} else {
|
||
$className = $this->getModuleNamespace($className) . $className;
|
||
}
|
||
}
|
||
return $className;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Retrieve module info from ModuleName.info.json or ModuleName.info.php
|
||
*
|
||
* @param string $moduleName
|
||
* @return array
|
||
*
|
||
*/
|
||
protected function getModuleInfoExternal($moduleName) {
|
||
// if($this->debug) $this->message("getModuleInfoExternal($moduleName)");
|
||
|
||
// ...attempt to load info by info file (Module.info.php or Module.info.json)
|
||
if(!empty($this->installable[$moduleName])) {
|
||
$path = dirname($this->installable[$moduleName]) . '/';
|
||
} else {
|
||
$path = $this->wire('config')->paths->$moduleName;
|
||
}
|
||
|
||
if(empty($path)) return array();
|
||
|
||
// module exists and has a dedicated path on the file system
|
||
// we will try to get info from a PHP or JSON info file
|
||
$filePHP = $path . "$moduleName.info.php";
|
||
$fileJSON = $path . "$moduleName.info.json";
|
||
|
||
$info = array();
|
||
if(file_exists($filePHP)) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include($filePHP); // will populate $info automatically
|
||
if(!is_array($info) || !count($info)) $this->error("Invalid PHP module info file for $moduleName");
|
||
|
||
} else if(file_exists($fileJSON)) {
|
||
$info = file_get_contents($fileJSON);
|
||
$info = json_decode($info, true);
|
||
if(!$info) {
|
||
$info = array();
|
||
$this->error("Invalid JSON module info file for $moduleName");
|
||
}
|
||
}
|
||
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* Retrieve module info from internal getModuleInfo function in the class
|
||
*
|
||
* @param Module|string $module
|
||
* @param string $namespace
|
||
* @return array
|
||
*
|
||
*/
|
||
protected function getModuleInfoInternal($module, $namespace = '') {
|
||
// if($this->debug) $this->message("getModuleInfoInternal($module)");
|
||
|
||
$info = array();
|
||
|
||
if($module instanceof ModulePlaceholder) {
|
||
$this->includeModule($module);
|
||
$module = $module->className();
|
||
}
|
||
|
||
if($module instanceof Module) {
|
||
if(method_exists($module, 'getModuleInfo')) {
|
||
$info = $module::getModuleInfo();
|
||
}
|
||
|
||
} else if($module) {
|
||
if(empty($namespace)) $namespace = $this->getModuleNamespace($module);
|
||
$className = wireClassName($namespace . $module, true);
|
||
if(!class_exists($className)) $this->includeModule($module);
|
||
if(is_callable("$className::getModuleInfo")) {
|
||
$info = call_user_func(array($className, 'getModuleInfo'));
|
||
}
|
||
}
|
||
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* Pull module info directly from the module file's getModuleInfo without letting PHP parse it
|
||
*
|
||
* Useful for getting module info from modules that extend another module not already on the file system.
|
||
*
|
||
* @param $className
|
||
* @return array Only includes module info specified in the module file itself.
|
||
*
|
||
protected function getModuleInfoInternalSafe($className) {
|
||
// future addition
|
||
// load file, preg_split by /^\s*(public|private|protected)[^;{]+function\s*([^)]*)[^{]*{/
|
||
// isolate the one that starts has getModuleInfo in matches[1]
|
||
// parse data from matches[2]
|
||
return array();
|
||
}
|
||
*/
|
||
|
||
/**
|
||
* Retrieve module info for system properties: PHP or ProcessWire
|
||
*
|
||
* @param $moduleName
|
||
* @return array
|
||
*
|
||
*/
|
||
protected function getModuleInfoSystem($moduleName) {
|
||
|
||
$info = array();
|
||
if($moduleName === 'PHP') {
|
||
$info['id'] = 0;
|
||
$info['name'] = $moduleName;
|
||
$info['title'] = $moduleName;
|
||
$info['version'] = PHP_VERSION;
|
||
return $info;
|
||
|
||
} else if($moduleName === 'ProcessWire') {
|
||
$info['id'] = 0;
|
||
$info['name'] = $moduleName;
|
||
$info['title'] = $moduleName;
|
||
$info['version'] = $this->wire('config')->version;
|
||
$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
|
||
$info['requiresVersions'] = array(
|
||
'PHP' => array('>=', '5.3.8'),
|
||
'PHP_modules' => array('=', 'PDO,mysqli'),
|
||
'Apache_modules' => array('=', 'mod_rewrite'),
|
||
'MySQL' => array('>=', '5.0.15'),
|
||
);
|
||
$info['requires'] = array_keys($info['requiresVersions']);
|
||
} else {
|
||
return array();
|
||
}
|
||
|
||
$info['versionStr'] = $info['version'];
|
||
|
||
return $info;
|
||
|
||
}
|
||
|
||
/**
|
||
* Returns an associative array of information for a Module
|
||
*
|
||
* The array returned by this method includes the following:
|
||
*
|
||
* - `id` (int): module database ID.
|
||
* - `name` (string): module class name.
|
||
* - `title` (string): module title.
|
||
* - `version` (int): module version.
|
||
* - `icon` (string): Optional icon name (excluding the "fa-") part.
|
||
* - `requires` (array): module names required by this module.
|
||
* - `requiresVersions` (array): required module versions–module name is key, value is array($operator, $version).
|
||
* - `installs` (array): module names that this module installs.
|
||
* - `permission` (string): permission name required to execute this module.
|
||
* - `autoload` (bool): true if module is autoload, false if not.
|
||
* - `singular` (bool): true if module is singular, false if not.
|
||
* - `created` (int): unix-timestamp of date/time module added to system (for uninstalled modules, it is the file date).
|
||
* - `installed` (bool): is the module currently installed? (boolean, or null when not determined)
|
||
* - `configurable` (bool|int): true or positive number when the module is configurable.
|
||
* - `namespace` (string): PHP namespace that module lives in.
|
||
*
|
||
* The following properties are also included when "verbose" mode is requested. When not in verbose mode, these
|
||
* properties are present but blank:
|
||
*
|
||
* - `versionStr` (string): formatted module version string.
|
||
* - `file` (string): module filename from PW installation root, or false when it can't be found.
|
||
* - `core` (bool): true when module is a core module, false when not.
|
||
* - `author` (string): module author, when specified.
|
||
* - `summary` (string): summary of what this module does.
|
||
* - `href` (string): URL to module details (when specified).
|
||
* - `permissions` (array): permissions installed by this module, associative array ('permission-name' => 'Description').
|
||
* - `page` (array): definition of page to create for Process module (see Process class)
|
||
*
|
||
* The following properties appear only for "Process" modules, and only if specified by module.
|
||
* See the Process class for more details:
|
||
*
|
||
* - `nav` (array): navigation definition
|
||
* - `useNavJSON` (bool): whether the Process module provides JSON navigation
|
||
* - `permissionMethod` (string|callable): method to call to determine permission
|
||
* - `page` (array): definition of page to create for Process module
|
||
*
|
||
* On error, an `error` index in returned array contains error message. You can also identify errors
|
||
* such as a non-existing module by the returned module info having an `id` index of `0`
|
||
*
|
||
* ~~~~~
|
||
* // example of getting module info
|
||
* $moduleInfo = $modules->getModuleInfo('InputfieldCKEditor');
|
||
*
|
||
* // example of getting verbose module info
|
||
* $moduleInfo = $modules->getModuleInfoVerbose('MarkupAdminDataTable');
|
||
* ~~~~~
|
||
*
|
||
* @param string|Module|int $class Specify one of the following:
|
||
* - Module object instance
|
||
* - Module class name (string)
|
||
* - Module ID (int)
|
||
* - To get info for ALL modules, specify `*` or `all`.
|
||
* - To get system information, specify `ProcessWire` or `PHP`.
|
||
* - To get a blank module info template, specify `info`.
|
||
* @param array $options Optional options to modify behavior of what gets returned
|
||
* - `verbose` (bool): Makes the info also include verbose properties, which are otherwise blank. (default=false)
|
||
* - `minify` (bool): Remove non-applicable and properties that match defaults? (default=false, or true when getting `all`)
|
||
* - `noCache` (bool): prevents use of cache to retrieve the module info. (default=false)
|
||
* @return array Associative array of module information.
|
||
* - On error, an `error` index is also populated with an error message.
|
||
* - When requesting a module that does not exist its `id` value will be `0` and its `name` will be blank.
|
||
* @see Modules::getModuleInfoVerbose()
|
||
* @todo move all getModuleInfo methods to their own ModuleInfo class and break this method down further.
|
||
*
|
||
*/
|
||
public function getModuleInfo($class, array $options = array()) {
|
||
|
||
$getAll = $class === '*' || $class === 'all';
|
||
$getSystem = $class === 'ProcessWire' || $class === 'PHP' || $class === 'info';
|
||
|
||
$defaults = array(
|
||
'verbose' => false,
|
||
'minify' => $getAll,
|
||
'noCache' => false,
|
||
'noInclude' => false,
|
||
);
|
||
|
||
$options = array_merge($defaults, $options);
|
||
$info = array();
|
||
$module = $class;
|
||
$moduleName = '';
|
||
$moduleID = 0;
|
||
$fromCache = false; // was the data loaded from cache?
|
||
|
||
if(!$getAll && !$getSystem) {
|
||
$moduleName = $this->getModuleClass($module);
|
||
$moduleID = (string) $this->getModuleID($module); // typecast to string for cache
|
||
}
|
||
|
||
static $infoTemplate = array(
|
||
// module database ID
|
||
'id' => 0,
|
||
// module class name
|
||
'name' => '',
|
||
// module title
|
||
'title' => '',
|
||
// module version
|
||
'version' => 0,
|
||
// module version (always formatted string)
|
||
'versionStr' => '0.0.0',
|
||
// who authored the module? (included in 'verbose' mode only)
|
||
'author' => '',
|
||
// summary of what this module does (included in 'verbose' mode only)
|
||
'summary' => '',
|
||
// URL to module details (included in 'verbose' mode only)
|
||
'href' => '',
|
||
// Optional name of icon representing this module (currently font-awesome icon names, excluding the "fa-" portion)
|
||
'icon' => '',
|
||
// this method converts this to array of module names, regardless of how the module specifies it
|
||
'requires' => array(),
|
||
// module name is key, value is array($operator, $version). Note 'requiresVersions' index is created by this function.
|
||
'requiresVersions' => array(),
|
||
// array of module class names
|
||
'installs' => array(),
|
||
// permission required to execute this module
|
||
'permission' => '',
|
||
// permissions automatically installed/uninstalled with this module. array of ('permission-name' => 'Description')
|
||
'permissions' => array(),
|
||
// true if module is autoload, false if not. null=unknown
|
||
'autoload' => null,
|
||
// true if module is singular, false if not. null=unknown
|
||
'singular' => null,
|
||
// unix-timestamp date/time module added to system (for uninstalled modules, it is the file date)
|
||
'created' => 0,
|
||
// is the module currently installed? (boolean, or null when not determined)
|
||
'installed' => null,
|
||
// this is set to true when the module is configurable, false when it's not, and null when it's not determined
|
||
'configurable' => null,
|
||
// verbose mode only: true when module implements SearchableModule interface, or null|false when not
|
||
'searchable' => null,
|
||
// namespace that module lives in (string)
|
||
'namespace' => null,
|
||
// verbose mode only: this is set to the module filename (from PW installation root), false when it can't be found, null when it hasn't been determined
|
||
'file' => null,
|
||
// verbose mode only: this is set to true when the module is a core module, false when it's not, and null when it's not determined
|
||
'core' => null,
|
||
// verbose mode only: any translations supplied with the module
|
||
// 'languages' => null,
|
||
|
||
// other properties that may be present, but are optional, for Process modules:
|
||
// 'nav' => array(), // navigation definition: see Process.php
|
||
// 'useNavJSON' => bool, // whether the Process module provides JSON navigation
|
||
// 'page' => array(), // page to create for Process module: see Process.php
|
||
// 'permissionMethod' => string or callable // method to call to determine permission: see Process.php
|
||
);
|
||
|
||
if($getAll) {
|
||
if(empty($this->moduleInfoCache)) $this->loadModuleInfoCache();
|
||
$modulesInfo = $this->moduleInfoCache;
|
||
if($options['verbose']) {
|
||
if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();
|
||
foreach($this->moduleInfoCacheVerbose as $moduleID => $moduleInfoVerbose) {
|
||
$modulesInfo[$moduleID] = array_merge($modulesInfo[$moduleID], $moduleInfoVerbose);
|
||
}
|
||
}
|
||
if(!$options['minify']) {
|
||
foreach($modulesInfo as $moduleID => $info) {
|
||
$modulesInfo[$moduleID] = array_merge($infoTemplate, $info);
|
||
}
|
||
}
|
||
return $modulesInfo;
|
||
|
||
} else if($getSystem) {
|
||
// module is a system
|
||
if($class === 'info') return $infoTemplate;
|
||
$info = $this->getModuleInfoSystem($module);
|
||
return $options['minify'] ? $info : array_merge($infoTemplate, $info);
|
||
|
||
} else if($module instanceof Module) {
|
||
// module is an instance
|
||
// $moduleName = method_exists($module, 'className') ? $module->className() : get_class($module);
|
||
// return from cache if available
|
||
|
||
if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) {
|
||
$info = $this->moduleInfoCache[$moduleID];
|
||
$fromCache = true;
|
||
} else {
|
||
$info = $this->getModuleInfoExternal($moduleName);
|
||
if(!count($info)) $info = $this->getModuleInfoInternal($module);
|
||
}
|
||
|
||
} else {
|
||
|
||
// module is a class name or ID
|
||
if(ctype_digit("$module")) $module = $moduleName;
|
||
|
||
// return from cache if available (as it almost always should be)
|
||
if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) {
|
||
$info = $this->moduleInfoCache[$moduleID];
|
||
$fromCache = true;
|
||
|
||
} else if(empty($options['noCache']) && $moduleID == 0) {
|
||
// uninstalled module
|
||
if(!count($this->moduleInfoCacheUninstalled)) $this->loadModuleInfoCacheVerbose(true);
|
||
if(isset($this->moduleInfoCacheUninstalled[$moduleName])) {
|
||
$info = $this->moduleInfoCacheUninstalled[$moduleName];
|
||
$fromCache = true;
|
||
}
|
||
}
|
||
|
||
if(!$fromCache) {
|
||
$namespace = $this->getModuleNamespace($moduleName);
|
||
if(class_exists($namespace . $moduleName, false)) {
|
||
// module is already in memory, check external first, then internal
|
||
$info = $this->getModuleInfoExternal($moduleName);
|
||
if(!count($info)) $info = $this->getModuleInfoInternal($moduleName, $namespace);
|
||
|
||
} else {
|
||
// module is not in memory, check external first, then internal
|
||
$info = $this->getModuleInfoExternal($moduleName);
|
||
if(!count($info)) {
|
||
if(isset($this->installable[$moduleName])) $this->includeModuleFile($this->installable[$moduleName], $moduleName);
|
||
// info not available externally, attempt to locate it interally
|
||
$info = $this->getModuleInfoInternal($moduleName, $namespace);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!$fromCache && !count($info)) {
|
||
$info = $infoTemplate;
|
||
$info['title'] = $module;
|
||
$info['summary'] = 'Inactive';
|
||
$info['error'] = 'Unable to locate module';
|
||
return $info;
|
||
}
|
||
|
||
if(!$options['minify']) $info = array_merge($infoTemplate, $info);
|
||
$info['id'] = (int) $moduleID;
|
||
|
||
if($fromCache) {
|
||
// since cache is loaded at init(), this is the most common scenario
|
||
|
||
if($options['verbose']) {
|
||
if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();
|
||
if(!empty($this->moduleInfoCacheVerbose[$moduleID])) {
|
||
$info = array_merge($info, $this->moduleInfoCacheVerbose[$moduleID]);
|
||
}
|
||
}
|
||
|
||
// populate defaults for properties omitted from cache
|
||
if(is_null($info['autoload'])) $info['autoload'] = false;
|
||
if(is_null($info['singular'])) $info['singular'] = false;
|
||
if(is_null($info['configurable'])) $info['configurable'] = false;
|
||
if(is_null($info['core'])) $info['core'] = false;
|
||
if(is_null($info['installed'])) $info['installed'] = true;
|
||
if(is_null($info['namespace'])) $info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
|
||
if(!empty($info['requiresVersions'])) $info['requires'] = array_keys($info['requiresVersions']);
|
||
if($moduleName == 'SystemUpdater') $info['configurable'] = 1; // fallback, just in case
|
||
|
||
// we skip everything else when module comes from cache since we can safely assume the checks below
|
||
// are already accounted for in the cached module info
|
||
|
||
} else {
|
||
// not from cache, only likely to occur when refreshing modules info caches
|
||
|
||
// if $info[requires] isn't already an array, make it one
|
||
if(!is_array($info['requires'])) {
|
||
$info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespace
|
||
if(strpos($info['requires'], ',') !== false) {
|
||
$info['requires'] = explode(',', $info['requires']);
|
||
} else {
|
||
$info['requires'] = array($info['requires']);
|
||
}
|
||
}
|
||
|
||
// populate requiresVersions
|
||
foreach($info['requires'] as $key => $class) {
|
||
if(!ctype_alnum($class)) {
|
||
// has a version string
|
||
list($class, $operator, $version) = $this->extractModuleOperatorVersion($class);
|
||
$info['requires'][$key] = $class; // convert to just class
|
||
} else {
|
||
// no version string
|
||
$operator = '>=';
|
||
$version = 0;
|
||
}
|
||
$info['requiresVersions'][$class] = array($operator, $version);
|
||
}
|
||
|
||
// what does it install?
|
||
// if $info[installs] isn't already an array, make it one
|
||
if(!is_array($info['installs'])) {
|
||
$info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespace
|
||
if(strpos($info['installs'], ',') !== false) {
|
||
$info['installs'] = explode(',', $info['installs']);
|
||
} else {
|
||
$info['installs'] = array($info['installs']);
|
||
}
|
||
}
|
||
|
||
// misc
|
||
if($options['verbose']) $info['versionStr'] = $this->formatVersion($info['version']); // versionStr
|
||
$info['name'] = $moduleName; // module name
|
||
|
||
// module configurable?
|
||
$configurable = $this->isConfigurable($moduleName, false);
|
||
if($configurable === true || is_int($configurable) && $configurable > 1) {
|
||
// configurable via ConfigurableModule interface
|
||
// true=static, 2=non-static, 3=non-static $data, 4=non-static wrap,
|
||
// 19=non-static getModuleConfigArray, 20=static getModuleConfigArray
|
||
$info['configurable'] = $configurable;
|
||
} else if($configurable) {
|
||
// configurable via external file: ModuleName.config.php or ModuleNameConfig.php file
|
||
$info['configurable'] = basename($configurable);
|
||
} else {
|
||
// not configurable
|
||
$info['configurable'] = false;
|
||
}
|
||
|
||
// created date
|
||
if(isset($this->createdDates[$moduleID])) $info['created'] = strtotime($this->createdDates[$moduleID]);
|
||
$info['installed'] = isset($this->installable[$moduleName]) ? false : true;
|
||
if(!$info['installed'] && !$info['created'] && isset($this->installable[$moduleName])) {
|
||
// uninstalled modules get their created date from the file or dir that they are in (whichever is newer)
|
||
$pathname = $this->installable[$moduleName];
|
||
$filemtime = (int) filemtime($pathname);
|
||
$dirname = dirname($pathname);
|
||
$dirmtime = substr($dirname, -7) == 'modules' || strpos($dirname, $this->paths[0]) !== false ? 0 : (int) filemtime($dirname);
|
||
$info['created'] = $dirmtime > $filemtime ? $dirmtime : $filemtime;
|
||
}
|
||
|
||
// namespace
|
||
if($info['core']) {
|
||
// default namespace, assumed since all core modules are in default namespace
|
||
$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
|
||
} else {
|
||
$info['namespace'] = $this->getModuleNamespace($moduleName, array(
|
||
'file' => $info['file'],
|
||
'noCache' => $options['noCache']
|
||
));
|
||
}
|
||
|
||
if(!$options['verbose']) foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]);
|
||
}
|
||
|
||
if(is_null($info['namespace'])) {
|
||
$info['namespace'] = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
|
||
}
|
||
|
||
if(empty($info['created']) && isset($this->createdDates[$moduleID])) {
|
||
$info['created'] = strtotime($this->createdDates[$moduleID]);
|
||
}
|
||
|
||
if($options['verbose']) {
|
||
// the file property is not stored in the verbose cache, but provided as a verbose key
|
||
$info['file'] = $this->getModuleFile($moduleName);
|
||
if($info['file']) $info['core'] = strpos($info['file'], $this->coreModulesDir) !== false; // is it core?
|
||
} else {
|
||
// module info may still contain verbose keys with undefined values
|
||
}
|
||
|
||
if($options['minify']) {
|
||
// when minify, any values that match defaults from infoTemplate are removed
|
||
if(!$options['verbose']) foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]);
|
||
foreach($info as $key => $value) {
|
||
if(!array_key_exists($key, $infoTemplate)) continue;
|
||
if($value !== $infoTemplate[$key]) continue;
|
||
unset($info[$key]);
|
||
}
|
||
}
|
||
|
||
// if($this->debug) $this->message("getModuleInfo($moduleName) " . ($fromCache ? "CACHE" : "NO-CACHE"));
|
||
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* Returns a verbose array of information for a Module
|
||
*
|
||
* This is the same as what’s returned by `Modules::getModuleInfo()` except that it has the following additional properties:
|
||
*
|
||
* - `versionStr` (string): formatted module version string.
|
||
* - `file` (string): module filename from PW installation root, or false when it can't be found.
|
||
* - `core` (bool): true when module is a core module, false when not.
|
||
* - `author` (string): module author, when specified.
|
||
* - `summary` (string): summary of what this module does.
|
||
* - `href` (string): URL to module details (when specified).
|
||
* - `permissions` (array): permissions installed by this module, associative array ('permission - name' => 'Description').
|
||
* - `page` (array): definition of page to create for Process module (see Process class)
|
||
*
|
||
* @param string|Module|int $class May be class name, module instance, or module ID
|
||
* @param array $options Optional options to modify behavior of what gets returned:
|
||
* - `noCache` (bool): prevents use of cache to retrieve the module info
|
||
* - `noInclude` (bool): prevents include() of the module file, applicable only if it hasn't already been included
|
||
* @return array Associative array of module information
|
||
* @see Modules::getModuleInfo()
|
||
*
|
||
*/
|
||
public function getModuleInfoVerbose($class, array $options = array()) {
|
||
$options['verbose'] = true;
|
||
$info = $this->getModuleInfo($class, $options);
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* Get just a single property of module info
|
||
*
|
||
* @param Module|string $class Module instance or module name
|
||
* @param string $property Name of property to get
|
||
* @param array $options Additional options (see getModuleInfo method for options)
|
||
* @return mixed|null Returns value of property or null if not found
|
||
* @since 3.0.107
|
||
*
|
||
*/
|
||
public function getModuleInfoProperty($class, $property, array $options = array()) {
|
||
if(in_array($property, $this->moduleInfoVerboseKeys)) {
|
||
$info = $this->getModuleInfoVerbose($class, $options);
|
||
$info['verbose'] = true;
|
||
} else {
|
||
$info = $this->getModuleInfo($class, $options);
|
||
}
|
||
if(!isset($info[$property]) && empty($info['verbose'])) {
|
||
// try again, just in case we can find it in verbose data
|
||
$info = $this->getModuleInfoVerbose($class, $options);
|
||
}
|
||
return isset($info[$property]) ? $info[$property] : null;
|
||
}
|
||
|
||
/**
|
||
* Get an array of all unique, non-default, non-root module namespaces mapped to directory names
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getNamespaces() {
|
||
if(!is_null($this->moduleNamespaceCache)) return $this->moduleNamespaceCache;
|
||
$defaultNamespace = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\" : "";
|
||
$namespaces = array();
|
||
foreach($this->moduleInfoCache as $moduleID => $info) {
|
||
if(!isset($info['namespace']) || $info['namespace'] === $defaultNamespace || $info['namespace'] === "\\") continue;
|
||
$moduleName = $info['name'];
|
||
$namespaces[$info['namespace']] = $this->wire('config')->paths->$moduleName;
|
||
}
|
||
$this->moduleNamespaceCache = $namespaces;
|
||
return $namespaces;
|
||
}
|
||
|
||
/**
|
||
* Get the namespace for the given module
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $moduleName
|
||
* @param array $options
|
||
* - `file` (string): Known module path/file, as an optimization.
|
||
* - `noCache` (bool): Specify true to force reload namespace info directly from module file. (default=false)
|
||
* - `noLoad` (bool): Specify true to prevent loading of file for namespace discovery. (default=false) Added 3.0.170
|
||
* @return null|string Returns namespace, or NULL if unable to determine. Namespace is ready to use in a string (i.e. has trailing slashes)
|
||
*
|
||
*/
|
||
public function getModuleNamespace($moduleName, $options = array()) {
|
||
|
||
$defaults = array(
|
||
'file' => null,
|
||
'noLoad' => false,
|
||
'noCache' => false,
|
||
);
|
||
|
||
$namespace = null;
|
||
$options = array_merge($defaults, $options);
|
||
|
||
if(is_object($moduleName) || strpos($moduleName, "\\") !== false) {
|
||
$className = is_object($moduleName) ? get_class($moduleName) : $moduleName;
|
||
$parts = explode("\\", $className);
|
||
array_pop($parts);
|
||
$namespace = count($parts) ? implode("\\", $parts) : "";
|
||
$namespace = $namespace == "" ? "\\" : "\\$namespace\\";
|
||
return $namespace;
|
||
}
|
||
|
||
if(empty($options['noCache'])) {
|
||
$moduleID = $this->getModuleID($moduleName);
|
||
$info = isset($this->moduleInfoCache[$moduleID]) ? $this->moduleInfoCache[$moduleID] : null;
|
||
if($info && isset($info['namespace'])) {
|
||
return $info['namespace'];
|
||
}
|
||
}
|
||
|
||
if(empty($options['file'])) {
|
||
$options['file'] = $this->getModuleFile($moduleName);
|
||
}
|
||
|
||
if(strpos($options['file'], $this->coreModulesDir) !== false) {
|
||
// all core modules use \ProcessWire\ namespace
|
||
$namespace = strlen(__NAMESPACE__) ? __NAMESPACE__ . "\\" : "";
|
||
return $namespace;
|
||
}
|
||
|
||
if(!$options['file'] || !file_exists($options['file'])) {
|
||
return null;
|
||
}
|
||
|
||
if(empty($options['noLoad'])) {
|
||
$namespace = $this->getFileNamespace($options['file']);
|
||
}
|
||
|
||
return $namespace;
|
||
}
|
||
|
||
/**
|
||
* Get the namespace used in the given .php or .module file
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $file
|
||
* @return string Includes leading and trailing backslashes where applicable
|
||
*
|
||
*/
|
||
public function getFileNamespace($file) {
|
||
$namespace = $this->wire()->files->getNamespace($file);
|
||
if($namespace !== "\\") $namespace = "\\" . trim($namespace, "\\") . "\\";
|
||
return $namespace;
|
||
}
|
||
|
||
/**
|
||
* Get the class defined in the file (or optionally the 'extends' or 'implements')
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $file
|
||
* @return array Returns array with these indexes:
|
||
* 'class' => string (class without namespace)
|
||
* 'className' => string (class with namespace)
|
||
* 'extends' => string
|
||
* 'namespace' => string
|
||
* 'implements' => array
|
||
*
|
||
*/
|
||
public function getFileClassInfo($file) {
|
||
|
||
$value = array(
|
||
'class' => '',
|
||
'className' => '',
|
||
'extends' => '',
|
||
'namespace' => '',
|
||
'implements' => array()
|
||
);
|
||
|
||
if(!is_file($file)) return $value;
|
||
$data = file_get_contents($file);
|
||
if(!strpos($data, 'class')) return $value;
|
||
if(!preg_match('/^\s*class\s+(.+)$/m', $data, $matches)) return $value;
|
||
|
||
if(strpos($matches[1], "\t") !== false) $matches[1] = str_replace("\t", " ", $matches[1]);
|
||
$parts = explode(' ', trim($matches[1]));
|
||
|
||
foreach($parts as $key => $part) {
|
||
if(empty($part)) unset($parts[$key]);
|
||
}
|
||
|
||
$className = array_shift($parts);
|
||
if(strpos($className, '\\') !== false) {
|
||
$className = trim($className, '\\');
|
||
$a = explode('\\', $className);
|
||
$value['className'] = "\\$className\\";
|
||
$value['class'] = array_pop($a);
|
||
$value['namespace'] = '\\' . implode('\\', $a) . '\\';
|
||
} else {
|
||
$value['className'] = '\\' . $className;
|
||
$value['class'] = $className;
|
||
$value['namespace'] = '\\';
|
||
}
|
||
|
||
while(count($parts)) {
|
||
$next = array_shift($parts);
|
||
if($next == 'extends') {
|
||
$value['extends'] = array_shift($parts);
|
||
} else if($next == 'implements') {
|
||
$implements = array_shift($parts);
|
||
if(strlen($implements)) {
|
||
$implements = str_replace(' ', '', $implements);
|
||
$value['implements'] = explode(',', $implements);
|
||
}
|
||
}
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Alias of getConfig() for backwards compatibility
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $className
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getModuleConfigData($className) {
|
||
return $this->getConfig($className);
|
||
}
|
||
|
||
/**
|
||
* Return the URL where the module can be edited, configured or uninstalled
|
||
*
|
||
* If module is not installed, it just returns the URL to ProcessModule.
|
||
*
|
||
* #pw-group-configuration
|
||
*
|
||
* @param string|Module $className
|
||
* @param bool $collapseInfo
|
||
* @return string
|
||
*
|
||
*/
|
||
public function getModuleEditUrl($className, $collapseInfo = true) {
|
||
if(!is_string($className)) $className = $this->getModuleClass($className);
|
||
$url = $this->wire('config')->urls->admin . 'module/';
|
||
if(empty($className) || !$this->isInstalled($className)) return $url;
|
||
$url .= "edit/?name=$className";
|
||
if($collapseInfo) $url .= "&collapse_info=1";
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Given a module name, return an associative array of configuration data for it
|
||
*
|
||
* - Applicable only for modules that support configuration.
|
||
* - Configuration data is stored encoded in the database "modules" table "data" field.
|
||
*
|
||
* ~~~~~~
|
||
* // Getting, modifying and saving module config data
|
||
* $data = $modules->getConfig('HelloWorld');
|
||
* $data['greeting'] = 'Hello World! How are you today?';
|
||
* $modules->saveConfig('HelloWorld', $data);
|
||
*
|
||
* // Getting just one property 'apiKey' from module config data
|
||
* @apiKey = $modules->getConfig('HelloWorld', 'apiKey');
|
||
* ~~~~~~
|
||
*
|
||
* #pw-group-configuration
|
||
* #pw-changelog 3.0.16 Changed from more verbose name `getModuleConfigData()`, which can still be used.
|
||
*
|
||
* @param string|Module $class
|
||
* @param string $property Optionally just get value for a specific property (omit to get all config)
|
||
* @return array|string|int|float Module configuration data, returns array unless a specific $property was requested
|
||
* @see Modules::saveConfig()
|
||
* @since 3.0.16 Use method getModuleConfigData() with same arguments for prior versions (can also be used on any version).
|
||
*
|
||
*/
|
||
public function getConfig($class, $property = '') {
|
||
|
||
$emptyReturn = $property ? null : array();
|
||
$className = $class;
|
||
if(is_object($className)) $className = wireClassName($className->className(), false);
|
||
if(!isset($this->moduleIDs[$className])) return $emptyReturn;
|
||
$id = $this->moduleIDs[$className];
|
||
if(!$id) return $emptyReturn;
|
||
if(!isset($this->configData[$id])) return $emptyReturn; // module has no config data
|
||
|
||
if(is_array($this->configData[$id])) {
|
||
$data = $this->configData[$id];
|
||
} else {
|
||
$configable = $this->isConfigable($className);
|
||
if(!$configable) return $emptyReturn;
|
||
$database = $this->wire()->database;
|
||
$query = $database->prepare("SELECT data FROM modules WHERE id=:id", "modules.getConfig($className)"); // QA
|
||
$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
$data = $query->fetchColumn();
|
||
$query->closeCursor();
|
||
if(strlen($data)) $data = wireDecodeJSON($data);
|
||
if(empty($data)) $data = array();
|
||
$this->configData[$id] = $data;
|
||
}
|
||
|
||
if($property) return isset($data[$property]) ? $data[$property] : null;
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* Get the path + filename (or optionally URL) for this module
|
||
*
|
||
* @param string|Module $class Module class name or object instance
|
||
* @param array|bool $options Options to modify default behavior:
|
||
* - `getURL` (bool): Specify true if you want to get the URL rather than file path (default=false).
|
||
* - `fast` (bool): Specify true to omit file_exists() checks (default=false).
|
||
* - `guess` (bool): Manufacture/guess a module location if one cannot be found (default=false) 3.0.170+
|
||
* - Note: If you specify a boolean for the $options argument, it is assumed to be the $getURL property.
|
||
* @return bool|string Returns string of module file, or false on failure.
|
||
*
|
||
*/
|
||
public function getModuleFile($class, $options = array()) {
|
||
|
||
$config = $this->wire()->config;
|
||
$className = $class;
|
||
if(is_bool($options)) $options = array('getURL' => $options);
|
||
if(!isset($options['getURL'])) $options['getURL'] = false;
|
||
if(!isset($options['fast'])) $options['fast'] = false;
|
||
|
||
$file = false;
|
||
|
||
// first see it's an object, and if we can get the file from the object
|
||
if(is_object($className)) {
|
||
$module = $className;
|
||
if($module instanceof ModulePlaceholder) $file = $module->file;
|
||
$moduleName = $module->className();
|
||
$className = $module->className(true);
|
||
} else {
|
||
$moduleName = wireClassName($className, false);
|
||
}
|
||
|
||
$hasDuplicate = $this->duplicates()->hasDuplicate($moduleName);
|
||
|
||
if(!$hasDuplicate) {
|
||
// see if we can determine it from already stored paths
|
||
$path = $config->paths->$moduleName;
|
||
if($path) {
|
||
$file = $path . $moduleName . ($this->moduleFileExts[$moduleName] === 2 ? '.module.php' : '.module');
|
||
if(!$options['fast'] && !file_exists($file)) $file = false;
|
||
}
|
||
}
|
||
|
||
// next see if we've already got the module filename cached locally
|
||
if(!$file && isset($this->installable[$moduleName]) && !$hasDuplicate) {
|
||
$file = $this->installable[$moduleName];
|
||
if(!$options['fast'] && !file_exists($file)) $file = false;
|
||
}
|
||
|
||
if(!$file) {
|
||
$dupFile = $this->duplicates()->getCurrent($moduleName);
|
||
if($dupFile) {
|
||
$rootPath = $config->paths->root;
|
||
$file = rtrim($rootPath, '/') . $dupFile;
|
||
if(!file_exists($file)) {
|
||
// module in use may have been deleted, find the next available one that exists
|
||
$file = '';
|
||
$dups = $this->duplicates()->getDuplicates($moduleName);
|
||
foreach($dups['files'] as $pathname) {
|
||
$pathname = rtrim($rootPath, '/') . $pathname;
|
||
if(file_exists($pathname)) $file = $pathname;
|
||
if($file) break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!$file) {
|
||
// see if it's a predefined core type that can be determined from the type
|
||
// this should only come into play if module has moved or had a load error
|
||
foreach($this->coreTypes as $typeName) {
|
||
if(strpos($moduleName, $typeName) !== 0) continue;
|
||
$checkFiles = array(
|
||
"$typeName/$moduleName/$moduleName.module",
|
||
"$typeName/$moduleName/$moduleName.module.php",
|
||
"$typeName/$moduleName.module",
|
||
"$typeName/$moduleName.module.php",
|
||
);
|
||
$path1 = $config->paths->modules;
|
||
foreach($checkFiles as $checkFile) {
|
||
$file1 = $path1 . $checkFile;
|
||
if(file_exists($file1)) $file = $file1;
|
||
if($file) break;
|
||
}
|
||
if($file) break;
|
||
}
|
||
if(!$file) {
|
||
// check site modules
|
||
$checkFiles = array(
|
||
"$moduleName/$moduleName.module",
|
||
"$moduleName/$moduleName.module.php",
|
||
"$moduleName.module",
|
||
"$moduleName.module.php",
|
||
);
|
||
$path1 = $config->paths->siteModules;
|
||
foreach($checkFiles as $checkFile) {
|
||
$file1 = $path1 . $checkFile;
|
||
if(file_exists($file1)) $file = $file1;
|
||
if($file) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!$file) {
|
||
// if all the above failed, try to get it from Reflection
|
||
try {
|
||
// note we don't call getModuleClass() here because it may result in a circular reference
|
||
if(strpos($className, "\\") === false) {
|
||
$moduleID = $this->getModuleID($moduleName);
|
||
if(!empty($this->moduleInfoCache[$moduleID]['namespace'])) {
|
||
$className = rtrim($this->moduleInfoCache[$moduleID]['namespace'], "\\") . "\\$moduleName";
|
||
} else {
|
||
$className = strlen(__NAMESPACE__) ? "\\" . __NAMESPACE__ . "\\$moduleName" : $moduleName;
|
||
}
|
||
}
|
||
$reflector = new \ReflectionClass($className);
|
||
$file = $reflector->getFileName();
|
||
|
||
} catch(\Exception $e) {
|
||
$file = false;
|
||
}
|
||
}
|
||
|
||
if(!$file && !empty($options['guess'])) {
|
||
// make a guess about where module would be if we had been able to find it
|
||
$file = $config->paths->siteModules . "$moduleName/$moduleName.module";
|
||
}
|
||
|
||
if($file) {
|
||
if(DIRECTORY_SEPARATOR != '/') $file = str_replace(DIRECTORY_SEPARATOR, '/', $file);
|
||
if($options['getURL']) $file = str_replace($config->paths->root, '/', $file);
|
||
}
|
||
|
||
return $file;
|
||
}
|
||
|
||
/**
|
||
* Is the given module interactively configurable?
|
||
*
|
||
* This method can be used to simply determine if a module is configurable (yes or no), or more specifically
|
||
* how it is configurable.
|
||
*
|
||
* ~~~~~
|
||
* // Determine IF a module is configurable
|
||
* if($modules->isConfigurable('HelloWorld')) {
|
||
* // Module is configurable
|
||
* } else {
|
||
* // Module is NOT configurable
|
||
* }
|
||
* ~~~~~
|
||
* ~~~~~
|
||
* // Determine HOW a module is configurable
|
||
* $configurable = $module->isConfigurable('HelloWorld');
|
||
* if($configurable === true) {
|
||
* // configurable in a way compatible with all past versions of ProcessWire
|
||
* } else if(is_string($configurable)) {
|
||
* // configurable via an external configuration file
|
||
* // file is identifed in $configurable variable
|
||
* } else if(is_int($configurable)) {
|
||
* // configurable via a method in the class
|
||
* // the $configurable variable contains a number with specifics
|
||
* } else {
|
||
* // module is NOT configurable
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* ### Return value details
|
||
*
|
||
* #### If module is configurable via external configuration file:
|
||
*
|
||
* - Returns string of full path/filename to `ModuleName.config.php` file
|
||
*
|
||
* #### If module is configurable because it implements a configurable module interface:
|
||
*
|
||
* - Returns boolean `true` if module is configurable via the static `getModuleConfigInputfields()` method.
|
||
* This particular method is compatible with all past versions of ProcessWire.
|
||
* - Returns integer `2` if module is configurable via the non-static `getModuleConfigInputfields()` and requires no arguments.
|
||
* - Returns integer `3` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `$data` array.
|
||
* - Returns integer `4` if module is configurable via the non-static `getModuleConfigInputfields()` and requires `InputfieldWrapper` argument.
|
||
* - Returns integer `19` if module is configurable via non-static `getModuleConfigArray()` method.
|
||
* - Returns integer `20` if module is configurable via static `getModuleConfigArray()` method.
|
||
*
|
||
* #### If module is not configurable:
|
||
*
|
||
* - Returns boolean `false` if not configurable
|
||
*
|
||
* *This method is named isConfigurableModule() in ProcessWire versions prior to to 3.0.16.*
|
||
*
|
||
* #pw-group-configuration
|
||
*
|
||
* @param Module|string $class Module name
|
||
* @param bool $useCache Use caching? This accepts a few options:
|
||
* - Specify boolean `true` to allow use of cache when available (default behavior).
|
||
* - Specify boolean `false` to disable retrieval of this property from getModuleInfo (forces a new check).
|
||
* - Specify string `interface` to check only if module implements ConfigurableModule interface.
|
||
* - Specify string `file` to check only if module has a separate configuration class/file.
|
||
* @return bool|string|int See details about return values in method description.
|
||
* @since 3.0.16
|
||
*
|
||
* @todo all ConfigurableModule methods need to be split out into their own class (ConfigurableModules?)
|
||
* @todo this method has two distinct parts (file and interface) that need to be split in two methods.
|
||
*
|
||
*/
|
||
public function isConfigurable($class, $useCache = true) {
|
||
|
||
$className = $class;
|
||
$moduleInstance = null;
|
||
$namespace = $this->getModuleNamespace($className);
|
||
if(is_object($className)) {
|
||
$moduleInstance = $className;
|
||
$className = $this->getModuleClass($moduleInstance);
|
||
}
|
||
$nsClassName = $namespace . $className;
|
||
|
||
if($useCache === true || $useCache === 1 || $useCache === "1") {
|
||
$info = $this->getModuleInfo($className);
|
||
// if regular module info doesn't have configurable info, attempt it from verbose module info
|
||
// should only be necessary for transition period between the 'configurable' property being
|
||
// moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7)
|
||
if($info['configurable'] === null) $info = $this->getModuleInfoVerbose($className);
|
||
if(!$info['configurable']) {
|
||
if($moduleInstance && $moduleInstance instanceof ConfigurableModule) {
|
||
// re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format
|
||
// this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12
|
||
$this->error(
|
||
"Configurable module check failed for $className. " .
|
||
"If this error persists, please do a Modules > Refresh.",
|
||
Notice::debug
|
||
);
|
||
$useCache = false;
|
||
} else {
|
||
return false;
|
||
}
|
||
} else {
|
||
if($info['configurable'] === true) return $info['configurable'];
|
||
if($info['configurable'] === 1 || $info['configurable'] === "1") return true;
|
||
if(is_int($info['configurable']) || ctype_digit("$info[configurable]")) return (int) $info['configurable'];
|
||
if(strpos($info['configurable'], $className) === 0) {
|
||
if(empty($info['file'])) $info['file'] = $this->getModuleFile($className);
|
||
if($info['file']) {
|
||
return dirname($info['file']) . "/$info[configurable]";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if($useCache !== "interface") {
|
||
// check for separate module configuration file
|
||
$dir = dirname($this->getModuleFile($className));
|
||
if($dir) {
|
||
$files = array(
|
||
"$dir/{$className}Config.php",
|
||
"$dir/$className.config.php"
|
||
);
|
||
$found = false;
|
||
foreach($files as $file) {
|
||
if(!is_file($file)) continue;
|
||
$config = null; // include file may override
|
||
$this->includeModuleFile($file, $className);
|
||
$classConfig = $nsClassName . 'Config';
|
||
if(class_exists($classConfig, false)) {
|
||
$parents = wireClassParents($classConfig, false);
|
||
if(is_array($parents) && in_array('ModuleConfig', $parents)) {
|
||
$found = $file;
|
||
break;
|
||
}
|
||
} else {
|
||
// bypass include_once, because we need to read $config every time
|
||
if(is_null($config)) {
|
||
$classInfo = $this->getFileClassInfo($file);
|
||
if($classInfo['class']) {
|
||
// not safe to include because this is not just a file with a $config array
|
||
} else {
|
||
$ns = $this->getFileNamespace($file);
|
||
$file = $this->compile($className, $file, $ns);
|
||
if($file) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include($file);
|
||
}
|
||
}
|
||
}
|
||
if(!is_null($config)) {
|
||
// included file specified a $config array
|
||
$found = $file;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if($found) return $found;
|
||
}
|
||
}
|
||
|
||
// if file-only check was requested and we reach this point, exit with false now
|
||
if($useCache === "file") return false;
|
||
|
||
// ConfigurableModule interface checks
|
||
|
||
$result = false;
|
||
|
||
foreach(array('getModuleConfigArray', 'getModuleConfigInputfields') as $method) {
|
||
|
||
$configurable = false;
|
||
|
||
// if we have a module instance, use that for our check
|
||
if($moduleInstance && $moduleInstance instanceof ConfigurableModule) {
|
||
if(method_exists($moduleInstance, $method)) {
|
||
$configurable = $method;
|
||
} else if(method_exists($moduleInstance, "___$method")) {
|
||
$configurable = "___$method";
|
||
}
|
||
}
|
||
|
||
// if we didn't have a module instance, load the file to find what we need to know
|
||
if(!$configurable) {
|
||
if(!wireClassExists($nsClassName, false)) {
|
||
$this->includeModule($className);
|
||
}
|
||
$interfaces = wireClassImplements($nsClassName, false);
|
||
if(is_array($interfaces) && in_array('ConfigurableModule', $interfaces)) {
|
||
if(wireMethodExists($nsClassName, $method)) {
|
||
$configurable = $method;
|
||
} else if(wireMethodExists($nsClassName, "___$method")) {
|
||
$configurable = "___$method";
|
||
}
|
||
}
|
||
}
|
||
|
||
// if still not determined to be configurable, move on to next method
|
||
if(!$configurable) continue;
|
||
|
||
// now determine if static or non-static
|
||
$ref = new \ReflectionMethod(wireClassName($nsClassName, true), $configurable);
|
||
|
||
if($ref->isStatic()) {
|
||
// config method is implemented as a static method
|
||
if($method == 'getModuleConfigInputfields') {
|
||
// static getModuleConfigInputfields
|
||
$result = true;
|
||
} else {
|
||
// static getModuleConfigArray
|
||
$result = 20;
|
||
}
|
||
|
||
} else if($method == 'getModuleConfigInputfields') {
|
||
// non-static getModuleConfigInputfields
|
||
// we allow for different arguments, so determine what it needs
|
||
$parameters = $ref->getParameters();
|
||
if(count($parameters)) {
|
||
$param0 = reset($parameters);
|
||
if(strpos($param0, 'array') !== false || strpos($param0, '$data') !== false) {
|
||
// method requires a $data array (for compatibility with non-static version)
|
||
$result = 3;
|
||
} else if(strpos($param0, 'InputfieldWrapper') !== false || strpos($param0, 'inputfields') !== false) {
|
||
// method requires an empty InputfieldWrapper (as a convenience)
|
||
$result = 4;
|
||
}
|
||
}
|
||
// method requires no arguments
|
||
if(!$result) $result = 2;
|
||
|
||
} else {
|
||
// non-static getModuleConfigArray
|
||
$result = 19;
|
||
}
|
||
|
||
// if we make it here, we know we already have a result so can stop now
|
||
break;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
|
||
/**
|
||
* Indicates whether module accepts config settings, whether interactively or API only
|
||
*
|
||
* - Returns false if module does not accept config settings.
|
||
* - Returns integer `30` if module accepts config settings but is not interactively configurable.
|
||
* - Returns true, int or string if module is interactively configurable, see `Modules::isConfigurable()` return values.
|
||
*
|
||
* @param string|Module $class
|
||
* @param bool $useCache
|
||
* @return bool|int|string
|
||
* @since 3.0.179
|
||
*
|
||
*/
|
||
public function isConfigable($class, $useCache = true) {
|
||
if(is_object($class)) {
|
||
if($class instanceof ConfigModule) {
|
||
$result = 30;
|
||
} else {
|
||
$result = $this->isConfigurable($class, $useCache);
|
||
}
|
||
} else {
|
||
$result = $this->isConfigurable($class, $useCache);
|
||
if(!$result && wireInstanceOf($class, 'ConfigModule')) $result = 30;
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Alias of isConfigurable() for backwards compatibility
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $className
|
||
* @param bool $useCache
|
||
* @return mixed
|
||
*
|
||
*/
|
||
public function isConfigurableModule($className, $useCache = true) {
|
||
return $this->isConfigurable($className, $useCache);
|
||
}
|
||
|
||
/**
|
||
* Populate configuration data to a ConfigurableModule
|
||
*
|
||
* If the Module has a 'setConfigData' method, it will send the array of data to that.
|
||
* Otherwise it will populate the properties individually.
|
||
*
|
||
* @param Module $module
|
||
* @param array|null $data Configuration data [key=value], or omit/null if you want it to retrieve the config data for you.
|
||
* @param array|null $extraData Additional runtime configuration data to merge (default=null) 3.0.169+
|
||
* @return bool True if configured, false if not configurable
|
||
*
|
||
*/
|
||
protected function setModuleConfigData(Module $module, $data = null, $extraData = null) {
|
||
|
||
$configurable = $this->isConfigable($module);
|
||
if(!$configurable) return false;
|
||
if(!is_array($data)) $data = $this->getConfig($module);
|
||
if($extraData !== null && is_array($extraData)) $data = array_merge($data, $extraData);
|
||
|
||
$nsClassName = $module->className(true);
|
||
$moduleName = $module->className(false);
|
||
|
||
if(is_string($configurable) && is_file($configurable) && strpos(basename($configurable), $moduleName) === 0) {
|
||
// get defaults from ModuleConfig class if available
|
||
$className = $nsClassName . 'Config';
|
||
$config = null; // may be overridden by included file
|
||
// $compile = strrpos($className, '\\') < 1 && $this->wire('config')->moduleCompile;
|
||
$configFile = '';
|
||
|
||
if(!class_exists($className, false)) {
|
||
$configFile = $this->compile($className, $configurable);
|
||
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
|
||
if($configFile) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include_once($configFile);
|
||
}
|
||
}
|
||
|
||
if(wireClassExists($className)) {
|
||
$parents = wireClassParents($className, false);
|
||
if(is_array($parents) && in_array('ModuleConfig', $parents)) {
|
||
$moduleConfig = $this->wire(new $className());
|
||
if($moduleConfig instanceof ModuleConfig) {
|
||
$defaults = $moduleConfig->getDefaults();
|
||
$data = array_merge($defaults, $data);
|
||
}
|
||
}
|
||
} else {
|
||
// the file may have already been include_once before, so $config would not be set
|
||
// so we try a regular include() next.
|
||
if(is_null($config)) {
|
||
if(!$configFile) {
|
||
$configFile = $this->compile($className, $configurable);
|
||
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
|
||
}
|
||
if($configFile) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include($configFile);
|
||
}
|
||
}
|
||
if(is_array($config)) {
|
||
// alternatively, file may just specify a $config array
|
||
$moduleConfig = $this->wire(new ModuleConfig());
|
||
$moduleConfig->add($config);
|
||
$defaults = $moduleConfig->getDefaults();
|
||
$data = array_merge($defaults, $data);
|
||
}
|
||
}
|
||
}
|
||
|
||
if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) {
|
||
/** @var _Module $module */
|
||
$module->setConfigData($data);
|
||
return true;
|
||
}
|
||
|
||
foreach($data as $key => $value) {
|
||
$module->$key = $value;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Alias of saveConfig() for backwards compatibility
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $className
|
||
* @param array $configData
|
||
* @return mixed
|
||
*
|
||
*/
|
||
public function ___saveModuleConfigData($className, array $configData) {
|
||
return $this->saveConfig($className, $configData);
|
||
}
|
||
|
||
/**
|
||
* Save provided configuration data for the given module
|
||
*
|
||
* - Applicable only for modules that support configuration.
|
||
* - Configuration data is stored encoded in the database "modules" table "data" field.
|
||
*
|
||
* ~~~~~~
|
||
* // Getting, modifying and saving module config data
|
||
* $data = $modules->getConfig('HelloWorld');
|
||
* $data['greeting'] = 'Hello World! How are you today?';
|
||
* $modules->saveConfig('HelloWorld', $data);
|
||
* ~~~~~~
|
||
*
|
||
* #pw-group-configuration
|
||
* #pw-group-manipulation
|
||
* #pw-changelog 3.0.16 Changed name from the more verbose saveModuleConfigData(), which will still work.
|
||
*
|
||
* @param string|Module $class Module or module name
|
||
* @param array|string $data Associative array of configuration data, or name of property you want to save.
|
||
* @param mixed|null $value If you specified a property in previous arg, the value for the property.
|
||
* @return bool True on success, false on failure
|
||
* @throws WireException
|
||
* @see Modules::getConfig()
|
||
* @since 3.0.16 Use method saveModuleConfigData() with same arguments for prior versions (can also be used on any version).
|
||
*
|
||
*/
|
||
public function ___saveConfig($class, $data, $value = null) {
|
||
$className = $class;
|
||
if(is_object($className)) $className = $className->className();
|
||
$moduleName = wireClassName($className, false);
|
||
if(!$id = $this->moduleIDs[$moduleName]) throw new WireException("Unable to find ID for Module '$moduleName'");
|
||
|
||
if(is_string($data)) {
|
||
// a property and value have been provided
|
||
$property = $data;
|
||
$data = $this->getConfig($class);
|
||
if(is_null($value)) {
|
||
// remove the property
|
||
unset($data[$property]);
|
||
} else {
|
||
// populate the value for the property
|
||
$data[$property] = $value;
|
||
}
|
||
} else {
|
||
// data must be an associative array of configuration data
|
||
if(!is_array($data)) return false;
|
||
}
|
||
|
||
// ensure original duplicates info is retained and validate that it is still current
|
||
$data = $this->duplicates()->getDuplicatesConfigData($moduleName, $data);
|
||
|
||
$this->configData[$id] = $data;
|
||
$json = count($data) ? wireEncodeJSON($data, true) : '';
|
||
$database = $this->wire('database');
|
||
$query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveConfig($moduleName)"); // QA
|
||
$query->bindValue(":data", $json, \PDO::PARAM_STR);
|
||
$query->bindValue(":id", (int) $id, \PDO::PARAM_INT);
|
||
$result = $query->execute();
|
||
// $this->log("Saved module '$moduleName' config data");
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Get the Inputfields that configure the given module or return null if not configurable
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module|int $moduleName
|
||
* @param InputfieldWrapper|null $form Optionally specify the form you want Inputfields appended to.
|
||
* @return InputfieldWrapper|null
|
||
*
|
||
*/
|
||
public function ___getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
|
||
|
||
$moduleName = $this->getModuleClass($moduleName);
|
||
$configurable = $this->isConfigurable($moduleName);
|
||
if(!$configurable) return null;
|
||
|
||
if(is_null($form)) $form = $this->wire(new InputfieldWrapper());
|
||
$data = $this->getConfig($moduleName);
|
||
$fields = null;
|
||
|
||
// check for configurable module interface
|
||
$configurableInterface = $this->isConfigurable($moduleName, "interface");
|
||
if($configurableInterface) {
|
||
if(is_int($configurableInterface) && $configurableInterface > 1 && $configurableInterface < 20) {
|
||
// non-static
|
||
/** @var ConfigurableModule|Module|_Module $module */
|
||
if($configurableInterface === 2) {
|
||
// requires no arguments
|
||
$module = $this->getModule($moduleName);
|
||
$fields = $module->getModuleConfigInputfields();
|
||
} else if($configurableInterface === 3) {
|
||
// requires $data array
|
||
$module = $this->getModule($moduleName, array('noInit' => true, 'noCache' => true));
|
||
$this->setModuleConfigData($module);
|
||
$fields = $module->getModuleConfigInputfields($data);
|
||
} else if($configurableInterface === 4) {
|
||
// requires InputfieldWrapper
|
||
// we allow for option of no return statement in the method
|
||
$module = $this->getModule($moduleName);
|
||
$fields = $this->wire(new InputfieldWrapper());
|
||
$fields->setParent($form);
|
||
$_fields = $module->getModuleConfigInputfields($fields);
|
||
if($_fields instanceof InputfieldWrapper) $fields = $_fields;
|
||
unset($_fields);
|
||
} else if($configurableInterface === 19) {
|
||
// non-static getModuleConfigArray method
|
||
$module = $this->getModule($moduleName);
|
||
$fields = $this->wire(new InputfieldWrapper());
|
||
$fields->importArray($module->getModuleConfigArray());
|
||
$fields->populateValues($module);
|
||
}
|
||
} else if($configurableInterface === 20) {
|
||
// static getModuleConfigArray method
|
||
$fields = $this->wire(new InputfieldWrapper());
|
||
$fields->importArray(call_user_func(array(wireClassName($moduleName, true), 'getModuleConfigArray')));
|
||
$fields->populateValues($data);
|
||
} else if($configurableInterface) {
|
||
// static getModuleConfigInputfields method
|
||
$nsClassName = $this->getModuleNamespace($moduleName) . $moduleName;
|
||
$fields = call_user_func(array($nsClassName, 'getModuleConfigInputfields'), $data);
|
||
}
|
||
if($fields instanceof InputfieldWrapper) {
|
||
foreach($fields as $field) {
|
||
$form->append($field);
|
||
}
|
||
} else if($fields instanceof Inputfield) {
|
||
$form->append($fields);
|
||
} else {
|
||
$this->error("$moduleName.getModuleConfigInputfields() did not return InputfieldWrapper");
|
||
}
|
||
}
|
||
|
||
// check for file-based config
|
||
$file = $this->isConfigurable($moduleName, "file");
|
||
if(!$file || !is_string($file) || !is_file($file)) {
|
||
// config is not file-based
|
||
} else {
|
||
// file-based config
|
||
$config = null;
|
||
$ns = $this->getModuleNamespace($moduleName);
|
||
$configClass = $ns . $moduleName . "Config";
|
||
if(!class_exists($configClass)) {
|
||
$configFile = $this->compile($moduleName, $file, $ns);
|
||
if($configFile) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include_once($configFile);
|
||
}
|
||
}
|
||
$configModule = null;
|
||
|
||
if(wireClassExists($configClass)) {
|
||
// file contains a ModuleNameConfig class
|
||
$configModule = $this->wire(new $configClass());
|
||
|
||
} else {
|
||
if(is_null($config)) {
|
||
$configFile = $this->compile($moduleName, $file, $ns);
|
||
// if(!$configFile) $configFile = $compile ? $this->wire('files')->compile($file) : $file;
|
||
if($configFile) {
|
||
/** @noinspection PhpIncludeInspection */
|
||
include($configFile); // in case of previous include_once
|
||
}
|
||
}
|
||
if(is_array($config)) {
|
||
// file contains a $config array
|
||
$configModule = $this->wire(new ModuleConfig());
|
||
$configModule->add($config);
|
||
}
|
||
}
|
||
|
||
if($configModule && $configModule instanceof ModuleConfig) {
|
||
$defaults = $configModule->getDefaults();
|
||
$data = array_merge($defaults, $data);
|
||
$configModule->setArray($data);
|
||
$fields = $configModule->getInputfields();
|
||
if($fields instanceof InputfieldWrapper) {
|
||
foreach($fields as $field) {
|
||
$form->append($field);
|
||
}
|
||
foreach($data as $key => $value) {
|
||
$f = $form->getChildByName($key);
|
||
if(!$f) continue;
|
||
if($f instanceof InputfieldCheckbox && $value) {
|
||
$f->attr('checked', 'checked');
|
||
} else {
|
||
$f->attr('value', $value);
|
||
}
|
||
}
|
||
} else {
|
||
$this->error("$configModule.getInputfields() did not return InputfieldWrapper");
|
||
}
|
||
}
|
||
} // file-based config
|
||
|
||
if($form) {
|
||
// determine how many visible Inputfields there are in the module configuration
|
||
// for assignment or removal of flagsNoUserConfig flag when applicable
|
||
$numVisible = 0;
|
||
foreach($form->getAll() as $inputfield) {
|
||
if($inputfield instanceof InputfieldHidden || $inputfield instanceof InputfieldWrapper) continue;
|
||
$numVisible++;
|
||
}
|
||
$flags = $this->getFlags($moduleName);
|
||
if($numVisible) {
|
||
if($flags & self::flagsNoUserConfig) {
|
||
$info = $this->getModuleInfoVerbose($moduleName);
|
||
if(empty($info['addFlag']) || !($info['addFlag'] & self::flagsNoUserConfig)) {
|
||
$this->setFlag($moduleName, self::flagsNoUserConfig, false); // remove flag
|
||
}
|
||
}
|
||
} else {
|
||
if(!($flags & self::flagsNoUserConfig)) {
|
||
if(empty($info['removeFlag']) || !($info['removeFlag'] & self::flagsNoUserConfig)) {
|
||
$this->setFlag($moduleName, self::flagsNoUserConfig, true); // add flag
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return $form;
|
||
}
|
||
|
||
|
||
/**
|
||
* Is the given module Singular (single instance)?
|
||
*
|
||
* isSingular and isAutoload Module methods have been deprecated. So this method, and isAutoload()
|
||
* exist in part to enable singular and autoload properties to be set in getModuleInfo, rather than
|
||
* with methods.
|
||
*
|
||
* Note that isSingular() and isAutoload() are not deprecated for ModulePlaceholder, so the Modules
|
||
* class isn't going to stop looking for them.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param Module|string $module Module instance or class name
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isSingular($module) {
|
||
$info = $this->getModuleInfo($module);
|
||
if(isset($info['singular']) && $info['singular'] !== null) return $info['singular'];
|
||
if(is_object($module)) {
|
||
if(method_exists($module, 'isSingular')) return $module->isSingular();
|
||
} else {
|
||
// singular status can't be determined if module not installed and not specified in moduleInfo
|
||
if(isset($this->installable[$module])) return null;
|
||
$this->includeModule($module);
|
||
$module = wireClassName($module, true);
|
||
if(method_exists($module, 'isSingular')) {
|
||
/** @var Module|_Module $moduleInstance */
|
||
$moduleInstance = $this->wire(new $module());
|
||
return $moduleInstance->isSingular();
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Is the given module Autoload (automatically loaded at runtime)?
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param Module|string $module Module instance or class name
|
||
* @return bool|string|null Returns string "conditional" if conditional autoload, true if autoload, or false if not. Or null if unavailable.
|
||
*
|
||
*/
|
||
public function isAutoload($module) {
|
||
|
||
$info = $this->getModuleInfo($module);
|
||
$autoload = null;
|
||
|
||
if(isset($info['autoload']) && $info['autoload'] !== null) {
|
||
// if autoload is a string (selector) or callable, then we flag it as autoload
|
||
if(is_string($info['autoload']) || wireIsCallable($info['autoload'])) return "conditional";
|
||
$autoload = $info['autoload'];
|
||
|
||
} else if(!is_object($module)) {
|
||
if(isset($this->installable[$module])) {
|
||
// module is not installed
|
||
// we are not going to be able to determine if this is autoload or not
|
||
$flags = $this->getFlags($module);
|
||
if($flags !== null) {
|
||
$autoload = $flags & self::flagsAutoload;
|
||
} else {
|
||
// unable to determine
|
||
return null;
|
||
}
|
||
} else {
|
||
// include for method exists call
|
||
$this->includeModule($module);
|
||
$module = wireClassName($module, true);
|
||
$module = $this->wire(new $module());
|
||
}
|
||
}
|
||
|
||
if($autoload === null && is_object($module) && method_exists($module, 'isAutoload')) {
|
||
/** @var module $module */
|
||
$autoload = $module->isAutoload();
|
||
}
|
||
|
||
return $autoload;
|
||
}
|
||
|
||
/**
|
||
* Returns whether the modules have been initialized yet
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isInitialized() {
|
||
return $this->initialized;
|
||
}
|
||
|
||
/**
|
||
* Does the given module name resolve to a module in the system (installed or uninstalled)
|
||
*
|
||
* If given module name also includes a namespace, then that namespace will be validated as well.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|Module $moduleName With or without namespace
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isModule($moduleName) {
|
||
|
||
if(!is_string($moduleName)) {
|
||
if(is_object($moduleName)) {
|
||
if($moduleName instanceof Module) return true;
|
||
return false;
|
||
}
|
||
$moduleName = $this->getModuleClass($moduleName);
|
||
}
|
||
/** @var string $moduleName */
|
||
|
||
if(strpos($moduleName, "\\") !== false) {
|
||
$namespace = wireClassName($moduleName, 1);
|
||
$moduleName = wireClassName($moduleName, false);
|
||
} else {
|
||
$namespace = false;
|
||
}
|
||
|
||
if(isset($this->moduleIDs[$moduleName])) {
|
||
$isModule = true;
|
||
} else if(isset($this->installable[$moduleName])) {
|
||
$isModule = true;
|
||
} else {
|
||
$isModule = false;
|
||
}
|
||
|
||
if($isModule && $namespace) {
|
||
$actualNamespace = $this->getModuleNamespace($moduleName);
|
||
if(trim("$namespace", '\\') != trim("$actualNamespace", '\\')) {
|
||
$isModule = false;
|
||
}
|
||
}
|
||
|
||
return $isModule;
|
||
}
|
||
|
||
/**
|
||
* Is the given namespace a unique recognized module namespace? If yes, returns the path to it. If not, returns boolean false.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $namespace
|
||
* @return bool|string
|
||
*
|
||
*/
|
||
public function getNamespacePath($namespace) {
|
||
if(is_null($this->moduleNamespaceCache)) $this->getNamespaces();
|
||
$namespace = "\\" . trim($namespace, "\\") . "\\";
|
||
return isset($this->moduleNamespaceCache[$namespace]) ? $this->moduleNamespaceCache[$namespace] : false;
|
||
}
|
||
|
||
/**
|
||
* Refresh the modules cache
|
||
*
|
||
* This forces the modules file and information cache to be re-created.
|
||
*
|
||
* #pw-group-manipulation
|
||
*
|
||
* @param bool $showMessages Show notification messages about what was found? (default=false) 3.0.172+
|
||
*
|
||
*/
|
||
public function ___refresh($showMessages = false) {
|
||
if($this->wire()->config->systemVersion < 6) return;
|
||
$this->refreshing = true;
|
||
$this->clearModuleInfoCache($showMessages);
|
||
$this->loadModulesTable();
|
||
foreach($this->paths as $path) $this->findModuleFiles($path, false);
|
||
foreach($this->paths as $path) $this->load($path);
|
||
if($this->duplicates()->numNewDuplicates() > 0) $this->duplicates()->updateDuplicates(); // PR#1020
|
||
$this->refreshing = false;
|
||
}
|
||
|
||
/**
|
||
* Alias of refresh() method for backwards compatibility
|
||
*
|
||
* #pw-internal
|
||
*
|
||
*/
|
||
public function resetCache() {
|
||
$this->refresh();
|
||
}
|
||
|
||
/**
|
||
* Return an array of module class names that require the given one
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $class
|
||
* @param bool $uninstalled Set to true to include modules dependent upon this one, even if they aren't installed.
|
||
* @param bool $installs Set to true to exclude modules that indicate their install/uninstall is controlled by $class.
|
||
* @return array()
|
||
*
|
||
*/
|
||
public function getRequiredBy($class, $uninstalled = false, $installs = false) {
|
||
|
||
$class = $this->getModuleClass($class);
|
||
$info = $this->getModuleInfo($class);
|
||
$dependents = array();
|
||
|
||
foreach($this as $module) {
|
||
$c = $this->getModuleClass($module);
|
||
if(!$uninstalled && !$this->isInstalled($c)) continue;
|
||
$i = $this->getModuleInfo($c);
|
||
if(!count($i['requires'])) continue;
|
||
if($installs && in_array($c, $info['installs'])) continue;
|
||
if(in_array($class, $i['requires'])) $dependents[] = $c;
|
||
}
|
||
|
||
return $dependents;
|
||
}
|
||
|
||
/**
|
||
* Return an array of module class names required by the given one
|
||
*
|
||
* Default behavior is to return all listed requirements, whether they are currently met by
|
||
* the environment or not. Specify TRUE for the 2nd argument to return only requirements
|
||
* that are not currently met.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $class
|
||
* @param bool $onlyMissing Set to true to return only required modules/versions that aren't
|
||
* yet installed or don't have the right version. It excludes those that the class says it
|
||
* will install (via 'installs' property of getModuleInfo)
|
||
* @param null|bool $versions Set to true to always include versions in the returned requirements list.
|
||
* Set to null to always exclude versions in requirements list (so only module class names will be there).
|
||
* Set to false (which is the default) to include versions only when version is the dependency issue.
|
||
* Note versions are already included when the installed version is not adequate.
|
||
* @return array of strings each with ModuleName Operator Version, i.e. "ModuleName>=1.0.0"
|
||
*
|
||
*/
|
||
public function getRequires($class, $onlyMissing = false, $versions = false) {
|
||
|
||
$class = $this->getModuleClass($class);
|
||
$info = $this->getModuleInfo($class);
|
||
$requires = $info['requires'];
|
||
$currentVersion = 0;
|
||
|
||
// quick exit if arguments permit it
|
||
if(!$onlyMissing) {
|
||
if($versions) foreach($requires as $key => $value) {
|
||
list($operator, $version) = $info['requiresVersions'][$value];
|
||
if(empty($version)) continue;
|
||
if(ctype_digit("$version")) $version = $this->formatVersion($version);
|
||
if(!empty($version)) $requires[$key] .= "$operator$version";
|
||
}
|
||
return $requires;
|
||
}
|
||
|
||
foreach($requires as $key => $requiresClass) {
|
||
|
||
if(in_array($requiresClass, $info['installs'])) {
|
||
// if this module installs the required class, then we can stop now
|
||
// and we assume it's installing the version it wants
|
||
unset($requires[$key]);
|
||
}
|
||
|
||
list($operator, $requiresVersion) = $info['requiresVersions'][$requiresClass];
|
||
$installed = true;
|
||
|
||
if($requiresClass == 'PHP') {
|
||
$currentVersion = PHP_VERSION;
|
||
|
||
} else if($requiresClass == 'ProcessWire') {
|
||
$currentVersion = $this->wire('config')->version;
|
||
|
||
} else if($this->isInstalled($requiresClass)) {
|
||
if(!$requiresVersion) {
|
||
// if no version is specified then requirement is already met
|
||
unset($requires[$key]);
|
||
continue;
|
||
}
|
||
$i = $this->getModuleInfo($requiresClass, array('noCache' => true));
|
||
$currentVersion = $i['version'];
|
||
} else {
|
||
// module is not installed
|
||
$installed = false;
|
||
}
|
||
|
||
if($installed && $this->versionCompare($currentVersion, $requiresVersion, $operator)) {
|
||
// required version is installed
|
||
unset($requires[$key]);
|
||
|
||
} else if(empty($requiresVersion)) {
|
||
// just the class name is fine
|
||
continue;
|
||
|
||
} else if(is_null($versions)) {
|
||
// request is for no versions to be included (just class names)
|
||
$requires[$key] = $requiresClass;
|
||
|
||
} else {
|
||
// update the requires string to clarify what version it requires
|
||
if(ctype_digit("$requiresVersion")) $requiresVersion = $this->formatVersion($requiresVersion);
|
||
$requires[$key] = "$requiresClass$operator$requiresVersion";
|
||
}
|
||
}
|
||
|
||
return $requires;
|
||
}
|
||
|
||
|
||
/**
|
||
* Compare one module version to another, returning TRUE if they match the $operator or FALSE otherwise
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|string $currentVersion May be a number like 123 or a formatted version like 1.2.3
|
||
* @param int|string $requiredVersion May be a number like 123 or a formatted version like 1.2.3
|
||
* @param string $operator
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function versionCompare($currentVersion, $requiredVersion, $operator) {
|
||
|
||
if(ctype_digit("$currentVersion") && ctype_digit("$requiredVersion")) {
|
||
// integer comparison is ok
|
||
$currentVersion = (int) $currentVersion;
|
||
$requiredVersion = (int) $requiredVersion;
|
||
$result = false;
|
||
|
||
switch($operator) {
|
||
case '=': $result = ($currentVersion == $requiredVersion); break;
|
||
case '>': $result = ($currentVersion > $requiredVersion); break;
|
||
case '<': $result = ($currentVersion < $requiredVersion); break;
|
||
case '>=': $result = ($currentVersion >= $requiredVersion); break;
|
||
case '<=': $result = ($currentVersion <= $requiredVersion); break;
|
||
case '!=': $result = ($currentVersion != $requiredVersion); break;
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
// if either version has no periods or only one, like "1.2" then format it to stanard: "1.2.0"
|
||
if(substr_count($currentVersion, '.') < 2) $currentVersion = $this->formatVersion($currentVersion);
|
||
if(substr_count($requiredVersion, '.') < 2) $requiredVersion = $this->formatVersion($requiredVersion);
|
||
|
||
return version_compare($currentVersion, $requiredVersion, $operator);
|
||
}
|
||
|
||
/**
|
||
* Return array of ($module, $operator, $requiredVersion)
|
||
*
|
||
* $version will be 0 and $operator blank if there are no requirements.
|
||
*
|
||
* @param string $require Module class name with operator and version string
|
||
* @return array of array($moduleClass, $operator, $version)
|
||
*
|
||
*/
|
||
protected function extractModuleOperatorVersion($require) {
|
||
|
||
if(ctype_alnum($require)) {
|
||
// no version is specified
|
||
return array($require, '', 0);
|
||
}
|
||
|
||
$operators = array('<=', '>=', '<', '>', '!=', '=');
|
||
$operator = '';
|
||
foreach($operators as $o) {
|
||
if(strpos($require, $o)) {
|
||
$operator = $o;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// if no operator found, then no version is being specified
|
||
if(!$operator) return array($require, '', 0);
|
||
|
||
// extract class and version
|
||
list($class, $version) = explode($operator, $require);
|
||
|
||
// make version an integer if possible
|
||
if(ctype_digit("$version")) $version = (int) $version;
|
||
|
||
return array($class, $operator, $version);
|
||
}
|
||
|
||
/**
|
||
* Return an array of module class names required by the given one to be installed before this one.
|
||
*
|
||
* Excludes modules that are required but already installed.
|
||
* Excludes uninstalled modules that $class indicates it handles via it's 'installs' getModuleInfo property.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $class
|
||
* @return array()
|
||
*
|
||
*/
|
||
public function getRequiresForInstall($class) {
|
||
return $this->getRequires($class, true);
|
||
}
|
||
|
||
/**
|
||
* Return an array of module class names required by the given one to be uninstalled before this one.
|
||
*
|
||
* Excludes modules that the given one says it handles via it's 'installs' getModuleInfo property.
|
||
* Module class names in returned array include operator and version in the string.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $class
|
||
* @return array()
|
||
*
|
||
*/
|
||
public function getRequiresForUninstall($class) {
|
||
return $this->getRequiredBy($class, false, true);
|
||
}
|
||
|
||
/**
|
||
* Return array of dependency errors for given module name
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $moduleName
|
||
* @return array If no errors, array will be blank. If errors, array will be of strings (error messages)
|
||
*
|
||
*/
|
||
public function getDependencyErrors($moduleName) {
|
||
|
||
$moduleName = $this->getModuleClass($moduleName);
|
||
$info = $this->getModuleInfo($moduleName);
|
||
$errors = array();
|
||
|
||
if(empty($info['requires'])) return $errors;
|
||
|
||
foreach($info['requires'] as $requiresName) {
|
||
$error = '';
|
||
|
||
if(!$this->isInstalled($requiresName)) {
|
||
$error = $requiresName;
|
||
|
||
} else if(!empty($info['requiresVersions'][$requiresName])) {
|
||
list($operator, $version) = $info['requiresVersions'][$requiresName];
|
||
$info2 = $this->getModuleInfo($requiresName);
|
||
$requiresVersion = $info2['version'];
|
||
if(!empty($version) && !$this->versionCompare($requiresVersion, $version, $operator)) {
|
||
$error = "$requiresName $operator $version";
|
||
}
|
||
}
|
||
|
||
if($error) $errors[] = sprintf($this->_('Failed module dependency: %s requires %s'), $moduleName, $error);
|
||
}
|
||
|
||
return $errors;
|
||
}
|
||
|
||
/**
|
||
* Find modules that are missing their module file on the file system
|
||
*
|
||
* Return value is array:
|
||
* ~~~~~
|
||
* [
|
||
* 'ModuleName' => [
|
||
* 'id' => 123,
|
||
* 'name' => 'ModuleName',
|
||
* 'file' => '/path/to/expected/file.module'
|
||
* ],
|
||
* 'ModuleName' => [
|
||
* ...
|
||
* ]
|
||
* ];
|
||
* ~~~~~
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return array
|
||
* @since 3.0.170
|
||
*
|
||
*/
|
||
public function findMissingModules() {
|
||
|
||
$missing = array();
|
||
$unflags = array();
|
||
|
||
$sql = "SELECT id, class FROM modules WHERE flags & :flagsNoFile ORDER BY class";
|
||
$query = $this->wire()->database->prepare($sql);
|
||
$query->bindValue(':flagsNoFile', self::flagsNoFile, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
|
||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||
|
||
$class = $row['class'];
|
||
|
||
$file = $this->getModuleFile($class, array('fast' => true));
|
||
|
||
if($file && file_exists($file)) {
|
||
$unflags[] = $class;
|
||
continue;
|
||
}
|
||
|
||
$fileAlt = $this->getModuleFile($class, array('fast' => false));
|
||
|
||
if($fileAlt) {
|
||
$file = $fileAlt;
|
||
if(file_exists($file)) continue;
|
||
}
|
||
|
||
if(!$file) {
|
||
$file = $this->getModuleFile($class, array('fast' => true, 'guess' => true));
|
||
}
|
||
|
||
$missing[$class] = array(
|
||
'id' => $row['id'],
|
||
'name' => $class,
|
||
'file' => $file,
|
||
);
|
||
}
|
||
|
||
foreach($unflags as $name) {
|
||
$this->setFlag($name, self::flagsNoFile, false);
|
||
}
|
||
|
||
return $missing;
|
||
}
|
||
|
||
/**
|
||
* Remove entry for module from modules table
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string|int $class Module class or ID
|
||
* @return bool
|
||
* @since 3.0.170
|
||
*
|
||
*/
|
||
public function removeModuleEntry($class) {
|
||
$database = $this->wire()->database;
|
||
if(ctype_digit("$class")) {
|
||
$query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1');
|
||
$query->bindValue(':id', (int) $class, \PDO::PARAM_INT);
|
||
} else {
|
||
$query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1');
|
||
$query->bindValue(':class', $class, \PDO::PARAM_STR);
|
||
}
|
||
$result = $query->execute() ? $query->rowCount() > 0 : false;
|
||
$query->closeCursor();
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Given a module version number, format it in a consistent way as 3 parts: 1.2.3
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param $version int|string
|
||
* @return string
|
||
*
|
||
*/
|
||
public function formatVersion($version) {
|
||
|
||
$version = trim($version);
|
||
|
||
if(!ctype_digit(str_replace('.', '', $version))) {
|
||
// if version has some characters other than digits or periods, remove them
|
||
$version = preg_replace('/[^\d.]/', '', $version);
|
||
}
|
||
|
||
if(ctype_digit("$version")) {
|
||
// version contains only digits
|
||
// make sure version is at least 3 characters in length, left padded with 0s
|
||
$len = strlen($version);
|
||
|
||
if($len < 3) {
|
||
$version = str_pad($version, 3, "0", STR_PAD_LEFT);
|
||
|
||
} else if($len > 3) {
|
||
// they really need to use a string for this type of version,
|
||
// as we can't really guess, but we'll try, converting 1234 to 1.2.34
|
||
}
|
||
|
||
// $version = preg_replace('/(\d)(?=\d)/', '$1.', $version);
|
||
$version =
|
||
substr($version, 0, 1) . '.' .
|
||
substr($version, 1, 1) . '.' .
|
||
substr($version, 2);
|
||
|
||
} else if(strpos($version, '.') !== false) {
|
||
// version is a formatted string
|
||
if(strpos($version, '.') == strrpos($version, '.')) {
|
||
// only 1 period, like: 2.0, convert that to 2.0.0
|
||
if(preg_match('/^\d\.\d$/', $version)) $version .= ".0";
|
||
}
|
||
|
||
} else {
|
||
// invalid version?
|
||
}
|
||
|
||
if(!strlen($version)) $version = '0.0.0';
|
||
|
||
return $version;
|
||
}
|
||
|
||
/**
|
||
* Load the module information cache
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function loadModuleInfoCache() {
|
||
$data = $this->wire('cache')->get(self::moduleInfoCacheName);
|
||
if($data) {
|
||
// if module class name keys in use (i.e. ProcessModule) it's an older version of
|
||
// module info cache, so we skip over it to force its re-creation
|
||
if(is_array($data) && !isset($data['ProcessModule'])) $this->moduleInfoCache = $data;
|
||
$data = $this->wire('cache')->get(self::moduleLastVersionsCacheName);
|
||
if(is_array($data)) $this->modulesLastVersions = $data;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Load the module information cache (verbose info: summary, author, href, file, core)
|
||
*
|
||
* @param bool $uninstalled If true, it will load the uninstalled verbose cache.
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function loadModuleInfoCacheVerbose($uninstalled = false) {
|
||
$name = $uninstalled ? self::moduleInfoCacheUninstalledName : self::moduleInfoCacheVerboseName;
|
||
$data = $this->wire('cache')->get($name);
|
||
if($data) {
|
||
if(is_array($data)) {
|
||
if($uninstalled) $this->moduleInfoCacheUninstalled = $data;
|
||
else $this->moduleInfoCacheVerbose = $data;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Clear the module information cache
|
||
*
|
||
* @param bool|null $showMessages Specify true to show message notifications
|
||
*
|
||
*/
|
||
protected function clearModuleInfoCache($showMessages = false) {
|
||
|
||
$cache = $this->wire()->cache;
|
||
$versionChanges = array();
|
||
$newModules = array();
|
||
$moveModules = array();
|
||
$missModules = array();
|
||
|
||
// record current module versions currently in moduleInfo
|
||
$moduleVersions = array();
|
||
foreach($this->moduleInfoCache as $id => $moduleInfo) {
|
||
if(isset($this->modulesLastVersions[$id])) {
|
||
$moduleVersions[$id] = $this->modulesLastVersions[$id];
|
||
} else {
|
||
$moduleVersions[$id] = $moduleInfo['version'];
|
||
}
|
||
}
|
||
|
||
// delete the caches
|
||
$cache->delete(self::moduleInfoCacheName);
|
||
$cache->delete(self::moduleInfoCacheVerboseName);
|
||
$cache->delete(self::moduleInfoCacheUninstalledName);
|
||
|
||
$this->moduleInfoCache = array();
|
||
$this->moduleInfoCacheVerbose = array();
|
||
$this->moduleInfoCacheUninstalled = array();
|
||
|
||
// save new moduleInfo cache
|
||
$this->saveModuleInfoCache();
|
||
|
||
// compare new moduleInfo versions with the previous ones, looking for changes
|
||
foreach($this->moduleInfoCache as $id => $moduleInfo) {
|
||
$moduleName = $moduleInfo['name'];
|
||
if(!isset($moduleVersions[$id])) {
|
||
if(isset($this->moduleIDs[$moduleName])) {
|
||
$moveModules[] = $moduleName;
|
||
} else {
|
||
$newModules[] = $moduleName;
|
||
}
|
||
continue;
|
||
}
|
||
if($moduleVersions[$id] != $moduleInfo['version']) {
|
||
$fromVersion = $this->formatVersion($moduleVersions[$id]);
|
||
$toVersion = $this->formatVersion($moduleInfo['version']);
|
||
$versionChanges[] = "$fromVersion => $toVersion: $moduleName";
|
||
$this->modulesLastVersions[$id] = $moduleVersions[$id];
|
||
if(strpos($moduleName, 'Fieldtype') === 0) {
|
||
// apply update now, to Fieldtype modules only (since they are loaded differently)
|
||
$this->getModule($moduleName);
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach($this->moduleIDs as $moduleName => $moduleID) {
|
||
if(isset($this->moduleInfoCache[$moduleID])) {
|
||
// module is present in moduleInfo
|
||
if($this->hasFlag($moduleID, self::flagsNoFile)) {
|
||
$file = $this->getModuleFile($moduleName, array('fast' => false));
|
||
if($file) {
|
||
// remove flagsNoFile if file is found
|
||
$this->setFlag($moduleID, self::flagsNoFile, false);
|
||
}
|
||
}
|
||
} else {
|
||
// module is missing moduleInfo
|
||
$file = $this->getModuleFile($moduleName, array('fast' => false));
|
||
if(!$file) {
|
||
$file = $this->getModuleFile($moduleName, array('fast' => true, 'guess' => true));
|
||
// add flagsNoFile if file cannot be located
|
||
$missModules[] = "$moduleName => $file";
|
||
$this->setFlag($moduleID, self::flagsNoFile, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->updateModuleVersionsCache();
|
||
|
||
// report detected changes
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$reports = array(
|
||
array(
|
||
'label' => $this->_('Found %d new module(s):'),
|
||
'items' => $newModules,
|
||
),
|
||
/*
|
||
array(
|
||
'label' => $this->_('Found %d moved module(s):'),
|
||
'items' => $moveModules,
|
||
),
|
||
*/
|
||
array(
|
||
'label' => $this->_('Found %d module(s) missing file:'),
|
||
'items' => $missModules,
|
||
),
|
||
array(
|
||
'label' => $this->_('Found %d module version changes (applied when each module is loaded):'),
|
||
'items' => $versionChanges,
|
||
),
|
||
);
|
||
|
||
foreach($reports as $report) {
|
||
if(!count($report['items'])) continue;
|
||
if($showMessages) $this->message(
|
||
$sanitizer->entities1(sprintf($report['label'], count($report['items']))) .
|
||
'<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;
|
||
}
|
||
|
||
}
|
||
|