PageRenderTime 68ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/wire/core/Modules.php

http://github.com/ryancramerdesign/ProcessWire
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
  1. <?php
  2. /**
  3. * ProcessWire Modules
  4. *
  5. * Loads and manages all runtime modules for ProcessWire
  6. *
  7. * Note that when iterating, find(), or calling any other method that returns module(s), excepting get(), a ModulePlaceholder may be
  8. * returned rather than a real Module. ModulePlaceholders are used in instances when the module may or may not be needed at runtime
  9. * in order to save resources. As a result, anything iterating through these Modules should check to make sure it's not a ModulePlaceholder
  10. * before using it. If it's a ModulePlaceholder, then the real Module can be instantiated/retrieved by $modules->get($className).
  11. *
  12. * ProcessWire 2.x
  13. * Copyright (C) 2015 by Ryan Cramer
  14. * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
  15. *
  16. * https://processwire.com
  17. *
  18. */
  19. class Modules extends WireArray {
  20. /**
  21. * Whether or not module debug mode is active
  22. *
  23. */
  24. protected $debug = false;
  25. /**
  26. * Flag indicating the module may have only one instance at runtime.
  27. *
  28. */
  29. const flagsSingular = 1;
  30. /**
  31. * Flag indicating that the module should be instantiated at runtime, rather than when called upon.
  32. *
  33. */
  34. const flagsAutoload = 2;
  35. /**
  36. * Flag indicating the module has more than one copy of it on the file system.
  37. *
  38. */
  39. const flagsDuplicate = 4;
  40. /**
  41. * When combined with flagsAutoload, indicates that the autoload is conditional
  42. *
  43. */
  44. const flagsConditional = 8;
  45. /**
  46. * When combined with flagsAutoload, indicates that the module's autoload state is temporarily disabled
  47. *
  48. */
  49. const flagsDisabled = 16;
  50. /**
  51. * Filename for module info cache file
  52. *
  53. */
  54. const moduleInfoCacheName = 'Modules.info';
  55. /**
  56. * Filename for verbose module info cache file
  57. *
  58. */
  59. const moduleInfoCacheVerboseName = 'ModulesVerbose.info';
  60. /**
  61. * Filename for uninstalled module info cache file
  62. *
  63. */
  64. const moduleInfoCacheUninstalledName = 'ModulesUninstalled.info';
  65. /**
  66. * Cache name for module version change cache
  67. *
  68. */
  69. const moduleLastVersionsCacheName = 'ModulesVersions.info';
  70. /**
  71. * Array of modules that are not currently installed, indexed by className => filename
  72. *
  73. */
  74. protected $installable = array();
  75. /**
  76. * An array of module database IDs indexed by: class => id
  77. *
  78. * Used internally for database operations
  79. *
  80. */
  81. protected $moduleIDs = array();
  82. /**
  83. * Full system paths where modules are stored
  84. *
  85. * index 0 must be the core modules path (/i.e. /wire/modules/)
  86. *
  87. */
  88. protected $paths = array();
  89. /**
  90. * Cached module configuration data indexed by module ID
  91. *
  92. * Values are integer 1 for modules that have config data but data is not yet loaded.
  93. * Values are an array for modules have have config data and has been loaded.
  94. *
  95. */
  96. protected $configData = array();
  97. /**
  98. * Module created dates indexed by module ID
  99. *
  100. */
  101. protected $createdDates = array();
  102. /**
  103. * Have the modules been init'd() ?
  104. *
  105. */
  106. protected $initialized = false;
  107. /**
  108. * Becomes an array if debug mode is on
  109. *
  110. */
  111. protected $debugLog = array();
  112. /**
  113. * Array of moduleName => condition
  114. *
  115. * Condition can be either an anonymous function or a selector string to be evaluated at ready().
  116. *
  117. */
  118. protected $conditionalAutoloadModules = array();
  119. /**
  120. * Cache of module information
  121. *
  122. */
  123. protected $moduleInfoCache = array();
  124. /**
  125. * Cache of module information (verbose text) including: summary, author, href, file, core
  126. *
  127. */
  128. protected $moduleInfoCacheVerbose = array();
  129. /**
  130. * Cache of uninstalled module information (verbose for uninstalled) including: summary, author, href, file, core
  131. *
  132. * Note that this one is indexed by class name rather than by ID (since uninstalled modules have no ID)
  133. *
  134. */
  135. protected $moduleInfoCacheUninstalled = array();
  136. /**
  137. * Cache of module information from DB used across multiple calls temporarily by load() method
  138. *
  139. */
  140. protected $modulesTableCache = array();
  141. /**
  142. * Last known versions of modules, for version change tracking
  143. *
  144. * @var array of ModuleName (string) => last known version (integer|string)
  145. *
  146. */
  147. protected $modulesLastVersions = array();
  148. /**
  149. * Array of module ID => flags (int)
  150. *
  151. * @var array
  152. *
  153. */
  154. protected $moduleFlags = array();
  155. /**
  156. * Array of moduleName => substituteModuleName to be used when moduleName doesn't exist
  157. *
  158. * Primarily for providing backwards compatiblity with modules assumed installed that
  159. * may no longer be in core.
  160. *
  161. * see setSubstitutes() method
  162. *
  163. */
  164. protected $substitutes = array();
  165. /**
  166. * Instance of ModulesDuplicates
  167. *
  168. * @var ModulesDuplicates
  169. *
  170. */
  171. protected $duplicates;
  172. /**
  173. * Properties that only appear in 'verbose' moduleInfo
  174. *
  175. * @var array
  176. *
  177. */
  178. protected $moduleInfoVerboseKeys = array(
  179. 'summary',
  180. 'author',
  181. 'href',
  182. 'file',
  183. 'core',
  184. 'versionStr',
  185. 'permissions',
  186. 'page',
  187. );
  188. /**
  189. * Construct the Modules
  190. *
  191. * @param string $path Core modules path (you may add other paths with addPath method)
  192. *
  193. */
  194. public function __construct($path) {
  195. $this->addPath($path);
  196. }
  197. /**
  198. * Get the ModulesDuplicates instance
  199. *
  200. * @return ModulesDuplicates
  201. *
  202. */
  203. public function duplicates() {
  204. if(is_null($this->duplicates)) $this->duplicates = new ModulesDuplicates();
  205. return $this->duplicates;
  206. }
  207. /**
  208. * Add another modules path, must be called before init()
  209. *
  210. * @param string $path
  211. *
  212. */
  213. public function addPath($path) {
  214. $this->paths[] = $path;
  215. }
  216. /**
  217. * Return all assigned module root paths
  218. *
  219. * @return array of modules paths, with index 0 always being the core modules path.
  220. *
  221. */
  222. public function getPaths() {
  223. return $this->paths;
  224. }
  225. /**
  226. * Initialize modules
  227. *
  228. * Must be called after construct before this class is ready to use
  229. *
  230. * @see load()
  231. *
  232. */
  233. public function init() {
  234. $this->setTrackChanges(false);
  235. $this->loadModuleInfoCache();
  236. $this->loadModulesTable();
  237. foreach($this->paths as $path) {
  238. $this->load($path);
  239. }
  240. $this->modulesTableCache = array(); // clear out data no longer needed
  241. }
  242. /**
  243. * Modules class accepts only Module instances, per the WireArray interface
  244. *
  245. */
  246. public function isValidItem($item) {
  247. return $item instanceof Module;
  248. }
  249. /**
  250. * The key/index used for each module in the array is it's class name, per the WireArray interface
  251. *
  252. */
  253. public function getItemKey($item) {
  254. return $this->getModuleClass($item);
  255. }
  256. /**
  257. * There is no blank/generic module type, so makeBlankItem returns null
  258. *
  259. */
  260. public function makeBlankItem() {
  261. return null;
  262. }
  263. /**
  264. * Make a new/blank WireArray
  265. *
  266. */
  267. public function makeNew() {
  268. // ensures that find(), etc. operations don't initalize a new Modules() class
  269. return new WireArray();
  270. }
  271. /**
  272. * Make a new populated copy of a WireArray containing all the modules
  273. *
  274. * @return WireArray
  275. *
  276. */
  277. public function makeCopy() {
  278. // ensures that find(), etc. operations don't initalize a new Modules() class
  279. $copy = $this->makeNew();
  280. foreach($this->data as $key => $value) $copy[$key] = $value;
  281. $copy->resetTrackChanges($this->trackChanges());
  282. return $copy;
  283. }
  284. /**
  285. * Initialize all the modules that are loaded at boot
  286. *
  287. */
  288. public function triggerInit($modules = null, $completed = array(), $level = 0) {
  289. if($this->debug) {
  290. $debugKey = $this->debugTimerStart("triggerInit$level");
  291. $this->message("triggerInit(level=$level)");
  292. }
  293. $queue = array();
  294. if(is_null($modules)) $modules = $this;
  295. foreach($modules as $class => $module) {
  296. if($module instanceof ModulePlaceholder) {
  297. // skip modules that aren't autoload and those that are conditional autoload
  298. if(!$module->autoload) continue;
  299. if(isset($this->conditionalAutoloadModules[$class])) continue;
  300. }
  301. if($this->debug) $debugKey2 = $this->debugTimerStart("triggerInit$level($class)");
  302. $info = $this->getModuleInfo($module);
  303. $skip = false;
  304. // module requires other modules
  305. foreach($info['requires'] as $requiresClass) {
  306. if(in_array($requiresClass, $completed)) continue;
  307. $dependencyInfo = $this->getModuleInfo($requiresClass);
  308. if(empty($dependencyInfo['autoload'])) {
  309. // if dependency isn't an autoload one, there's no point in waiting for it
  310. if($this->debug) $this->warning("Autoload module '$module' requires a non-autoload module '$requiresClass'");
  311. continue;
  312. } else if(isset($this->conditionalAutoloadModules[$requiresClass])) {
  313. // autoload module requires another autoload module that may or may not load
  314. if($this->debug) $this->warning("Autoload module '$module' requires a conditionally autoloaded module '$requiresClass'");
  315. continue;
  316. }
  317. // dependency is autoload and required by this module, so queue this module to init later
  318. $queue[$class] = $module;
  319. $skip = true;
  320. break;
  321. }
  322. if(!$skip) {
  323. if($info['autoload'] !== false) {
  324. if($info['autoload'] === true || $this->isAutoload($module)) {
  325. $this->initModule($module);
  326. }
  327. }
  328. $completed[] = $class;
  329. }
  330. if($this->debug) $this->debugTimerStop($debugKey2);
  331. }
  332. // if there is a dependency queue, go recursive till the queue is completed
  333. if(count($queue) && $level < 3) {
  334. $this->triggerInit($queue, $completed, $level + 1);
  335. }
  336. $this->initialized = true;
  337. if($this->debug) if($debugKey) $this->debugTimerStop($debugKey);
  338. if(!$level && (empty($this->moduleInfoCache))) { // || empty($this->moduleInfoCacheVerbose))) {
  339. if($this->debug) $this->message("saveModuleInfoCache from triggerInit");
  340. $this->saveModuleInfoCache();
  341. }
  342. }
  343. /**
  344. * Given a class name, return the constructed module
  345. *
  346. * @param string $className Module class name
  347. * @return Module
  348. *
  349. */
  350. protected function newModule($className) {
  351. if($this->debug) $debugKey = $this->debugTimerStart("newModule($className)");
  352. if(!class_exists($className, false)) $this->includeModule($className);
  353. $module = new $className();
  354. if($this->debug) $this->debugTimerStop($debugKey);
  355. return $module;
  356. }
  357. /**
  358. * Return a new ModulePlaceholder for the given className
  359. *
  360. * @param string $className Module class this placeholder will stand in for
  361. * @param string $file Full path and filename of $className
  362. * @param bool $singular Is the module a singular module?
  363. * @param bool $autoload Is the module an autoload module?
  364. * @return ModulePlaceholder
  365. *
  366. */
  367. protected function newModulePlaceholder($className, $file, $singular, $autoload) {
  368. $module = new ModulePlaceholder();
  369. $module->setClass($className);
  370. $module->singular = $singular;
  371. $module->autoload = $autoload;
  372. $module->file = $file;
  373. return $module;
  374. }
  375. /**
  376. * Initialize a single module
  377. *
  378. * @param Module $module
  379. * @param bool $clearSettings If true, module settings will be cleared when appropriate to save space.
  380. *
  381. */
  382. protected function initModule(Module $module, $clearSettings = true) {
  383. if($this->debug) {
  384. static $n = 0;
  385. $this->message("initModule (" . (++$n) . "): $module");
  386. }
  387. // if the module is configurable, then load its config data
  388. // and set values for each before initializing the module
  389. $this->setModuleConfigData($module);
  390. $className = get_class($module);
  391. $moduleID = isset($this->moduleIDs[$className]) ? $this->moduleIDs[$className] : 0;
  392. if($moduleID && isset($this->modulesLastVersions[$moduleID])) {
  393. $this->checkModuleVersion($module);
  394. }
  395. if(method_exists($module, 'init')) {
  396. if($this->debug) {
  397. $className = get_class($module);
  398. $debugKey = $this->debugTimerStart("initModule($className)");
  399. }
  400. $module->init();
  401. if($this->debug) {
  402. $this->debugTimerStop($debugKey);
  403. }
  404. }
  405. // if module is autoload (assumed here) and singular, then
  406. // we no longer need the module's config data, so remove it
  407. if($clearSettings && $this->isSingular($module)) {
  408. if(!$moduleID) $moduleID = $this->getModuleID($module);
  409. if(isset($this->configData[$moduleID])) $this->configData[$moduleID] = 1;
  410. }
  411. }
  412. /**
  413. * Call ready for a single module
  414. *
  415. */
  416. protected function readyModule(Module $module) {
  417. if(method_exists($module, 'ready')) {
  418. if($this->debug) $debugKey = $this->debugTimerStart("readyModule(" . $module->className() . ")");
  419. $module->ready();
  420. if($this->debug) {
  421. $this->debugTimerStop($debugKey);
  422. static $n = 0;
  423. $this->message("readyModule (" . (++$n) . "): $module");
  424. }
  425. }
  426. }
  427. /**
  428. * Init conditional autoload modules, if conditions allow
  429. *
  430. * @return array of skipped module names
  431. *
  432. */
  433. protected function triggerConditionalAutoload() {
  434. // conditional autoload modules that are skipped (className => 1)
  435. $skipped = array();
  436. // init conditional autoload modules, now that $page is known
  437. foreach($this->conditionalAutoloadModules as $className => $func) {
  438. if($this->debug) {
  439. $moduleID = $this->getModuleID($className);
  440. $flags = $this->moduleFlags[$moduleID];
  441. $this->message("Conditional autoload: $className (flags=$flags, condition=" . (is_string($func) ? $func : 'func') . ")");
  442. }
  443. $load = true;
  444. if(is_string($func)) {
  445. // selector string
  446. if(!$this->wire('page')->is($func)) $load = false;
  447. } else {
  448. // anonymous function
  449. if(!is_callable($func)) $load = false;
  450. else if(!$func()) $load = false;
  451. }
  452. if($load) {
  453. $module = $this->newModule($className);
  454. $this->set($className, $module);
  455. $this->initModule($module);
  456. if($this->debug) $this->message("Conditional autoload: $className LOADED");
  457. } else {
  458. $skipped[$className] = $className;
  459. if($this->debug) $this->message("Conditional autoload: $className SKIPPED");
  460. }
  461. }
  462. // clear this out since we don't need it anymore
  463. $this->conditionalAutoloadModules = array();
  464. return $skipped;
  465. }
  466. /**
  467. * Trigger all modules 'ready' method, if they have it.
  468. *
  469. * This is to indicate to them that the API environment is fully ready and $page is in fuel.
  470. *
  471. * This is triggered by ProcessPageView::ready
  472. *
  473. */
  474. public function triggerReady() {
  475. if($this->debug) $debugKey = $this->debugTimerStart("triggerReady");
  476. $skipped = $this->triggerConditionalAutoload();
  477. // trigger ready method on all applicable modules
  478. foreach($this as $module) {
  479. if($module instanceof ModulePlaceholder) continue;
  480. // $info = $this->getModuleInfo($module);
  481. // if($info['autoload'] === false) continue;
  482. // if(!$this->isAutoload($module)) continue;
  483. $class = $this->getModuleClass($module);
  484. if(isset($skipped[$class])) continue;
  485. $id = $this->moduleIDs[$class];
  486. if(!($this->moduleFlags[$id] & self::flagsAutoload)) continue;
  487. if(!method_exists($module, 'ready')) continue;
  488. $this->readyModule($module);
  489. }
  490. if($this->debug) $this->debugTimerStop($debugKey);
  491. }
  492. /**
  493. * Retrieve the installed module info as stored in the database
  494. *
  495. * @return array Indexed by module class name => array of module info
  496. *
  497. */
  498. protected function loadModulesTable() {
  499. $database = $this->wire('database');
  500. // we use SELECT * so that this select won't be broken by future DB schema additions
  501. // Currently: id, class, flags, data, with created added at sysupdate 7
  502. $query = $database->prepare("SELECT * FROM modules ORDER BY class", "modules.loadModulesTable()"); // QA
  503. $query->execute();
  504. while($row = $query->fetch(PDO::FETCH_ASSOC)) {
  505. $moduleID = (int) $row['id'];
  506. $flags = (int) $row['flags'];
  507. $class = $row['class'];
  508. $this->moduleIDs[$class] = $moduleID;
  509. $this->moduleFlags[$moduleID] = $flags;
  510. $loadSettings = ($flags & self::flagsAutoload) || ($flags & self::flagsDuplicate) || ($class == 'SystemUpdater');
  511. if($loadSettings) {
  512. // preload config data for autoload modules since we'll need it again very soon
  513. $data = strlen($row['data']) ? wireDecodeJSON($row['data']) : array();
  514. $this->configData[$moduleID] = $data;
  515. // populate information about duplicates, if applicable
  516. if($flags & self::flagsDuplicate) $this->duplicates()->addFromConfigData($class, $data);
  517. } else if(!empty($row['data'])) {
  518. // indicate that it has config data, but not yet loaded
  519. $this->configData[$moduleID] = 1;
  520. }
  521. if(isset($row['created']) && $row['created'] != '0000-00-00 00:00:00') {
  522. $this->createdDates[$moduleID] = $row['created'];
  523. }
  524. unset($row['data']); // info we don't want stored in modulesTableCache
  525. $this->modulesTableCache[$class] = $row;
  526. }
  527. $query->closeCursor();
  528. }
  529. /**
  530. * Given a disk path to the modules, determine all installed modules and keep track of all uninstalled (installable) modules.
  531. *
  532. * @param string $path
  533. *
  534. */
  535. protected function load($path) {
  536. if($this->debug) $debugKey = $this->debugTimerStart("load($path)");
  537. $installed =& $this->modulesTableCache;
  538. $modulesLoaded = array();
  539. $modulesDelayed = array();
  540. $modulesRequired = array();
  541. foreach($this->findModuleFiles($path, true) as $pathname) {
  542. $pathname = trim($pathname);
  543. $requires = array();
  544. $moduleName = $this->loadModule($path, $pathname, $requires, $installed);
  545. if(!$moduleName) continue;
  546. if(count($requires)) {
  547. // module not loaded because it required other module(s) not yet loaded
  548. foreach($requires as $requiresModuleName) {
  549. if(!isset($modulesRequired[$requiresModuleName])) $modulesRequired[$requiresModuleName] = array();
  550. if(!isset($modulesDelayed[$moduleName])) $modulesDelayed[$moduleName] = array();
  551. // queue module for later load
  552. $modulesRequired[$requiresModuleName][$moduleName] = $pathname;
  553. $modulesDelayed[$moduleName][] = $requiresModuleName;
  554. }
  555. continue;
  556. }
  557. // module was successfully loaded
  558. $modulesLoaded[$moduleName] = 1;
  559. $loadedNames = array($moduleName);
  560. // now determine if this module had any other modules waiting on it as a dependency
  561. while($moduleName = array_shift($loadedNames)) {
  562. // iternate through delayed modules that require this one
  563. if(empty($modulesRequired[$moduleName])) continue;
  564. foreach($modulesRequired[$moduleName] as $delayedName => $delayedPathName) {
  565. $loadNow = true;
  566. if(isset($modulesDelayed[$delayedName])) {
  567. foreach($modulesDelayed[$delayedName] as $requiresModuleName) {
  568. if(!isset($modulesLoaded[$requiresModuleName])) {
  569. $loadNow = false;
  570. }
  571. }
  572. }
  573. if(!$loadNow) continue;
  574. // all conditions satisified to load delayed module
  575. unset($modulesDelayed[$delayedName], $modulesRequired[$moduleName][$delayedName]);
  576. $unused = array();
  577. $loadedName = $this->loadModule($path, $delayedPathName, $unused, $installed);
  578. if(!$loadedName) continue;
  579. $modulesLoaded[$loadedName] = 1;
  580. $loadedNames[] = $loadedName;
  581. }
  582. }
  583. }
  584. if(count($modulesDelayed)) foreach($modulesDelayed as $moduleName => $requiredNames) {
  585. $this->error("Module '$moduleName' dependency not fulfilled for: " . implode(', ', $requiredNames), Notice::debug);
  586. }
  587. if($this->debug) $this->debugTimerStop($debugKey);
  588. }
  589. /**
  590. * Load a module into memory (companion to load bootstrap method)
  591. *
  592. * @param string $basepath Base path of modules being processed (path provided to the load method)
  593. * @param string $pathname
  594. * @param array $requires This method will populate this array with required dependencies (class names) if present.
  595. * @param array $installed Array of installed modules info, indexed by module class name
  596. * @return Returns module name (classname)
  597. *
  598. */
  599. protected function loadModule($basepath, $pathname, array &$requires, array &$installed) {
  600. $pathname = $basepath . $pathname;
  601. $dirname = dirname($pathname);
  602. $filename = basename($pathname);
  603. $basename = basename($filename, '.php');
  604. $basename = basename($basename, '.module');
  605. $requires = array();
  606. $duplicates = $this->duplicates();
  607. // check if module has duplicate files, where one to use has already been specified to use first
  608. $currentFile = $duplicates->getCurrent($basename); // returns the current file in use, if more than one
  609. if($currentFile) {
  610. // there is a duplicate file in use
  611. $file = rtrim($this->wire('config')->paths->root, '/') . $currentFile;
  612. if(file_exists($file) && $pathname != $file) {
  613. // file in use is different from the file we are looking at
  614. // check if this is a new/yet unknown duplicate
  615. if(!$duplicates->hasDuplicate($basename, $pathname)) {
  616. // new duplicate
  617. $duplicates->recordDuplicate($basename, $pathname, $file, $installed);
  618. }
  619. return '';
  620. }
  621. }
  622. // check if module has already been loaded, or maybe we've got duplicates
  623. if(class_exists($basename, false)) {
  624. $module = parent::get($basename);
  625. $dir = rtrim($this->wire('config')->paths->$basename, '/');
  626. if($module && $dir && $dirname != $dir) {
  627. $duplicates->recordDuplicate($basename, $pathname, "$dir/$filename", $installed);
  628. return '';
  629. }
  630. if($module) return $basename;
  631. }
  632. // if the filename doesn't end with .module or .module.php, then stop and move onto the next
  633. if(!strpos($filename, '.module') || (substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php')) return false;
  634. // if the filename doesn't start with the requested path, then continue
  635. if(strpos($pathname, $basepath) !== 0) return '';
  636. // if the file isn't there, it was probably uninstalled, so ignore it
  637. if(!file_exists($pathname)) return '';
  638. // if the module isn't installed, then stop and move on to next
  639. if(!array_key_exists($basename, $installed)) {
  640. $this->installable[$basename] = $pathname;
  641. return '';
  642. }
  643. $info = $installed[$basename];
  644. $this->setConfigPaths($basename, $dirname);
  645. $module = null;
  646. $autoload = false;
  647. if($info['flags'] & self::flagsAutoload) {
  648. // this is an Autoload module.
  649. // include the module and instantiate it but don't init() it,
  650. // because it will be done by Modules::init()
  651. $moduleInfo = $this->getModuleInfo($basename);
  652. // determine if module has dependencies that are not yet met
  653. if(count($moduleInfo['requires'])) {
  654. foreach($moduleInfo['requires'] as $requiresClass) {
  655. if(!class_exists($requiresClass, false)) {
  656. $requiresInfo = $this->getModuleInfo($requiresClass);
  657. if(!empty($requiresInfo['error'])
  658. || $requiresInfo['autoload'] === true
  659. || !$this->isInstalled($requiresClass)) {
  660. // we only handle autoload===true since load() only instantiates other autoload===true modules
  661. $requires[] = $requiresClass;
  662. }
  663. }
  664. }
  665. if(count($requires)) {
  666. // module has unmet requirements
  667. return $basename;
  668. }
  669. }
  670. // if not defined in getModuleInfo, then we'll accept the database flag as enough proof
  671. // since the module may have defined it via an isAutoload() function
  672. if(!isset($moduleInfo['autoload'])) $moduleInfo['autoload'] = true;
  673. $autoload = $moduleInfo['autoload'];
  674. if($autoload === 'function') {
  675. // function is stored by the moduleInfo cache to indicate we need to call a dynamic function specified with the module itself
  676. $i = $this->getModuleInfoExternal($basename);
  677. if(empty($i)) {
  678. include_once($pathname);
  679. $i = $basename::getModuleInfo();
  680. }
  681. $autoload = isset($i['autoload']) ? $i['autoload'] : true;
  682. unset($i);
  683. }
  684. // check for conditional autoload
  685. if(!is_bool($autoload) && (is_string($autoload) || is_callable($autoload)) && !($info['flags'] & self::flagsDisabled)) {
  686. // anonymous function or selector string
  687. $this->conditionalAutoloadModules[$basename] = $autoload;
  688. $this->moduleIDs[$basename] = $info['id'];
  689. $autoload = true;
  690. } else if($autoload) {
  691. include_once($pathname);
  692. if(!($info['flags'] & self::flagsDisabled)) {
  693. $module = $this->newModule($basename);
  694. }
  695. }
  696. }
  697. if(is_null($module)) {
  698. // placeholder for a module, which is not yet included and instantiated
  699. $module = $this->newModulePlaceholder($basename, $pathname, $info['flags'] & self::flagsSingular, $autoload);
  700. }
  701. $this->moduleIDs[$basename] = $info['id'];
  702. $this->set($basename, $module);
  703. return $basename;
  704. }
  705. /**
  706. * Find new module files in the given $path
  707. *
  708. * If $readCache is true, this will perform the find from the cache
  709. *
  710. * @param string $path Path to the modules
  711. * @param bool $readCache Optional. If set to true, then this method will attempt to read modules from the cache.
  712. * @param int $level For internal recursive use.
  713. * @return array Array of module files
  714. *
  715. */
  716. protected function findModuleFiles($path, $readCache = false, $level = 0) {
  717. static $startPath;
  718. static $callNum = 0;
  719. $callNum++;
  720. $config = $this->wire('config');
  721. $cache = $this->wire('cache');
  722. if($level == 0) {
  723. $startPath = $path;
  724. $cacheName = "Modules." . str_replace($config->paths->root, '', $path);
  725. if($readCache && $cache) {
  726. $cacheContents = $cache->get($cacheName);
  727. if($cacheContents !== null) {
  728. if(empty($cacheContents) && $callNum === 1) {
  729. // don't accept empty cache for first path (/wire/modules/)
  730. } else {
  731. $cacheContents = explode("\n", $cacheContents);
  732. return $cacheContents;
  733. }
  734. }
  735. }
  736. }
  737. $files = array();
  738. try {
  739. $dir = new DirectoryIterator($path);
  740. } catch(Exception $e) {
  741. $this->trackException($e, false, true);
  742. $dir = null;
  743. }
  744. if($dir) foreach($dir as $file) {
  745. if($file->isDot()) continue;
  746. $filename = $file->getFilename();
  747. $pathname = $file->getPathname();
  748. if(DIRECTORY_SEPARATOR != '/') {
  749. $pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);
  750. $filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
  751. }
  752. if(strpos($pathname, '/.') !== false) {
  753. $pos = strrpos(rtrim($pathname, '/'), '/');
  754. if($pathname[$pos+1] == '.') continue; // skip hidden files and dirs
  755. }
  756. // if it's a directory with a .module file in it named the same as the dir, then descend into it
  757. if($file->isDir() && ($level < 1 || (is_file("$pathname/$filename.module") || is_file("$pathname/$filename.module.php")))) {
  758. $files = array_merge($files, $this->findModuleFiles($pathname, false, $level + 1));
  759. }
  760. // if the filename doesn't end with .module or .module.php, then stop and move onto the next
  761. if(!strpos($filename, '.module')) continue;
  762. if(substr($filename, -7) !== '.module' && substr($filename, -11) !== '.module.php') {
  763. continue;
  764. }
  765. $files[] = str_replace($startPath, '', $pathname);
  766. }
  767. if($level == 0 && $dir !== null) {
  768. if($cache) $cache->save($cacheName, implode("\n", $files), WireCache::expireNever);
  769. }
  770. return $files;
  771. }
  772. /**
  773. * Setup entries in config->urls and config->paths for the given module
  774. *
  775. * @param string $moduleName
  776. * @param string $path
  777. *
  778. */
  779. protected function setConfigPaths($moduleName, $path) {
  780. $config = $this->wire('config');
  781. $path = rtrim($path, '/');
  782. $path = substr($path, strlen($config->paths->root)) . '/';
  783. $config->paths->set($moduleName, $path);
  784. $config->urls->set($moduleName, $path);
  785. }
  786. /**
  787. * Get the requsted Module or NULL if it doesn't exist.
  788. *
  789. * If the module is a ModulePlaceholder, then it will be converted to the real module (included, instantiated, init'd) .
  790. * If the module is not installed, but is installable, it will be installed, instantiated, and init'd.
  791. * This method is the only one guaranteed to return a real [non-placeholder] module.
  792. *
  793. * @param string|int $key Module className or database ID
  794. * @return Module|Inputfield|Fieldtype|Process|Textformatter|null
  795. * @throws WirePermissionException If module requires a particular permission the user does not have
  796. *
  797. */
  798. public function get($key) {
  799. return $this->getModule($key);
  800. }
  801. /**
  802. * Attempt to find a substitute for moduleName and return module if found or null if not
  803. *
  804. * @param $moduleName
  805. * @param array $options See getModule() options
  806. * @return Module|null
  807. *
  808. */
  809. protected function getSubstituteModule($moduleName, array $options = array()) {
  810. $module = null;
  811. $options['noSubstitute'] = true; // prevent recursion
  812. while(isset($this->substitutes[$moduleName]) && !$module) {
  813. $substituteName = $this->substitutes[$moduleName];
  814. $module = $this->getModule($substituteName, $options);
  815. if(!$module) $moduleName = $substituteName;
  816. }
  817. return $module;
  818. }
  819. /**
  820. * Get the requested Module or NULL if it doesn't exist + specify one or more options
  821. *
  822. * @param string|int $key Module className or database ID
  823. * @param array $options Optional settings to change load behavior:
  824. * - noPermissionCheck: Specify true to disable module permission checks (and resulting exception).
  825. * - noInstall: Specify true to prevent a non-installed module from installing from this request.
  826. * - noInit: Specify true to prevent the module from being initialized.
  827. * - noSubstitute: Specify true to prevent inclusion of a substitute module.
  828. * @return Module|null
  829. * @throws WirePermissionException If module requires a particular permission the user does not have
  830. *
  831. */
  832. public function getModule($key, array $options = array()) {
  833. if(empty($key)) return null;
  834. $module = null;
  835. $needsInit = false;
  836. // check for optional module ID and convert to classname if found
  837. if(ctype_digit("$key")) {
  838. if(!$key = array_search($key, $this->moduleIDs)) return null;
  839. }
  840. $module = parent::get($key);
  841. if(!$module && empty($options['noSubstitute'])) {
  842. if($this->isInstallable($key) && empty($options['noInstall'])) {
  843. // module is on file system and may be installed, no need to substitute
  844. } else {
  845. $module = $this->getSubstituteModule($key, $options);
  846. if($module) return $module; // returned module is ready to use
  847. }
  848. }
  849. if($module) {
  850. // check if it's a placeholder, and if it is then include/instantiate/init the real module
  851. // OR check if it's non-singular, so that a new instance is created
  852. if($module instanceof ModulePlaceholder || !$this->isSingular($module)) {
  853. $placeholder = $module;
  854. $class = $this->getModuleClass($placeholder);
  855. if($module instanceof ModulePlaceholder) $this->includeModule($module);
  856. $module = $this->newModule($class);
  857. // if singular, save the instance so it can be used in later calls
  858. if($this->isSingular($module)) $this->set($key, $module);
  859. $needsInit = true;
  860. }
  861. } else if(empty($options['noInstall']) && array_key_exists($key, $this->getInstallable())) {
  862. // check if the request is for an uninstalled module
  863. // if so, install it and return it
  864. $module = $this->install($key);
  865. $needsInit = true;
  866. }
  867. if($module && empty($options['noPermissionCheck'])) {
  868. if(!$this->hasPermission($module, $this->wire('user'), $this->wire('page'))) {
  869. throw new WirePermissionException($this->_('You do not have permission to execute this module') . ' - ' . $class);
  870. }
  871. }
  872. // skip autoload modules because they have already been initialized in the load() method
  873. // unless they were just installed, in which case we need do init now
  874. if($module && $needsInit) {
  875. // if the module is configurable, then load it's config data
  876. // and set values for each before initializing the module
  877. // $this->setModuleConfigData($module);
  878. // if(method_exists($module, 'init')) $module->init();
  879. if(empty($options['noInit'])) $this->initModule($module, false);
  880. }
  881. return $module;
  882. }
  883. /**
  884. * Check if user has permission for given module
  885. *
  886. * @param string|object $moduleName
  887. * @param User $user Optionally specify different user to consider than current.
  888. * @param Page $page Optionally specify different page to consider than current.
  889. * @param bool $strict If module specifies no permission settings, assume no permission.
  890. * Default (false) is to assume permission when module doesn't say anything about it.
  891. * Process modules (for instance) generally assume no permission when it isn't specifically defined
  892. * (though this method doesn't get involved in that, leaving you to specify $strict instead).
  893. *
  894. * @return bool
  895. *
  896. */
  897. public function hasPermission($moduleName, User $user = null, Page $page = null, $strict = false) {
  898. $info = $this->getModuleInfo($moduleName);
  899. if(empty($info['permission']) && empty($info['permissionMethod'])) return $strict ? false : true;
  900. if(is_null($user)) $user = $this->wire('user');
  901. if($user && $user->isSuperuser()) return true;
  902. if(is_object($moduleName)) $moduleName = $moduleName->className();
  903. if(!empty($info['permission'])) {
  904. if(!$user->hasPermission($info['permission'])) return false;
  905. }
  906. if(!empty($info['permissionMethod'])) {
  907. // module specifies a static method to call for permission
  908. if(is_null($page)) $page = $this->wire('page');
  909. $data = array(
  910. 'wire' => $this->wire(),
  911. 'page' => $page,
  912. 'user' => $user,
  913. 'info' => $info,
  914. );
  915. $method = $info['permissionMethod'];
  916. $this->includeModule($moduleName);
  917. return $moduleName::$method($data);
  918. }
  919. return true;
  920. }
  921. /**
  922. * Get the requested module and reset cache + install it if necessary.
  923. *
  924. * This is exactly the same as get() except that this one will rebuild the modules cache if
  925. * it doesn't find the module at first. If the module is on the file system, this
  926. * one will return it in some instances that a regular get() can't.
  927. *
  928. * @param string|int $key Module className or database ID
  929. * @return Module|null
  930. *
  931. */
  932. public function getInstall($key) {
  933. $module = $this->get($key);
  934. if(!$module) {
  935. $this->resetCache();
  936. $module = $this->getModule($key);
  937. }
  938. return $module;
  939. }
  940. /**
  941. * Include the file for a given module, but don't instantiate it
  942. *
  943. * @param ModulePlaceholder|Module|string Expects a ModulePlaceholder or className
  944. * @return bool true on success
  945. *
  946. */
  947. public function includeModule($module) {
  948. $className = '';
  949. if(is_object($module)) $className = $module->className();
  950. else if(is_string($module)) $className = $module;
  951. if($className && class_exists($className, false)) return true; // already included
  952. // attempt to retrieve module
  953. if(is_string($module)) $module = parent::get($module);
  954. if(!$module && $className) {
  955. // unable to retrieve module, must be an uninstalled module
  956. $file = $this->getModuleFile($className);
  957. if($file) {
  958. @include_once($file);
  959. if(class_exists($className, false)) return true;
  960. }
  961. }
  962. if(!$module) return false;
  963. if($module instanceof ModulePlaceholder) {
  964. include_once($module->file);
  965. } else {
  966. // it's already been included, no doubt
  967. }
  968. return true;
  969. }
  970. /**
  971. * Find modules based on a selector string and ensure any ModulePlaceholders are loaded in the returned result
  972. *
  973. * @param string $selector
  974. * @return Modules
  975. *
  976. */
  977. public function find($selector) {
  978. $a = parent::find($selector);
  979. if($a) {
  980. foreach($a as $key => $value) {
  981. $a[$key] = $this->get($value->className());
  982. }
  983. }
  984. return $a;
  985. }
  986. /**
  987. * Find modules matching the given prefix
  988. *
  989. * @param string $prefix Specify prefix, i.e. Process, Fieldtype, Inputfield, etc.
  990. * @param bool $instantiate Specify true to return Module instances, or false to return class names (default=false)
  991. * @return array of module class names or Module objects. In either case, array indexes are class names.
  992. *
  993. */
  994. public function findByPrefix($prefix, $instantiate = false) {
  995. $results = array();
  996. foreach($this as $key => $value) {
  997. $className = $value->className();
  998. if(strpos($className, $prefix) !== 0) continue;
  999. if($instantiate) {
  1000. $results[$className] = $this->get($className);
  1001. } else {
  1002. $results[$className] = $className;
  1003. }
  1004. }
  1005. return $results;
  1006. }
  1007. /**
  1008. * Get an array of all modules that aren't currently installed
  1009. *
  1010. * @return array Array of elements with $className => $pathname
  1011. *
  1012. */
  1013. public function getInstallable() {
  1014. return $this->installable;
  1015. }
  1016. /**
  1017. * Is the given class name installed?
  1018. *
  1019. * @param string $class Just a ModuleClassName, or optionally: ModuleClassName>=1.2.3 (operator and version)
  1020. * @return bool
  1021. *
  1022. */
  1023. public function isInstalled($class) {
  1024. if(is_object($class)) $class = $this->getModuleClass($class);
  1025. $operator = null;
  1026. $requiredVersion = null;
  1027. $currentVersion = null;
  1028. if(!ctype_alnum($class)) {
  1029. // class has something other than just a classnae, likely operator + version
  1030. if(preg_match('/^([a-zA-Z0-9_]+)\s*([<>=!]+)\s*([\d.]+)$/', $class, $matches)) {
  1031. $class = $matches[1];
  1032. $operator = $matches[2];
  1033. $requiredVersion = $matches[3];
  1034. }
  1035. }
  1036. if($class === 'PHP' || $class === 'ProcessWire') {
  1037. $installed = true;
  1038. if(!is_null($requiredVersion)) {
  1039. $currentVersion = $class === 'PHP' ? PHP_VERSION : $this->wire('config')->version;
  1040. }
  1041. } else {
  1042. $installed = parent::get($class) !== null;
  1043. if($installed && !is_null($requiredVersion)) {
  1044. $info = $this->getModuleInfo($class);
  1045. $currentVersion = $info['version'];
  1046. }
  1047. }
  1048. if($installed && !is_null($currentVersion)) {
  1049. $installed = $this->versionCompare($currentVersion, $requiredVersion, $operator);
  1050. }
  1051. return $installed;
  1052. }
  1053. /**
  1054. * Is the given class name not installed?
  1055. *
  1056. * @param string $class
  1057. * @param bool $now Is module installable RIGHT NOW? This makes it check that all dependencies are already fulfilled (default=false)
  1058. * @return bool
  1059. *
  1060. */
  1061. public function isInstallable($class, $now = false) {
  1062. $installable = array_key_exists($class, $this->installable);
  1063. if(!$installable) return false;
  1064. if($now) {
  1065. $requires = $this->getRequiresForInstall($class);
  1066. if(count($requires)) return false;
  1067. }
  1068. return $installable;
  1069. }
  1070. /**
  1071. * Install the given class name
  1072. *
  1073. * @param string $class
  1074. * @param array|bool $options Associative array of:
  1075. * - dependencies (boolean, default=true): When true, dependencies will also be installed where possible. Specify false to prevent installation of uninstalled modules.
  1076. * - resetCache (boolean, default=true): When true, module caches will be reset after installation.
  1077. * @return null|Module Returns null if unable to install, or instantiated Module object if successfully installed.
  1078. * @throws WireException
  1079. *
  1080. */
  1081. public function ___install($class, $options = array()) {
  1082. $defaults = array(
  1083. 'dependencies' => true,
  1084. 'resetCache' => true,
  1085. );
  1086. if(is_bool($options)) {
  1087. // dependencies argument allowed instead of $options, for backwards compatibility
  1088. $dependencies = $options;
  1089. $options = array('dependencies' => $dependencies);
  1090. }
  1091. $options = array_merge($defaults, $options);
  1092. $dependencyOptions = $options;
  1093. $dependencyOptions['resetCache'] = false;
  1094. if(!$this->isInstallable($class)) return null;
  1095. $requires = $this->getRequiresForInstall($class);
  1096. if(count($requires)) {
  1097. $error = '';
  1098. $installable = false;
  1099. if($options['dependencies']) {
  1100. $installable = true;
  1101. foreach($requires as $requiresModule) {
  1102. if(!$this->isInstallable($requiresModule)) $installable = false;
  1103. }
  1104. if($installable) {
  1105. foreach($requires as $requiresModule) {
  1106. if(!$this->install($requiresModule, $dependencyOptions)) {
  1107. $error = $this->_('Unable to install required module') . " - $requiresModule. ";
  1108. $installable = false;
  1109. break;
  1110. }
  1111. }
  1112. }
  1113. }
  1114. if(!$installable) {
  1115. throw new WireException($error . "Module $class requires: " . implode(", ", $requires));
  1116. }
  1117. }
  1118. $languages = $this->wire('languages');
  1119. if($languages) $languages->setDefault();
  1120. $pathname = $this->installable[$class];
  1121. require_once($pathname);
  1122. $this->setConfigPaths($class, dirname($pathname));
  1123. $module = $this->newModule($class);
  1124. $flags = 0;
  1125. $database = $this->wire('database');
  1126. $moduleID = 0;
  1127. if($this->isSingular($module)) $flags = $flags | self::flagsSingular;
  1128. if($this->isAutoload($module)) $flags = $flags | self::flagsAutoload;
  1129. $sql = "INSERT INTO modules SET class=:class, flags=:flags, data=''";
  1130. if($this->wire('config')->systemVersion >=7) $sql .= ", created=NOW()";
  1131. $query = $database->prepare($sql, "modules.install($class)");
  1132. $query->bindValue(":class", $class, PDO::PARAM_STR);
  1133. $query->bindValue(":flags", $flags, PDO::PARAM_INT);
  1134. try {
  1135. if($query->execute()) $moduleID = (int) $database->lastInsertId();
  1136. } catch(Exception $e) {
  1137. if($languages) $languages->unsetDefault();
  1138. $this->trackException($e, false, true);
  1139. return null;
  1140. }
  1141. $this->moduleIDs[$class] = $moduleID;
  1142. $this->add($module);
  1143. unset($this->installable[$class]);
  1144. // note: the module's install is called here because it may need to know it's module ID for installation of permissions, etc.
  1145. if(method_exists($module, '___install') || method_exists($module, 'install')) {
  1146. try {
  1147. $module->install();
  1148. } catch(Exception $e) {
  1149. // remove the module from the modules table if the install failed
  1150. $moduleID = (int) $moduleID;
  1151. $error = "Unable to install module '$class': " . $e->getMessage();
  1152. $ee = null;
  1153. try {
  1154. $query = $database->prepare('DELETE FROM modules WHERE id=:id LIMIT 1'); // QA
  1155. $query->bindValue(":id", $moduleID, PDO::PARAM_INT);
  1156. $query->execute();
  1157. } catch(Exception $ee) {
  1158. $this->trackException($e, false, $error)->trackException($ee, true);
  1159. }
  1160. if($languages) $languages->unsetDefault();
  1161. if(is_null($ee)) $this->trackException($e, false, $error);
  1162. return null;
  1163. }
  1164. }
  1165. $info = $this->getModuleInfoVerbose($class, array('noCache' => true));
  1166. // if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
  1167. foreach($info['permissions'] as $name => $title) {
  1168. $name = $this->wire('sanitizer')->pageName($name);
  1169. if(ctype_digit("$name") || empty($name)) continue; // permission name not valid
  1170. $permission = $this->wire('permissions')->get($name);
  1171. if($permission->id) continue; // permision already there
  1172. try {
  1173. $permission = $this->wire('permissions')->add($name);
  1174. $permission->title = $title;
  1175. $this->wire('permissions')->save($permission);
  1176. if($languages) $languages->unsetDefault();
  1177. $this->message(sprintf($this->_('Added Permission: %s'), $permission->name));
  1178. } catch(Exception $e) {
  1179. if($languages) $languages->unsetDefault();
  1180. $error = sprintf($this->_('Error adding permission: %s'), $name);
  1181. $this->trackException($e, false, $error);
  1182. }
  1183. }
  1184. // check if there are any modules in 'installs' that this module didn't handle installation of, and install them
  1185. $label = $this->_('Module Auto Install');
  1186. foreach($info['installs'] as $name) {
  1187. if(!$this->isInstalled($name)) {
  1188. try {
  1189. $this->install($name, $dependencyOptions);
  1190. $this->message("$label: $name");
  1191. } catch(Exception $e) {
  1192. $error = "$label: $name - " . $e->getMessage();
  1193. $this->trackException($e, false, $error);
  1194. }
  1195. }
  1196. }
  1197. $this->log("Installed module '$module'");
  1198. if($languages) $languages->unsetDefault();
  1199. if($options['resetCache']) $this->clearModuleInfoCache();
  1200. return $module;
  1201. }
  1202. /**
  1203. * Returns whether the module can be uninstalled
  1204. *
  1205. * @param string|Module $class
  1206. * @param bool $returnReason If true, the reason why it can't be uninstalled with be returned rather than boolean false.
  1207. * @return bool|string
  1208. *
  1209. */
  1210. public function isUninstallable($class, $returnReason = false) {
  1211. $reason = '';
  1212. $reason1 = "Module is not already installed";
  1213. $class = $this->getModuleClass($class);
  1214. if(!$this->isInstalled($class)) {
  1215. $reason = $reason1;
  1216. } else {
  1217. $this->includeModule($class);
  1218. if(!class_exists($class, false)) $reason = $reason1;
  1219. }
  1220. if(!$reason) {
  1221. // if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable
  1222. $info = $this->getModuleInfo($class);
  1223. if(!empty($info['permanent'])) {
  1224. $reason = "Module is permanent";
  1225. } else {
  1226. $dependents = $this->getRequiresForUninstall($class);
  1227. if(count($dependents)) $reason = "Module is required by other modules that must be removed first";
  1228. }
  1229. if(!$reason && in_array('Fieldtype', class_parents($class))) {
  1230. foreach(wire('fields') as $field) {
  1231. $fieldtype = get_class($field->type);
  1232. if($fieldtype == $class) {
  1233. $reason = "This module is a Fieldtype currently in use by one or more fields";
  1234. break;
  1235. }
  1236. }
  1237. }
  1238. }
  1239. if($returnReason && $reason) return $reason;
  1240. return $reason ? false : true;
  1241. }
  1242. /**
  1243. * Returns whether the module can be deleted (have it's files physically removed)
  1244. *
  1245. * @param string|Module $class
  1246. * @param bool $returnReason If true, the reason why it can't be removed will be returned rather than boolean false.
  1247. * @return bool|string
  1248. *
  1249. */
  1250. public function isDeleteable($class, $returnReason = false) {
  1251. $reason = '';
  1252. $class = $this->getModuleClass($class);
  1253. $filename = isset($this->installable[$class]) ? $this->installable[$class] : null;
  1254. $dirname = dirname($filename);
  1255. if(empty($filename) || $this->isInstalled($class)) {
  1256. $reason = "Module must be uninstalled before it can be deleted.";
  1257. } else if(is_link($filename) || is_link($dirname) || is_link(dirname($dirname))) {
  1258. $reason = "Module is linked to another location";
  1259. } else if(!is_file($filename)) {
  1260. $reason = "Module file does not exist";
  1261. } else if(strpos($filename, $this->paths[0]) === 0) {
  1262. $reason = "Core modules may not be deleted.";
  1263. } else if(!is_writable($filename)) {
  1264. $reason = "We have no write access to the module file, it must be removed manually.";
  1265. }
  1266. if($returnReason && $reason) return $reason;
  1267. return $reason ? false : true;
  1268. }
  1269. /**
  1270. * Delete the given module, physically removing its files
  1271. *
  1272. * @param string $class
  1273. * @return bool|int
  1274. * @throws WireException If module can't be deleted, exception will be thrown containing reason.
  1275. *
  1276. */
  1277. public function ___delete($class) {
  1278. $class = $this->getModuleClass($class);
  1279. $reason = $this->isDeleteable($class, true);
  1280. if($reason !== true) throw new WireException($reason);
  1281. $filename = $this->installable[$class];
  1282. $basename = basename($filename);
  1283. // double check that $class is consistent with the actual $basename
  1284. if($basename === "$class.module" || $basename === "$class.module.php") {
  1285. // good, this is consistent with the format we require
  1286. } else {
  1287. throw new WireException("Unrecognized module filename format");
  1288. }
  1289. // now determine if module is the owner of the directory it exists in
  1290. // this is the case if the module class name is the same as the directory name
  1291. $path = dirname($filename); // full path to directory, i.e. .../site/modules/ProcessHello
  1292. $name = basename($path); // just name of directory that module is, i.e. ProcessHello
  1293. $parentPath = dirname($path); // full path to parent directory, i.e. ../site/modules
  1294. $backupPath = $parentPath . "/.$name"; // backup path, in case module is backed up
  1295. // first check that we are still in the /site/modules/ (or another non core modules path)
  1296. $inPath = false; // is module somewhere beneath /site/modules/ ?
  1297. $inRoot = false; // is module in /site/modules/ root? i.e. /site/modules/ModuleName.module
  1298. foreach($this->paths as $key => $modulesPath) {
  1299. if($key === 0) continue; // skip core modules path
  1300. if(strpos("$parentPath/", $modulesPath) === 0) $inPath = true;
  1301. if($modulesPath === $path) $inRoot = true;
  1302. }
  1303. $basename = basename($basename, '.php');
  1304. $basename = basename($basename, '.module');
  1305. $files = array(
  1306. "$basename.module",
  1307. "$basename.module.php",
  1308. "$basename.info.php",
  1309. "$basename.info.json",
  1310. "$basename.config.php",
  1311. "{$basename}Config.php",
  1312. );
  1313. if($inPath) {
  1314. // module is in /site/modules/[ModuleName]/
  1315. $numOtherModules = 0; // num modules in dir other than this one
  1316. $numLinks = 0; // number of symbolic links
  1317. $dirs = array("$path/");
  1318. do {
  1319. $dir = array_shift($dirs);
  1320. $this->message("Scanning: $dir", Notice::debug);
  1321. foreach(new DirectoryIterator($dir) as $file) {
  1322. if($file->isDot()) continue;
  1323. if($file->isLink()) {
  1324. $numLinks++;
  1325. continue;
  1326. }
  1327. if($file->isDir()) {
  1328. $dirs[] = $file->getPathname();
  1329. continue;
  1330. }
  1331. if(in_array($file->getBasename(), $files)) continue; // skip known files
  1332. if(strpos($file->getBasename(), '.module') && preg_match('{(\.module|\.module\.php)$}', $file->getBasename())) {
  1333. // another module exists in this dir, so we don't want to delete that
  1334. $numOtherModules++;
  1335. }
  1336. if(preg_match('{^(' . $basename . '\.[-_.a-zA-Z0-9]+)$}', $file->getBasename(), $matches)) {
  1337. // keep track of potentially related files in case we have to delete them individually
  1338. $files[] = $matches[1];
  1339. }
  1340. }
  1341. } while(count($dirs));
  1342. if(!$inRoot && !$numOtherModules && !$numLinks) {
  1343. // the modulePath had no other modules or directories in it, so we can delete it entirely
  1344. $success = wireRmdir($path, true);
  1345. if($success) {
  1346. $this->message("Removed directory: $path", Notice::debug);
  1347. if(is_dir($backupPath)) {
  1348. if(wireRmdir($backupPath, true)) $this->message("Removed directory: $backupPath", Notice::debug);
  1349. }
  1350. $files = array();
  1351. } else {
  1352. $this->error("Failed to remove directory: $path", Notice::debug);
  1353. }
  1354. }
  1355. }
  1356. // remove module files individually
  1357. foreach($files as $file) {
  1358. $file = "$path/$file";
  1359. if(!file_exists($file)) continue;
  1360. if(unlink($file)) {
  1361. $this->message("Removed file: $file", Notice::debug);
  1362. } else {
  1363. $this->error("Unable to remove file: $file", Notice::debug);
  1364. }
  1365. }
  1366. if($success) $this->log("Deleted module '$class'");
  1367. else $this->error("Failed to delete module '$class'");
  1368. return $success;
  1369. }
  1370. /**
  1371. * Uninstall the given class name
  1372. *
  1373. * @param string $class
  1374. * @return bool
  1375. * @throws WireException
  1376. *
  1377. */
  1378. public function ___uninstall($class) {
  1379. $class = $this->getModuleClass($class);
  1380. $reason = $this->isUninstallable($class, true);
  1381. if($reason !== true) {
  1382. // throw new WireException("$class - Can't Uninstall - $reason");
  1383. return false;
  1384. }
  1385. // check if there are any modules still installed that this one says it is responsible for installing
  1386. foreach($this->getUninstalls($class) as $name) {
  1387. // catch uninstall exceptions at this point since original module has already been uninstalled
  1388. $label = $this->_('Module Auto Uninstall');
  1389. try {
  1390. $this->uninstall($name);
  1391. $this->message("$label: $name");
  1392. } catch(Exception $e) {
  1393. $error = "$label: $name - " . $e->getMessage();
  1394. $this->trackException($e, false, $error);
  1395. }
  1396. }
  1397. $info = $this->getModuleInfoVerbose($class);
  1398. $module = $this->getModule($class, array(
  1399. 'noPermissionCheck' => true,
  1400. 'noInstall' => true,
  1401. // 'noInit' => true
  1402. ));
  1403. if(!$module) return false;
  1404. // remove all hooks attached to this module
  1405. $hooks = $module instanceof Wire ? $module->getHooks() : array();
  1406. foreach($hooks as $hook) {
  1407. if($hook['method'] == 'uninstall') continue;
  1408. $this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
  1409. $module->removeHook($hook['id']);
  1410. }
  1411. // remove all hooks attached to other ProcessWire objects
  1412. $hooks = array_merge(wire()->getHooks('*'), Wire::$allLocalHooks);
  1413. foreach($hooks as $hook) {
  1414. $toClass = get_class($hook['toObject']);
  1415. $toMethod = $hook['toMethod'];
  1416. if($class === $toClass && $toMethod != 'uninstall') {
  1417. $hook['toObject']->removeHook($hook['id']);
  1418. $this->message("Removed hook $class => " . $hook['options']['fromClass'] . " $hook[method]", Notice::debug);
  1419. }
  1420. }
  1421. if(method_exists($module, '___uninstall') || method_exists($module, 'uninstall')) {
  1422. // note module's uninstall method may throw an exception to abort the uninstall
  1423. $module->uninstall();
  1424. }
  1425. $database = $this->wire('database');
  1426. $query = $database->prepare('DELETE FROM modules WHERE class=:class LIMIT 1'); // QA
  1427. $query->bindValue(":class", $class, PDO::PARAM_STR);
  1428. $query->execute();
  1429. // add back to the installable list
  1430. if(class_exists("ReflectionClass")) {
  1431. $reflector = new ReflectionClass($class);
  1432. $this->installable[$class] = $reflector->getFileName();
  1433. }
  1434. unset($this->moduleIDs[$class]);
  1435. $this->remove($module);
  1436. // delete permissions installed by this module
  1437. if(isset($info['permissions']) && is_array($info['permissions'])) {
  1438. foreach($info['permissions'] as $name => $title) {
  1439. $name = $this->wire('sanitizer')->pageName($name);
  1440. if(ctype_digit("$name") || empty($name)) continue;
  1441. $permission = $this->wire('permissions')->get($name);
  1442. if(!$permission->id) continue;
  1443. try {
  1444. $this->wire('permissions')->delete($permission);
  1445. $this->message(sprintf($this->_('Deleted Permission: %s'), $name));
  1446. } catch(Exception $e) {
  1447. $error = sprintf($this->_('Error deleting permission: %s'), $name);
  1448. $this->trackException($e, false, $error);
  1449. }
  1450. }
  1451. }
  1452. $this->log("Uninstalled module '$class'");
  1453. $this->resetCache();
  1454. return true;
  1455. }
  1456. /**
  1457. * Get flags for the given module
  1458. *
  1459. * @param int|string|Module $class Module to add flag to
  1460. * @return int|false Returns integer flags on success, or boolean false on fail
  1461. *
  1462. */
  1463. public function getFlags($class) {
  1464. $id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
  1465. if(isset($this->moduleFlags[$id])) return $this->moduleFlags[$id];
  1466. if(!$id) return false;
  1467. $query = $this->wire('database')->prepare('SELECT flags FROM modules WHERE id=:id');
  1468. $query->bindValue(':id', $id, PDO::PARAM_INT);
  1469. $query->execute();
  1470. if(!$query->rowCount()) return false;
  1471. list($flags) = $query->fetch(PDO::FETCH_NUM);
  1472. $flags = (int) $flags;
  1473. $this->moduleFlags[$id] = $flags;
  1474. return $flags;
  1475. }
  1476. /**
  1477. * Set module flags
  1478. *
  1479. * @param $class
  1480. * @param $flags
  1481. * @return bool
  1482. *
  1483. */
  1484. public function setFlags($class, $flags) {
  1485. $flags = (int) $flags;
  1486. $id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
  1487. if(!$id) return false;
  1488. if($this->moduleFlags[$id] === $flags) return true;
  1489. $query = $this->wire('database')->prepare('UPDATE modules SET flags=:flags WHERE id=:id');
  1490. $query->bindValue(':flags', $flags);
  1491. $query->bindValue(':id', $id);
  1492. if($this->debug) $this->message("setFlags(" . $this->getModuleClass($class) . ", " . $this->moduleFlags[$id] . " => $flags)");
  1493. $this->moduleFlags[$id] = $flags;
  1494. return $query->execute();
  1495. }
  1496. /**
  1497. * Add or remove a flag from a module
  1498. *
  1499. * @param int|string|Module $class Module to add flag to
  1500. * @param int Flag to add (see flags* constants)
  1501. * @param bool $add Specify true to add the flag or false to remove it
  1502. * @return bool True on success, false on fail
  1503. *
  1504. */
  1505. public function setFlag($class, $flag, $add = true) {
  1506. $id = ctype_digit("$class") ? (int) $class : $this->getModuleID($class);
  1507. if(!$id) return false;
  1508. $flag = (int) $flag;
  1509. if(!$flag) return false;
  1510. $flags = $this->getFlags($id);
  1511. if($add) {
  1512. if($flags & $flag) return true; // already has the flag
  1513. $flags = $flags | $flag;
  1514. } else {
  1515. if(!($flags & $flag)) return true; // doesn't already have the flag
  1516. $flags = $flags & ~$flag;
  1517. }
  1518. $this->setFlags($id, $flags);
  1519. return true;
  1520. }
  1521. /**
  1522. * Return an array of other module class names that are uninstalled when the given one is
  1523. *
  1524. * The opposite of this function is found in the getModuleInfo array property 'installs'.
  1525. * Note that 'installs' and uninstalls may be different, as only modules in the 'installs' list
  1526. * that indicate 'requires' for the installer module will be uninstalled.
  1527. *
  1528. * @param $class
  1529. * @return array
  1530. *
  1531. */
  1532. public function getUninstalls($class) {
  1533. $uninstalls = array();
  1534. $class = $this->getModuleClass($class);
  1535. if(!$class) return $uninstalls;
  1536. $info = $this->getModuleInfoVerbose($class);
  1537. // check if there are any modules still installed that this one says it is responsible for installing
  1538. foreach($info['installs'] as $name) {
  1539. // if module isn't installed, then great
  1540. if(!$this->isInstalled($name)) continue;
  1541. // if an 'installs' module doesn't indicate that it requires this one, then leave it installed
  1542. $i = $this->getModuleInfo($name);
  1543. if(!in_array($class, $i['requires'])) continue;
  1544. // add it to the uninstalls array
  1545. $uninstalls[] = $name;
  1546. }
  1547. return $uninstalls;
  1548. }
  1549. /**
  1550. * Returns the database ID of a given module class, or 0 if not found
  1551. *
  1552. * @param string|Module $class
  1553. * @return int
  1554. *
  1555. */
  1556. public function getModuleID($class) {
  1557. $id = 0;
  1558. if(is_object($class)) {
  1559. if($class instanceof Module) {
  1560. $class = $this->getModuleClass($class);
  1561. } else {
  1562. // Class is not a module
  1563. return $id;
  1564. }
  1565. }
  1566. if(isset($this->moduleIDs[$class])) {
  1567. $id = (int) $this->moduleIDs[$class];
  1568. } else foreach($this->moduleInfoCache as $key => $info) {
  1569. if($info['name'] == $class) {
  1570. $id = (int) $key;
  1571. break;
  1572. }
  1573. }
  1574. return $id;
  1575. }
  1576. /**
  1577. * Returns the module's class name.
  1578. *
  1579. * Given a numeric database ID, returns the associated module class name or false if it doesn't exist
  1580. *
  1581. * Given a Module or ModulePlaceholder instance, returns the Module's class name.
  1582. *
  1583. * If the module has a className() method then it uses that rather than PHP's get_class().
  1584. * This is important because of placeholder modules. For example, get_class would return
  1585. * 'ModulePlaceholder' rather than the correct className for a Module.
  1586. *
  1587. * @param string|int|Module
  1588. * @return string|bool The Module's class name or false if not found.
  1589. * Note that 'false' is only possible if you give this method a non-Module, or an integer ID
  1590. * that doesn't correspond to a module ID.
  1591. *
  1592. */
  1593. public function getModuleClass($module) {
  1594. if($module instanceof Module) {
  1595. if(method_exists($module, 'className')) return $module->className();
  1596. return get_class($module);
  1597. } else if(is_int($module) || ctype_digit("$module")) {
  1598. return array_search((int) $module, $this->moduleIDs);
  1599. } else if(is_string($module)) {
  1600. // remove extensions if they were included in the module name
  1601. if(strpos($module, '.') !== false) $module = basename(basename($module, '.php'), '.module');
  1602. if(array_key_exists($module, $this->moduleIDs)) return $module;
  1603. if(array_key_exists($module, $this->installable)) return $module;
  1604. }
  1605. return false;
  1606. }
  1607. /**
  1608. * Retrieve module info from ModuleName.info.json or ModuleName.info.php
  1609. *
  1610. * @param $moduleName
  1611. * @return array
  1612. *
  1613. */
  1614. protected function getModuleInfoExternal($moduleName) {
  1615. // if($this->debug) $this->message("getModuleInfoExternal($moduleName)");
  1616. // ...attempt to load info by info file (Module.info.php or Module.info.json)
  1617. if(!empty($this->installable[$moduleName])) {
  1618. $path = dirname($this->installable[$moduleName]) . '/';
  1619. } else {
  1620. $path = $this->wire('config')->paths->$moduleName;
  1621. }
  1622. if(empty($path)) return array();
  1623. // module exists and has a dedicated path on the file system
  1624. // we will try to get info from a PHP or JSON info file
  1625. $filePHP = $path . "$moduleName.info.php";
  1626. $fileJSON = $path . "$moduleName.info.json";
  1627. $info = array();
  1628. if(file_exists($filePHP)) {
  1629. include($filePHP); // will populate $info automatically
  1630. if(!is_array($info) || !count($info)) $this->error("Invalid PHP module info file for $moduleName");
  1631. } else if(file_exists($fileJSON)) {
  1632. $info = file_get_contents($fileJSON);
  1633. $info = json_decode($info, true);
  1634. if(!$info) {
  1635. $info = array();
  1636. $this->error("Invalid JSON module info file for $moduleName");
  1637. }
  1638. }
  1639. return $info;
  1640. }
  1641. /**
  1642. * Retrieve module info from internal getModuleInfo function in the class
  1643. *
  1644. * @param $module
  1645. * @return array
  1646. *
  1647. */
  1648. protected function getModuleInfoInternal($module) {
  1649. // if($this->debug) $this->message("getModuleInfoInternal($module)");
  1650. $info = array();
  1651. if($module instanceof ModulePlaceholder) {
  1652. $this->includeModule($module);
  1653. $module = $module->className();
  1654. }
  1655. if($module instanceof Module) {
  1656. if(method_exists($module, 'getModuleInfo')) {
  1657. $info = $module::getModuleInfo();
  1658. }
  1659. } else if($module) {
  1660. if(is_string($module) && !class_exists($module)) $this->includeModule($module);
  1661. //if(method_exists($module, 'getModuleInfo')) {
  1662. if(is_callable("$module::getModuleInfo")) {
  1663. $info = call_user_func(array($module, 'getModuleInfo'));
  1664. }
  1665. }
  1666. return $info;
  1667. }
  1668. /**
  1669. * Pull module info directly from the module file's getModuleInfo without letting PHP parse it
  1670. *
  1671. * Useful for getting module info from modules that extend another module not already on the file system.
  1672. *
  1673. * @param $className
  1674. * @return array Only includes module info specified in the module file itself.
  1675. *
  1676. */
  1677. protected function getModuleInfoInternalSafe($className) {
  1678. // future addition
  1679. // load file, preg_split by /^\s*(public|private|protected)[^;{]+function\s*([^)]*)[^{]*{/
  1680. // isolate the one that starts has getModuleInfo in matches[1]
  1681. // parse data from matches[2]
  1682. }
  1683. /**
  1684. * Retrieve module info for system properties: PHP or ProcessWire
  1685. *
  1686. * @param $moduleName
  1687. * @return array
  1688. *
  1689. */
  1690. protected function getModuleInfoSystem($moduleName) {
  1691. $info = array();
  1692. if($moduleName === 'PHP') {
  1693. $info['id'] = 0;
  1694. $info['name'] = $moduleName;
  1695. $info['title'] = $moduleName;
  1696. $info['version'] = PHP_VERSION;
  1697. return $info;
  1698. } else if($moduleName === 'ProcessWire') {
  1699. $info['id'] = 0;
  1700. $info['name'] = $moduleName;
  1701. $info['title'] = $moduleName;
  1702. $info['version'] = $this->wire('config')->version;
  1703. $info['requiresVersions'] = array(
  1704. 'PHP' => array('>=', '5.3.8'),
  1705. 'PHP_modules' => array('=', 'PDO,mysqli'),
  1706. 'Apache_modules' => array('=', 'mod_rewrite'),
  1707. 'MySQL' => array('>=', '5.0.15'),
  1708. );
  1709. $info['requires'] = array_keys($info['requiresVersions']);
  1710. } else {
  1711. return array();
  1712. }
  1713. $info['versionStr'] = $info['version'];
  1714. return $info;
  1715. }
  1716. /**
  1717. * Returns the standard array of information for a Module
  1718. *
  1719. * @param string|Module|int $module May be class name, module instance, or module ID
  1720. * @param array $options Optional options to modify behavior of what gets returned
  1721. * - verbose: Makes the info also include summary, author, file, core, href, versionStr (they will be usually blank without this option specified)
  1722. * - noCache: prevents use of cache to retrieve the module info
  1723. * - noInclude: prevents include() of the module file, applicable only if it hasn't already been included
  1724. * @return array
  1725. * @throws WireException when a module exists but has no means of returning module info
  1726. * @todo move all getModuleInfo methods to their own ModuleInfo class and break this method down further.
  1727. *
  1728. */
  1729. public function getModuleInfo($module, array $options = array()) {
  1730. if(!isset($options['verbose'])) $options['verbose'] = false;
  1731. if(!isset($options['noCache'])) $options['noCache'] = false;
  1732. $info = array();
  1733. $moduleName = $this->getModuleClass($module);
  1734. $moduleID = (string) $this->getModuleID($module); // typecast to string for cache
  1735. $fromCache = false; // was the data loaded from cache?
  1736. static $infoTemplate = array(
  1737. // module database ID
  1738. 'id' => 0,
  1739. // module class name
  1740. 'name' => '',
  1741. // module title
  1742. 'title' => '',
  1743. // module version
  1744. 'version' => 0,
  1745. // module version (always formatted string)
  1746. 'versionStr' => '0.0.0',
  1747. // who authored the module? (included in 'verbose' mode only)
  1748. 'author' => '',
  1749. // summary of what this module does (included in 'verbose' mode only)
  1750. 'summary' => '',
  1751. // URL to module details (included in 'verbose' mode only)
  1752. 'href' => '',
  1753. // Optional name of icon representing this module (currently font-awesome icon names, excluding the "fa-" portion)
  1754. 'icon' => '',
  1755. // this method converts this to array of module names, regardless of how the module specifies it
  1756. 'requires' => array(),
  1757. // module name is key, value is array($operator, $version). Note 'requiresVersions' index is created by this function.
  1758. 'requiresVersions' => array(),
  1759. // array of module class names
  1760. 'installs' => array(),
  1761. // permission required to execute this module
  1762. 'permission' => '',
  1763. // permissions automatically installed/uninstalled with this module. array of ('permission-name' => 'Description')
  1764. 'permissions' => array(),
  1765. // true if module is autoload, false if not. null=unknown
  1766. 'autoload' => null,
  1767. // true if module is singular, false if not. null=unknown
  1768. 'singular' => null,
  1769. // unix-timestamp date/time module added to system (for uninstalled modules, it is the file date)
  1770. 'created' => 0,
  1771. // is the module currently installed? (boolean, or null when not determined)
  1772. 'installed' => null,
  1773. // this is set to true when the module is configurable, false when it's not, and null when it's not determined
  1774. 'configurable' => null,
  1775. // verbose mode only: this is set to the module filename (from PW installation root), false when it can't be found, null when it hasn't been determined
  1776. 'file' => null,
  1777. // verbose mode only: this is set to true when the module is a core module, false when it's not, and null when it's not determined
  1778. 'core' => null,
  1779. // other properties that may be present, but are optional, for Process modules:
  1780. // 'nav' => array(), // navigation definition: see Process.php
  1781. // 'useNavJSON' => bool, // whether the Process module provides JSON navigation
  1782. // 'page' => array(), // page to create for Process module: see Process.php
  1783. // 'permissionMethod' => string or callable // method to call to determine permission: see Process.php
  1784. );
  1785. if($module instanceof Module) {
  1786. // module is an instance
  1787. // $moduleName = method_exists($module, 'className') ? $module->className() : get_class($module);
  1788. // return from cache if available
  1789. if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) {
  1790. $info = $this->moduleInfoCache[$moduleID];
  1791. $fromCache = true;
  1792. } else {
  1793. $info = $this->getModuleInfoExternal($moduleName);
  1794. if(!count($info)) $info = $this->getModuleInfoInternal($module);
  1795. }
  1796. } else if($module == 'PHP' || $module == 'ProcessWire') {
  1797. // module is a system
  1798. $info = $this->getModuleInfoSystem($module);
  1799. return array_merge($infoTemplate, $info);
  1800. } else {
  1801. // module is a class name or ID
  1802. if(ctype_digit("$module")) $module = $moduleName;
  1803. // return from cache if available
  1804. if(empty($options['noCache']) && !empty($this->moduleInfoCache[$moduleID])) {
  1805. $info = $this->moduleInfoCache[$moduleID];
  1806. $fromCache = true;
  1807. } else if(empty($options['noCache']) && $moduleID == 0) {
  1808. // uninstalled module
  1809. if(!count($this->moduleInfoCacheUninstalled)) $this->loadModuleInfoCacheVerbose(true);
  1810. if(isset($this->moduleInfoCacheUninstalled[$moduleName])) {
  1811. $info = $this->moduleInfoCacheUninstalled[$moduleName];
  1812. $fromCache = true;
  1813. }
  1814. }
  1815. if(!$fromCache) {
  1816. if(class_exists($moduleName, false)) {
  1817. // module is already in memory, check external first, then internal
  1818. $info = $this->getModuleInfoExternal($moduleName);
  1819. if(!count($info)) $info = $this->getModuleInfoInternal($moduleName);
  1820. } else {
  1821. // module is not in memory, check external first, then internal
  1822. $info = $this->getModuleInfoExternal($moduleName);
  1823. if(!count($info)) {
  1824. if(isset($this->installable[$moduleName])) include_once($this->installable[$moduleName]);
  1825. // info not available externally, attempt to locate it interally
  1826. $info = $this->getModuleInfoInternal($moduleName);
  1827. }
  1828. }
  1829. }
  1830. }
  1831. if(!$fromCache && !count($info)) {
  1832. $info = $infoTemplate;
  1833. $info['title'] = $module;
  1834. $info['summary'] = 'Inactive';
  1835. $info['error'] = 'Unable to locate module';
  1836. return $info;
  1837. }
  1838. $info = array_merge($infoTemplate, $info);
  1839. $info['id'] = (int) $moduleID;
  1840. if($fromCache) {
  1841. if($options['verbose']) {
  1842. if(empty($this->moduleInfoCacheVerbose)) $this->loadModuleInfoCacheVerbose();
  1843. if(!empty($this->moduleInfoCacheVerbose[$moduleID])) {
  1844. $info = array_merge($info, $this->moduleInfoCacheVerbose[$moduleID]);
  1845. }
  1846. }
  1847. // populate defaults for properties omitted from cache
  1848. if(is_null($info['autoload'])) $info['autoload'] = false;
  1849. if(is_null($info['singular'])) $info['singular'] = false;
  1850. if(is_null($info['configurable'])) $info['configurable'] = false;
  1851. if(is_null($info['core'])) $info['core'] = false;
  1852. if(is_null($info['installed'])) $info['installed'] = true;
  1853. if(!empty($info['requiresVersions'])) $info['requires'] = array_keys($info['requiresVersions']);
  1854. if($moduleName == 'SystemUpdater') $info['configurable'] = 1; // fallback, just in case
  1855. // we skip everything else when module comes from cache since we can safely assume the checks below
  1856. // are already accounted for in the cached module info
  1857. } else {
  1858. // if $info[requires] or $info[installs] isn't already an array, make it one
  1859. if(!is_array($info['requires'])) {
  1860. $info['requires'] = str_replace(' ', '', $info['requires']); // remove whitespace
  1861. if(strpos($info['requires'], ',') !== false) $info['requires'] = explode(',', $info['requires']);
  1862. else $info['requires'] = array($info['requires']);
  1863. }
  1864. // populate requiresVersions
  1865. foreach($info['requires'] as $key => $class) {
  1866. if(!ctype_alnum($class)) {
  1867. // has a version string
  1868. list($class, $operator, $version) = $this->extractModuleOperatorVersion($class);
  1869. $info['requires'][$key] = $class; // convert to just class
  1870. } else {
  1871. // no version string
  1872. $operator = '>=';
  1873. $version = 0;
  1874. }
  1875. $info['requiresVersions'][$class] = array($operator, $version);
  1876. }
  1877. // what does it install?
  1878. if(!is_array($info['installs'])) {
  1879. $info['installs'] = str_replace(' ', '', $info['installs']); // remove whitespace
  1880. if(strpos($info['installs'], ',') !== false) $info['installs'] = explode(',', $info['installs']);
  1881. else $info['installs'] = array($info['installs']);
  1882. }
  1883. // misc
  1884. $info['versionStr'] = $this->formatVersion($info['version']); // versionStr
  1885. $info['name'] = $moduleName; // module name
  1886. $info['file'] = $this->getModuleFile($moduleName, false); // module file
  1887. if($info['file']) $info['core'] = strpos($info['file'], '/wire/modules/') !== false; // is it core?
  1888. // module configurable?
  1889. $configurable = $this->isConfigurableModule($moduleName, false);
  1890. if($configurable === true || is_int($configurable) && $configurable > 1) {
  1891. // configurable via ConfigurableModule interface
  1892. // true=static, 2=non-static, 3=non-static $data, 4=non-static wrap,
  1893. // 19=non-static getModuleConfigArray, 20=static getModuleConfigArray
  1894. $info['configurable'] = $configurable;
  1895. } else if($configurable) {
  1896. // configurable via external file: ModuleName.config.php or ModuleNameConfig.php file
  1897. $info['configurable'] = basename($configurable);
  1898. } else {
  1899. // not configurable
  1900. $info['configurable'] = false;
  1901. }
  1902. // created date
  1903. if(isset($this->createdDates[$moduleID])) $info['created'] = strtotime($this->createdDates[$moduleID]);
  1904. $info['installed'] = isset($this->installable[$moduleName]) ? false : true;
  1905. if(!$info['installed'] && !$info['created'] && isset($this->installable[$moduleName])) {
  1906. // uninstalled modules get their created date from the file or dir that they are in (whichever is newer)
  1907. $pathname = $this->installable[$moduleName];
  1908. $filemtime = (int) filemtime($pathname);
  1909. $dirname = dirname($pathname);
  1910. $dirmtime = substr($dirname, -7) == 'modules' || strpos($dirname, $this->paths[0]) !== false ? 0 : (int) filemtime($dirname);
  1911. $info['created'] = $dirmtime > $filemtime ? $dirmtime : $filemtime;
  1912. }
  1913. if(!$options['verbose']) foreach($this->moduleInfoVerboseKeys as $key) unset($info[$key]);
  1914. }
  1915. if(empty($info['created']) && isset($this->createdDates[$moduleID])) {
  1916. $info['created'] = strtotime($this->createdDates[$moduleID]);
  1917. }
  1918. if(!empty($info['file']) && (strpos($info['file'], 'wire') === 0 || strpos($info['file'], 'site') === 0)) {
  1919. // convert relative (as stored in moduleInfo) to absolute (verbose info only)
  1920. $info['file'] = $this->wire('config')->paths->root . $info['file'];
  1921. }
  1922. // if($this->debug) $this->message("getModuleInfo($moduleName) " . ($fromCache ? "CACHE" : "NO-CACHE"));
  1923. return $info;
  1924. }
  1925. /**
  1926. * Returns the verbose array of information for a Module
  1927. *
  1928. * @param string|Module|int $module May be class name, module instance, or module ID
  1929. * @param array $options Optional options to modify behavior of what gets returned
  1930. * - noCache: prevents use of cache to retrieve the module info
  1931. * - noInclude: prevents include() of the module file, applicable only if it hasn't already been included
  1932. * @return array
  1933. * @throws WireException when a module exists but has no means of returning module info
  1934. *
  1935. */
  1936. public function getModuleInfoVerbose($module, array $options = array()) {
  1937. $options['verbose'] = true;
  1938. $info = $this->getModuleInfo($module, $options);
  1939. return $info;
  1940. }
  1941. /**
  1942. * Given a class name, return an array of configuration data specified for the Module
  1943. *
  1944. * Corresponds to the modules.data table in the database
  1945. *
  1946. * Applicable only for modules that implement the ConfigurableModule interface
  1947. *
  1948. * @param string|Module $className
  1949. * @return array
  1950. *
  1951. */
  1952. public function getModuleConfigData($className) {
  1953. if(is_object($className)) $className = $className->className();
  1954. if(!$id = $this->moduleIDs[$className]) return array();
  1955. if(!isset($this->configData[$id])) return array(); // module has no config data
  1956. if(is_array($this->configData[$id])) return $this->configData[$id];
  1957. // first verify that module doesn't have a config file
  1958. $configurable = $this->isConfigurableModule($className);
  1959. if(!$configurable) return array();
  1960. $database = $this->wire('database');
  1961. $query = $database->prepare("SELECT data FROM modules WHERE id=:id", "modules.getModuleConfigData($className)"); // QA
  1962. $query->bindValue(":id", (int) $id, PDO::PARAM_INT);
  1963. $query->execute();
  1964. $data = $query->fetchColumn();
  1965. $query->closeCursor();
  1966. if(empty($data)) $data = array();
  1967. else $data = wireDecodeJSON($data);
  1968. if(empty($data)) $data = array();
  1969. $this->configData[$id] = $data;
  1970. return $data;
  1971. }
  1972. /**
  1973. * Get the path + filename for this module
  1974. *
  1975. * @param string|Module $className Module class name or object instance
  1976. * @param bool $getURL If true, will return it as a URL from PW root install path (for shorter display purposes)
  1977. * @return bool|string Returns string of module file, or false on failure.
  1978. *
  1979. */
  1980. public function getModuleFile($className, $getURL = false) {
  1981. $file = false;
  1982. // first see it's an object, and if we can get the file from the object
  1983. if(is_object($className)) {
  1984. $module = $className;
  1985. if($module instanceof ModulePlaceholder) $file = $module->file;
  1986. $className = $module->className();
  1987. }
  1988. // next see if we've already got the module filename cached locally
  1989. if(!$file && isset($this->installable[$className])) {
  1990. $file = $this->installable[$className];
  1991. }
  1992. if(!$file) {
  1993. $dupFile = $this->duplicates()->getCurrent($className);
  1994. if($dupFile) {
  1995. $rootPath = $this->wire('config')->paths->root;
  1996. $file = rtrim($rootPath, '/') . $dupFile;
  1997. if(!file_exists($file)) {
  1998. // module in use may have been deleted, find the next available one that exist
  1999. $file = '';
  2000. $dups = $this->duplicates()->getDuplicates($className);
  2001. foreach($dups['files'] as $pathname) {
  2002. $pathname = rtrim($rootPath, '/') . $pathname;
  2003. if(file_exists($pathname)) {
  2004. $file = $pathname;
  2005. break;
  2006. }
  2007. }
  2008. }
  2009. }
  2010. }
  2011. if(!$file) {
  2012. // next see if we can determine it from already stored paths
  2013. $path = $this->wire('config')->paths->$className;
  2014. if(file_exists($path)) {
  2015. $file = "$path$className.module";
  2016. if(!file_exists($file)) {
  2017. $file = "$path$className.module.php";
  2018. if(!file_exists($file)) $file = false;
  2019. }
  2020. }
  2021. }
  2022. if(!$file) {
  2023. // if the above two failed, try to get it from Reflection
  2024. try {
  2025. $reflector = new ReflectionClass($className);
  2026. $file = $reflector->getFileName();
  2027. } catch(Exception $e) {
  2028. $file = false;
  2029. }
  2030. }
  2031. if($file && DIRECTORY_SEPARATOR != '/') $file = str_replace(DIRECTORY_SEPARATOR, '/', $file);
  2032. if($getURL) $file = str_replace($this->wire('config')->paths->root, '/', $file);
  2033. return $file;
  2034. }
  2035. /**
  2036. * Is the given module configurable?
  2037. *
  2038. * External configuration file:
  2039. * ============================
  2040. * Returns string of full path/filename to ModuleName.config.php file if configurable via separate file.
  2041. *
  2042. * ModuleConfig interface:
  2043. * =======================
  2044. * Returns boolean true if module is configurable via the static getModuleConfigInputfields method.
  2045. * Returns integer 2 if module is configurable via the non-static getModuleConfigInputfields and requires no arguments.
  2046. * Returns integer 3 if module is configurable via the non-static getModuleConfigInputfields and requires $data array.
  2047. * Returns integer 4 if module is configurable via the non-static getModuleConfigInputfields and requires InputfieldWrapper argument.
  2048. * Returns integer 19 if module is configurable via non-static getModuleConfigArray method.
  2049. * Returns integer 20 if module is configurable via static getModuleConfigArray method.
  2050. *
  2051. * Not configurable:
  2052. * =================
  2053. * Returns boolean false if not configurable
  2054. *
  2055. * @param Module|string $className
  2056. * @param bool $useCache This accepts a few options:
  2057. * - Specify boolean true to allow use of cache when available (default behavior).
  2058. * - Specify boolean false to disable retrieval of this property from getModuleInfo (forces a new check).
  2059. * - Specify string 'interface' to check only if module implements ConfigurableModule interface.
  2060. * - Specify string 'file' to check only if module has a separate configuration class/file.
  2061. * @return bool|string See details about return values above.
  2062. *
  2063. * @todo all ConfigurableModule methods need to be split out into their own class (ConfigurableModules?)
  2064. * @todo this method has two distinct parts (file and interface) that need to be split in two methods.
  2065. *
  2066. */
  2067. public function isConfigurableModule($className, $useCache = true) {
  2068. $moduleInstance = null;
  2069. if(is_object($className)) {
  2070. $moduleInstance = $className;
  2071. $className = $this->getModuleClass($moduleInstance);
  2072. }
  2073. if($useCache === true || $useCache === 1 || $useCache === "1") {
  2074. $info = $this->getModuleInfo($className);
  2075. // if regular module info doesn't have configurable info, attempt it from verbose module info
  2076. // should only be necessary for transition period between the 'configurable' property being
  2077. // moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7)
  2078. if($info['configurable'] === null) $info = $this->getModuleInfoVerbose($className);
  2079. if(!$info['configurable']) {
  2080. if($moduleInstance && $moduleInstance instanceof ConfigurableModule) {
  2081. // re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format
  2082. // this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12
  2083. $this->error("Configurable module check failed for $className, retrying...", Notice::debug);
  2084. $useCache = false;
  2085. } else {
  2086. return false;
  2087. }
  2088. } else {
  2089. if($info['configurable'] === true) return $info['configurable'];
  2090. if($info['configurable'] === 1 || $info['configurable'] === "1") return true;
  2091. if(is_int($info['configurable']) || ctype_digit("$info[configurable]")) return (int) $info['configurable'];
  2092. if(strpos($info['configurable'], $className) === 0) {
  2093. if(empty($info['file'])) $info['file'] = $this->getModuleFile($className);
  2094. if($info['file']) {
  2095. return dirname($info['file']) . "/$info[configurable]";
  2096. }
  2097. }
  2098. }
  2099. }
  2100. if($useCache !== "interface") {
  2101. // check for separate module configuration file
  2102. $dir = dirname($this->getModuleFile($className));
  2103. if($dir) {
  2104. $files = array(
  2105. "$dir/{$className}Config.php",
  2106. "$dir/$className.config.php"
  2107. );
  2108. $found = false;
  2109. foreach($files as $file) {
  2110. if(!is_file($file)) continue;
  2111. $config = null; // include file may override
  2112. include_once($file);
  2113. $classConfig = $className . 'Config';
  2114. if(class_exists($classConfig, false)) {
  2115. $interfaces = @class_parents($classConfig, false);
  2116. if(is_array($interfaces) && isset($interfaces['ModuleConfig'])) {
  2117. $found = $file;
  2118. break;
  2119. }
  2120. } else {
  2121. // bypass include_once, because we need to read $config every time
  2122. if(is_null($config)) include($file);
  2123. if(!is_null($config)) {
  2124. // included file specified a $config array
  2125. $found = $file;
  2126. break;
  2127. }
  2128. }
  2129. }
  2130. if($found) return $file;
  2131. }
  2132. }
  2133. // if file-only check was requested and we reach this point, exit with false now
  2134. if($useCache === "file") return false;
  2135. // ConfigurableModule interface checks
  2136. $result = false;
  2137. foreach(array('getModuleConfigArray', 'getModuleConfigInputfields') as $method) {
  2138. $configurable = false;
  2139. // if we have a module instance, use that for our check
  2140. if($moduleInstance && $moduleInstance instanceof ConfigurableModule) {
  2141. if(method_exists($moduleInstance, $method)) {
  2142. $configurable = $method;
  2143. } else if(method_exists($moduleInstance, "___$method")) {
  2144. $configurable = "___$method";
  2145. }
  2146. }
  2147. // if we didn't have a module instance, load the file to find what we need to know
  2148. if(!$configurable) {
  2149. if(!class_exists($className, false)) $this->includeModule($className);
  2150. $interfaces = @class_implements($className, false);
  2151. if(is_array($interfaces) && isset($interfaces['ConfigurableModule'])) {
  2152. if(method_exists($className, $method)) {
  2153. $configurable = $method;
  2154. } else if(method_exists($className, "___$method")) {
  2155. $configurable = "___$method";
  2156. }
  2157. }
  2158. }
  2159. // if still not determined to be configurable, move on to next method
  2160. if(!$configurable) continue;
  2161. // now determine if static or non-static
  2162. $ref = new ReflectionMethod($className, $configurable);
  2163. if($ref->isStatic()) {
  2164. // config method is implemented as a static method
  2165. if($method == 'getModuleConfigInputfields') {
  2166. // static getModuleConfigInputfields
  2167. $result = true;
  2168. } else {
  2169. // static getModuleConfigArray
  2170. $result = 20;
  2171. }
  2172. } else if($method == 'getModuleConfigInputfields') {
  2173. // non-static getModuleConfigInputfields
  2174. // we allow for different arguments, so determine what it needs
  2175. $parameters = $ref->getParameters();
  2176. if(count($parameters)) {
  2177. $param0 = reset($parameters);
  2178. if(strpos($param0, 'array') !== false || strpos($param0, '$data') !== false) {
  2179. // method requires a $data array (for compatibility with non-static version)
  2180. $result = 3;
  2181. } else if(strpos($param0, 'InputfieldWrapper') !== false || strpos($param0, 'inputfields') !== false) {
  2182. // method requires an empty InputfieldWrapper (as a convenience)
  2183. $result = 4;
  2184. }
  2185. }
  2186. // method requires no arguments
  2187. if(!$result) $result = 2;
  2188. } else {
  2189. // non-static getModuleConfigArray
  2190. $result = 19;
  2191. }
  2192. // if we make it here, we know we already have a result so can stop now
  2193. break;
  2194. }
  2195. return $result;
  2196. }
  2197. /**
  2198. * Populate configuration data to a ConfigurableModule
  2199. *
  2200. * If the Module has a 'setConfigData' method, it will send the array of data to that.
  2201. * Otherwise it will populate the properties individually.
  2202. *
  2203. * @param Module $module
  2204. * @param array $data Configuration data (key = value), or omit if you want it to retrieve the config data for you.
  2205. * @return bool True if configured, false if not configurable
  2206. *
  2207. */
  2208. protected function setModuleConfigData(Module $module, $data = null) {
  2209. $configurable = $this->isConfigurableModule($module);
  2210. if(!$configurable) return false;
  2211. if(!is_array($data)) $data = $this->getModuleConfigData($module);
  2212. if(is_string($configurable) && is_file($configurable) && strpos(basename($configurable), $module->className()) === 0) {
  2213. // get defaults from ModuleConfig class if available
  2214. $className = $module->className() . 'Config';
  2215. $config = null; // may be overridden by included file
  2216. include_once($configurable);
  2217. if(class_exists($className)) {
  2218. $interfaces = @class_parents($className, false);
  2219. if(is_array($interfaces) && isset($interfaces['ModuleConfig'])) {
  2220. $moduleConfig = new $className();
  2221. if($moduleConfig instanceof ModuleConfig) {
  2222. $defaults = $moduleConfig->getDefaults();
  2223. $data = array_merge($defaults, $data);
  2224. }
  2225. }
  2226. } else {
  2227. // the file may have already been include_once before, so $config would not be set
  2228. // so we try a regular include() next.
  2229. if(is_null($config)) include($configurable);
  2230. if(is_array($config)) {
  2231. // alternatively, file may just specify a $config array
  2232. $moduleConfig = new ModuleConfig();
  2233. $moduleConfig->add($config);
  2234. $defaults = $moduleConfig->getDefaults();
  2235. $data = array_merge($defaults, $data);
  2236. }
  2237. }
  2238. }
  2239. if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) {
  2240. $module->setConfigData($data);
  2241. return true;
  2242. }
  2243. foreach($data as $key => $value) {
  2244. $module->$key = $value;
  2245. }
  2246. return true;
  2247. }
  2248. /**
  2249. * Given a module class name and an array of configuration data, save it for the module
  2250. *
  2251. * @param string|Module $className
  2252. * @param array $configData
  2253. * @return bool True on success
  2254. * @throws WireException
  2255. *
  2256. */
  2257. public function ___saveModuleConfigData($className, array $configData) {
  2258. if(is_object($className)) $className = $className->className();
  2259. if(!$id = $this->moduleIDs[$className]) throw new WireException("Unable to find ID for Module '$className'");
  2260. // ensure original duplicates info is retained and validate that it is still current
  2261. $configData = $this->duplicates()->getDuplicatesConfigData($className, $configData);
  2262. $this->configData[$id] = $configData;
  2263. $json = count($configData) ? wireEncodeJSON($configData, true) : '';
  2264. $database = $this->wire('database');
  2265. $query = $database->prepare("UPDATE modules SET data=:data WHERE id=:id", "modules.saveModuleConfigData($className)"); // QA
  2266. $query->bindValue(":data", $json, PDO::PARAM_STR);
  2267. $query->bindValue(":id", (int) $id, PDO::PARAM_INT);
  2268. $result = $query->execute();
  2269. $this->log("Saved module '$className' config data");
  2270. return $result;
  2271. }
  2272. /**
  2273. * Get the Inputfields that configure the given module or return null if not configurable
  2274. *
  2275. * @param string|Module|int $moduleName
  2276. * @param InputfieldWrapper|null $form Optionally specify the form you want Inputfields appended to.
  2277. * @return InputfieldWrapper|null
  2278. *
  2279. */
  2280. public function ___getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) {
  2281. $moduleName = $this->getModuleClass($moduleName);
  2282. $configurable = $this->isConfigurableModule($moduleName);
  2283. if(!$configurable) return null;
  2284. if(is_null($form)) $form = new InputfieldWrapper();
  2285. $data = $this->modules->getModuleConfigData($moduleName);
  2286. // check for configurable module interface
  2287. $configurableInterface = $this->isConfigurableModule($moduleName, "interface");
  2288. if($configurableInterface) {
  2289. if(is_int($configurableInterface) && $configurableInterface > 1 && $configurableInterface < 20) {
  2290. // non-static
  2291. /** @var ConfigurableModule $module */
  2292. $module = $this->getModule($moduleName);
  2293. if($configurableInterface === 2) {
  2294. // requires no arguments
  2295. $fields = $module->getModuleConfigInputfields();
  2296. } else if($configurableInterface === 3) {
  2297. // requires $data array
  2298. $fields = $module->getModuleConfigInputfields($data);
  2299. } else if($configurableInterface === 4) {
  2300. // requires InputfieldWrapper
  2301. // we allow for option of no return statement in the method
  2302. $fields = new InputfieldWrapper();
  2303. $_fields = $module->getModuleConfigInputfields($fields);
  2304. if($_fields instanceof InputfieldWrapper) $fields = $_fields;
  2305. unset($_fields);
  2306. } else if($configurableInterface === 19) {
  2307. // non-static getModuleConfigArray method
  2308. $fields = new InputfieldWrapper();
  2309. $fields->importArray($module->getModuleConfigArray());
  2310. $fields->populateValues($module);
  2311. }
  2312. } else if($configurableInterface === 20) {
  2313. // static getModuleConfigArray method
  2314. $fields = new InputfieldWrapper();
  2315. $fields->importArray(call_user_func(array($moduleName, 'getModuleConfigArray')));
  2316. $fields->populateValues($data);
  2317. } else if($configurableInterface) {
  2318. // static getModuleConfigInputfields method
  2319. $fields = call_user_func(array($moduleName, 'getModuleConfigInputfields'), $data);
  2320. }
  2321. if($fields instanceof InputfieldWrapper) {
  2322. foreach($fields as $field) {
  2323. $form->append($field);
  2324. }
  2325. } else if($fields instanceof Inputfield) {
  2326. $form->append($fields);
  2327. } else {
  2328. $this->error("$moduleName.getModuleConfigInputfields() did not return InputfieldWrapper");
  2329. }
  2330. }
  2331. // check for file-based config
  2332. $file = $this->isConfigurableModule($moduleName, "file");
  2333. if(!$file || !is_string($file) || !is_file($file)) return $form;
  2334. $config = null;
  2335. include_once($file);
  2336. $configClass = $moduleName . "Config";
  2337. $configModule = null;
  2338. if(class_exists($configClass)) {
  2339. // file contains a ModuleNameConfig class
  2340. $configModule = new $configClass();
  2341. } else {
  2342. if(is_null($config)) include($file); // in case of previous include_once
  2343. if(is_array($config)) {
  2344. // file contains a $config array
  2345. $configModule = new ModuleConfig();
  2346. $configModule->add($config);
  2347. }
  2348. }
  2349. if($configModule && $configModule instanceof ModuleConfig) {
  2350. $defaults = $configModule->getDefaults();
  2351. $data = array_merge($defaults, $data);
  2352. $configModule->setArray($data);
  2353. $fields = $configModule->getInputfields();
  2354. if($fields instanceof InputfieldWrapper) {
  2355. foreach($fields as $field) {
  2356. $form->append($field);
  2357. }
  2358. foreach($data as $key => $value) {
  2359. $f = $form->getChildByName($key);
  2360. if($f) $f->attr('value', $value);
  2361. }
  2362. } else {
  2363. $this->error("$configModule.getInputfields() did not return InputfieldWrapper");
  2364. }
  2365. }
  2366. return $form;
  2367. }
  2368. /**
  2369. * Is the given module Singular (single instance)?
  2370. *
  2371. * isSingular and isAutoload Module methods have been deprecated. So this method, and isAutoload()
  2372. * exist in part to enable singular and autoload properties to be set in getModuleInfo, rather than
  2373. * with methods.
  2374. *
  2375. * Note that isSingular() and isAutoload() are not deprecated for ModulePlaceholder, so the Modules
  2376. * class isn't going to stop looking for them.
  2377. *
  2378. * @param Module|string $module Module instance or class name
  2379. * @return bool
  2380. *
  2381. */
  2382. public function isSingular($module) {
  2383. $info = $this->getModuleInfo($module);
  2384. if(isset($info['singular']) && $info['singular'] !== null) return $info['singular'];
  2385. if(!is_object($module)) {
  2386. // singular status can't be determined if module not installed and not specified in moduleInfo
  2387. if(isset($this->installable[$module])) return null;
  2388. $this->includeModule($module);
  2389. }
  2390. if(method_exists($module, 'isSingular')) return $module->isSingular();
  2391. return false;
  2392. }
  2393. /**
  2394. * Is the given module Autoload (automatically loaded at runtime)?
  2395. *
  2396. * @param Module|string $module Module instance or class name
  2397. * @return bool|string|null Returns string "conditional" if conditional autoload, true if autoload, or false if not. Or null if unavailable.
  2398. *
  2399. */
  2400. public function isAutoload($module) {
  2401. $info = $this->getModuleInfo($module);
  2402. $autoload = null;
  2403. if(isset($info['autoload']) && $info['autoload'] !== null) {
  2404. // if autoload is a string (selector) or callable, then we flag it as autoload
  2405. if(is_string($info['autoload']) || is_callable($info['autoload'])) return "conditional";
  2406. $autoload = $info['autoload'];
  2407. } else if(!is_object($module)) {
  2408. if(isset($this->installable[$module])) {
  2409. // module is not installed
  2410. // we are not going to be able to determine if this is autoload or not
  2411. $flags = $this->getFlags($module);
  2412. if($flags !== null) {
  2413. $autoload = $flags & self::flagsAutoload;
  2414. } else {
  2415. // unable to determine
  2416. return null;
  2417. }
  2418. } else {
  2419. // include for method exists call
  2420. $this->includeModule($module);
  2421. }
  2422. }
  2423. if($autoload === null && method_exists($module, 'isAutoload')) {
  2424. $autoload = $module->isAutoload();
  2425. }
  2426. return $autoload;
  2427. }
  2428. /**
  2429. * Returns whether the modules have been initialized yet
  2430. *
  2431. * @return bool
  2432. *
  2433. */
  2434. public function isInitialized() {
  2435. return $this->initialized;
  2436. }
  2437. /**
  2438. * Reset the cache that stores module files by recreating it
  2439. *
  2440. */
  2441. public function resetCache() {
  2442. if($this->wire('config')->systemVersion < 6) return;
  2443. $this->clearModuleInfoCache();
  2444. foreach($this->paths as $path) $this->findModuleFiles($path, false);
  2445. foreach($this->paths as $path) $this->load($path);
  2446. if($this->duplicates()->numNewDuplicates() > 0) $this->duplicates()->updateDuplicates(); // PR#1020
  2447. }
  2448. /**
  2449. * Return an array of module class names that require the given one
  2450. *
  2451. * @param string $class
  2452. * @param bool $uninstalled Set to true to include modules dependent upon this one, even if they aren't installed.
  2453. * @param bool $installs Set to true to exclude modules that indicate their install/uninstall is controlled by $class.
  2454. * @return array()
  2455. *
  2456. */
  2457. public function getRequiredBy($class, $uninstalled = false, $installs = false) {
  2458. $class = $this->getModuleClass($class);
  2459. $info = $this->getModuleInfo($class);
  2460. $dependents = array();
  2461. foreach($this as $module) {
  2462. $c = $this->getModuleClass($module);
  2463. if(!$uninstalled && !$this->isInstalled($c)) continue;
  2464. $i = $this->getModuleInfo($c);
  2465. if(!count($i['requires'])) continue;
  2466. if($installs && in_array($c, $info['installs'])) continue;
  2467. if(in_array($class, $i['requires'])) $dependents[] = $c;
  2468. }
  2469. return $dependents;
  2470. }
  2471. /**
  2472. * Return an array of module class names required by the given one
  2473. *
  2474. * Default behavior is to return all listed requirements, whether they are currently met by
  2475. * the environment or not. Specify TRUE for the 2nd argument to return only requirements
  2476. * that are not currently met.
  2477. *
  2478. * @param string $class
  2479. * @param bool $onlyMissing Set to true to return only required modules/versions that aren't
  2480. * yet installed or don't have the right version. It excludes those that the class says it
  2481. * will install (via 'installs' property of getModuleInfo)
  2482. * @param null|bool $versions Set to true to always include versions in the returned requirements list.
  2483. * Set to null to always exclude versions in requirements list (so only module class names will be there).
  2484. * Set to false (which is the default) to include versions only when version is the dependency issue.
  2485. * Note versions are already included when the installed version is not adequate.
  2486. * @return array of strings each with ModuleName Operator Version, i.e. "ModuleName>=1.0.0"
  2487. *
  2488. */
  2489. public function getRequires($class, $onlyMissing = false, $versions = false) {
  2490. $class = $this->getModuleClass($class);
  2491. $info = $this->getModuleInfo($class);
  2492. $requires = $info['requires'];
  2493. // quick exit if arguments permit it
  2494. if(!$onlyMissing) {
  2495. if($versions) foreach($requires as $key => $value) {
  2496. list($operator, $version) = $info['requiresVersions'][$value];
  2497. if(empty($version)) continue;
  2498. if(ctype_digit("$version")) $version = $this->formatVersion($version);
  2499. if(!empty($version)) $requires[$key] .= "$operator$version";
  2500. }
  2501. return $requires;
  2502. }
  2503. foreach($requires as $key => $requiresClass) {
  2504. if(in_array($requiresClass, $info['installs'])) {
  2505. // if this module installs the required class, then we can stop now
  2506. // and we assume it's installing the version it wants
  2507. unset($requires[$key]);
  2508. }
  2509. list($operator, $requiresVersion) = $info['requiresVersions'][$requiresClass];
  2510. $installed = true;
  2511. if($requiresClass == 'PHP') {
  2512. $currentVersion = PHP_VERSION;
  2513. } else if($requiresClass == 'ProcessWire') {
  2514. $currentVersion = $this->wire('config')->version;
  2515. } else if($this->isInstalled($requiresClass)) {
  2516. if(!$requiresVersion) {
  2517. // if no version is specified then requirement is already met
  2518. unset($requires[$key]);
  2519. continue;
  2520. }
  2521. $i = $this->getModuleInfo($requiresClass, array('noCache' => true));
  2522. $currentVersion = $i['version'];
  2523. } else {
  2524. // module is not installed
  2525. $installed = false;
  2526. }
  2527. if($installed && $this->versionCompare($currentVersion, $requiresVersion, $operator)) {
  2528. // required version is installed
  2529. unset($requires[$key]);
  2530. } else if(empty($requiresVersion)) {
  2531. // just the class name is fine
  2532. continue;
  2533. } else if(is_null($versions)) {
  2534. // request is for no versions to be included (just class names)
  2535. $requires[$key] = $requiresClass;
  2536. } else {
  2537. // update the requires string to clarify what version it requires
  2538. if(ctype_digit("$requiresVersion")) $requiresVersion = $this->formatVersion($requiresVersion);
  2539. $requires[$key] = "$requiresClass$operator$requiresVersion";
  2540. }
  2541. }
  2542. return $requires;
  2543. }
  2544. /**
  2545. * Compare one module version to another, returning TRUE if they match the $operator or FALSE otherwise
  2546. *
  2547. * @param int|string $currentVersion May be a number like 123 or a formatted version like 1.2.3
  2548. * @param int|string $requiredVersion May be a number like 123 or a formatted version like 1.2.3
  2549. * @param string $operator
  2550. * @return bool
  2551. *
  2552. */
  2553. public function versionCompare($currentVersion, $requiredVersion, $operator) {
  2554. if(ctype_digit("$currentVersion") && ctype_digit("$requiredVersion")) {
  2555. // integer comparison is ok
  2556. $currentVersion = (int) $currentVersion;
  2557. $requiredVersion = (int) $requiredVersion;
  2558. $result = false;
  2559. switch($operator) {
  2560. case '=': $result = ($currentVersion == $requiredVersion); break;
  2561. case '>': $result = ($currentVersion > $requiredVersion); break;
  2562. case '<': $result = ($currentVersion < $requiredVersion); break;
  2563. case '>=': $result = ($currentVersion >= $requiredVersion); break;
  2564. case '<=': $result = ($currentVersion <= $requiredVersion); break;
  2565. case '!=': $result = ($currentVersion != $requiredVersion); break;
  2566. }
  2567. return $result;
  2568. }
  2569. // if either version has no periods or only one, like "1.2" then format it to stanard: "1.2.0"
  2570. if(substr_count($currentVersion, '.') < 2) $currentVersion = $this->formatVersion($currentVersion);
  2571. if(substr_count($requiredVersion, '.') < 2) $requiredVersion = $this->formatVersion($requiredVersion);
  2572. return version_compare($currentVersion, $requiredVersion, $operator);
  2573. }
  2574. /**
  2575. * Return array of ($module, $operator, $requiredVersion)
  2576. *
  2577. * $version will be 0 and $operator blank if there are no requirements.
  2578. *
  2579. * @param string $require Module class name with operator and version string
  2580. * @return array of array($moduleClass, $operator, $version)
  2581. *
  2582. */
  2583. protected function extractModuleOperatorVersion($require) {
  2584. if(ctype_alnum($require)) {
  2585. // no version is specified
  2586. return array($require, '', 0);
  2587. }
  2588. $operators = array('<=', '>=', '<', '>', '!=', '=');
  2589. $operator = '';
  2590. foreach($operators as $o) {
  2591. if(strpos($require, $o)) {
  2592. $operator = $o;
  2593. break;
  2594. }
  2595. }
  2596. // if no operator found, then no version is being specified
  2597. if(!$operator) return array($require, '', 0);
  2598. // extract class and version
  2599. list($class, $version) = explode($operator, $require);
  2600. // make version an integer if possible
  2601. if(ctype_digit("$version")) $version = (int) $version;
  2602. return array($class, $operator, $version);
  2603. }
  2604. /**
  2605. * Return an array of module class names required by the given one to be installed before this one.
  2606. *
  2607. * Excludes modules that are required but already installed.
  2608. * Excludes uninstalled modules that $class indicates it handles via it's 'installs' getModuleInfo property.
  2609. *
  2610. * @param string $class
  2611. * @return array()
  2612. *
  2613. */
  2614. public function getRequiresForInstall($class) {
  2615. return $this->getRequires($class, true);
  2616. }
  2617. /**
  2618. * Return an array of module class names required by the given one to be uninstalled before this one.
  2619. *
  2620. * Excludes modules that the given one says it handles via it's 'installs' getModuleInfo property.
  2621. * Module class names in returned array include operator and version in the string.
  2622. *
  2623. * @param string $class
  2624. * @return array()
  2625. *
  2626. */
  2627. public function getRequiresForUninstall($class) {
  2628. return $this->getRequiredBy($class, false, true);
  2629. }
  2630. /**
  2631. * Return array of dependency errors for given module name
  2632. *
  2633. * @param $moduleName
  2634. * @return array If no errors, array will be blank. If errors, array will be of strings (error messages)
  2635. *
  2636. */
  2637. public function getDependencyErrors($moduleName) {
  2638. $moduleName = $this->getModuleClass($moduleName);
  2639. $info = $this->getModuleInfo($moduleName);
  2640. $errors = array();
  2641. if(empty($info['requires'])) return $errors;
  2642. foreach($info['requires'] as $requiresName) {
  2643. $error = '';
  2644. if(!$this->isInstalled($requiresName)) {
  2645. $error = $requiresName;
  2646. } else if(!empty($info['requiresVersions'][$requiresName])) {
  2647. list($operator, $version) = $info['requiresVersions'][$requiresName];
  2648. $info2 = $this->getModuleInfo($requiresName);
  2649. $requiresVersion = $info2['version'];
  2650. if(!empty($version) && !$this->versionCompare($requiresVersion, $version, $operator)) {
  2651. $error = "$requiresName $operator $version";
  2652. }
  2653. }
  2654. if($error) $errors[] = sprintf($this->_('Failed module dependency: %s requires %s'), $moduleName, $error);
  2655. }
  2656. return $errors;
  2657. }
  2658. /**
  2659. * Given a module version number, format it in a consistent way as 3 parts: 1.2.3
  2660. *
  2661. * @param $version int|string
  2662. * @return string
  2663. *
  2664. */
  2665. public function formatVersion($version) {
  2666. $version = trim($version);
  2667. if(!ctype_digit(str_replace('.', '', $version))) {
  2668. // if version has some characters other than digits or periods, remove them
  2669. $version = preg_replace('/[^\d.]/', '', $version);
  2670. }
  2671. if(ctype_digit("$version")) {
  2672. // version contains only digits
  2673. // make sure version is at least 3 characters in length, left padded with 0s
  2674. $len = strlen($version);
  2675. if($len < 3) {
  2676. $version = str_pad($version, 3, "0", STR_PAD_LEFT);
  2677. } else if($len > 3) {
  2678. // they really need to use a string for this type of version,
  2679. // as we can't really guess, but we'll try, converting 1234 to 1.2.34
  2680. }
  2681. // $version = preg_replace('/(\d)(?=\d)/', '$1.', $version);
  2682. $version =
  2683. substr($version, 0, 1) . '.' .
  2684. substr($version, 1, 1) . '.' .
  2685. substr($version, 2);
  2686. } else if(strpos($version, '.') !== false) {
  2687. // version is a formatted string
  2688. if(strpos($version, '.') == strrpos($version, '.')) {
  2689. // only 1 period, like: 2.0, convert that to 2.0.0
  2690. if(preg_match('/^\d\.\d$/', $version)) $version .= ".0";
  2691. }
  2692. } else {
  2693. // invalid version?
  2694. }
  2695. if(!strlen($version)) $version = '0.0.0';
  2696. return $version;
  2697. }
  2698. /**
  2699. * Load the module information cache
  2700. *
  2701. * @return bool
  2702. *
  2703. */
  2704. protected function loadModuleInfoCache() {
  2705. $data = $this->wire('cache')->get(self::moduleInfoCacheName);
  2706. if($data) {
  2707. // if module class name keys in use (i.e. ProcessModule) it's an older version of
  2708. // module info cache, so we skip over it to force its re-creation
  2709. if(is_array($data) && !isset($data['ProcessModule'])) $this->moduleInfoCache = $data;
  2710. $data = $this->wire('cache')->get(self::moduleLastVersionsCacheName);
  2711. if(is_array($data)) $this->modulesLastVersions = $data;
  2712. return true;
  2713. }
  2714. return false;
  2715. }
  2716. /**
  2717. * Load the module information cache (verbose info: summary, author, href, file, core)
  2718. *
  2719. * @param bool $uninstalled If true, it will load the uninstalled verbose cache.
  2720. * @return bool
  2721. *
  2722. */
  2723. protected function loadModuleInfoCacheVerbose($uninstalled = false) {
  2724. $name = $uninstalled ? self::moduleInfoCacheUninstalledName : self::moduleInfoCacheVerboseName;
  2725. $data = $this->wire('cache')->get($name);
  2726. if($data) {
  2727. if(is_array($data)) {
  2728. if($uninstalled) $this->moduleInfoCacheUninstalled = $data;
  2729. else $this->moduleInfoCacheVerbose = $data;
  2730. }
  2731. return true;
  2732. }
  2733. return false;
  2734. }
  2735. /**
  2736. * Clear the module information cache
  2737. *
  2738. */
  2739. protected function clearModuleInfoCache() {
  2740. // record current module versions currently in moduleInfo
  2741. $moduleVersions = array();
  2742. foreach($this->moduleInfoCache as $id => $moduleInfo) {
  2743. if(isset($this->modulesLastVersions[$id])) {
  2744. $moduleVersions[$id] = $this->modulesLastVersions[$id];
  2745. } else {
  2746. $moduleVersions[$id] = $moduleInfo['version'];
  2747. }
  2748. // $moduleVersions[$id] = $moduleInfo['version'];
  2749. }
  2750. // delete the caches
  2751. $this->wire('cache')->delete(self::moduleInfoCacheName);
  2752. $this->wire('cache')->delete(self::moduleInfoCacheVerboseName);
  2753. $this->wire('cache')->delete(self::moduleInfoCacheUninstalledName);
  2754. $this->moduleInfoCache = array();
  2755. $this->moduleInfoCacheVerbose = array();
  2756. $this->moduleInfoCacheUninstalled = array();
  2757. // save new moduleInfo cache
  2758. $this->saveModuleInfoCache();
  2759. $versionChanges = array();
  2760. $newModules = array();
  2761. // compare new moduleInfo versions with the previous ones, looking for changes
  2762. foreach($this->moduleInfoCache as $id => $moduleInfo) {
  2763. if(!isset($moduleVersions[$id])) {
  2764. $newModules[] = $moduleInfo['name'];
  2765. continue;
  2766. }
  2767. if($moduleVersions[$id] != $moduleInfo['version']) {
  2768. $fromVersion = $this->formatVersion($moduleVersions[$id]);
  2769. $toVersion = $this->formatVersion($moduleInfo['version']);
  2770. $versionChanges[] = "$moduleInfo[name]: $fromVersion => $toVersion";
  2771. $this->modulesLastVersions[$id] = $moduleVersions[$id];
  2772. }
  2773. }
  2774. // report on any changes
  2775. if(count($newModules)) {
  2776. $this->message(
  2777. sprintf($this->_n('Detected %d new module: %s', 'Detected %d new modules: %s', count($newModules)),
  2778. count($newModules), '<pre>' . implode("\n", $newModules)) . '</pre>',
  2779. Notice::allowMarkup);
  2780. }
  2781. if(count($versionChanges)) {
  2782. $this->message(
  2783. sprintf($this->_n('Detected %d module version change', 'Detected %d module version changes',
  2784. count($versionChanges)), count($versionChanges)) .
  2785. ' (' . $this->_('will be applied the next time each module is loaded') . '):' .
  2786. '<pre>' . implode("\n", $versionChanges) . '</pre>',
  2787. Notice::allowMarkup | Notice::debug);
  2788. }
  2789. $this->updateModuleVersionsCache();
  2790. }
  2791. /**
  2792. * Update the cache of queued module version changes
  2793. *
  2794. */
  2795. protected function updateModuleVersionsCache() {
  2796. foreach($this->modulesLastVersions as $id => $version) {
  2797. // clear out stale data, if present
  2798. if(!in_array($id, $this->moduleIDs)) unset($this->modulesLastVersions[$id]);
  2799. }
  2800. if(count($this->modulesLastVersions)) {
  2801. $this->wire('cache')->save(self::moduleLastVersionsCacheName, $this->modulesLastVersions, WireCache::expireNever);
  2802. } else {
  2803. $this->wire('cache')->delete(self::moduleLastVersionsCacheName);
  2804. }
  2805. }
  2806. /**
  2807. * Check the module version to make sure it is consistent with our moduleInfo
  2808. *
  2809. * When not consistent, this triggers the moduleVersionChanged hook, which in turn
  2810. * triggers the $module->___upgrade($fromVersion, $toVersion) method.
  2811. *
  2812. * @param Module $module
  2813. *
  2814. */
  2815. protected function checkModuleVersion(Module $module) {
  2816. $id = $this->getModuleID($module);
  2817. $moduleInfo = $this->getModuleInfo($module);
  2818. $lastVersion = isset($this->modulesLastVersions[$id]) ? $this->modulesLastVersions[$id] : null;
  2819. if(!is_null($lastVersion)) {
  2820. if($lastVersion != $moduleInfo['version']) {
  2821. $this->moduleVersionChanged($module, $lastVersion, $moduleInfo['version']);
  2822. unset($this->modulesLastVersions[$id]);
  2823. }
  2824. $this->updateModuleVersionsCache();
  2825. }
  2826. }
  2827. /**
  2828. * Hook called when a module's version changes
  2829. *
  2830. * This calls the module's ___upgrade($fromVersion, $toVersion) method.
  2831. *
  2832. * @param Module $module
  2833. * @param int|string $fromVersion
  2834. * @param int|string $toVersion
  2835. *
  2836. */
  2837. protected function ___moduleVersionChanged(Module $module, $fromVersion, $toVersion) {
  2838. $moduleName = get_class($module);
  2839. $moduleID = $this->getModuleID($module);
  2840. $fromVersionStr = $this->formatVersion($fromVersion);
  2841. $toVersionStr = $this->formatVersion($toVersion);
  2842. $this->message($this->_('Upgrading module') . " ($moduleName: $fromVersionStr => $toVersionStr)");
  2843. try {
  2844. if(method_exists($module, '___upgrade')) {
  2845. $module->upgrade($fromVersion, $toVersion);
  2846. }
  2847. unset($this->modulesLastVersions[$moduleID]);
  2848. } catch(Exception $e) {
  2849. $this->error("Error upgrading module ($moduleName): " . $e->getMessage());
  2850. }
  2851. }
  2852. /**
  2853. * Update module flags if any happen to differ from what's in the given moduleInfo
  2854. *
  2855. * @param $moduleID
  2856. * @param array $info
  2857. *
  2858. */
  2859. protected function updateModuleFlags($moduleID, array $info) {
  2860. $flags = (int) $this->getFlags($moduleID);
  2861. if($info['autoload']) {
  2862. // module is autoload
  2863. if(!($flags & self::flagsAutoload)) {
  2864. // add autoload flag
  2865. $this->setFlag($moduleID, self::flagsAutoload, true);
  2866. }
  2867. if(is_string($info['autoload'])) {
  2868. // requires conditional flag
  2869. // value is either: "function", or the conditional string (like key=value)
  2870. if(!($flags & self::flagsConditional)) $this->setFlag($moduleID, self::flagsConditional, true);
  2871. } else {
  2872. // should not have conditional flag
  2873. if($flags & self::flagsConditional) $this->setFlag($moduleID, self::flagsConditional, false);
  2874. }
  2875. } else if($info['autoload'] !== null) {
  2876. // module is not autoload
  2877. if($flags & self::flagsAutoload) {
  2878. // remove autoload flag
  2879. $this->setFlag($moduleID, self::flagsAutoload, false);
  2880. }
  2881. if($flags & self::flagsConditional) {
  2882. // remove conditional flag
  2883. $this->setFlag($moduleID, self::flagsConditional, false);
  2884. }
  2885. }
  2886. if($info['singular']) {
  2887. if(!($flags & self::flagsSingular)) $this->setFlag($moduleID, self::flagsSingular, true);
  2888. } else {
  2889. if($flags & self::flagsSingular) $this->setFlag($moduleID, self::flagsSingular, false);
  2890. }
  2891. }
  2892. /**
  2893. * Save the module information cache
  2894. *
  2895. */
  2896. protected function saveModuleInfoCache() {
  2897. if($this->debug) {
  2898. static $n = 0;
  2899. $this->message("saveModuleInfoCache (" . (++$n) . ")");
  2900. }
  2901. $this->moduleInfoCache = array();
  2902. $this->moduleInfoCacheVerbose = array();
  2903. $this->moduleInfoCacheUninstalled = array();
  2904. $user = $this->wire('user');
  2905. $languages = $this->wire('languages');
  2906. if($languages) {
  2907. // switch to default language to prevent caching of translated title/summary data
  2908. $language = $user->language;
  2909. try {
  2910. if($language && $language->id && !$language->isDefault()) $user->language = $languages->getDefault(); // save
  2911. } catch(Exception $e) {
  2912. $this->trackException($e, false, true);
  2913. }
  2914. }
  2915. foreach(array(true, false) as $installed) {
  2916. $items = $installed ? $this : array_keys($this->installable);
  2917. foreach($items as $module) {
  2918. $class = is_object($module) ? $module->className() : $module;
  2919. $info = $this->getModuleInfo($class, array('noCache' => true, 'verbose' => true));
  2920. $moduleID = (int) $info['id']; // note ID is always 0 for uninstalled modules
  2921. if(!empty($info['error'])) {
  2922. if($this->debug) $this->warning("$class reported error: $info[error]");
  2923. continue;
  2924. }
  2925. if(!$moduleID && $installed) {
  2926. if($this->debug) $this->warning("No module ID for $class");
  2927. continue;
  2928. }
  2929. if(!$this->debug) unset($info['id']); // no need to double store this property since it is already the array key
  2930. if(is_null($info['autoload'])) {
  2931. // module info does not indicate an autoload state
  2932. $info['autoload'] = $this->isAutoload($module);
  2933. } else if(!is_bool($info['autoload']) && !is_string($info['autoload']) && is_callable($info['autoload'])) {
  2934. // runtime function, identify it only with 'function' so that it can be recognized later as one that
  2935. // needs to be dynamically loaded
  2936. $info['autoload'] = 'function';
  2937. }
  2938. if(is_null($info['singular'])) {
  2939. $info['singular'] = $this->isSingular($module);
  2940. }
  2941. if(is_null($info['configurable'])) {
  2942. $info['configurable'] = $this->isConfigurableModule($module, false);
  2943. }
  2944. if($moduleID) $this->updateModuleFlags($moduleID, $info);
  2945. // no need to store full path
  2946. $info['file'] = str_replace($this->wire('config')->paths->root, '', $info['file']);
  2947. if($installed) {
  2948. $verboseKeys = $this->moduleInfoVerboseKeys;
  2949. $verboseInfo = array();
  2950. foreach($verboseKeys as $key) {
  2951. if(!empty($info[$key])) $verboseInfo[$key] = $info[$key];
  2952. unset($info[$key]); // remove from regular moduleInfo
  2953. }
  2954. $this->moduleInfoCache[$moduleID] = $info;
  2955. $this->moduleInfoCacheVerbose[$moduleID] = $verboseInfo;
  2956. } else {
  2957. $this->moduleInfoCacheUninstalled[$class] = $info;
  2958. }
  2959. }
  2960. }
  2961. $caches = array(
  2962. self::moduleInfoCacheName => 'moduleInfoCache',
  2963. self::moduleInfoCacheVerboseName => 'moduleInfoCacheVerbose',
  2964. self::moduleInfoCacheUninstalledName => 'moduleInfoCacheUninstalled',
  2965. );
  2966. foreach($caches as $cacheName => $varName) {
  2967. $data = $this->$varName;
  2968. foreach($data as $moduleID => $moduleInfo) {
  2969. foreach($moduleInfo as $key => $value) {
  2970. // remove unpopulated properties
  2971. if($key == 'installed') {
  2972. // no need to store an installed==true property
  2973. if($value) unset($data[$moduleID][$key]);
  2974. } else if($key == 'requires' && !empty($value) && !empty($data[$moduleID]['requiresVersions'])) {
  2975. // requiresVersions has enough info to re-construct requires, so no need to store it
  2976. unset($data[$moduleID][$key]);
  2977. } else if(($key == 'created' && empty($value))
  2978. || ($value === 0 && ($key == 'singular' || $key == 'autoload' || $key == 'configurable'))
  2979. || ($value === null || $value === "" || $value === false)
  2980. || (is_array($value) && !count($value))) {
  2981. // no need to store these false, null, 0, or blank array properties
  2982. unset($data[$moduleID][$key]);
  2983. }
  2984. }
  2985. }
  2986. $this->wire('cache')->save($cacheName, $data, WireCache::expireNever);
  2987. }
  2988. $this->log('Saved module info caches');
  2989. if($languages && $language) $user->language = $language; // restore
  2990. }
  2991. /**
  2992. * Start a debug timer, only works when module debug mode is on ($this->debug)
  2993. *
  2994. * @param $note
  2995. * @return int|null Returns a key for the debug timer
  2996. *
  2997. */
  2998. protected function debugTimerStart($note) {
  2999. if(!$this->debug) return null;
  3000. $key = count($this->debugLog);
  3001. while(isset($this->debugLog[$key])) $key++;
  3002. $this->debugLog[$key] = array(
  3003. 0 => Debug::timer("Modules$key"),
  3004. 1 => $note
  3005. );
  3006. return $key;
  3007. }
  3008. /**
  3009. * Stop a debug timer, only works when module debug mode is on ($this->debug)
  3010. *
  3011. * @param int $key The key returned by debugTimerStart
  3012. *
  3013. */
  3014. protected function debugTimerStop($key) {
  3015. if(!$this->debug) return;
  3016. $log = $this->debugLog[$key];
  3017. $timerKey = $log[0];
  3018. $log[0] = Debug::timer($timerKey);
  3019. $this->debugLog[$key] = $log;
  3020. Debug::removeTimer($timerKey);
  3021. }
  3022. /**
  3023. * Return a log of module construct, init and ready times, active only when debug mode is on ($this->debug)
  3024. *
  3025. * @return array
  3026. *
  3027. */
  3028. public function getDebugLog() {
  3029. return $this->debugLog;
  3030. }
  3031. /**
  3032. * Substitute one module for another, to be used only when $moduleName doesn't exist.
  3033. *
  3034. * @param string $moduleName Module class name that may need a substitute
  3035. * @param string $substituteName Module class name you want to substitute when $moduleName isn't found.
  3036. * Specify null to remove substitute.
  3037. *
  3038. */
  3039. public function setSubstitute($moduleName, $substituteName = null) {
  3040. if(is_null($substituteName)) {
  3041. unset($this->substitutes[$moduleName]);
  3042. } else {
  3043. $this->substitutes[$moduleName] = $substituteName;
  3044. }
  3045. }
  3046. /**
  3047. * Substitute modules for other modules, to be used only when $moduleName doesn't exist.
  3048. *
  3049. * This appends existing entries rather than replacing them.
  3050. *
  3051. * @param array $substitutes Array of module name => substitute module name
  3052. *
  3053. */
  3054. public function setSubstitutes(array $substitutes) {
  3055. $this->substitutes = array_merge($this->substitutes, $substitutes);
  3056. }
  3057. /**
  3058. * Load module related CSS and JS files
  3059. *
  3060. * Applies only to modules that carry class-named CSS and/or JS files,
  3061. * such as Process, Inputfield and ModuleJS modules.
  3062. *
  3063. * @param Module|int|string $module Module object or class name
  3064. * @return array Returns number of files that were added
  3065. *
  3066. */
  3067. public function loadModuleFileAssets($module) {
  3068. $class = $this->getModuleClass($module);
  3069. static $classes = array();
  3070. if(isset($classes[$class])) return 0; // already loaded
  3071. $info = null;
  3072. $config = $this->wire('config');
  3073. $path = $config->paths->$class;
  3074. $url = $config->urls->$class;
  3075. $debug = $config->debug;
  3076. $version = 0;
  3077. $cnt = 0;
  3078. foreach(array('styles' => 'css', 'scripts' => 'js') as $type => $ext) {
  3079. $fileURL = '';
  3080. $modified = 0;
  3081. $file = "$path$class.$ext";
  3082. $minFile = "$path$class.min.$ext";
  3083. if(!$debug && is_file($minFile)) {
  3084. $fileURL = "$url$class.min.$ext";
  3085. $modified = filemtime($minFile);
  3086. } else if(is_file($file)) {
  3087. $fileURL = "$url$class.$ext";
  3088. $modified = filemtime($file);
  3089. }
  3090. if($fileURL) {
  3091. if(!$version) {
  3092. $info = $this->getModuleInfo($module, array('verbose' => false));
  3093. $version = (int) isset($info['version']) ? $info['version'] : 0;
  3094. }
  3095. $config->$type->add("$fileURL?v=$version-$modified");
  3096. $cnt++;
  3097. }
  3098. }
  3099. $classes[$class] = true;
  3100. return $cnt;
  3101. }
  3102. /**
  3103. * Enables use of $modules('ModuleName')
  3104. *
  3105. * @param string $key
  3106. * @return mixed
  3107. *
  3108. */
  3109. public function __invoke($key) {
  3110. return $this->get($key);
  3111. }
  3112. /**
  3113. * Save to the modules log
  3114. *
  3115. * @param string $str Message to log
  3116. * @param string $moduleName
  3117. * @return WireLog
  3118. *
  3119. */
  3120. public function log($str, $moduleName = '') {
  3121. if(!in_array('modules', $this->wire('config')->logs)) return $this->___log();
  3122. if(!is_string($moduleName)) $moduleName = (string) $moduleName;
  3123. if($moduleName && strpos($str, $moduleName) === false) $str .= " (Module: $moduleName)";
  3124. return $this->___log($str, array('name' => 'modules'));
  3125. }
  3126. public function error($text, $flags = 0) {
  3127. $this->log($text);
  3128. return parent::error($text, $flags);
  3129. }
  3130. }