1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-07 15:26:54 +02:00

Major overhaul of Modules class, splitting it up into 6 other more focused classes

This commit is contained in:
Ryan Cramer
2023-06-02 14:53:12 -04:00
parent 02e3e0cd1b
commit e2179a6ec1
10 changed files with 5565 additions and 3983 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Class
*
* Base for Modules helper classes.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
abstract class ModulesClass extends Wire {
/**
* @var Modules
*
*/
protected $modules;
/**
* Debug mode?
*
* @var bool
*
*/
protected $debug = false;
/**
* Construct
*
* @param Modules $modules
*/
public function __construct(Modules $modules) {
$this->modules = $modules;
$modules->wire($this);
parent::__construct();
}
/**
* Convert given value to module ID
*
* @param string|int|Module $name
* @return int Returns 0 if module not found
*
*/
protected function moduleID($name) {
return $this->modules->moduleID($name);
}
/**
* Convert given value to module name
*
* @param int|string|Module $id
* @return string Returns blank string if not found
*
*/
protected function moduleName($id) {
return $this->modules->moduleName($id);
}
/**
* Save to the modules log
*
* @param string $str Message to log
* @param array|string $options Specify module name (string) or options array
* @return WireLog
*
*/
public function log($str, $options = array()) {
return $this->modules->log($str, $options);
}
/**
* Record and log error message
*
* #pw-internal
*
* @param array|Wire|string $text
* @param int $flags
* @return Modules|WireArray
*
*/
public function error($text, $flags = 0) {
return $this->modules->error($text, $flags);
}
public function getDebugData() {
return array();
}
}

View File

@@ -0,0 +1,721 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Configs
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesConfigs extends ModulesClass {
/**
* 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();
/**
* Get or set module configuration data
*
* #pw-internal
*
* @param int $moduleID
* @param array $setData
* @return array|int|null Returns one of the following:
* - Array of module config data
* - Null if requested moduleID is not found
* - Integer 1 if config data is present but must be loaded from DB
*
*/
public function configData($moduleID, $setData = null) {
$moduleID = (int) $moduleID;
if($setData) {
$this->configData[$moduleID] = $setData;
return array();
} else if(isset($this->configData[$moduleID])) {
return $this->configData[$moduleID];
} else {
return null;
}
}
/**
* Return the URL where the module can be edited, configured or uninstalled
*
* If module is not installed, it returns URL to install the module.
*
* #pw-group-configuration
*
* @param string|Module $className
* @param bool $collapseInfo
* @return string
*
*/
public function getModuleEditUrl($className, $collapseInfo = true) {
if(!is_string($className)) $className = $this->modules->getModuleClass($className);
$url = $this->wire()->config->urls->admin . 'module/';
if(empty($className)) return $url;
if(!$this->modules->isInstalled($className)) return $this->modules->getModuleInstallUrl($className);
$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);
$id = $this->moduleID($className);
if(!$id) return $emptyReturn;
$data = isset($this->configData[$id]) ? $this->configData[$id] : null;
if($data === null) return $emptyReturn; // module has no config data
if(is_array($data)) {
// great
} else {
// configData===1 indicates data must be loaded from DB
$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[(int) $id] = $data;
}
if($property) return isset($data[$property]) ? $data[$property] : null;
return $data;
}
/**
* 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 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->modules->info->getModuleNamespace($className);
if(is_object($className)) {
$moduleInstance = $className;
$className = $this->modules->getModuleClass($moduleInstance);
}
$nsClassName = $namespace . $className;
if($useCache === true || $useCache === 1 || $useCache === "1") {
$info = $this->modules->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->modules->getModuleInfoVerbose($className);
}
if(!$info['configurable']) {
if($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->modules->files->getModuleFile($className);
}
if($info['file']) {
return dirname($info['file']) . "/$info[configurable]";
}
}
}
}
if($useCache !== "interface") {
// check for separate module configuration file
$dir = dirname($this->modules->files->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->modules->files->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->modules->files->getFileClassInfo($file);
if($classInfo['class']) {
// not safe to include because this is not just a file with a $config array
} else {
$ns = $this->modules->files->getFileNamespace($file);
$file = $this->modules->files->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 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->modules->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;
}
/**
* 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
*
*/
public 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(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->modules->files->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->modules->files->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
/** @var ModuleConfig $moduleConfig */
$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;
}
/**
* 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);
$id = $this->moduleID($moduleName);
if(!$id) 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->modules->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->modules->getModuleClass($moduleName);
$configurable = $this->isConfigurable($moduleName);
if(!$configurable) return null;
/** @var InputfieldWrapper $form */
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->modules->getModule($moduleName);
$fields = $module->getModuleConfigInputfields();
} else if($configurableInterface === 3) {
// requires $data array
$module = $this->modules->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->modules->getModule($moduleName);
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$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->modules->getModule($moduleName);
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$fields->importArray($module->getModuleConfigArray());
$fields->populateValues($module);
}
} else if($configurableInterface === 20) {
// static getModuleConfigArray method
$fields = $this->wire(new InputfieldWrapper()); /** @var InputfieldWrapper $fields */
$fields->importArray(call_user_func(array(wireClassName($moduleName, true), 'getModuleConfigArray')));
$fields->populateValues($data);
} else {
// static getModuleConfigInputfields method
$nsClassName = $this->modules->info->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->modules->info->getModuleNamespace($moduleName);
$configClass = $ns . $moduleName . "Config";
if(!class_exists($configClass)) {
$configFile = $this->modules->files->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->modules->files->compile($moduleName, $file, $ns);
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 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->modules->flags->getFlags($moduleName);
if($numVisible) {
if($flags & Modules::flagsNoUserConfig) {
$info = $this->modules->info->getModuleInfoVerbose($moduleName);
if(empty($info['addFlag']) || !($info['addFlag'] & Modules::flagsNoUserConfig)) {
$this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, false); // remove flag
}
}
} else {
if(!($flags & Modules::flagsNoUserConfig)) {
if(empty($info['removeFlag']) || !($info['removeFlag'] & Modules::flagsNoUserConfig)) {
$this->modules->flags->setFlag($moduleName, Modules::flagsNoUserConfig, true); // add flag
}
}
}
}
return $form;
}
public function getDebugData() {
return array(
'configData' => $this->configData
);
}
}

