PageRenderTime 64ms CodeModel.GetById 9ms RepoModel.GetById 1ms app.codeStats 0ms

/sally/core/lib/sly/Service/AddOn/Base.php

https://bitbucket.org/SallyCMS/0.6
PHP | 1132 lines | 567 code | 207 blank | 358 comment | 118 complexity | e5991b50821e48e084a908a6307b08e3 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /*
  3. * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
  4. *
  5. * This file is released under the terms of the MIT license. You can find the
  6. * complete text in the attached LICENSE file or online at:
  7. *
  8. * http://www.opensource.org/licenses/mit-license.php
  9. */
  10. /**
  11. * @author christoph@webvariants.de
  12. * @ingroup service
  13. */
  14. abstract class sly_Service_AddOn_Base {
  15. protected static $loaded = array(); ///< array list of loaded addOns and plugins for depedency aware loading
  16. private static $loadInfo = array();
  17. /**
  18. * @param mixed $component
  19. * @return string
  20. */
  21. abstract public function baseFolder($component);
  22. /**
  23. * @param mixed $component
  24. * @param string $property
  25. * @param mixed $value
  26. * @return mixed
  27. */
  28. abstract public function setProperty($component, $property, $value);
  29. /**
  30. * @param mixed $component
  31. * @param string $property
  32. * @param mixed $default
  33. * @return mixed
  34. */
  35. abstract public function getProperty($component, $property, $default = null);
  36. /**
  37. * @param string $type
  38. * @param mixed $component
  39. * @return string
  40. */
  41. abstract protected function dynFolder($type, $component);
  42. /**
  43. * @param string $time
  44. * @param string $type
  45. * @param mixed $component
  46. * @param boolean $state
  47. * @return mixed
  48. */
  49. abstract protected function extend($time, $type, $component, $state);
  50. /**
  51. * @param mixed $component
  52. * @return string
  53. */
  54. abstract protected function getVersionKey($component);
  55. /**
  56. * @param mixed $component
  57. * @return string
  58. */
  59. abstract protected function getConfPath($component);
  60. /**
  61. * @param mixed $component
  62. * @return boolean
  63. */
  64. abstract protected function exists($component);
  65. /**
  66. * Include file
  67. *
  68. * This prevents the included file from messing with the variables of the
  69. * surrounding code.
  70. *
  71. * @param string $filename
  72. */
  73. protected function req($filename) {
  74. require $filename;
  75. }
  76. /**
  77. * Loads the YAML config file
  78. *
  79. * Loads globals.yml and defaults.yml.
  80. *
  81. * @param mixed $component addOn as string, plugin as array
  82. */
  83. protected function loadConfig($component, $forceInstall = false, $forceActivated = false) {
  84. if ($forceInstall || $forceActivated || $this->isInstalled($component)) {
  85. $config = sly_Core::config();
  86. $baseFolder = $this->baseFolder($component);
  87. $defaultsFile = $baseFolder.'defaults.yml';
  88. $globalsFile = $baseFolder.'globals.yml';
  89. if ($forceActivated || $this->isActivated($component)) {
  90. $this->loadStatic($component, $baseFolder);
  91. if (file_exists($defaultsFile)) {
  92. $config->loadProjectDefaults($defaultsFile, false, $this->getConfPath($component));
  93. }
  94. }
  95. if (file_exists($globalsFile)) {
  96. $config->loadStatic($globalsFile);
  97. }
  98. }
  99. }
  100. /**
  101. * @param mixed $component
  102. */
  103. protected function loadStatic($component, $baseFolder = null) {
  104. $config = sly_Core::config();
  105. $baseFolder = $baseFolder === null ? $this->baseFolder($component) : $baseFolder;
  106. $staticFile = $baseFolder.'static.yml';
  107. if (file_exists($staticFile)) {
  108. $config->loadStatic($staticFile, $this->getConfPath($component));
  109. }
  110. }
  111. /**
  112. * Check if a version number matches
  113. *
  114. * This will take a well-formed version number (X.Y.Z) and compare it against
  115. * the system version. You can leave out parts from the right to make it
  116. * more general (i.e. '0.2' will match any 0.2.x version).
  117. *
  118. * @param string $version the version number to check against
  119. * @return boolean true for a match, else false
  120. */
  121. public function checkVersion($version) {
  122. $thisVersion = sly_Core::getVersion('X.Y.Z');
  123. return preg_match('#^'.preg_quote($version, '#').'.*#i', $thisVersion) == 1;
  124. }
  125. /**
  126. * Check if a component changed and must be disabled
  127. *
  128. * @return boolean true if there were changes, else false
  129. */
  130. public function deactivateIncompatibleComponents() {
  131. $addonService = sly_Service_Factory::getAddOnService();
  132. $pluginService = sly_Service_Factory::getPluginService();
  133. $changes = false;
  134. foreach ($addonService->getRegisteredAddons() as $addonName) {
  135. $oldVal = $addonService->getProperty($addonName, 'compatible');
  136. $newVal = $addonService->isCompatible($addonName, true);
  137. $changes |= ($oldVal !== $newVal);
  138. if ($oldVal !== $newVal) {
  139. $addonService->setProperty($addonName, 'compatible', $newVal);
  140. }
  141. // disable all dependencies
  142. if ($oldVal !== $newVal && $newVal === false) {
  143. $deps = $addonService->getRecursiveDependencies($addonName);
  144. $addonService->setProperty($addonName, 'status', false);
  145. foreach ($deps as $dep) {
  146. if (is_array($dep)) {
  147. $pluginService->setProperty($dep, 'status', false);
  148. }
  149. else {
  150. $addonService->setProperty($dep, 'status', false);
  151. }
  152. }
  153. }
  154. foreach ($pluginService->getRegisteredPlugins($addonName) as $pluginName) {
  155. $plugin = array($addonName, $pluginName);
  156. $oldVal = $pluginService->getProperty($plugin, 'compatible');
  157. $newVal = $pluginService->isCompatible($plugin, true);
  158. $changes |= ($oldVal !== $newVal);
  159. if ($oldVal !== $newVal) {
  160. $pluginService->setProperty($plugin, 'compatible', $newVal);
  161. }
  162. // disable all dependencies
  163. if ($oldVal !== $newVal && $newVal === false) {
  164. $deps = $pluginService->getRecursiveDependencies($plugin);
  165. $pluginService->setProperty($plugin, 'status', false);
  166. foreach ($deps as $dep) {
  167. if (is_array($dep)) {
  168. $pluginService->setProperty($dep, 'status', false);
  169. }
  170. else {
  171. $addonService->setProperty($dep, 'status', false);
  172. }
  173. }
  174. }
  175. }
  176. }
  177. if ($changes) {
  178. $this->clearLoadCache();
  179. }
  180. return $changes;
  181. }
  182. /**
  183. * Adds a new component to the global config
  184. *
  185. * @param mixed $component addOn as string, plugin as array
  186. */
  187. public function add($component) {
  188. $this->setProperty($component, 'install', false);
  189. $this->setProperty($component, 'status', false);
  190. $this->setProperty($component, 'compatible', $this->isCompatible($component, true));
  191. // only add plugins key on addOns
  192. if (!is_array($component)) {
  193. $this->setProperty($component, 'plugins', array());
  194. }
  195. }
  196. /**
  197. * Removes a component from the global config
  198. *
  199. * @param mixed $component addOn as string, plugin as array
  200. */
  201. public function removeConfig($component) {
  202. $config = sly_Core::config();
  203. $config->remove($this->getConfPath($component));
  204. }
  205. /**
  206. * Get string with links to support pages
  207. *
  208. * @param mixed $component addOn as string, plugin as array
  209. * @return string a comma separated list of URLs
  210. */
  211. public function getSupportPageEx($component) {
  212. $supportPage = $this->getSupportPage($component, '');
  213. $author = $this->getAuthor($component);
  214. if ($supportPage) {
  215. $supportPages = sly_makeArray($supportPage);
  216. $links = array();
  217. foreach ($supportPages as $idx => $page) {
  218. $infos = parse_url($page);
  219. if (!isset($infos['host'])) $infos = parse_url('http://'.$page);
  220. if (!isset($infos['host'])) continue;
  221. $page = sprintf('%s://%s%s', $infos['scheme'], $infos['host'], isset($infos['path']) ? $infos['path'] : '');
  222. $host = substr($infos['host'], 0, 4) == 'www.' ? substr($infos['host'], 4) : $infos['host'];
  223. $name = $idx === 0 && !empty($author) ? $author : $host;
  224. $name = sly_Util_String::cutText($name, 40);
  225. $links[] = '<a href="'.sly_html($page).'" class="sly-blank">'.sly_html($name).'</a>';
  226. }
  227. $supportPage = implode(', ', $links);
  228. }
  229. return $supportPage;
  230. }
  231. /**
  232. * @param mixed $component addOn as string, plugin as array
  233. * @return string
  234. */
  235. private function buildComponentName($component) {
  236. return is_array($component) ? implode('/', $component) : $component;
  237. }
  238. /**
  239. * Install a component
  240. *
  241. * @param mixed $component addOn as string, plugin as array
  242. * @param boolean $installDump
  243. * @return mixed message or true if successful
  244. */
  245. public function install($component, $installDump = true) {
  246. $baseDir = $this->baseFolder($component);
  247. $configFile = $baseDir.'config.inc.php';
  248. $name = $this->buildComponentName($component);
  249. // return error message if an addOn wants to stop the install process
  250. $state = $this->extend('PRE', 'INSTALL', $component, true);
  251. if ($state !== true) {
  252. return $state;
  253. }
  254. // check for config.inc.php before we do anything
  255. if (!is_readable($configFile)) {
  256. return t('component_config_not_found');
  257. }
  258. // check requirements
  259. if (!$this->isAvailable($component)) {
  260. $this->loadStatic($component); // static.yml
  261. }
  262. $msg = $this->checkRequirements($component);
  263. if ($msg !== true) {
  264. return $msg;
  265. }
  266. // check Sally version
  267. $sallyVersions = $this->getProperty($component, 'sally');
  268. if (!empty($sallyVersions)) {
  269. if (!$this->isCompatible($component)) {
  270. return t('component_incompatible', $name, sly_Core::getVersion('X.Y.Z'));
  271. }
  272. }
  273. else {
  274. return t('component_has_no_sally_version_info', $name);
  275. }
  276. // include install.inc.php if available
  277. $installFile = $baseDir.'install.inc.php';
  278. if (is_readable($installFile)) {
  279. try {
  280. $this->req($installFile);
  281. }
  282. catch (Exception $e) {
  283. return t('component_install_failed', $name, $e->getMessage());
  284. }
  285. }
  286. // read install.sql and install DB
  287. $installSQL = $baseDir.'install.sql';
  288. if ($installDump && is_readable($installSQL)) {
  289. $state = $this->installDump($installSQL);
  290. if ($state !== true) {
  291. return t('component_install_sql_failed', $name, $state);
  292. }
  293. }
  294. // copy assets to data/dyn/public
  295. if (is_dir($baseDir.'assets')) {
  296. $this->copyAssets($component);
  297. }
  298. // load globals.yml
  299. $globalsFile = $this->baseFolder($component).'globals.yml';
  300. if (!$this->isAvailable($component) && file_exists($globalsFile)) {
  301. sly_Core::config()->loadStatic($globalsFile);
  302. }
  303. // mark component as installed
  304. $this->setProperty($component, 'install', true);
  305. $this->clearLoadCache();
  306. // store current component version
  307. $version = $this->getProperty($component, 'version', false);
  308. if ($version !== false) {
  309. sly_Util_Versions::set($this->getVersionKey($component), $version);
  310. }
  311. // notify listeners
  312. return $this->extend('POST', 'INSTALL', $component, true);
  313. }
  314. /**
  315. * Uninstall a component
  316. *
  317. * @param $component addOn as string, plugin as array
  318. * @return mixed message or true if successful
  319. */
  320. public function uninstall($component) {
  321. // if not installed, try to disable if needed
  322. if (!$this->isInstalled($component)) {
  323. return $this->deactivate($component);
  324. }
  325. // check for dependencies
  326. $state = $this->checkRemoval($component);
  327. if ($state !== true) return $state;
  328. // stop if addOn forbids uninstall
  329. $state = $this->extend('PRE', 'UNINSTALL', $component, true);
  330. if ($state !== true) {
  331. return $state;
  332. }
  333. // deactivate addOn first
  334. $state = $this->deactivate($component);
  335. if ($state !== true) {
  336. return $state;
  337. }
  338. // include uninstall.inc.php if available
  339. $baseDir = $this->baseFolder($component);
  340. $uninstallFile = $baseDir.'uninstall.inc.php';
  341. $name = $this->buildComponentName($component);
  342. if (is_readable($uninstallFile)) {
  343. try {
  344. $this->req($uninstallFile);
  345. }
  346. catch (Exception $e) {
  347. return t('component_uninstall_failed', $name, $e->getMessage());
  348. }
  349. }
  350. // read uninstall.sql
  351. $uninstallSQL = $baseDir.'uninstall.sql';
  352. if (is_readable($uninstallSQL)) {
  353. $state = $this->installDump($uninstallSQL);
  354. if ($state !== true) {
  355. return t('component_uninstall_sql_failed', $name, $state);
  356. }
  357. }
  358. // mark component as not installed
  359. $this->setProperty($component, 'install', false);
  360. $this->clearLoadCache();
  361. // delete files
  362. $state = $this->deletePublicFiles($component);
  363. $stateB = $this->deleteInternalFiles($component);
  364. if ($stateB !== true) {
  365. // overwrite or concat stati
  366. $state = $state === true ? $stateB : $stateA.'<br />'.$stateB;
  367. }
  368. sly_Util_Versions::remove($this->getVersionKey($component));
  369. // notify listeners
  370. return $this->extend('POST', 'UNINSTALL', $component, $state);
  371. }
  372. /**
  373. * Activate a component
  374. *
  375. * @param mixed $component addOn as string, plugin as array
  376. * @return mixed true if successful, else an error message as a string
  377. */
  378. public function activate($component) {
  379. if ($this->isActivated($component)) {
  380. return true;
  381. }
  382. $name = $this->buildComponentName($component);
  383. if (!$this->isInstalled($component, $name)) {
  384. return t('component_activate_failed');
  385. }
  386. // We can't use the service to get the list of required addOns since the
  387. // static.yml has not yet been loaded.
  388. @$this->loadStatic($component);
  389. $msg = $this->checkRequirements($component);
  390. if ($msg !== true) {
  391. return $msg;
  392. }
  393. $state = $this->extend('PRE', 'ACTIVATE', $component, true);
  394. if ($state !== true) {
  395. return $state;
  396. }
  397. $this->checkUpdate($component);
  398. $this->setProperty($component, 'status', true);
  399. $this->clearLoadCache();
  400. return $this->extend('POST', 'ACTIVATE', $component, true);
  401. }
  402. /**
  403. * Deactivate a component
  404. *
  405. * @param mixed $component addOn as string, plugin as array
  406. * @return mixed true if successful, else an error message as a string
  407. */
  408. public function deactivate($component) {
  409. if (!$this->isActivated($component)) {
  410. return true;
  411. }
  412. $state = $this->checkRemoval($component);
  413. if ($state !== true) return $state;
  414. $state = $this->extend('PRE', 'DEACTIVATE', $component, true);
  415. if ($state !== true) return $state;
  416. $this->setProperty($component, 'status', false);
  417. $this->clearLoadCache();
  418. return $this->extend('POST', 'DEACTIVATE', $component, true);
  419. }
  420. /**
  421. * Check if a component may be removed
  422. *
  423. * @param mixed $component addOn as string, plugin as array
  424. * @return mixed true if successful, else an error message as a string
  425. */
  426. private function checkRemoval($component) {
  427. // Check if this component is required
  428. $dependencies = $this->getDependencies($component, true);
  429. if (!empty($dependencies)) {
  430. $dep = reset($dependencies);
  431. $msg = is_array($dep) ? 'requires_plugin' : 'requires_addon';
  432. $comp = $this->buildComponentName($component);
  433. $dep = $this->buildComponentName($dep);
  434. return t('component_'.$msg, $comp, $dep);
  435. }
  436. return true;
  437. }
  438. /**
  439. * Get the full path to the public folder
  440. *
  441. * @param mixed $component addOn as string, plugin as array
  442. * @return string full path
  443. */
  444. public function publicFolder($component) {
  445. return $this->dynFolder('public', $component);
  446. }
  447. /**
  448. * Get the full path to the internal folder
  449. *
  450. * @param mixed $component addOn as string, plugin as array
  451. * @return string full path
  452. */
  453. public function internalFolder($component) {
  454. return $this->dynFolder('internal', $component);
  455. }
  456. /**
  457. * Removes all public files
  458. *
  459. * @param mixed $component addOn as string, plugin as array
  460. * @return mixed true if successful, else an error message as a string
  461. */
  462. public function deletePublicFiles($component) {
  463. return $this->deleteFiles('public', $component);
  464. }
  465. /**
  466. * Removes all internal files
  467. *
  468. * @param mixed $component addOn as string, plugin as array
  469. * @return mixed true if successful, else an error message as a string
  470. */
  471. public function deleteInternalFiles($component) {
  472. return $this->deleteFiles('internal', $component);
  473. }
  474. /**
  475. * Removes all files in a directory
  476. *
  477. * @param string $type 'public' or 'internal'
  478. * @param mixed $component addOn as string, plugin as array
  479. * @return mixed true if successful, else an error message as a string
  480. */
  481. protected function deleteFiles($type, $component) {
  482. $dir = $this->dynFolder($type, $component);
  483. $state = $this->extend('PRE', 'DELETE_'.strtoupper($type), $component, true);
  484. if ($state !== true) {
  485. return $state;
  486. }
  487. $obj = new sly_Util_Directory($dir);
  488. if (!$obj->delete(true)) {
  489. return t('component_cleanup_failed', $dir);
  490. }
  491. return $this->extend('POST', 'DELETE_'.strtoupper($type), $component, true);
  492. }
  493. /**
  494. * Check if a component is installed and activated
  495. *
  496. * @param mixed $component addOn as string, plugin as array
  497. * @return boolean true if available, else false
  498. */
  499. public function isAvailable($component) {
  500. // If we execute both checks in this order, we avoid the overhead of checking
  501. // the install status of a disabled addon.
  502. return $this->isActivated($component) && $this->isInstalled($component);
  503. }
  504. /**
  505. * Check if a component is installed
  506. *
  507. * @param mixed $component addOn as string, plugin as array
  508. * @return boolean true if installed, else false
  509. */
  510. public function isInstalled($component) {
  511. return $this->getProperty($component, 'install', false) == true;
  512. }
  513. /**
  514. * Check if a component is activated
  515. *
  516. * @param mixed $component addOn as string, plugin as array
  517. * @return boolean true if activated, else false
  518. */
  519. public function isActivated($component) {
  520. return $this->getProperty($component, 'status', false) == true;
  521. }
  522. /**
  523. * Get component author
  524. *
  525. * @param mixed $component addOn as string, plugin as array
  526. * @param mixed $default default value if no author was specified in static.yml
  527. * @return mixed the author as given in static.yml
  528. */
  529. public function getAuthor($component, $default = null) {
  530. return $this->readConfigValue($component, 'author', $default);
  531. }
  532. /**
  533. * Get support page
  534. *
  535. * @param mixed $component addOn as string, plugin as array
  536. * @param mixed $default default value if no page was specified in static.yml
  537. * @return mixed the support page as given in static.yml
  538. */
  539. public function getSupportPage($component, $default = null) {
  540. return $this->readConfigValue($component, 'supportpage', $default);
  541. }
  542. /**
  543. * Get version
  544. *
  545. * This method tries to get the version from the static.yml. If no version is
  546. * found, it tries to read the contents of a version file in the component's
  547. * directory.
  548. *
  549. * @param mixed $component addOn as string, plugin as array
  550. * @param mixed $default default value if no version was specified
  551. * @return string the version
  552. */
  553. public function getVersion($component, $default = null) {
  554. $version = $this->readConfigValue($component, 'version', null);
  555. $baseFolder = $this->baseFolder($component);
  556. $versionFile = $baseFolder.'/version';
  557. if ($version === null && file_exists($versionFile)) {
  558. $version = trim(file_get_contents($versionFile));
  559. }
  560. $versionFile = $baseFolder.'/VERSION';
  561. if ($version === null && file_exists($versionFile)) {
  562. $version = trim(file_get_contents($versionFile));
  563. }
  564. return $version === null ? $default : $version;
  565. }
  566. /**
  567. * Get last known version
  568. *
  569. * This method reads the last known version from the local config. This can
  570. * be used to determine whether a component has been updated.
  571. *
  572. * @param mixed $component addOn as string, plugin as array
  573. * @param mixed $default default value if no version was specified
  574. * @return string the version
  575. */
  576. public function getKnownVersion($component, $default = null) {
  577. $key = $this->getVersionKey($component);
  578. $version = sly_Util_Versions::get($key);
  579. return $version === false ? $default : $version;
  580. }
  581. /**
  582. * Copy assets from component to it's public folder
  583. *
  584. * This method copies all files in 'assets' and pipis CSS files through
  585. * Scaffold.
  586. *
  587. * @param mixed $component addOn as string, plugin as array
  588. * @return mixed true if successful, else an error message as a string
  589. */
  590. public function copyAssets($component) {
  591. $baseDir = $this->baseFolder($component);
  592. $assetsDir = sly_Util_Directory::join($baseDir, 'assets');
  593. $target = $this->publicFolder($component);
  594. if (!is_dir($assetsDir)) return true;
  595. $dir = new sly_Util_Directory($assetsDir);
  596. if (!$dir->copyTo($target)) {
  597. return t('component_assets_failed', $assetsDir);
  598. }
  599. return true;
  600. }
  601. /**
  602. * Check if a component version has changed
  603. *
  604. * This method detects changing versions and tries to include the
  605. * update.inc.php if available.
  606. *
  607. * @param mixed $component addOn as string, plugin as array
  608. */
  609. public function checkUpdate($component) {
  610. $version = $this->getVersion($component, false);
  611. $key = $this->getVersionKey($component);
  612. $known = sly_Util_Versions::get($key, false);
  613. if ($known !== false && $version !== false && $known !== $version) {
  614. $updateFile = $this->baseFolder($component).'update.inc.php';
  615. if (file_exists($updateFile)) {
  616. $this->req($updateFile);
  617. }
  618. }
  619. if ($version !== false && $known !== $version) {
  620. sly_Util_Versions::set($key, $version);
  621. }
  622. }
  623. /**
  624. * @param string $file
  625. * @return mixed error message (string) or true
  626. */
  627. private function installDump($file) {
  628. try {
  629. $dump = new sly_DB_Dump($file);
  630. $sql = sly_DB_Persistence::getInstance();
  631. foreach ($dump->getQueries(true) as $query) {
  632. $sql->query($query);
  633. }
  634. }
  635. catch (sly_DB_Exception $e) {
  636. return $e->getMessage();
  637. }
  638. return true;
  639. }
  640. /**
  641. * @param mixed $component
  642. * @return mixed true if OK, else error message (string)
  643. */
  644. private function checkRequirements($component) {
  645. $requires = $this->getRequirements($component);
  646. $aService = sly_Service_Factory::getAddOnService();
  647. $pService = sly_Service_Factory::getPluginService();
  648. $componentName = $this->buildComponentName($component);
  649. foreach ($requires as $requiredComponent) {
  650. $requirement = explode('/', $requiredComponent, 2);
  651. if (count($requirement) === 1 && !$aService->isAvailable($requirement[0])) {
  652. return t('component_requires_addon', $requirement[0], $componentName);
  653. }
  654. if (count($requirement) === 2 && !$pService->isAvailable($requirement)) {
  655. return t('component_requires_plugin', $requiredComponent, $componentName);
  656. }
  657. }
  658. return true;
  659. }
  660. /**
  661. * Returns a list of dependent components
  662. *
  663. * This method will go through all addOns and plugins and check whether they
  664. * require the given component.
  665. *
  666. * @param mixed $component addOn as string, plugin as array
  667. * @param boolean $onlyMissing if true, only not available components will be returned
  668. * @return array a list of components (containing strings for addOns and arrays for plugins)
  669. */
  670. public function getDependencies($component, $onlyMissing = false) {
  671. return $this->dependencyHelper($component, $onlyMissing);
  672. }
  673. /**
  674. * Returns a list of dependent components
  675. *
  676. * This method will go through all addOns and plugins and check whether they
  677. * require the given component.
  678. *
  679. * @param mixed $component addOn as string, plugin as array
  680. * @param boolean $inclDeactivated if true non-enabled components will be included as well
  681. * @return array a list of components (containing strings for addOns and arrays for plugins)
  682. */
  683. public function getRecursiveDependencies($component, $inclDeactivated = false) {
  684. $stack = $this->dependencyHelper($component, false, false, $inclDeactivated);
  685. $result = array();
  686. while (!empty($stack)) {
  687. $comp = array_shift($stack);
  688. $stack = array_merge($stack, $this->dependencyHelper($comp, false, false, $inclDeactivated));
  689. $stack = array_unique($stack);
  690. $result[] = $comp;
  691. }
  692. return $result;
  693. }
  694. /**
  695. * Returns a list of dependent components
  696. *
  697. * This method will go through all addOns and plugins and check whether they
  698. * require the given component. The return value will only contain direct
  699. * dependencies, it's not recursive.
  700. *
  701. * @param mixed $component addOn as string, plugin as array
  702. * @param boolean $onlyMissing if true, only not available components will be returned
  703. * @param boolean $onlyFirst set this to true if you're only want to know whether a dependency exists
  704. * @param boolean $inclDeactivated if true non-enabled components will be included as well
  705. * @return array a list of components (containing strings for addOns and arrays for plugins)
  706. */
  707. public function dependencyHelper($component, $onlyMissing = false, $onlyFirst = false, $inclDeactivated = false) {
  708. $addonService = sly_Service_Factory::getAddOnService();
  709. $pluginService = sly_Service_Factory::getPluginService();
  710. $addons = $inclDeactivated ? $addonService->getInstalledAddons() : $addonService->getAvailableAddons();
  711. $result = array();
  712. $compAsString = $this->buildComponentName($component);
  713. foreach ($addons as $addon) {
  714. // don't check yourself
  715. if ($compAsString === $addon) continue;
  716. $requires = $addonService->getRequirements($addon, true);
  717. $inArray = in_array($compAsString, $requires);
  718. $visible = !$onlyMissing || !$addonService->isActivated($addon);
  719. if ($visible && $inArray) {
  720. if ($onlyFirst) return array($addon);
  721. $result[] = $addon;
  722. }
  723. $plugins = $inclDeactivated ? $pluginService->getInstalledPlugins($addon) : $pluginService->getAvailablePlugins($addon);
  724. foreach ($plugins as $plugin) {
  725. $pComp = array($addon, $plugin);
  726. $requires = $pluginService->getRequirements($pComp, true);
  727. $inArray = in_array($compAsString, $requires);
  728. $visible = !$onlyMissing || !$pluginService->isActivated($pComp);
  729. if ($visible && $inArray) {
  730. if ($onlyFirst) return array($pComp);
  731. $result[] = $pComp;
  732. }
  733. }
  734. }
  735. return $onlyFirst ? (empty($result) ? '' : reset($result)) : $result;
  736. }
  737. /**
  738. * Check if a component is required
  739. *
  740. * @param mixed $component addOn as string, plugin as array
  741. * @return mixed false if not required, else the first found dependency
  742. */
  743. public function isRequired($component) {
  744. $dependency = $this->dependencyHelper($component, false, true);
  745. return empty($dependency) ? false : reset($dependency);
  746. }
  747. /**
  748. * Return a list of required addOns / plugins
  749. *
  750. * @param mixed $component addOn as string, plugin as array
  751. * @return array list of required components
  752. */
  753. public function getRequirements($component, $forceRefresh = false) {
  754. $req = sly_makeArray($this->readConfigValue($component, 'requires', null, $forceRefresh));
  755. foreach ($req as $idx => $r) {
  756. if (strpos($r, '/') !== false) {
  757. $req[$idx] = explode('/', $r);
  758. }
  759. }
  760. return $req;
  761. }
  762. /**
  763. * Return a list of Sally versions the component is compatible with
  764. *
  765. * @param mixed $component addOn as string, plugin as array
  766. * @return array list of sally versions
  767. */
  768. public function getRequiredSallyVersions($component, $forceRefresh = false) {
  769. return sly_makeArray($this->readConfigValue($component, 'sally', null, $forceRefresh));
  770. }
  771. /**
  772. * Check if a component is compatible with this Sally version
  773. *
  774. * @param mixed $component addOn as string, plugin as array
  775. * @return boolean true if compatible, else false
  776. */
  777. public function isCompatible($component, $forceRefresh = false) {
  778. if (!$forceRefresh) {
  779. return $this->getProperty($component, 'compatible', false);
  780. }
  781. $sallyVersions = $this->getRequiredSallyVersions($component, true);
  782. $versionOK = false;
  783. foreach ($sallyVersions as $version) {
  784. $versionOK |= $this->checkVersion($version);
  785. }
  786. return (boolean) $versionOK;
  787. }
  788. protected function clearLoadCache() {
  789. sly_Core::cache()->delete('sly', 'componentorder');
  790. sly_Core::cache()->delete('sly', 'availableaddons');
  791. }
  792. public function loadComponents() {
  793. // Make sure we don't accidentally load components that have become
  794. // incompatible due to Sally and/or component updates.
  795. if (sly_Core::isDeveloperMode()) {
  796. $changes = $this->deactivateIncompatibleComponents();
  797. }
  798. else {
  799. $changes = false;
  800. }
  801. $cache = sly_Core::cache();
  802. $prodMode = !sly_Core::isDeveloperMode();
  803. $order = (!$changes && $prodMode) ? $cache->get('sly', 'componentorder') : null;
  804. $addonService = sly_Service_Factory::getAddOnService();
  805. $pluginService = sly_Service_Factory::getPluginService();
  806. // if there is no cache yet, we load all components the slow way
  807. if (!is_array($order)) {
  808. // reset our helper to keep track of the component stati
  809. self::$loadInfo = array();
  810. foreach ($addonService->getRegisteredAddons() as $addonName) {
  811. $addonService->load($addonName);
  812. foreach ($pluginService->getRegisteredPlugins($addonName) as $pluginName) {
  813. $pluginService->load(array($addonName, $pluginName));
  814. }
  815. }
  816. // and now we have a nice list in self::$loadInfo that we can cache
  817. $cache->set('sly', 'componentorder', self::$loadInfo);
  818. }
  819. // yay, a cache, let's skip the whole dependency stuff
  820. else {
  821. foreach ($order as $name => $info) {
  822. list($component, $installed, $activated) = $info;
  823. $service = is_array($component) ? $pluginService : $addonService;
  824. // load component config files
  825. $service->loadConfig($component, $installed, $activated);
  826. // init the component
  827. if ($activated) {
  828. $this->addAuthTokenIfNeeded($component);
  829. $configFile = $service->baseFolder($component).'config.inc.php';
  830. $service->req($configFile);
  831. self::$loaded[$name] = $component;
  832. }
  833. }
  834. }
  835. }
  836. /**
  837. * @param mixed $component addOn as string, plugin as array
  838. * @param boolean $force load the component even if it's not active
  839. */
  840. protected function load($component, $force = false) {
  841. $compAsString = $this->buildComponentName($component);
  842. if (isset(self::$loaded[$compAsString])) {
  843. return true;
  844. }
  845. $service = $this->getService($component);
  846. $compatible = $this->isCompatible($component);
  847. $activated = $compatible && $this->isAvailable($component);
  848. $installed = $compatible && ($activated || $this->isInstalled($component));
  849. if (!$service->exists($component)) {
  850. if ($activated || $installed) {
  851. trigger_error('Component '.$compAsString.' does not exist.', E_USER_WARNING);
  852. }
  853. sly_Core::cache()->flush('sly.staticyml');
  854. $this->clearLoadCache();
  855. $this->deleteInternalFiles($component);
  856. $this->deletePublicFiles($component);
  857. return false;
  858. }
  859. if ($installed || $force) {
  860. $this->loadConfig($component, $installed, $activated);
  861. self::$loadInfo[$compAsString] = array($component, $installed, $activated);
  862. }
  863. if ($activated || $force) {
  864. $requires = $service->getProperty($component, 'requires');
  865. if (!empty($requires)) {
  866. if (!is_array($requires)) $requires = sly_makeArray($requires);
  867. foreach ($requires as $required) {
  868. $required = explode('/', $required, 2);
  869. // first load the addon
  870. $this->load($required[0], $force);
  871. // then the plugin
  872. if (count($required) === 2) {
  873. $this->load($required, $force);
  874. }
  875. }
  876. }
  877. $this->checkUpdate($component);
  878. $this->addAuthTokenIfNeeded($component);
  879. $configFile = $this->baseFolder($component).'config.inc.php';
  880. $this->req($configFile);
  881. self::$loaded[$compAsString] = $component;
  882. }
  883. }
  884. /**
  885. * Read a config value directly (without using the config system)
  886. *
  887. * @param mixed $component addOn as string, plugin as array
  888. * @param string $key array key
  889. * @param mixed $default value if key is not set
  890. * @return mixed value or default
  891. */
  892. private function readConfigValue($component, $key, $default = null, $forceRefresh = false) {
  893. // To make this method work on components that are not yet installed or
  894. // activated, we have to get their static.yml's content on our own. The
  895. // project config at this point already contains 'empty' information for
  896. // the component and would not the static.yml via loadStatic().
  897. if (!$forceRefresh && $this->isAvailable($component)) {
  898. return $this->getService($component)->getProperty($component, $key, $default);
  899. }
  900. $file = $this->baseFolder($component).'static.yml';
  901. if (!file_exists($file)) return $default; // bad component
  902. $cache = sly_Core::cache();
  903. $ckey = md5($file);
  904. $mtime = filemtime($file);
  905. $data = $cache->get('sly.staticyml', $ckey, null);
  906. if (!is_array($data) || $data['mtime'] != $mtime) {
  907. $config = sly_Util_YAML::load($file);
  908. $data = array('mtime' => $mtime, 'config' => $config);
  909. $cache->set('sly.staticyml', $ckey, $data);
  910. }
  911. $config = $data['config'];
  912. return isset($config[$key]) ? $config[$key] : $default;
  913. }
  914. private function getService($component) {
  915. return is_array($component) ? sly_Service_Factory::getPluginService() : sly_Service_Factory::getAddOnService();
  916. }
  917. private function addAuthTokenIfNeeded($component) {
  918. $page = $this->getProperty($component, 'page', '');
  919. $name = $this->getProperty($component, 'name', '');
  920. $config = sly_Core::config();
  921. if (!empty($page) && !$config->has('authorisation/pages/token/'.$page)) {
  922. sly_Core::config()->set('authorisation/pages/token', array($page => $name), sly_Configuration::STORE_STATIC);
  923. }
  924. }
  925. }