Overview

Packages

  • ClipIt
    • clipit
      • api
    • urjc
      • backend
  • Elgg
    • Core
      • Access
      • Authentication
      • Cache
      • Caches
      • Core
      • DataMode
        • Site
      • DataModel
        • Annotations
        • Entities
        • Extender
        • File
        • Importable
        • Loggable
        • Notable
        • Object
        • User
      • DataStorage
      • Exception
      • Exceptions
        • Stub
      • FileStore
        • Disk
      • Groups
      • Helpers
      • HMAC
      • Memcache
      • Metadata
      • Navigation
      • ODD
      • Output
      • Plugins
        • Settings
      • Sessions
      • SocialModel
        • Friendable
        • Locatable
      • WebServicesAPI
      • Widgets
      • XML
      • XMLRPC
    • Exceptions
      • Stub
  • None
  • PHP

Classes

  • ElggPluginManifest
  • ElggPluginManifestParser
  • ElggPluginManifestParser17
  • ElggPluginManifestParser18
  • ElggPluginPackage
  • Overview
  • Package
  • Class
  • Tree
  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: 
API documentation generated by ApiGen 2.8.0