View File

@@ -40,6 +40,7 @@ class ModulesDuplicates extends Wire {
*
*/
protected $numNewDuplicates = 0;
/**
* Return quantity of new duplicates found while loading modules
@@ -360,4 +361,11 @@ class ModulesDuplicates extends Wire {
}
return $configData;
}
public function getDebugData() {
return array(
'duplicates' => $this->duplicates,
'duplicatesUse' => $this->duplicatesUse
);
}
}

656
wire/core/ModulesFiles.php Normal file
View File

@@ -0,0 +1,656 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Files
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesFiles extends ModulesClass {
/**
* Core module types that are isolated by directory
*
* @var array
*
*/
protected $coreTypes = array(
'AdminTheme',
'Fieldtype',
'Inputfield',
'Jquery',
'LanguageSupport',
'Markup',
'Process',
'Session',
'System',
'Textformatter',
);
/**
* Module file extensions indexed by module name where value 1=.module, and 2=.module.php
*
* @var array
*
*/
protected $moduleFileExts = array();
/**
* Get or set module file extension type (1 or 2)
*
* @param string $class Module class name
* @param int $setValue 1 for '.module' or 2 for '.module.php', or omit to get current value
* @return int
*
*/
public function moduleFileExt($class, $setValue = null) {
if($setValue !== null) {
$this->moduleFileExts[$class] = (int) $setValue;
return $setValue;
}
return isset($this->moduleFileExts[$class]) ? $this->moduleFileExts[$class] : 0;
}
/**
* 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
*
*/
public function findModuleFiles($path, $readCache = false, $level = 0) {
static $startPath;
static $prependFiles = array();
$config = $this->wire()->config;
$cacheName = '';
if($level == 0) {
$startPath = $path;
$cacheName = "Modules." . str_replace($config->paths->root, '', $path);
if($readCache) {
$cacheContents = $this->modules->getCache($cacheName);
if($cacheContents) return explode("\n", trim($cacheContents));
}
}
$files = array();
$autoloadOrders = $this->modules->loader->getAutoloadOrders();
if(count($autoloadOrders) && $path !== $config->paths->modules) {
// ok
} else {
$autoloadOrders = null;
}
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($cacheName) {
$this->modules->saveCache($cacheName, implode("\n", $files));
}
}
return $files;
}
/**
* Get the path + filename (or optionally URL) for 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->modules->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->moduleFileExt($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) {
$installableFile = $this->modules->installableFile($moduleName);
if($installableFile && !$hasDuplicate) {
$file = $installableFile;
if(!$options['fast'] && !file_exists($file)) $file = false;
}
}
if(!$file) {
$dupFile = $this->modules->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->modules->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->moduleID($moduleName);
$namespace = $this->modules->info->moduleInfoCache($moduleID, 'namespace');
if(!empty($namespace)) {
$className = rtrim($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;
}
/**
* Include the given filename
*
* @param string $file
* @param string $moduleName
* @return bool
*
*/
public 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->modules->info->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;
}
/**
* Compile and return the given file for module, if allowed to do so
*
* @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->modules->getModuleFile($moduleName);
// don't compile when module compilation is disabled
if(!$allowCompile) return $file;
// don't compile core modules
if(strpos($file, $this->modules->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->modules->info->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;
}
/**
* 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', Modules::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->modules->flags->setFlag($name, Modules::flagsNoFile, false);
}
return $missing;
}
/**
* 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->modules->getModuleClass($module);
static $classes = array();
if(isset($classes[$class])) return 0; // already loaded
$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->modules->info->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->modules->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;
}
/**
* Setup entries in config->urls and config->paths for the given module
*
* @param string $moduleName
* @param string $path
*
*/
public 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 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;
}
public function getDebugData() {
return array(
'moduleFileExts' => $this->moduleFileExts
);
}
}

