PageRenderTime 58ms CodeModel.GetById 15ms 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

Large files files are truncated, but you can click here to view the full file

  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. retu

Large files files are truncated, but you can click here to view the full file