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