200
wire/core/ModulesFlags.php Normal file
View File

@@ -0,0 +1,200 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Flags
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesFlags extends ModulesClass {
/**
* Array of module ID => flags (int)
*
* @var array
*
*/
protected $moduleFlags = array();
/**
* Get or set flags for module by module ID
*
* Omit all arguments to get flags for all modules indexed by module ID.
*
* Returns null if given module ID not found.
*
* @param int $moduleID This method only accepts module ID
* @param int $setValue Flag(s) to set
* @return array|mixed|null
*
*/
public function moduleFlags($moduleID = null, $setValue = null) {
if($moduleID === null) return $this->moduleFlags;
if(!ctype_digit("$moduleID")) $moduleID = $this->moduleID($moduleID);
if($setValue !== null) {
$this->moduleFlags[(int) $moduleID] = (int) $setValue;
} else if(isset($this->moduleFlags[$moduleID])) {
return $this->moduleFlags[$moduleID];
}
return null;
}
/**
* Get flags for the given module
*
* @param int|string|Module $id Module to add flag to
* @return int|false Returns integer flags on success, or boolean false on fail
*
*/
public function getFlags($id) {
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
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 $id Module ID, class name or instance
* @param int $flag
* @return bool
* @since 3.0.170
*
*/
public function hasFlag($id, $flag) {
$flags = $this->getFlags($id);
return $flags === false ? false : ($flags & $flag);
}
/**
* Set module flags
*
* #pw-internal
*
* @param string|int $id Module id or class
* @param $flags
* @return bool
*
*/
public function setFlags($id, $flags) {
$flags = (int) $flags;
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
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->modules->getModuleClass($id) . ", " . $this->moduleFlags[$id] . " => $flags)");
$this->moduleFlags[$id] = $flags;
return $query->execute();
}
/**
* Add or remove a flag from a module
*
* #pw-internal
*
* @param $id 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($id, $flag, $add = true) {
$id = ctype_digit("$id") ? (int) $id : $this->modules->getModuleID($id);
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;
}
/**
* Update module flags if any happen to differ from what's in the given moduleInfo
*
* @param int $moduleID
* @param array $info
*
*/
public function updateModuleFlags($moduleID, array $info) {
$flags = (int) $this->getFlags($moduleID);
if($info['autoload']) {
// module is autoload
if(!($flags & Modules::flagsAutoload)) {
// add autoload flag
$this->setFlag($moduleID, Modules::flagsAutoload, true);
}
if(is_string($info['autoload'])) {
// requires conditional flag
// value is either: "function", or the conditional string (like key=value)
if(!($flags & Modules::flagsConditional)) $this->setFlag($moduleID, Modules::flagsConditional, true);
} else {
// should not have conditional flag
if($flags & Modules::flagsConditional) $this->setFlag($moduleID, Modules::flagsConditional, false);
}
} else if($info['autoload'] !== null) {
// module is not autoload
if($flags & Modules::flagsAutoload) {
// remove autoload flag
$this->setFlag($moduleID, Modules::flagsAutoload, false);
}
if($flags & Modules::flagsConditional) {
// remove conditional flag
$this->setFlag($moduleID, Modules::flagsConditional, false);
}
}
if($info['singular']) {
if(!($flags & Modules::flagsSingular)) $this->setFlag($moduleID, Modules::flagsSingular, true);
} else {
if($flags & Modules::flagsSingular) $this->setFlag($moduleID, Modules::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);
}
}
}
}
public function getDebugData() {
return array(
'moduleFlags' => $this->moduleFlags
);
}
}

