ElggPluginPackage.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <?php
  2. /**
  3. * Manages plugin packages under mod.
  4. *
  5. * @todo This should eventually be merged into \ElggPlugin.
  6. * Currently \ElggPlugin objects are only used to get and save
  7. * plugin settings and user settings, so not every plugin
  8. * has an \ElggPlugin object. It's not implemented in \ElggPlugin
  9. * right now because of conflicts with at least the constructor,
  10. * enable(), disable(), and private settings.
  11. *
  12. * Around 1.9 or so we should each plugin over to using
  13. * \ElggPlugin and merge \ElggPluginPackage and \ElggPlugin.
  14. *
  15. * @package Elgg.Core
  16. * @subpackage Plugins
  17. * @since 1.8
  18. */
  19. class ElggPluginPackage {
  20. /**
  21. * The required files in the package
  22. *
  23. * @var array
  24. */
  25. private $requiredFiles = array(
  26. 'start.php', 'manifest.xml'
  27. );
  28. /**
  29. * The optional files that can be read and served through the markdown page handler
  30. * @var array
  31. */
  32. private $textFiles = array(
  33. 'README.txt', 'CHANGES.txt',
  34. 'INSTALL.txt', 'COPYRIGHT.txt', 'LICENSE.txt',
  35. 'README', 'README.md', 'README.markdown'
  36. );
  37. /**
  38. * Valid types for provides.
  39. *
  40. * @var array
  41. */
  42. private $providesSupportedTypes = array(
  43. 'plugin', 'php_extension'
  44. );
  45. /**
  46. * The type of requires/conflicts supported
  47. *
  48. * @var array
  49. */
  50. private $depsSupportedTypes = array(
  51. 'elgg_version', 'elgg_release', 'php_version', 'php_extension', 'php_ini', 'plugin', 'priority',
  52. );
  53. /**
  54. * An invalid plugin error.
  55. */
  56. private $errorMsg = '';
  57. /**
  58. * The plugin's manifest object
  59. *
  60. * @var \ElggPluginManifest
  61. */
  62. protected $manifest;
  63. /**
  64. * The plugin's full path
  65. *
  66. * @var string
  67. */
  68. protected $path;
  69. /**
  70. * Is the plugin valid?
  71. *
  72. * @var mixed Bool after validation check, null before.
  73. */
  74. protected $valid = null;
  75. /**
  76. * The plugin ID (dir name)
  77. *
  78. * @var string
  79. */
  80. protected $id;
  81. /**
  82. * Load a plugin package from mod/$id or by full path.
  83. *
  84. * @param string $plugin The ID (directory name) or full path of the plugin.
  85. * @param bool $validate Automatically run isValid()?
  86. *
  87. * @throws PluginException
  88. */
  89. public function __construct($plugin, $validate = true) {
  90. $plugin_path = _elgg_services()->config->getPluginsPath();
  91. // @todo wanted to avoid another is_dir() call here.
  92. // should do some profiling to see how much it affects
  93. if (strpos($plugin, $plugin_path) === 0 || is_dir($plugin)) {
  94. // this is a path
  95. $path = sanitise_filepath($plugin);
  96. // the id is the last element of the array
  97. $path_array = explode('/', trim($path, '/'));
  98. $id = array_pop($path_array);
  99. } else {
  100. // this is a plugin id
  101. // strict plugin names
  102. if (preg_match('/[^a-z0-9\.\-_]/i', $plugin)) {
  103. throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidID', array($plugin)));
  104. }
  105. $path = "{$plugin_path}$plugin/";
  106. $id = $plugin;
  107. }
  108. if (!is_dir($path)) {
  109. throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPath', array($path)));
  110. }
  111. $this->path = $path;
  112. $this->id = $id;
  113. if ($validate && !$this->isValid()) {
  114. if ($this->errorMsg) {
  115. throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin:Details',
  116. array($plugin, $this->errorMsg)));
  117. } else {
  118. throw new \PluginException(_elgg_services()->translator->translate('PluginException:InvalidPlugin', array($plugin)));
  119. }
  120. }
  121. }
  122. /********************************
  123. * Validation and sanity checks *
  124. ********************************/
  125. /**
  126. * Checks if this is a valid Elgg plugin.
  127. *
  128. * Checks for requires files as defined at the start of this
  129. * class. Will check require manifest fields via \ElggPluginManifest
  130. * for Elgg 1.8 plugins.
  131. *
  132. * @note This doesn't check dependencies or conflicts.
  133. * Use {@link \ElggPluginPackage::canActivate()} or
  134. * {@link \ElggPluginPackage::checkDependencies()} for that.
  135. *
  136. * @return bool
  137. */
  138. public function isValid() {
  139. if (!isset($this->valid)) {
  140. $this->valid = $this->validate();
  141. }
  142. return $this->valid;
  143. }
  144. /**
  145. * @return bool
  146. */
  147. private function validate() {
  148. // check required files.
  149. $have_req_files = true;
  150. foreach ($this->requiredFiles as $file) {
  151. if (!is_readable($this->path . $file)) {
  152. $have_req_files = false;
  153. $this->errorMsg =
  154. _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:MissingFile', array($file));
  155. return false;
  156. }
  157. }
  158. // check required files
  159. if (!$have_req_files) {
  160. return $this->valid = false;
  161. }
  162. // check for valid manifest.
  163. if (!$this->loadManifest()) {
  164. return false;
  165. }
  166. if (!$this->isNamedCorrectly()) {
  167. return false;
  168. }
  169. // can't require or conflict with yourself or something you provide.
  170. // make sure provides are all valid.
  171. if (!$this->hasSaneDependencies()) {
  172. return false;
  173. }
  174. return true;
  175. }
  176. /**
  177. * Check that the plugin is installed in the directory with name specified
  178. * in the manifest's "id" element.
  179. *
  180. * @return bool
  181. */
  182. private function isNamedCorrectly() {
  183. $manifest = $this->getManifest();
  184. if ($manifest) {
  185. $required_id = $manifest->getID();
  186. if (!empty($required_id) && ($required_id !== $this->id)) {
  187. $this->errorMsg =
  188. _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidId', array($required_id));
  189. return false;
  190. }
  191. }
  192. return true;
  193. }
  194. /**
  195. * Check the plugin doesn't require or conflict with itself
  196. * or something provides. Also check that it only list
  197. * valid provides. Deps are checked in checkDependencies()
  198. *
  199. * @note Plugins always provide themselves.
  200. *
  201. * @todo Don't let them require and conflict the same thing
  202. *
  203. * @return bool
  204. */
  205. private function hasSaneDependencies() {
  206. // protection against plugins with no manifest file
  207. if (!$this->getManifest()) {
  208. return false;
  209. }
  210. // Note: $conflicts and $requires are not unused. They're called dynamically
  211. $conflicts = $this->getManifest()->getConflicts();
  212. $requires = $this->getManifest()->getRequires();
  213. $provides = $this->getManifest()->getProvides();
  214. foreach ($provides as $provide) {
  215. // only valid provide types
  216. if (!in_array($provide['type'], $this->providesSupportedTypes)) {
  217. $this->errorMsg =
  218. _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidProvides', array($provide['type']));
  219. return false;
  220. }
  221. // doesn't conflict or require any of its provides
  222. $name = $provide['name'];
  223. foreach (array('conflicts', 'requires') as $dep_type) {
  224. foreach (${$dep_type} as $dep) {
  225. if (!in_array($dep['type'], $this->depsSupportedTypes)) {
  226. $this->errorMsg =
  227. _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:InvalidDependency', array($dep['type']));
  228. return false;
  229. }
  230. // make sure nothing is providing something it conflicts or requires.
  231. if (isset($dep['name']) && $dep['name'] == $name) {
  232. $version_compare = version_compare($provide['version'], $dep['version'], $dep['comparison']);
  233. if ($version_compare) {
  234. $this->errorMsg =
  235. _elgg_services()->translator->translate('ElggPluginPackage:InvalidPlugin:CircularDep',
  236. array($dep['type'], $dep['name'], $this->id));
  237. return false;
  238. }
  239. }
  240. }
  241. }
  242. }
  243. return true;
  244. }
  245. /************
  246. * Manifest *
  247. ************/
  248. /**
  249. * Returns a parsed manifest file.
  250. *
  251. * @return \ElggPluginManifest
  252. */
  253. public function getManifest() {
  254. if (!$this->manifest) {
  255. if (!$this->loadManifest()) {
  256. return false;
  257. }
  258. }
  259. return $this->manifest;
  260. }
  261. /**
  262. * Loads the manifest into this->manifest as an
  263. * \ElggPluginManifest object.
  264. *
  265. * @return bool
  266. */
  267. private function loadManifest() {
  268. $file = $this->path . 'manifest.xml';
  269. try {
  270. $this->manifest = new \ElggPluginManifest($file, $this->id);
  271. } catch (Exception $e) {
  272. $this->errorMsg = $e->getMessage();
  273. return false;
  274. }
  275. if ($this->manifest instanceof \ElggPluginManifest) {
  276. return true;
  277. }
  278. $this->errorMsg = _elgg_services()->translator->translate('unknown_error');
  279. return false;
  280. }
  281. /****************
  282. * Readme Files *
  283. ***************/
  284. /**
  285. * Returns an array of present and readable text files
  286. *
  287. * @return array
  288. */
  289. public function getTextFilenames() {
  290. return $this->textFiles;
  291. }
  292. /***********************
  293. * Dependencies system *
  294. ***********************/
  295. /**
  296. * Returns if the Elgg system meets the plugin's dependency
  297. * requirements. This includes both requires and conflicts.
  298. *
  299. * Full reports can be requested. The results are returned
  300. * as an array of arrays in the form array(
  301. * 'type' => requires|conflicts,
  302. * 'dep' => array( dependency array ),
  303. * 'status' => bool if depedency is met,
  304. * 'comment' => optional comment to display to the user.
  305. * )
  306. *
  307. * @param bool $full_report Return a full report.
  308. * @return bool|array
  309. */
  310. public function checkDependencies($full_report = false) {
  311. // Note: $conflicts and $requires are not unused. They're called dynamically
  312. $requires = $this->getManifest()->getRequires();
  313. $conflicts = $this->getManifest()->getConflicts();
  314. $enabled_plugins = elgg_get_plugins('active');
  315. $this_id = $this->getID();
  316. $report = array();
  317. // first, check if any active plugin conflicts with us.
  318. foreach ($enabled_plugins as $plugin) {
  319. $temp_conflicts = array();
  320. $temp_manifest = $plugin->getManifest();
  321. if ($temp_manifest instanceof \ElggPluginManifest) {
  322. $temp_conflicts = $plugin->getManifest()->getConflicts();
  323. }
  324. foreach ($temp_conflicts as $conflict) {
  325. if ($conflict['type'] == 'plugin' && $conflict['name'] == $this_id) {
  326. $result = $this->checkDepPlugin($conflict, $enabled_plugins, false);
  327. // rewrite the conflict to show the originating plugin
  328. $conflict['name'] = $plugin->getManifest()->getName();
  329. if (!$full_report && !$result['status']) {
  330. $this->errorMsg = "Conflicts with plugin \"{$plugin->getManifest()->getName()}\".";
  331. return $result['status'];
  332. } else {
  333. $report[] = array(
  334. 'type' => 'conflicted',
  335. 'dep' => $conflict,
  336. 'status' => $result['status'],
  337. 'value' => $this->getManifest()->getVersion()
  338. );
  339. }
  340. }
  341. }
  342. }
  343. $check_types = array('requires', 'conflicts');
  344. if ($full_report) {
  345. // Note: $suggests is not unused. It's called dynamically
  346. $suggests = $this->getManifest()->getSuggests();
  347. $check_types[] = 'suggests';
  348. }
  349. foreach ($check_types as $dep_type) {
  350. $inverse = ($dep_type == 'conflicts') ? true : false;
  351. foreach (${$dep_type} as $dep) {
  352. switch ($dep['type']) {
  353. case 'elgg_version':
  354. elgg_deprecated_notice("elgg_version in manifest.xml files is deprecated. Use elgg_release", 1.9);
  355. $result = $this->checkDepElgg($dep, elgg_get_version(), $inverse);
  356. break;
  357. case 'elgg_release':
  358. $result = $this->checkDepElgg($dep, elgg_get_version(true), $inverse);
  359. break;
  360. case 'plugin':
  361. $result = $this->checkDepPlugin($dep, $enabled_plugins, $inverse);
  362. break;
  363. case 'priority':
  364. $result = $this->checkDepPriority($dep, $enabled_plugins, $inverse);
  365. break;
  366. case 'php_version':
  367. $result = $this->checkDepPhpVersion($dep, $inverse);
  368. break;
  369. case 'php_extension':
  370. $result = $this->checkDepPhpExtension($dep, $inverse);
  371. break;
  372. case 'php_ini':
  373. $result = $this->checkDepPhpIni($dep, $inverse);
  374. break;
  375. default:
  376. $result = null;//skip further check
  377. break;
  378. }
  379. if ($result !== null) {
  380. // unless we're doing a full report, break as soon as we fail.
  381. if (!$full_report && !$result['status']) {
  382. $this->errorMsg = "Missing dependencies.";
  383. return $result['status'];
  384. } else {
  385. // build report element and comment
  386. $report[] = array(
  387. 'type' => $dep_type,
  388. 'dep' => $dep,
  389. 'status' => $result['status'],
  390. 'value' => $result['value']
  391. );
  392. }
  393. }
  394. }
  395. }
  396. if ($full_report) {
  397. // add provides to full report
  398. $provides = $this->getManifest()->getProvides();
  399. foreach ($provides as $provide) {
  400. $report[] = array(
  401. 'type' => 'provides',
  402. 'dep' => $provide,
  403. 'status' => true,
  404. 'value' => ''
  405. );
  406. }
  407. return $report;
  408. }
  409. return true;
  410. }
  411. /**
  412. * Checks if $plugins meets the requirement by $dep.
  413. *
  414. * @param array $dep An Elgg manifest.xml deps array
  415. * @param array $plugins A list of plugins as returned by elgg_get_plugins();
  416. * @param bool $inverse Inverse the results to use as a conflicts.
  417. * @return bool
  418. */
  419. private function checkDepPlugin(array $dep, array $plugins, $inverse = false) {
  420. $r = _elgg_check_plugins_provides('plugin', $dep['name'], $dep['version'], $dep['comparison']);
  421. if ($inverse) {
  422. $r['status'] = !$r['status'];
  423. }
  424. return $r;
  425. }
  426. /**
  427. * Checks if $plugins meets the requirement by $dep.
  428. *
  429. * @param array $dep An Elgg manifest.xml deps array
  430. * @param array $plugins A list of plugins as returned by elgg_get_plugins();
  431. * @param bool $inverse Inverse the results to use as a conflicts.
  432. * @return bool
  433. */
  434. private function checkDepPriority(array $dep, array $plugins, $inverse = false) {
  435. // grab the \ElggPlugin using this package.
  436. $plugin_package = elgg_get_plugin_from_id($this->getID());
  437. $plugin_priority = $plugin_package->getPriority();
  438. $test_plugin = elgg_get_plugin_from_id($dep['plugin']);
  439. // If this isn't a plugin or the plugin isn't installed or active
  440. // priority doesn't matter. Use requires to check if a plugin is active.
  441. if (!$plugin_package || !$test_plugin || !$test_plugin->isActive()) {
  442. return array(
  443. 'status' => true,
  444. 'value' => 'uninstalled'
  445. );
  446. }
  447. $test_plugin_priority = $test_plugin->getPriority();
  448. switch ($dep['priority']) {
  449. case 'before':
  450. $status = $plugin_priority < $test_plugin_priority;
  451. break;
  452. case 'after':
  453. $status = $plugin_priority > $test_plugin_priority;
  454. break;
  455. default;
  456. $status = false;
  457. }
  458. // get the current value
  459. if ($plugin_priority < $test_plugin_priority) {
  460. $value = 'before';
  461. } else {
  462. $value = 'after';
  463. }
  464. if ($inverse) {
  465. $status = !$status;
  466. }
  467. return array(
  468. 'status' => $status,
  469. 'value' => $value
  470. );
  471. }
  472. /**
  473. * Checks if $elgg_version meets the requirement by $dep.
  474. *
  475. * @param array $dep An Elgg manifest.xml deps array
  476. * @param array $elgg_version An Elgg version (either YYYYMMDDXX or X.Y.Z)
  477. * @param bool $inverse Inverse the result to use as a conflicts.
  478. * @return bool
  479. */
  480. private function checkDepElgg(array $dep, $elgg_version, $inverse = false) {
  481. $status = version_compare($elgg_version, $dep['version'], $dep['comparison']);
  482. if ($inverse) {
  483. $status = !$status;
  484. }
  485. return array(
  486. 'status' => $status,
  487. 'value' => $elgg_version
  488. );
  489. }
  490. /**
  491. * Checks if $php_version meets the requirement by $dep.
  492. *
  493. * @param array $dep An Elgg manifest.xml deps array
  494. * @param bool $inverse Inverse the result to use as a conflicts.
  495. * @return bool
  496. */
  497. private function checkDepPhpVersion(array $dep, $inverse = false) {
  498. $php_version = phpversion();
  499. $status = version_compare($php_version, $dep['version'], $dep['comparison']);
  500. if ($inverse) {
  501. $status = !$status;
  502. }
  503. return array(
  504. 'status' => $status,
  505. 'value' => $php_version
  506. );
  507. }
  508. /**
  509. * Checks if the PHP extension in $dep is loaded.
  510. *
  511. * @todo Can this be merged with the plugin checker?
  512. *
  513. * @param array $dep An Elgg manifest.xml deps array
  514. * @param bool $inverse Inverse the result to use as a conflicts.
  515. * @return array An array in the form array(
  516. * 'status' => bool
  517. * 'value' => string The version provided
  518. * )
  519. */
  520. private function checkDepPhpExtension(array $dep, $inverse = false) {
  521. $name = $dep['name'];
  522. $version = $dep['version'];
  523. $comparison = $dep['comparison'];
  524. // not enabled.
  525. $status = extension_loaded($name);
  526. // enabled. check version.
  527. $ext_version = phpversion($name);
  528. if ($status) {
  529. // some extensions (like gd) don't provide versions. neat.
  530. // don't check version info and return a lie.
  531. if ($ext_version && $version) {
  532. $status = version_compare($ext_version, $version, $comparison);
  533. }
  534. if (!$ext_version) {
  535. $ext_version = '???';
  536. }
  537. }
  538. // some php extensions can be emulated, so check provides.
  539. if ($status == false) {
  540. $provides = _elgg_check_plugins_provides('php_extension', $name, $version, $comparison);
  541. $status = $provides['status'];
  542. $ext_version = $provides['value'];
  543. }
  544. if ($inverse) {
  545. $status = !$status;
  546. }
  547. return array(
  548. 'status' => $status,
  549. 'value' => $ext_version
  550. );
  551. }
  552. /**
  553. * Check if the PHP ini setting satisfies $dep.
  554. *
  555. * @param array $dep An Elgg manifest.xml deps array
  556. * @param bool $inverse Inverse the result to use as a conflicts.
  557. * @return bool
  558. */
  559. private function checkDepPhpIni($dep, $inverse = false) {
  560. $name = $dep['name'];
  561. $value = $dep['value'];
  562. $comparison = $dep['comparison'];
  563. // ini_get() normalizes truthy values to 1 but falsey values to 0 or ''.
  564. // version_compare() considers '' < 0, so normalize '' to 0.
  565. // \ElggPluginManifest normalizes all bool values and '' to 1 or 0.
  566. $setting = ini_get($name);
  567. if ($setting === '') {
  568. $setting = 0;
  569. }
  570. $status = version_compare($setting, $value, $comparison);
  571. if ($inverse) {
  572. $status = !$status;
  573. }
  574. return array(
  575. 'status' => $status,
  576. 'value' => $setting
  577. );
  578. }
  579. /**
  580. * Returns the Plugin ID
  581. *
  582. * @return string
  583. */
  584. public function getID() {
  585. return $this->id;
  586. }
  587. /**
  588. * Returns the last error message.
  589. *
  590. * @return string
  591. */
  592. public function getError() {
  593. return $this->errorMsg;
  594. }
  595. }