PageRenderTime 24ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/wire/core/ModulesDuplicates.php

http://github.com/ryancramerdesign/ProcessWire
PHP | 357 lines | 186 code | 27 blank | 144 comment | 46 complexity | 24ef55ac8aa846e6d0be0f5cfd2d843c MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * ProcessWire Modules Duplicates
  4. *
  5. * Provides functions for managing sitautions where more than one
  6. * copy of the same module is intalled. This is a helper for the Modules class.
  7. *
  8. * ProcessWire 2.x
  9. * Copyright (C) 2015 by Ryan Cramer
  10. * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
  11. *
  12. * https://processwire.com
  13. *
  14. */
  15. class ModulesDuplicates extends Wire {
  16. /**
  17. * Array of modules where more than one copy was found
  18. *
  19. * Associative array of 'ModuleName' => array(path1, path2, ...)
  20. *
  21. * @var array
  22. *
  23. */
  24. protected $duplicates = array();
  25. /**
  26. * Specifies which module file to use in cases where there is more than one
  27. *
  28. * Array of 'ModuleName' => '/path/to/file/from/pw/root/file.module'
  29. *
  30. * @var array
  31. *
  32. */
  33. protected $duplicatesUse = array();
  34. /**
  35. * Number of new duplicates found while loading modules
  36. *
  37. * @var int
  38. *
  39. */
  40. protected $numNewDuplicates = 0;
  41. /**
  42. * Return quantity of new duplicates found while loading modules
  43. *
  44. * @return int
  45. *
  46. */
  47. public function numNewDuplicates() {
  48. return $this->numNewDuplicates;
  49. }
  50. /**
  51. * Get the current duplicate in use (string) or null if not specified
  52. *
  53. * @param $className
  54. * @return string|null Pathname or null
  55. *
  56. */
  57. public function getCurrent($className) {
  58. return isset($this->duplicatesUse[$className]) ? $this->duplicatesUse[$className] : null;
  59. }
  60. /**
  61. * Does the given module class have a duplicate?
  62. *
  63. * @param string $className
  64. * @param string $pathname Optionally specify the duplicate to check
  65. * @return bool
  66. *
  67. */
  68. public function hasDuplicate($className, $pathname = '') {
  69. if(!isset($this->duplicates[$className])) return false;
  70. if($pathname) {
  71. $rootPath = $this->wire('config')->paths->root;
  72. if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
  73. return in_array($pathname, $this->duplicates[$className]);
  74. }
  75. return true;
  76. }
  77. /**
  78. * Add a duplicate to the list
  79. *
  80. * @param $className
  81. * @param $pathname
  82. * @param bool $current Is this the current one in use?
  83. *
  84. */
  85. public function addDuplicate($className, $pathname, $current = false) {
  86. if(!isset($this->duplicates[$className])) $this->duplicates[$className] = array();
  87. $rootPath = $this->wire('config')->paths->root;
  88. if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
  89. if(!in_array($pathname, $this->duplicates[$className])) {
  90. $this->duplicates[$className][] = $pathname;
  91. }
  92. if($current) {
  93. $this->duplicatesUse[$className] = $pathname;
  94. }
  95. }
  96. /**
  97. * Add multiple duplicates
  98. *
  99. * @param $className
  100. * @param array $files
  101. *
  102. */
  103. public function addDuplicates($className, array $files) {
  104. foreach($files as $file) {
  105. $this->addDuplicate($className, $file);
  106. }
  107. }
  108. /**
  109. * Add duplicates from module config data
  110. *
  111. * @param $className
  112. * @param array $data
  113. *
  114. */
  115. public function addFromConfigData($className, array $data) {
  116. $files = isset($data['-dups']) ? $data['-dups'] : array();
  117. $using = isset($data['-dups-use']) ? $data['-dups-use'] : '';
  118. if(count($files)) $this->addDuplicates($className, $files);
  119. if($using) $this->addDuplicate($className, $using, true); // set current, in-use
  120. }
  121. /**
  122. * Return a list of duplicate modules that were found
  123. *
  124. * If given a module className, the following is returned:
  125. *
  126. * Array(
  127. * 'files' => array(file1, file2, ...)
  128. * 'using' => '/path/to/file/from/pw/root/ModuleName.module' or blank if not defined
  129. * )
  130. *
  131. * If no className is specivied, the following is returned:
  132. *
  133. * Array(
  134. * 'ModuleName' => array(file1, file2, ...),
  135. * 'ModuleName' => array(file1, file2, ...),
  136. * ...and so on...
  137. * )
  138. *
  139. * @param string|Module|int $className Optionally return only duplicates for given module name
  140. *
  141. * @return array
  142. *
  143. */
  144. public function getDuplicates($className = '') {
  145. if(!$className) return $this->duplicates;
  146. $className = $this->wire('modules')->getModuleClass($className);
  147. $files = isset($this->duplicates[$className]) ? $this->duplicates[$className] : array();
  148. $using = isset($this->duplicatesUse[$className]) ? $this->duplicatesUse[$className] : '';
  149. $rootPath = $this->wire('config')->paths->root;
  150. foreach($files as $key => $file) {
  151. $file = rtrim($rootPath, '/') . $file;
  152. if(!file_exists($file)) {
  153. unset($files[$key]);
  154. }
  155. }
  156. if(count($files) > 1 && !$using) {
  157. $using = $this->wire('modules')->getModuleFile($className);
  158. $using = str_replace($rootPath, '/', $using);
  159. }
  160. if(count($files) < 2) {
  161. // no need to store duplicate info if only 0 or 1
  162. //unset($this->duplicates[$className], $this->duplicatesUse[$className]);
  163. $files = array();
  164. $using = '';
  165. }
  166. return array('files' => $files, 'using' => $using);
  167. }
  168. /**
  169. * For a module that has duplicates, tell it which file to use
  170. *
  171. * @param string $className
  172. * @param string $pathname Full path and filename to module file
  173. *
  174. * @throws WireException if given information that can't be resolved
  175. *
  176. */
  177. public function setUseDuplicate($className, $pathname) {
  178. $className = $this->wire('modules')->getModuleClass($className);
  179. $rootPath = $this->wire('config')->paths->root;
  180. if(!isset($this->duplicates[$className])) {
  181. throw new WireException("Module $className does not have duplicates");
  182. }
  183. $pathname = str_replace($rootPath, '/', $pathname);
  184. if(!in_array($pathname, $this->duplicates[$className])) {
  185. throw new WireException("Duplicate module pathname must be one of: " . implode(" \n", $this->duplicates[$className]));
  186. }
  187. if(!file_exists($rootPath . ltrim($pathname, '/'))) {
  188. throw new WireException("Duplicate module file does not exist: $pathname");
  189. }
  190. $this->duplicatesUse[$className] = $pathname;
  191. $configData = $this->wire('modules')->getModuleConfigData($className);
  192. $configData['-dups-use'] = $pathname;
  193. $this->wire('modules')->saveModuleConfigData($className, $configData);
  194. }
  195. /**
  196. * Update the database so that modules have information on their duplicates
  197. *
  198. */
  199. public function updateDuplicates() {
  200. $rootPath = $this->wire('config')->paths->root;
  201. // store duplicate information in each module's data field
  202. foreach($this->getDuplicates() as $moduleName => $files) {
  203. $dup = $this->getDuplicates($moduleName); // so that we also have 'using' info
  204. $files = $dup['files'];
  205. $using = $dup['using'];
  206. foreach($files as $key => $file) {
  207. // make files relative to site root, for portability
  208. $file = str_replace($rootPath, '/', $file);
  209. $files[$key] = $file;
  210. }
  211. $files = array_unique($files);
  212. $configData = $this->wire('modules')->getModuleConfigData($moduleName);
  213. if((empty($configData['-dups']) && !empty($files))
  214. || (empty($configData['-dups-use']) || $configData['-dups-use'] != $using)
  215. || (isset($configData['-dups']) && implode(' ', $configData['-dups']) != implode(' ', $files))
  216. ) {
  217. $this->duplicates[$moduleName] = $files;
  218. $this->duplicatesUse[$moduleName] = $using;
  219. $configData['-dups'] = $files;
  220. $configData['-dups-use'] = $using;
  221. $this->wire('modules')->saveModuleConfigData($moduleName, $configData);
  222. }
  223. }
  224. // update any modules that no longer have duplicates
  225. $removals = array();
  226. $query = $this->wire('database')->prepare("SELECT `class`, `flags` FROM modules WHERE `flags` & :flag");
  227. $query->bindValue(':flag', Modules::flagsDuplicate, PDO::PARAM_INT);
  228. $query->execute();
  229. while($row = $query->fetch(PDO::FETCH_NUM)) {
  230. list($class, $flags) = $row;
  231. if(empty($this->duplicates[$class])) {
  232. $flags = $flags & ~Modules::flagsDuplicate;
  233. $removals[$class] = $flags;
  234. }
  235. unset($this->duplicatesUse[$class]); // just in case
  236. }
  237. foreach($removals as $class => $flags) {
  238. $this->wire('modules')->setFlags($class, $flags);
  239. $configData = $this->wire('modules')->getModuleConfigData($class);
  240. unset($configData['-dups'], $configData['-dups-use']);
  241. $this->wire('modules')->saveModuleConfigData($class, $configData);
  242. }
  243. }
  244. /**
  245. * Record a duplicate at runtime
  246. *
  247. * @param string $basename Name of module
  248. * @param string $pathname Path of module
  249. * @param string $pathname2 Second path of module
  250. * @param array $installed Installed module info array
  251. *
  252. */
  253. public function recordDuplicate($basename, $pathname, $pathname2, &$installed) {
  254. $rootPath = $this->wire('config')->paths->root;
  255. // ensure paths start from root of PW install
  256. if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
  257. if(strpos($pathname2, $rootPath) === 0) $pathname2 = str_replace($rootPath, '/', $pathname2);
  258. // there are two copies of the module on the file system (likely one in /site/modules/ and another in /wire/modules/)
  259. if(!isset($this->duplicates[$basename])) {
  260. $this->duplicates[$basename] = array($pathname, $pathname2); // array(str_replace($rootPath, '/', $this->getModuleFile($basename)));
  261. $this->numNewDuplicates++;
  262. }
  263. if(!in_array($pathname, $this->duplicates[$basename])) {
  264. $this->duplicates[$basename][] = $pathname;
  265. $this->numNewDuplicates++;
  266. }
  267. if(!in_array($pathname2, $this->duplicates[$basename])) {
  268. $this->duplicates[$basename][] = $pathname2;
  269. $this->numNewDuplicates++;
  270. }
  271. if(isset($installed[$basename]['flags'])) {
  272. $flags = $installed[$basename]['flags'];
  273. } else {
  274. $flags = $this->wire('modules')->getFlags($basename);
  275. }
  276. if(!($installed[$basename]['flags'] & Modules::flagsDuplicate)) {
  277. // make database aware this module has multiple files by adding the duplicate flag
  278. $this->numNewDuplicates++; // trigger update needed
  279. $flags = $flags | Modules::flagsDuplicate;
  280. $this->wire('modules')->setFlags($basename, $flags);
  281. }
  282. $err = sprintf($this->_('There appear to be multiple copies of module "%s" on the file system.'), $basename) . ' ';
  283. $this->wire('log')->save('modules', $err);
  284. $user = $this->wire('user');
  285. if($user && $user->isSuperuser()) {
  286. $err .= $this->_('Please edit the module settings to tell ProcessWire which one to use:') . ' ' .
  287. "<a href='" . $this->wire('config')->urls->admin . 'module/edit?name=' . $basename . "'>$basename</a>";
  288. $this->warning($err, Notice::allowMarkup);
  289. }
  290. //$this->message("recordDuplicate($basename, $pathname) $this->numNewDuplicates"); //DEBUG
  291. //$this->message($this->duplicates[$basename]);//DEBUG
  292. }
  293. /**
  294. * Populate duplicates info into config data, when applicable
  295. *
  296. * @param $className
  297. * @param array $configData
  298. *
  299. * @return array Updated configData
  300. *
  301. */
  302. public function getDuplicatesConfigData($className, array $configData = array()) {
  303. // ensure original duplicates info is retained and validate that it is still current
  304. if(isset($this->duplicates[$className])) {
  305. foreach($this->duplicates[$className] as $key => $file) {
  306. $pathname = rtrim($this->wire('config')->paths->root, '/') . $file;
  307. if(!file_exists($pathname)) {
  308. unset($this->duplicates[$className][$key]);
  309. }
  310. }
  311. if(count($this->duplicates[$className]) < 2) {
  312. // no need to store any info for this if there's only 0 or 1
  313. unset($this->duplicates[$className], $this->duplicatesUse[$className], $configData['-dups'], $configData['-dups-use']);
  314. } else {
  315. $configData['-dups'] = $this->duplicates[$className];
  316. if(isset($this->duplicatesUse[$className])) {
  317. $pathname = rtrim($this->wire('config')->paths->root, '/') . $this->duplicatesUse[$className];
  318. if(file_exists($pathname)) {
  319. $configData['-dups-use'] = $this->duplicatesUse[$className];
  320. } else {
  321. unset($configData['-dups-use'], $this->duplicatesUse[$className]);
  322. }
  323. }
  324. }
  325. } else if(empty($this->duplicates[$className]) && isset($configData['-dups'])) {
  326. unset($configData['-dups'], $configData['-dups-use']);
  327. }
  328. return $configData;
  329. }
  330. }