1383
wire/core/ModulesInfo.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,829 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Installer
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesInstaller extends ModulesClass {
/**
* Get an associative array [name => path] for all modules that arent currently installed.
*
* #pw-internal
*
* @return array Array of elements with $moduleName => $pathName
*
*/
public function getInstallable() {
return $this->modules->getInstallable();
}
/**
* 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) {
$installableFiles = $this->modules->installableFiles;
if(!array_key_exists($class, $installableFiles)) return false;
if(!wireInstanceOf($class, 'Module')) {
$nsClass = $this->modules->getModuleClass($class, true);
if(!wireInstanceOf($nsClass, 'ProcessWire\\Module')) return false;
}
if($now) {
$requires = $this->getRequiresForInstall($class);
if(count($requires)) return false;
}
return true;
}
/**
* 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->modules->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);
}
}
}
$database = $this->wire()->database;
$languages = $this->wire()->languages;
$config = $this->wire()->config;
if($languages) $languages->setDefault();
$pathname = $this->modules->installableFile($class);
$this->modules->files->includeModuleFile($pathname, $class);
$this->modules->files->setConfigPaths($class, dirname($pathname));
$module = $this->modules->newModule($class);
if(!$module) return null;
$flags = 0;
$moduleID = 0;
if($this->modules->isSingular($module)) $flags = $flags | Modules::flagsSingular;
if($this->modules->isAutoload($module)) $flags = $flags | Modules::flagsAutoload;
$sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";
if($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->modules->moduleID($class, $moduleID);
$this->modules->add($module);
$this->modules->installableFile($class, false); // unset
// 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->modules->info->getModuleInfoVerbose($class, array('noCache' => true));
$sanitizer = $this->wire()->sanitizer;
$permissions = $this->wire()->permissions;
// if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
foreach($info['permissions'] as $name => $title) {
$name = $sanitizer->pageName($name);
if(ctype_digit("$name") || empty($name)) continue; // permission name not valid
$permission = $permissions->get($name);
if($permission->id) continue; // permision already there
try {
$permission = $permissions->add($name);
$permission->title = $title;
$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->modules->isInstalled($name)) {
try {
$this->modules->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->modules->info->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->modules->info->getModuleNamespace($class);
$class = $this->modules->getModuleClass($class);
if(!$this->modules->isInstalled($class)) {
$reason = $reason1 . ' (a)';
} else {
$this->modules->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->modules->info->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->modules->getModuleClass($class);
$filename = $this->modules->installableFile($class);
$dirname = dirname($filename);
if(empty($filename) || $this->modules->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->modules->coreModulesPath) === 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
* @throws WireException If module can't be deleted, exception will be thrown containing reason.
*
*/
public function delete($class) {
$config = $this->wire()->config;
$fileTools = $this->wire()->files;
$class = $this->modules->getModuleClass($class);
$success = true;
$reason = $this->isDeleteable($class, true);
if($reason !== true) throw new WireException($reason);
$siteModulesPath = $config->paths->siteModules;
$filename = $this->modules->installableFile($class);
$basename = basename($filename);
// 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->modules->getPaths() 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 = (bool) $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->modules->getModuleClass($class);
$reason = $this->modules->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->modules->uninstall($name);
$this->message("$label: $name");
} catch(\Exception $e) {
$error = "$label: $name - " . $e->getMessage();
$this->trackException($e, false, $error);
}
}
$info = $this->modules->info->getModuleInfoVerbose($class);
$module = $this->modules->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->modules->getModuleClass($module, true));
$this->modules->installableFile($class, $reflector->getFileName());
}
$this->modules->moduleID($class, false);
$this->modules->remove($module);
$sanitizer = $this->wire()->sanitizer;
$permissions = $this->wire()->permissions;
// delete permissions installed by this module
if(isset($info['permissions']) && is_array($info['permissions'])) {
foreach($info['permissions'] as $name => $title) {
$name = $sanitizer->pageName($name);
if(ctype_digit("$name") || empty($name)) continue;
$permission = $permissions->get($name);
if(!$permission->id) continue;
try {
$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->modules->refresh();
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->modules->getModuleClass($class);
if(!$class) return $uninstalls;
$info = $this->modules->info->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->modules->isInstalled($name)) continue;
// if an 'installs' module doesn't indicate that it requires this one, then leave it installed
$i = $this->modules->info->getModuleInfo($name);
if(!in_array($class, $i['requires'])) continue;
// add it to the uninstalls array
$uninstalls[] = $name;
}
return $uninstalls;
}
/**
* 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->modules->getModuleClass($class);
$info = $this->modules->info->getModuleInfo($class);
$dependents = array();
foreach($this as $module) {
$c = $this->modules->getModuleClass($module);
if(!$uninstalled && !$this->modules->isInstalled($c)) continue;
$i = $this->modules->info->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->modules->getModuleClass($class);
$info = $this->modules->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->modules->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->modules->isInstalled($requiresClass)) {
if(!$requiresVersion) {
// if no version is specified then requirement is already met
unset($requires[$key]);
continue;
}
$i = $this->modules->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->modules->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->modules->formatVersion($currentVersion);
if(substr_count($requiredVersion, '.') < 2) $requiredVersion = $this->modules->formatVersion($requiredVersion);
return version_compare($currentVersion, $requiredVersion, $operator);
}
/**
* Return an array of module class names required by the given one to be installed before this one.
*
* Excludes modules that are required but already installed.
* Excludes uninstalled modules that $class indicates it handles via it's 'installs' getModuleInfo property.
*
* #pw-internal
*
* @param string $class
* @return array()
*
*/
public function getRequiresForInstall($class) {
return $this->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->modules->getModuleClass($moduleName);
$info = $this->modules->getModuleInfo($moduleName);
$errors = array();
if(empty($info['requires'])) return $errors;
foreach($info['requires'] as $requiresName) {
$error = '';
if(!$this->modules->isInstalled($requiresName)) {
$error = $requiresName;
} else if(!empty($info['requiresVersions'][$requiresName])) {
list($operator, $version) = $info['requiresVersions'][$requiresName];
$info2 = $this->modules->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;
}
/**
* Get URL where an administrator can install given module name
*
* If module is already installed, it returns the URL to edit the module.
*
* @param string $className
* @return string
*
*/
public function getModuleInstallUrl($className) {
if(!is_string($className)) $className = $this->modules->getModuleClass($className);
$className = $this->wire()->sanitizer->fieldName($className);
if($this->modules->isInstalled($className)) return $this->modules->getModuleEditUrl($className);
return $this->wire()->config->urls->admin . "module/installConfirm?name=$className";
}
}

909
wire/core/ModulesLoader.php Normal file
View File

@@ -0,0 +1,909 @@
<?php namespace ProcessWire;
/**
* ProcessWire Modules: Loader
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesLoader extends ModulesClass {
/**
* Array of moduleName => order to indicate autoload order when necessary
*
* @var array
*
*/
protected $autoloadOrders = 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 from DB used across multiple calls temporarily by loadPath() method
*
*/
protected $modulesTableCache = array();
/**
* Module created dates indexed by module ID
*
*/
protected $createdDates = array();
/**
* 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->modules->debugTimerStart("triggerInit$level");
$this->message("triggerInit(level=$level)");
}
$queue = array();
if($modules === null) $modules = $this->modules;
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->modules->debugTimerStart("triggerInit$level($class)");
$info = $this->modules->getModuleInfo($module);
$skip = false;
// module requires other modules
foreach($info['requires'] as $requiresClass) {
if(in_array($requiresClass, $completed)) continue;
$dependencyInfo = $this->modules->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->modules->isAutoload($module)) {
$this->initModule($module);
}
}
$completed[] = $class;
}
if($this->debug) $this->modules->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->modules->isInitialized(true);
if($this->debug) if($debugKey) $this->modules->debugTimerStop($debugKey);
if(!$level && $this->modules->info->moduleInfoCacheEmpty()) {
if($this->debug) $this->message("saveModuleInfoCache from triggerInit");
$this->modules->info->saveModuleInfoCache();
}
}
/**
* Initialize a single module
*
* @param Module $module
* @param array $options
* - `clearSettings` (bool): When true, module settings will be cleared when appropriate to save space. (default=true)
* - `configOnly` (bool): When true, module init() method NOT called, but config data still set (default=false) 3.0.169+
* - `configData` (array): Extra config data merge with modules config data (default=[]) 3.0.169+
* - `throw` (bool): When true, exceptions will be allowed to pass through. (default=false)
* @return bool True on success, false on fail
* @throws \Exception Only if the `throw` option is true.
*
*/
public 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->modules->configs->setModuleConfigData($module, null, $extraConfigData);
$moduleName = wireClassName($module, false);
$moduleID = $this->modules->moduleID($moduleName);
if($moduleID && $this->modules->info->modulesLastVersions($moduleID)) {
$this->modules->info->checkModuleVersion($module);
}
if(method_exists($module, 'init') && empty($options['configOnly'])) {
if($this->debug) {
$debugKey = $this->modules->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->modules->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->modules->isSingular($module)) {
if(!$moduleID) $moduleID = $this->modules->getModuleID($module);
if($moduleID && $this->modules->configs->configData($moduleID) !== null) {
$this->modules->configs->configData($moduleID, 1);
}
}
return $result;
}
/**
* Call ready for a single module
*
* @param Module $module
* @return bool
*
*/
public function readyModule(Module $module) {
$result = true;
if(method_exists($module, 'ready')) {
$debugKey = $this->debug ? $this->modules->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->modules->debugTimerStop($debugKey);
static $n = 0;
$this->message("readyModule (" . (++$n) . "): " . wireClassName($module));
}
}
return $result;
}
/**
* 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->modules->debugTimerStart("triggerReady") : null;
$skipped = $this->triggerConditionalAutoload();
// trigger ready method on all applicable modules
foreach($this->modules as $module) {
/** @var Module $module */
if($module instanceof ModulePlaceholder) continue;
$class = $this->modules->getModuleClass($module);
if(isset($skipped[$class])) continue;
$id = $this->modules->moduleID($class);
$flags = $this->modules->flags->moduleFlags($id);
if($flags & Modules::flagsAutoload) $this->readyModule($module);
}
if($this->debug) $this->modules->debugTimerStop($debugKey);
}
/**
* Init conditional autoload modules, if conditions allow
*
* @return array of skipped module names
*
*/
public 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->modules->getModuleID($className);
$flags = $this->modules->flags->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->modules->newModule($className);
if($module) {
$this->modules->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;
}
/**
* Retrieve the installed module info as stored in the database
*
*/
public function loadModulesTable() {
$this->autoloadOrders = array();
$database = $this->wire()->database;
// skip loading dymanic caches at this stage
$skipCaches = array(
ModulesInfo::moduleInfoCacheUninstalledName,
ModulesInfo::moduleInfoCacheVerboseName
);
$query = $database->query(
// Currently: id, class, flags, data, with created added at sysupdate 7
"SELECT * FROM modules " .
"WHERE class NOT IN('" . implode("','", $skipCaches) . "') " .
"ORDER BY class",
"modules.loadModulesTable()"
);
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$moduleID = (int) $row['id'];
$flags = (int) $row['flags'];
$class = $row['class'];
if($flags & Modules::flagsSystemCache) {
// system cache names are prefixed with a '.' so they load first
$this->modules->memcache(ltrim($class, '.'), $row['data']);
continue;
}
$this->modules->moduleID($class, $moduleID);
$this->modules->moduleName($moduleID, $class);
$this->modules->flags->moduleFlags($moduleID, $flags);
$autoload = $flags & Modules::flagsAutoload;
$loadSettings = $autoload || ($flags & Modules::flagsDuplicate) || ($class === 'SystemUpdater');
if($loadSettings) {
// preload config data for autoload modules since we'll need it again very soon
$data = $row['data'] ? json_decode($row['data'], true) : array();
$this->modules->configs->configData($moduleID, $data);
// populate information about duplicates, if applicable
if($flags & Modules::flagsDuplicate) $this->modules->duplicates()->addFromConfigData($class, $data);
} else if(!empty($row['data'])) {
// indicate that it has config data, but not yet loaded
$this->modules->configs->configData($moduleID, 1);
}
if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {
$this->createdDates[$moduleID] = $row['created'];
}
if($autoload) {
$value = $this->modules->info->moduleInfoCache($moduleID, 'autoload');
if(!empty($value)) {
$autoload = $value;
$disabled = $flags & Modules::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
*
*/
public function loadPath($path) {
$config = $this->wire()->config;
$debugKey = $this->debug ? $this->modules->debugTimerStart("loadPath($path)") : null;
$installed =& $this->modulesTableCache;
$modulesLoaded = array();
$modulesDelayed = array();
$modulesRequired = array();
$modulesFiles = $this->modules->files;
$rootPath = $config->paths->root;
$basePath = substr($path, strlen($rootPath));
foreach($modulesFiles->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"
$modulesFiles->moduleFileExt($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)) $modulesFiles->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->modules->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)
*
*/
public 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->modules->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 = $this->modules->offsetGet($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') === false) return false;
list(, $ext) = explode('.module', $filename, 2);
if(!empty($ext) && $ext !== '.php') return false;
// if the filename doesn't start with the requested path, then skip
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(!isset($installed[$basename])) {
// array_key_exists is used as secondary to check the null case
$this->modules->installableFile($basename, $pathname);
return '';
}
$info = $installed[$basename];
$this->modules->files->setConfigPaths($basename, $dirname);
$module = null;
$autoload = false;
if($info['flags'] & Modules::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()
// determine if module has dependencies that are not yet met
$requiresClasses = $this->modules->info->getModuleInfoProperty($basename, 'requires');
if(!empty($requiresClasses)) {
foreach($requiresClasses as $requiresClass) {
$nsRequiresClass = $this->modules->getModuleClass($requiresClass, true);
if(!wireClassExists($nsRequiresClass, false)) {
$requiresInfo = $this->modules->getModuleInfo($requiresClass);
if(!empty($requiresInfo['error'])
|| $requiresInfo['autoload'] === true
|| !$this->modules->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
/** @var bool|string|callable $autoload */
$autoload = $this->modules->info->moduleInfoCache($basename, 'autoload');
if(empty($autoload)) $autoload = true;
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->modules->info->getModuleInfoExternal($basename);
if(empty($i)) {
$this->modules->files->includeModuleFile($pathname, $basename);
$namespace = $this->modules->info->getModuleNamespace($basename);
$className = $namespace . $basename;
if(method_exists($className, 'getModuleInfo')) {
$i = $className::getModuleInfo();
} else {
$i = $this->modules->getModuleInfo($className);
}
}
$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'] & Modules::flagsDisabled)) {
// anonymous function or selector string
$this->conditionalAutoloadModules[$basename] = $autoload;
$this->modules->moduleID($basename, (int) $info['id']);
$this->modules->moduleName((int) $info['id'], $basename);
$autoload = true;
} else if($autoload) {
$this->modules->files->includeModuleFile($pathname, $basename);
if(!($info['flags'] & Modules::flagsDisabled)) {
if($this->modules->refreshing) {
$module = $this->modules->offsetGet($basename);
} else if(isset($this->autoloadOrders[$basename]) && $this->autoloadOrders[$basename] >= 10000) {
$module = $this->modules->offsetGet($basename); // preloaded module
}
if(!$module) $module = $this->modules->newModule($basename);
}
}
}
if($module === null) {
// placeholder for a module, which is not yet included and instantiated
// if(!$moduleInfo) $moduleInfo = $this->getModuleInfo($basename);
$ns = $moduleInfo ? $moduleInfo['namespace'] : $this->modules->info->moduleInfoCache($basename, 'namespace');
if(empty($namespace)) $ns = __NAMESPACE__ . "\\";
$singular = $info['flags'] & Modules::flagsSingular;
$module = $this->newModulePlaceholder($basename, $ns, $pathname, $singular, $autoload);
}
$this->modules->moduleID($basename, (int) $info['id']);
$this->modules->moduleName((int) $info['id'], $basename);
$this->modules->set($basename, $module);
return $basename;
}
/**
* 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->modules->getModuleClass($module, false);
$className = $this->modules->getModuleClass($module, true);
}
if(!$className) return false;
// already included
if(class_exists($className, false)) return true;
// attempt to retrieve module
$module = $this->modules->offsetGet($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->modules->getModuleFile($moduleName, array('fast' => true));
if(!$file) {
$fast = false;
$file = $this->modules->getModuleFile($moduleName, array('fast' => false));
}
// still can't figure out what file is? fail
if(!$file) return false;
}
if(!$this->modules->files->includeModuleFile($file, $moduleName)) {
// module file failed to include(), try to identify and include file again
if($fast) {
$filePrev = $file;
$file = $this->modules->getModuleFile($moduleName, array('fast' => false));
if($file && $file !== $filePrev) {
if($this->modules->files->includeModuleFile($file, $moduleName)) {
// module is missing a module file
return false;
}
}
} 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 = (string) $this->modules->info->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->modules->files->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;
}
}
/**
* 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 = $this->modules->getModuleClass($moduleName, true); // ???
$moduleName = wireClassName($moduleName, false);
}
$info = $this->modules->getModuleInfo($module ? $module : $moduleName);
if(empty($info['permission']) && empty($info['permissionMethod'])) {
return ($strict ? false : true);
}
if(!$user instanceof User) $user = $this->wire()->user;
if($user && $user->isSuperuser()) return true;
if(!empty($info['permission'])) {
if(!$user->hasPermission($info['permission'])) return false;
}
if(!empty($info['permissionMethod'])) {
// module specifies a static method to call for permission
if(is_null($page)) $page = $this->wire()->page;
$data = array(
'wire' => $this->wire(),
'page' => $page,
'user' => $user,
'info' => $info,
);
$method = $info['permissionMethod'];
$this->includeModule($moduleName);
return method_exists($className, $method) ? $className::$method($data) : false;
}
return true;
}
/**
* 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.
*
* @param string $path
* @since 3.0.173
*
*/
public function preloadModules($path) {
if(empty($this->autoloadOrders)) return;
arsort($this->autoloadOrders);
foreach($this->autoloadOrders as $moduleName => $order) {
if($order < 10000) break;
$info = $this->modules->info->moduleInfoCache($moduleName);
if(empty($info)) continue;
if(empty($info['singular'])) continue;
$file = $path . "$moduleName/$moduleName.module.php";
if(!file_exists($file) || !$this->modules->files->includeModuleFile($file, $moduleName)) continue;
if(!isset($info['namespace'])) $info['namespace'] = '';
$className = $info['namespace'] . $moduleName;
$module = $this->modules->newModule($className, $moduleName);
if($module) {
$this->modules->offsetSet($moduleName, $module);
}
}
}
/**
* Get or set created date for given module ID
*
* #pw-internal
*
* @param int $moduleID Module ID or omit to get all
* @param string $setValue Set created date value
* @return string|array|null
* @since 3.0.219
*
*/
public function createdDate($moduleID = null, $setValue = null) {
if($moduleID === null) return $this->createdDates;
if($setValue) {
$this->createdDates[$moduleID] = $setValue;
return $setValue;
}
return isset($this->createdDates[$moduleID]) ? $this->createdDates[$moduleID] : null;
}
/**
* Return a new ModulePlaceholder for the given className
*
* #pw-internal
*
* @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
*
*/
public function newModulePlaceholder($className, $ns, $file, $singular, $autoload) {
/** @var ModulePlaceholder $module */
$module = $this->wire(new ModulePlaceholder());
$module->setClass($className);
$module->setNamespace($ns);
$module->singular = $singular;
$module->autoload = $autoload;
$module->file = $file;
return $module;
}
/**
* Called by Modules class when init has finished
*
*/
public function loaded() {
$this->modulesTableCache = array();
}
/**
* Get the autoload orders
*
* @return array Array of [ moduleName (string => order (int) ]
*
*/
public function getAutoloadOrders() {
return $this->autoloadOrders;
}
public function getDebugData() {
return array(
'autoloadOrders' => $this->autoloadOrders,
'conditionalAutoloadModules' => $this->conditionalAutoloadModules,
'modulesTableCache' => $this->modulesTableCache,
'createdDates' => $this->createdDates,
);
}
}

View File

@@ -87,6 +87,7 @@ class ProcessModule extends Process {
protected $newCoreModules = array(
'SystemNotifications',
'InputfieldCKEditor',
'InputfieldTinyMCE',
'FieldtypeOptions',
'InputfieldIcon',
'ProcessLogger',
@@ -173,6 +174,9 @@ class ProcessModule extends Process {
if($this->wire()->input->get('update')) {
$this->labels['download_install'] = $this->_('Download and Update');
}
/** @var JqueryUI $jQueryUI */
$jQueryUI = $this->wire()->modules->getModule('JqueryUI');
if($jQueryUI) $jQueryUI->use('modal');
}
/**
@@ -248,7 +252,7 @@ class ProcessModule extends Process {
if($configurable) {
if(!$info['configurable'] || !$info['installed']) continue;
$flags = $modules->getFlags($moduleName);
$flags = $modules->flags->getFlags($moduleName);
if($flags & Modules::flagsNoUserConfig) continue;
}
@@ -443,7 +447,7 @@ class ProcessModule extends Process {
}
if($info['configurable'] && $info['installed']) {
$flags = $modules->getFlags($name);
$flags = $modules->flags->getFlags($name);
if(!($flags & Modules::flagsNoUserConfig)) {
$configurableArray[$name] = $installed;
}
@@ -958,7 +962,7 @@ class ProcessModule extends Process {
$editUrl = '#';
} else if($configurable) {
$flags = $modules->getFlags($name);
$flags = $modules->flags->getFlags($name);
if(!($flags & Modules::flagsNoUserConfig)) {
$buttons .=
"<button type='button' class='ProcessModuleSettings ui-state-default ui-button'>" .
@@ -1624,7 +1628,7 @@ class ProcessModule extends Process {
}
$sinfo = self::getModuleInfo();
$flags = $modules->getFlags($moduleName);
$flags = $modules->flags->getFlags($moduleName);
$allowDisabledFlag =
($config->debug && $config->advanced && ($flags & Modules::flagsAutoload)) ||
($flags & Modules::flagsDisabled);
@@ -1828,10 +1832,10 @@ class ProcessModule extends Process {
// module is autoload and has an option to diable
if($input->post('_flags_disabled')) {
// add disabled flag
if(!($flags & Modules::flagsDisabled)) $modules->setFlag($moduleName, Modules::flagsDisabled, true);
if(!($flags & Modules::flagsDisabled)) $modules->flags->setFlag($moduleName, Modules::flagsDisabled, true);
} else {
// remove disabled flag
if($flags & Modules::flagsDisabled) $modules->setFlag($moduleName, Modules::flagsDisabled, false);
if($flags & Modules::flagsDisabled) $modules->flags->setFlag($moduleName, Modules::flagsDisabled, false);
}
}
@@ -2130,4 +2134,3 @@ class ProcessModule extends Process {
}
}