/wire/core/Modules.php
PHP | 3635 lines | 2009 code | 505 blank | 1121 comment | 739 complexity | bbe35d66880f0499b0b7a91061b15ed1 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception
Large files files are truncated, but you can click here to view the full file
- <?php
- /**
- * ProcessWire Modules
- *
- * Loads and manages all runtime modules for ProcessWire
- *
- * Note that when iterating, find(), or calling any other method that returns module(s), excepting get(), a ModulePlaceholder may be
- * returned rather than a real Module. ModulePlaceholders are used in instances when the module may or may not be needed at runtime
- * in order to save resources. As a result, anything iterating through these Modules should check to make sure it's not a ModulePlaceholder
- * before using it. If it's a ModulePlaceholder, then the real Module can be instantiated/retrieved by $modules->get($className).
- *
- * ProcessWire 2.x
- * Copyright (C) 2015 by Ryan Cramer
- * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
- *
- * https://processwire.com
- *
- */
- class Modules extends WireArray {
-
- /**
- * Whether or not module debug mode is active
- *
- */
- protected $debug = false;
- /**
- * Flag indicating the module may have only one instance at runtime.
- *
- */
- const flagsSingular = 1;
- /**
- * Flag indicating that the module should be instantiated at runtime, rather than when called upon.
- *
- */
- const flagsAutoload = 2;
- /**
- * Flag indicating the module has more than one copy of it on the file system.
- *
- */
- const flagsDuplicate = 4;
- /**
- * When combined with flagsAutoload, indicates that the autoload is conditional
- *
- */
- const flagsConditional = 8;
- /**
- * When combined with flagsAutoload, indicates that the module's autoload state is temporarily disabled
- *
- */
- const flagsDisabled = 16;
- /**
- * Filename for module info cache file
- *
- */
- const moduleInfoCacheName = 'Modules.info';
-
- /**
- * Filename for verbose module info cache file
- *
- */
- const moduleInfoCacheVerboseName = 'ModulesVerbose.info';
-
- /**
- * Filename for uninstalled module info cache file
- *
- */
- const moduleInfoCacheUninstalledName = 'ModulesUninstalled.info';
- /**
- * Cache name for module version change cache
- *
- */
- const moduleLastVersionsCacheName = 'ModulesVersions.info';
- /**
- * Array of modules that are not currently installed, indexed by className => filename
- *
- */
- protected $installable = array();
- /**
- * An array of module database IDs indexed by: class => id
- *
- * Used internally for database operations
- *
- */
- protected $moduleIDs = array();
- /**
- * Full system paths where modules are stored
- *
- * index 0 must be the core modules path (/i.e. /wire/modules/)
- *
- */
- protected $paths = array();
- /**
- * Cached module configuration data indexed by module ID
- *
- * Values are integer 1 for modules that have config data but data is not yet loaded.
- * Values are an array for modules have have config data and has been loaded.
- *
- */
- protected $configData = array();
-
- /**
- * Module created dates indexed by module ID
- *
- */
- protected $createdDates = array();
- /**
- * Have the modules been init'd() ?
- *
- */
- protected $initialized = false;
- /**
- * Becomes an array if debug mode is on
- *
- */
- protected $debugLog = array();
- /**
- * Array of moduleName => condition
- *
- * Condition can be either an anonymous function or a selector string to be evaluated at ready().
- *
- */
- protected $conditionalAutoloadModules = array();
- /**
- * Cache of module information
- *
- */
- protected $moduleInfoCache = array();
-
- /**
- * Cache of module information (verbose text) including: summary, author, href, file, core
- *
- */
- protected $moduleInfoCacheVerbose = array();
-
- /**
- * Cache of uninstalled module information (verbose for uninstalled) including: summary, author, href, file, core
- *
- * Note that this one is indexed by class name rather than by ID (since uninstalled modules have no ID)
- *
- */
- protected $moduleInfoCacheUninstalled = array();
- /**
- * Cache of module information from DB used across multiple calls temporarily by load() method
- *
- */
- protected $modulesTableCache = array();
-
- /**
- * Last known versions of modules, for version change tracking
- *
- * @var array of ModuleName (string) => last known version (integer|string)
- *
- */
- protected $modulesLastVersions = array();
- /**
- * Array of module ID => flags (int)
- *
- * @var array
- *
- */
- protected $moduleFlags = array();
-
- /**
- * Array of moduleName => substituteModuleName to be used when moduleName doesn't exist
- *
- * Primarily for providing backwards compatiblity with modules assumed installed that
- * may no longer be in core.
- *
- * see setSubstitutes() method
- *
- */
- protected $substitutes = array();
- /**
- * Instance of ModulesDuplicates
- *
- * @var ModulesDuplicates
- *
- */
- protected $duplicates;
- /**
- * Properties that only appear in 'verbose' moduleInfo
- *
- * @var array
- *
- */
- protected $moduleInfoVerboseKeys = array(
- 'summary',
- 'author',
- 'href',
- 'file',
- 'core',
- 'versionStr',
- 'permissions',
- 'page',
- );
- /**
- * Construct the Modules
- *
- * @param string $path Core modules path (you may add other paths with addPath method)
- *
- */
- public function __construct($path) {
- $this->addPath($path);
- }
- /**
- * Get the ModulesDuplicates instance
- *
- * @return ModulesDuplicates
- *
- */
- public function duplicates() {
- if(is_null($this->duplicates)) $this->duplicates = new ModulesDuplicates();
- return $this->duplicates;
- }
- /**
- * Add another modules path, must be called before init()
- *
- * @param string $path
- *
- */
- public function addPath($path) {
- $this->paths[] = $path;
- }
- /**
- * Return all assigned module root paths
- *
- * @return array of modules paths, with index 0 always being the core modules path.
- *
- */
- public function getPaths() {
- return $this->paths;
- }
- /**
- * Initialize modules
- *
- * Must be called after construct before this class is ready to use
- *
- * @see load()
- *
- */
- public function init() {
- $this->setTrackChanges(false);
- $this->loadModuleInfoCache();
- $this->loadModulesTable();
- foreach($this->paths as $path) {
- $this->load($path);
- }
- $this->modulesTableCache = array(); // clear out data no longer needed
- }
- /**
- * Modules class accepts only Module instances, per the WireArray interface
- *
- */
- public function isValidItem($item) {
- return $item instanceof Module;
- }
- /**
- * The key/index used for each module in the array is it's class name, per the WireArray interface
- *
- */
- public function getItemKey($item) {
- return $this->getModuleClass($item);
- }
- /**
- * There is no blank/generic module type, so makeBlankItem returns null
- *
- */
- public function makeBlankItem() {
- return null;
- }
- /**
- * Make a new/blank WireArray
- *
- */
- public function makeNew() {
- // ensures that find(), etc. operations don't initalize a new Modules() class
- return new WireArray();
- }
- /**
- * Make a new populated copy of a WireArray containing all the modules
- *
- * @return WireArray
- *
- */
- public function makeCopy() {
- // ensures that find(), etc. operations don't initalize a new Modules() class
- $copy = $this->makeNew();
- foreach($this->data as $key => $value) $copy[$key] = $value;
- $copy->resetTrackChanges($this->trackChanges());
- return $copy;
- }
- /**
- * Initialize all the modules that are loaded at boot
- *
- */
- public function triggerInit($modules = null, $completed = array(), $level = 0) {
-
- if($this->debug) {
- $debugKey = $this->debugTimerStart("triggerInit$level");
- $this->message("triggerInit(level=$level)");
- }
-
- $queue = array();
- if(is_null($modules)) $modules = $this;
- foreach($modules as $class => $module) {
-
- if($module instanceof ModulePlaceholder) {
- // skip modules that aren't autoload and those that are conditional autoload
- if(!$module->autoload) continue;
- if(isset($this->conditionalAutoloadModules[$class])) continue;
- }
-
- if($this->debug) $debugKey2 = $this->debugTimerStart("triggerInit$level($class)");
-
- $info = $this->getModuleInfo($module);
- $skip = false;
- // module requires other modules
- foreach($info['requires'] as $requiresClass) {
- if(in_array($requiresClass, $completed)) continue;
- $dependencyInfo = $this->getModuleInfo($requiresClass);
- if(empty($dependencyInfo['autoload'])) {
- // if dependency isn't an autoload one, there's no point in waiting for it
- if($this->debug) $this->warning("Autoload module '$module' requires a non-autoload module '$requiresClass'");
- continue;
- } else if(isset($this->conditionalAutoloadModules[$requiresClass])) {
- // autoload module requires another autoload module that may or may not load
- if($this->debug) $this->warning("Autoload module '$module' requires a conditionally autoloaded module '$requiresClass'");
- continue;
- }
- // dependency is autoload and required by this module, so queue this module to init later
- $queue[$class] = $module;
- $skip = true;
- break;
- }
-
- if(!$skip) {
- if($info['autoload'] !== false) {
- if($info['autoload'] === true || $this->isAutoload($module)) {
- $this->initModule($module);
- }
- }
- $completed[] = $class;
- }
-
- if($this->debug) $this->debugTimerStop($debugKey2);
- }
- // if there is a dependency queue, go recursive till the queue is completed
- if(count($queue) && $level < 3) {
- $this->triggerInit($queue, $completed, $level + 1);
- }
- $this->initialized = true;
-
- if($this->debug) if($debugKey) $this->debugTimerStop($debugKey);
-
- if(!$level && (empty($this->moduleInfoCache))) { // || empty($this->moduleInfoCacheVerbose))) {
- if($this->debug) $this->message("saveModuleInfoCache from triggerInit");
- $this->saveModuleInfoCache();
- }
- }
- /**
- * Given a class name, return the constructed module
- *
- * @param string $className Module class name
- * @return Module
- *
- */
- protected function newModule($className) {
- if($this->debug) $debugKey = $this->debugTimerStart("newModule($className)");
- if(!class_exists($className, false)) $this->includeModule($className);
- $module = new $className();
- if($this->debug) $this->debugTimerStop($debugKey);
- return $module;
- }
- /**
- * Return a new ModulePlaceholder for the given className
- *
- * @param string $className Module class this placeholder will stand in for
- * @param string $file Full path and filename of $className
- * @param bool $singular Is the module a singular module?
- * @param bool $autoload Is the module an autoload module?
- * @return ModulePlaceholder
- *
- */
- protected function newModulePlaceholder($className, $file, $singular, $autoload) {
- $module = new ModulePlaceholder();
- $module->setClass($className);
- $module->singular = $singular;
- $module->autoload = $autoload;
- $module->file = $file;
- return $module;
- }
- /**
- * Initialize a single module
- *
- * @param Module $module
- * @param bool $clearSettings If true, module settings will be cleared when appropriate to save space.
- *
- */
- protected function initModule(Module $module, $clearSettings = true) {
-
- if($this->debug) {
- static $n = 0;
- $this->message("initModule (" . (++$n) . "): $module");
- }
-
- // if the module is configurable, then load its config data
- // and set values for each before initializing the module
- $this->setModuleConfigData($module);
-
- $className = get_class($module);
- $moduleID = isset($this->moduleIDs[$className]) ? $this->moduleIDs[$className] : 0;
- if($moduleID && isset($this->modulesLastVersions[$moduleID])) {
- $this->checkModuleVersion($module);
- }
-
- if(method_exists($module, 'init')) {
-
- if($this->debug) {
- $className = get_class($module);
- $debugKey = $this->debugTimerStart("initModule($className)");
- }
-
- $module->init();
-
- if($this->debug) {
- $this->debugTimerStop($debugKey);
- }
- }
-
- // if module is autoload (assumed here) and singular, then
- // we no longer need the module's config data, so remove it
- if($clearSettings && $this->isSingular($module)) {
- if(!$moduleID) $moduleID = $this->getModuleID($module);
- if(isset($this->configData[$moduleID])) $this->configData[$moduleID] = 1;
- }
-
- }
- /**
- * Call ready for a single module
- *
- */
- protected function readyModule(Module $module) {
- if(method_exists($module, 'ready')) {
- if($this->debug) $debugKey = $this->debugTimerStart("readyModule(" . $module->className() . ")");
- $module->ready();
- if($this->debug) {
- $this->debugTimerStop($debugKey);
- static $n = 0;
- $this->message("readyModule (" . (++$n) . "): $module");
- }
- }
- }
- /**
- * Init conditional autoload modules, if conditions allow
- *
- * @return array of skipped module names
- *
- */
- protected function triggerConditionalAutoload() {
-
- // conditional autoload modules that are skipped (className => 1)
- $skipped = array();
- // init conditional autoload modules, now that $page is known
- foreach($this->conditionalAutoloadModules as $className => $func) {
- if($this->debug) {
- $moduleID = $this->getModuleID($className);
- $flags = $this->moduleFlags[$moduleID];
- $this->message("Conditional autoload: $className (flags=$flags, condition=" . (is_string($func) ? $func : 'func') . ")");
- }
- $load = true;
- if(is_string($func)) {
- // selector string
- if(!$this->wire('page')->is($func)) $load = false;
- } else {
- // anonymous function
- if(!is_callable($func)) $load = false;
- else if(!$func()) $load = false;
- }
- if($load) {
- $module = $this->newModule($className);
- $this->set($className, $module);
- $this->initModule($module);
- if($this->debug) $this->message("Conditional autoload: $className LOADED");
- } else {
- $skipped[$className] = $className;
- if($this->debug) $this->message("Conditional autoload: $className SKIPPED");
- }
- }
-
- // clear this out since we don't need it anymore
- $this->conditionalAutoloadModules = array();
-
- return $skipped;
- }
- /**
- * Trigger all modules 'ready' method, if they have it.
- *
- * This is to indicate to them that the API environment is fully ready and $page is in fuel.
- *
- * This is triggered by ProcessPageView::ready
- *
- */
- public function triggerReady() {
-
- if($this->debug) $debugKey = $this->debugTimerStart("triggerReady");
-
- $skipped = $this->triggerConditionalAutoload();
-
- // trigger ready method on all applicable modules
- foreach($this as $module) {
-
- if($module instanceof ModulePlaceholder) continue;
-
- // $info = $this->getModuleInfo($module);
- // if($info['autoload'] === false) continue;
- // if(!$this->isAutoload($module)) continue;
-
- $class = $this->getModuleClass($module);
- if(isset($skipped[$class])) continue;
-
- $id = $this->moduleIDs[$class];
- if(!($this->moduleFlags[$id] & self::flagsAutoload)) continue;
-
- if(!method_exists($module, 'ready')) continue;
-
- $this->readyModule($module);
- }
-
- if($this->debug) $this->debugTimerStop($debugKey);
- }
- /**
- * Retrieve the installed module info as stored in the database
- *
- * @return array Indexed by module class name => array of module info
- *
- */
- protected function loadModulesTable() {
- $database = $this->wire('database');
- // we use SELECT * so that this select won't be broken by future DB schema additions
- // Currently: id, class, flags, data, with created added at sysupdate 7
- $query = $database->prepare("SELECT * FROM modules ORDER BY class", "modules.loadModulesTable()"); // QA
- $query->execute();
-
- while($row = $query->fetch(PDO::FETCH_ASSOC)) {
-
- $moduleID = (int) $row['id'];
- $flags = (int) $row['flags'];
- $class = $row['class'];
- $this->moduleIDs[$class] = $moduleID;
- $this->moduleFlags[$moduleID] = $flags;
- $loadSettings = ($flags & self::flagsAutoload) || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');
-
- if($loadSettings) {
- // preload config data for autoload modules since we'll need it again very soon
- $data = strlen($row['data']) ? wireDecodeJSON($row['data']) : array();
- $this->configData[$moduleID] = $data;
- // populate information about duplicates, if applicable
- if($flags & self::flagsDuplicate) $this->duplicates()->addFromConfigData($class, $data);
-
- } else if(!empty($row['data'])) {
- // indicate that it has config data, but not yet loaded
- $this->configData[$moduleID] = 1;
- }
-
- if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {
- $this->createdDates[$moduleID] = $row['created'];
- }
-
- unset($row['data']); // info we don't want stored in modulesTableCache
- $this->modulesTableCache[$class] = $row;
- }
-
- $query->closeCursor();
- }
- /**
- * Given a disk path to the modules, determine all installed modules and keep track of all uninstalled (installable) modules.
- *
- * @param string $path
- *
- */
- protected function load($path) {
- if($this->debug) $debugKey = $this->debugTimerStart("load($path)");
- $installed =& $this->modulesTableCache;
- $modulesLoaded = array();
- $modulesDelayed = array();
- $modulesRequired = array();
- foreach($this->findModuleFiles($path, true) as $pathname) {
- $pathname = trim($pathname);
- $requires = array();
- $moduleName = $this->loadModule($path, $pathname, $requires, $installed);
- 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
- while($moduleName = array_shift($loadedNames)) {
- // iternate through delayed modules that require this one
- if(empty($modulesRequired[$moduleName])) continue;
-
- foreach($modulesRequired[$moduleName] as $delayedName => $delayedPathName) {
- $loadNow = true;
- if(isset($modulesDelayed[$delayedName])) {
- foreach($modulesDelayed[$delayedName] as $requiresModuleName) {
- if(!isset($modulesLoaded[$requiresModuleName])) {
- $loadNow = false;
- }
- }
- }
- if(!$loadNow) continue;
- // all conditions satisified to load delayed module
- unset($modulesDelayed[$delayedName], $modulesRequired[$moduleName][$delayedName]);
- $unused = array();
- $loadedName = $this->loadModule($path, $delayedPathName, $unused, $installed);
- if(!$loadedName) continue;
- $modulesLoaded[$loadedName] = 1;
- $loadedNames[] = $loadedName;
- }
- }
- }
- if(count($modulesDelayed)) foreach($modulesDelayed as $moduleName => $requiredNames) {
- $this->error("Module '$moduleName' dependency not fulfilled for: " . implode(', ', $requiredNames), Notice::debug);
- }
-
- if($this->debug) $this->debugTimerStop($debugKey);
- }
- /**
- * Load a module into memory (companion to load bootstrap method)
- *
- * @param string $basepath Base path of modules being processed (path provided to the load method)
- * @param string $pathname
- * @param array $requires This method will populate this array with required dependencies (class names) if present.
- * @param array $installed Array of installed modules info, indexed by module class name
- * @return Returns module name (classname)
- *
- */
- protected function loadModule($basepath, $pathname, array &$requires, array &$installed) {
-
- $pathname = $basepath . $pathname;
- $dirname = dirname($pathname);
- $filename = basename($pathname);
- $basename = basename($filename, '.php');
- $basename = basename($basename, '.module');
- $requires = array();
- $duplicates = $this->duplicates();
-
- // 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(class_exists($basename, false)) {
- $module = parent::get($basename);
- $dir = rtrim($this->wire('config')->paths->$basename, '/');
- if($module && $dir && $dirname != $dir) {
- $duplicates->recordDuplicate($basename, $pathname, "$dir/$filename", $installed);
- return '';
- }
- if($module) return $basename;
- }
- // if the filename doesn't end with .module or .module.php, then stop and move onto the next
- if(!strpos($filename, '.module') || (substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php')) return false;
-
- // if the filename doesn't start with the requested path, then continue
- if(strpos($pathname, $basepath) !== 0) return '';
- // if the file isn't there, it was probably uninstalled, so ignore it
- if(!file_exists($pathname)) return '';
- // if the module isn't installed, then stop and move on to next
- if(!array_key_exists($basename, $installed)) {
- $this->installable[$basename] = $pathname;
- return '';
- }
- $info = $installed[$basename];
-
- $this->setConfigPaths($basename, $dirname);
- $module = null;
- $autoload = false;
- if($info['flags'] & self::flagsAutoload) {
-
- // this is an Autoload module.
- // include the module and instantiate it but don't init() it,
- // because it will be done by Modules::init()
- $moduleInfo = $this->getModuleInfo($basename);
- // determine if module has dependencies that are not yet met
- if(count($moduleInfo['requires'])) {
- foreach($moduleInfo['requires'] as $requiresClass) {
- if(!class_exists($requiresClass, false)) {
- $requiresInfo = $this->getModuleInfo($requiresClass);
- if(!empty($requiresInfo['error'])
- || $requiresInfo['autoload'] === true
- || !$this->isInstalled($requiresClass)) {
- // we only handle autoload===true since load() only instantiates other autoload===true modules
- $requires[] = $requiresClass;
- }
- }
- }
- if(count($requires)) {
- // module has unmet requirements
- return $basename;
- }
- }
- // if not defined in getModuleInfo, then we'll accept the database flag as enough proof
- // since the module may have defined it via an isAutoload() function
- if(!isset($moduleInfo['autoload'])) $moduleInfo['autoload'] = true;
- $autoload = $moduleInfo['autoload'];
- if($autoload === 'function') {
- // function is stored by the moduleInfo cache to indicate we need to call a dynamic function specified with the module itself
- $i = $this->getModuleInfoExternal($basename);
- if(empty($i)) {
- include_once($pathname);
- $i = $basename::getModuleInfo();
- }
- $autoload = isset($i['autoload']) ? $i['autoload'] : true;
- unset($i);
- }
- // check for conditional autoload
- if(!is_bool($autoload) && (is_string($autoload) || is_callable($autoload)) && !($info['flags'] & self::flagsDisabled)) {
- // anonymous function or selector string
- $this->conditionalAutoloadModules[$basename] = $autoload;
- $this->moduleIDs[$basename] = $info['id'];
- $autoload = true;
- } else if($autoload) {
- include_once($pathname);
- if(!($info['flags'] & self::flagsDisabled)) {
- $module = $this->newModule($basename);
- }
- }
- }
- if(is_null($module)) {
- // placeholder for a module, which is not yet included and instantiated
- $module = $this->newModulePlaceholder($basename, $pathname, $info['flags'] & self::flagsSingular, $autoload);
- }
- $this->moduleIDs[$basename] = $info['id'];
- $this->set($basename, $module);
-
- return $basename;
- }
- /**
- * Find new module files in the given $path
- *
- * If $readCache is true, this will perform the find from the cache
- *
- * @param string $path Path to the modules
- * @param bool $readCache Optional. If set to true, then this method will attempt to read modules from the cache.
- * @param int $level For internal recursive use.
- * @return array Array of module files
- *
- */
- protected function findModuleFiles($path, $readCache = false, $level = 0) {
- static $startPath;
- static $callNum = 0;
- $callNum++;
- $config = $this->wire('config');
- $cache = $this->wire('cache');
- if($level == 0) {
- $startPath = $path;
- $cacheName = "Modules." . str_replace($config->paths->root, '', $path);
- if($readCache && $cache) {
- $cacheContents = $cache->get($cacheName);
- if($cacheContents !== null) {
- if(empty($cacheContents) && $callNum === 1) {
- // don't accept empty cache for first path (/wire/modules/)
- } else {
- $cacheContents = explode("\n", $cacheContents);
- return $cacheContents;
- }
- }
- }
- }
- $files = array();
-
- 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);
- $filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
- }
- 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
- if(!strpos($filename, '.module')) continue;
- if(substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php') {
- continue;
- }
-
- $files[] = str_replace($startPath, '', $pathname);
- }
- if($level == 0 && $dir !== null) {
- if($cache) $cache->save($cacheName, implode("\n", $files), WireCache::expireNever);
- }
- return $files;
- }
- /**
- * Setup entries in config->urls and config->paths for the given module
- *
- * @param string $moduleName
- * @param string $path
- *
- */
- protected function setConfigPaths($moduleName, $path) {
- $config = $this->wire('config');
- $path = rtrim($path, '/');
- $path = substr($path, strlen($config->paths->root)) . '/';
- $config->paths->set($moduleName, $path);
- $config->urls->set($moduleName, $path);
- }
- /**
- * Get the requsted Module or NULL if it doesn't exist.
- *
- * If the module is a ModulePlaceholder, then it will be converted to the real module (included, instantiated, init'd) .
- * If the module is not installed, but is installable, it will be installed, instantiated, and init'd.
- * This method is the only one guaranteed to return a real [non-placeholder] module.
- *
- * @param string|int $key Module className or database ID
- * @return Module|Inputfield|Fieldtype|Process|Textformatter|null
- * @throws WirePermissionException If module requires a particular permission the user does not have
- *
- */
- public function get($key) {
- return $this->getModule($key);
- }
- /**
- * Attempt to find a substitute for moduleName and return module if found or null if not
- *
- * @param $moduleName
- * @param array $options See getModule() options
- * @return Module|null
- *
- */
- protected function getSubstituteModule($moduleName, array $options = array()) {
-
- $module = null;
- $options['noSubstitute'] = true; // prevent recursion
-
- while(isset($this->substitutes[$moduleName]) && !$module) {
- $substituteName = $this->substitutes[$moduleName];
- $module = $this->getModule($substituteName, $options);
- if(!$module) $moduleName = $substituteName;
- }
-
- return $module;
- }
- /**
- * Get the requested Module or NULL if it doesn't exist + specify one or more options
- *
- * @param string|int $key Module className or database ID
- * @param array $options Optional settings to change load behavior:
- * - noPermissionCheck: Specify true to disable module permission checks (and resulting exception).
- * - noInstall: Specify true to prevent a non-installed module from installing from this request.
- * - noInit: Specify true to prevent the module from being initialized.
- * - noSubstitute: Specify true to prevent inclusion of a substitute module.
- * @return Module|null
- * @throws WirePermissionException If module requires a particular permission the user does not have
- *
- */
- public function getModule($key, array $options = array()) {
-
- if(empty($key)) return null;
- $module = null;
- $needsInit = false;
- // check for optional module ID and convert to classname if found
- if(ctype_digit("$key")) {
- if(!$key = array_search($key, $this->moduleIDs)) return null;
- }
-
- $module = parent::get($key);
- if(!$module && empty($options['noSubstitute'])) {
- if($this->isInstallable($key) && empty($options['noInstall'])) {
- // module is on file system and may be installed, no need to substitute
- } else {
- $module = $this->getSubstituteModule($key, $options);
- if($module) return $module; // returned module is ready to use
- }
- }
-
- if($module) {
- // check if it's a placeholder, and if it is then include/instantiate/init the real module
- // OR check if it's non-singular, so that a new instance is created
- if($module instanceof ModulePlaceholder || !$this->isSingular($module)) {
- $placeholder = $module;
- $class = $this->getModuleClass($placeholder);
- if($module instanceof ModulePlaceholder) $this->includeModule($module);
- $module = $this->newModule($class);
- // if singular, save the instance so it can be used in later calls
- if($this->isSingular($module)) $this->set($key, $module);
- $needsInit = true;
- }
- } else if(empty($options['noInstall']) && array_key_exists($key, $this->getInstallable())) {
- // check if the request is for an uninstalled module
- // if so, install it and return it
- $module = $this->install($key);
- $needsInit = true;
- }
-
- if($module && empty($options['noPermissionCheck'])) {
- if(!$this->hasPermission($module, $this->wire('user'), $this->wire('page'))) {
- throw new WirePermissionException($this->_('You do not have permission to execute this module') . ' - ' . $class);
- }
- }
- // skip autoload modules because they have already been initialized in the load() method
- // unless they were just installed, in which case we need do init now
- if($module && $needsInit) {
- // if the module is configurable, then load it's config data
- // and set values for each before initializing the module
- // $this->setModuleConfigData($module);
- // if(method_exists($module, 'init')) $module->init();
- if(empty($options['noInit'])) $this->initModule($module, false);
- }
-
- return $module;
- }
- /**
- * Check if user has permission for given module
- *
- * @param string|object $moduleName
- * @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) {
- $info = $this->getModuleInfo($moduleName);
- if(empty($info['permission']) && empty($info['permissionMethod'])) return $strict ? false : true;
-
- if(is_null($user)) $user = $this->wire('user');
- if($user && $user->isSuperuser()) return true;
- if(is_object($moduleName)) $moduleName = $moduleName->className();
-
- 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 $moduleName::$method($data);
- }
-
- return true;
- }
- /**
- * Get the requested module and reset cache + install it if necessary.
- *
- * This is exactly the same as get() except that this one will rebuild the modules cache if
- * it doesn't find the module at first. If the module is on the file system, this
- * one will return it in some instances that a regular get() can't.
- *
- * @param string|int $key Module className or database ID
- * @return Module|null
- *
- */
- public function getInstall($key) {
- $module = $this->get($key);
- if(!$module) {
- $this->resetCache();
- $module = $this->getModule($key);
- }
- return $module;
- }
- /**
- * Include the file for a given module, but don't instantiate it
- *
- * @param ModulePlaceholder|Module|string Expects a ModulePlaceholder or className
- * @return bool true on success
- *
- */
- public function includeModule($module) {
- $className = '';
- if(is_object($module)) $className = $module->className();
- else if(is_string($module)) $className = $module;
- if($className && class_exists($className, false)) return true; // already included
-
- // attempt to retrieve module
- if(is_string($module)) $module = parent::get($module);
-
- if(!$module && $className) {
- // unable to retrieve module, must be an uninstalled module
- $file = $this->getModuleFile($className);
- if($file) {
- @include_once($file);
- if(class_exists($className, false)) return true;
- }
- }
-
- if(!$module) return false;
- if($module instanceof ModulePlaceholder) {
- include_once($module->file);
- } else {
- // it's already been included, no doubt
- }
- return true;
- }
- /**
- * Find modules based on a selector string and ensure any ModulePlaceholders are loaded in the returned result
- *
- * @param string $selector
- * @return Modules
- *
- */
- public function find($selector) {
- $a = parent::find($selector);
- if($a) {
- foreach($a as $key => $value) {
- $a[$key] = $this->get($value->className());
- }
- }
- return $a;
- }
- /**
- * Find modules matching the given prefix
- *
- * @param string $prefix Specify prefix, i.e. Process, Fieldtype, Inputfield, etc.
- * @param bool $instantiate Specify true to return Module instances, or false to return class names (default=false)
- * @return array of module class names or Module objects. In either case, array indexes are class names.
- *
- */
- public function findByPrefix($prefix, $instantiate = false) {
- $results = array();
- foreach($this as $key => $value) {
- $className = $value->className();
- if(strpos($className, $prefix) !== 0) continue;
- if($instantiate) {
- $results[$className] = $this->get($className);
- } else {
- $results[$className] = $className;
- }
- }
- return $results;
- }
- /**
- * Get an array of all modules that aren't currently installed
- *
- * @return array Array of elements with $className => $pathname
- *
- */
- public function getInstallable() {
- return $this->installable;
- }
- /**
- * Is the given class name installed?
- *
- * @param string $class Just a ModuleClassName, or optionally: ModuleClassName>=1.2.3 (operator and version)
- * @return bool
- *
- */
- public function isInstalled($class) {
- if(is_object($class)) $class = $this->getModuleClass($class);
- $operator = null;
- $requiredVersion = null;
- $currentVersion = null;
-
- if(!ctype_alnum($class)) {
- // class has something other than just a classnae, likely operator + version
- if(preg_match('/^([a-zA-Z0-9_]+)\s*([<>=!]+)\s*([\d.]+)$/', $class, $matches)) {
- $class = $matches[1];
- $operator = $matches[2];
- $requiredVersion = $matches[3];
- }
- }
-
- if($class === 'PHP' || $class === 'ProcessWire') {
- $installed = true;
- if(!is_null($requiredVersion)) {
- $currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire('config')->version;
- }
- } else {
- $installed = parent::get($class) !== null;
- if($installed && !is_null($requiredVersion)) {
- $info = $this->getModuleInfo($class);
- $currentVersion = $info['version'];
- }
- }
-
- if($installed && !is_null($currentVersion)) {
- $installed = $this->versionCompare($currentVersion, $requiredVersion, $operator);
- }
-
- return $installed;
-
- }
- /**
- * Is the given class name not installed?
- *
- * @param string $class
- * @param bool $now Is module installable RIGHT NOW? This makes it check that all dependencies are already fulfilled (default=false)
- * @return bool
- *
- */
- public function isInstallable($class, $now = false) {
- $installable = array_key_exists($class, $this->installable);
- if(!$installable) return false;
- if($now) {
- $requires = $this->getRequiresForInstall($class);
- if(count($requires)) return false;
- }
- return $installable;
- }
-
- /**
- * Install the given class name
- *
- * @param string $class
- * @param array|bool $options Associative array of:
- * - dependencies (boolean, default=true): When true, dependencies will also be installed where possible. Specify false to prevent installation of uninstalled modules.
- * - resetCache (boolean, default=true): When true, module caches will be reset after installation.
- * @return null|Module Returns null if unable to install, or instantiated Module object if successfully installed.
- * @throws WireException
- *
- */
- public function ___install($class, $options = array()) {
-
- $defaults = array(
- 'dependencies' => true,
- 'resetCache' => true,
- );
- if(is_bool($options)) {
- // dependencies argument allowed instead of $options, for backwards compatibility
- $dependencies = $options;
- $options = array('dependencies' => $dependencies);
- }
- $options = array_merge($defaults, $options);
- $dependencyOptions = $options;
- $dependencyOptions['resetCache'] = false;
- if(!$this->isInstallable($class)) return null;
- $requires = $this->getRequiresForInstall($class);
- if(count($requires)) {
- $error = '';
- $installable = false;
- if($options['dependencies']) {
- $installable = true;
- foreach($requires as $requiresModule) {
- if(!$this->isInstallable($requiresModule)) $installable = false;
- }
- if($installable) {
- foreach($requires as $requiresModule) {
- if(!$this->install($requiresModule, $dependencyOptions)) {
- $error = $this->_('Unable to install required module') . " - $requiresModule. ";
- $installable = false;
- break;
- }
- }
- }
- }
- if(!$installable) {
- throw new WireException($error . "Module $class requires: " . implode(", ", $requires));
- }
- }
-
- $languages = $this->wire('languages');
- if($languages) $languages->setDefault();
- $pathname = $this->installable[$class];
- require_once($pathname);
- $this->setConfigPaths($class, dirname($pathname));
- $module = $this->newModule($class);
- $flags = 0;
- $database = $this->wire('database');
- $moduleID = 0;
-
- if($this->isSingular($module)) $flags = $flags | self::flagsSingular;
- if($this->isAutoload($module)) $flags = $flags | self::flagsAutoload;
- $sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";
- if($this->wire('config')->systemVersion >=7) $sql .= ", created=NOW()";
- $query = $database->prepare($sql, "modules.install($class)");
- $query->bindValue(":class", $class, PDO::PARAM_STR);
- $query->bindValue(":flags", $flags, PDO::PARAM_INT);
-
- try {
- if($query->execute()) $moduleID = (int) $database->lastInsertId();
- } catch(Exception $e) {
- if($languages) $languages->unsetDefault();
- $this->trackException($e, false, true);
- return null;
- }
-
- $this->moduleIDs[$class] = $moduleID;
- $this->add($module);
- unset($this->installable[$class]);
-
- // note: the module's install is called here because it may need to know it's module ID for installation of permissions, etc.
- if(method_exists($module, '___install') || method_exists($module, 'install')) {
- try {
- $module->install();
- } catch(Exception $e) {
- // remove the module from the modules table if the install failed
- $moduleID = (int) $moduleID;
- $error = "Unable to install module '$class': " . $e->getMessage();
- $ee = null;
- try {
- $query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1'); // QA
- $query->bindValue(":id", $moduleID, PDO::PARAM_INT);
- $query->execute();
- } catch(Exception $ee) {
- $this->trackException($e, false, $error)->trackException($ee, true);
- }
- if($languages) $languages->unsetDefault();
- if(is_null($ee)) $this->trackException($e, false, $error);
- return null;
- }
- }
- $info = $this->getModuleInfoVerbose($class, array('noCache' => true));
-
- // if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
- foreach($info['permissions'] as $name => $title) {
- $name = $this->wire('sanitizer')->pageName($name);
- if(ctype_digit("$name") || empty($name)) continue; // permission name not valid
- $permission = $this->wire('permissions')->get($name);
- if($permission->id) continue; // permision already there
- try {
- $permission = $this->wire('permissions')->add($name);
- $permission->title = $title;
- $this->wire('permissions')->save($permission);
- if($languages) $languages->unsetDefault();
- $this->message(sprintf($this->_('Added Permission: %s'), $permission->name));
- } catch(Exception $e) {
- if($languages) $languages->unsetDefault();
- $error = sprintf($this->_('Error adding permission: %s'), $name);
- $this->trackException($e, false, $error);
- }
- }
- // check if there are any modules in 'installs' that this module didn't handle installation of, and install them
- $label = $this->_('Module Auto Install');
-
- foreach($info['installs'] as $name) {
- if(!$this->isInstalled($name)) {
- try {
- $this->install($name, $dependencyOptions);
- $this->message("$label: $name");
- } catch(Exception $e) {
- $error = "$label: $name - " . $e->getMessage();
- $this->trackException($e, false, $error);
- }
- }
- }
- $this->log("Installed module '$module'");
- if($languages) $languages->unsetDefault();
- if($options['resetCache']) $this->clearModuleInfoCache();
- return $module;
- }
- /**
- * Returns whether the module can be uninstalled
- *
- * @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 = "Module is not already installed";
- $class = $this->getModuleClass($class);
- if(!$this->isInstalled($class)) {
- $reason = $reason1;
- } else {
- $this->includeModule($class);
- if(!class_exists($class, false)) $reason = $reason1;
- }
- if(!$reason) {
- // if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable
- $info = $this->getModuleInfo($class);
- if(!empty($info['permanent'])) {
- $reason = "Module is permanent";
- } else {
- $dependents = $this->getRequiresForUninstall($class);
- if(count($dependents)) $reason = "Module is required by other modules that must be removed first";
- }
- if(!$reason && in_array('Fieldtype', class_parents($class))) {
- foreach(wire('fields') as $field) {
- $fieldtype = get_class($field->type);
- if($fieldtype == $class) {
- $reason = "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)
- *
- * @param string|Module $class
- * @param bool $returnReason If true, the reason why it can't be removed will be returned rather than boolean false.
- * @return bool|string
- *
- */
- public function isDeleteable($class, $returnReason = false) {
- $reason = '';
- $class = $this->getModuleClass($class);
- $filename = isset($this->installable[$class]) ? $this->installable[$class] : null;
- $dirname = dirname($filename);
- if(empty($filename) || $this->isInstalled($class)) {
- $reason = "Module must be uninstalled before it can be deleted.";
- } else if(is_link($filename) || is_link($dirname) || is_link(dirname($dirname))) {
- $reason = "Module is linked to another location";
- } else if(!is_file($filename)) {
- $reason = "Module file does not exist";
- } else if(strpos($filename, $this->paths[0]) === 0) {
- $reason = "Core modules may not be deleted.";
- } else if(!is_writable($filename)) {
- $reason = "We have no write access to the module file, it must be removed manually.";
- }
- if($returnReason && $reason) return $reason;
-
- return $reason ? false : true;
- }
- /**
- * Delete the given module, physically removing its files
- *
- * @param string $class
- * @return bool|int
- * @throws WireException If module can't be deleted, exception will be thrown containing reason.
- *
- */
- public function ___delete($class) {
- $class = $this->getModuleClass($class);
- $reason = $this->isDeleteable($class, true);
- if($reason !== true) throw new WireException($reason);
- $filename = $this->installable[$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->paths as $key => $modulesPath) {
- if($key === 0) continue; // skip core modules path
- if(strpos("$parentPath/", $modulesPath) === 0) $inPath = true;
- if($modulesPath === $path) $inRoot = true;
- }
- $basename = basename($basename, '.php');
- $basename = basename($basename, '.module');
-
- $files = array(
- "$basename.module",
- "$basename.module.php",
- "$basename.info.php",
- "$basename.info.json",
- "$basename.config.php",
- "{$basename}Config.php",
- );
-
- if($inPath) {
- // module is in /site/modules/[ModuleName]/
-
- $numOtherModules = 0; // num modules in dir other than this one
- $numLinks = 0; // number of symbolic links
- $dirs = array("$path/");
-
- do {
- $dir = array_shift($dirs);
- $this->message("Scanning: $dir", Notice::debug);
-
- foreach(new DirectoryIterator($dir) as $file) {
- if($file->isDot()) continue;
- if($file->isLink()) {
- $numLinks++;
- continue;
- }
- if($file->isDir()) {
- $dirs[] = $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 = wireRmdir($path, true);
- if($success) {
- $this->message("Removed directory: $path", Notice::debug);
- if(is_dir($backupPath)) {
- if(wireRmdir($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(unlink($file)) {
- $this->message("Removed file: $file", Notice::debug);
- } else {
- $this->error("Unable to remove file: $file", Notice::debug);
- }
- }
-
- if($success) $this->log("Deleted module '$class'");
- else $this->error("Failed to delete module '$class'");
-
- retu…
Large files files are truncated, but you can click here to view the full file