907 lines
30 KiB
PHP
907 lines
30 KiB
PHP
<?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 module’s config data (default=[]) 3.0.169+
|
||
* - `throw` (bool): When true, exceptions will be allowed to pass through. (default=false)
|
||
* @return bool True on success, false on fail
|
||
* @throws \Exception Only if the `throw` option is true.
|
||
*
|
||
*/
|
||
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();
|
||
|
||
// 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
|
||
$ns = $this->modules->info->moduleInfoCache($basename, 'namespace');
|
||
if(empty($ns)) $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) {
|
||
// the ModulePlaceholder indicates what file to load
|
||
return $this->modules->files->includeModuleFile($module->file, $moduleName);
|
||
|
||
} 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,
|
||
);
|
||
}
|
||
|
||
}
|