1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
21:
22: class Modules extends WireArray {
23:
24: 25: 26: 27:
28: const flagsSingular = 1;
29:
30: 31: 32: 33:
34: const flagsAutoload = 2;
35:
36: 37: 38: 39:
40: protected $installable = array();
41:
42: 43: 44: 45: 46: 47:
48: protected $moduleIDs = array();
49:
50: 51: 52: 53:
54: protected $modulePath = '';
55:
56: 57: 58: 59:
60: protected $modulePath2 = '';
61:
62: 63: 64: 65:
66: protected $configData = array();
67:
68: 69: 70: 71:
72: protected $initialized = false;
73:
74: 75: 76: 77: 78: 79: 80: 81:
82: public function __construct($path, $path2 = null) {
83: $this->setTrackChanges(false);
84: $this->modulePath = $path;
85: $this->load($path);
86: if($path2 && is_dir($path2)) {
87: $this->modulePath2 = $path2;
88: $this->load($path2);
89: }
90: }
91:
92: 93: 94: 95:
96: public function isValidItem($item) {
97: return $item instanceof Module;
98: }
99:
100: 101: 102: 103:
104: public function getItemKey($item) {
105: return $this->getModuleClass($item);
106: }
107:
108: 109: 110: 111:
112: public function makeBlankItem() {
113: return null;
114: }
115:
116: 117: 118: 119:
120: public function triggerInit() {
121:
122: foreach($this as $module) {
123:
124:
125: $this->setModuleConfigData($module);
126: $module->init();
127:
128:
129:
130: if($this->isSingular($module)) {
131: $id = $this->getModuleID($module);
132: unset($this->configData[$id]);
133: }
134: }
135: $this->initialized = true;
136: }
137:
138: 139: 140: 141: 142: 143: 144: 145:
146: public function triggerReady() {
147:
148: foreach($this as $module) {
149: if($module instanceof ModulePlaceholder) continue;
150: if(!method_exists($module, 'ready')) continue;
151: if(!$this->isAutoload($module)) continue;
152: $module->ready();
153: }
154: }
155:
156: 157: 158: 159: 160: 161:
162: protected function load($path) {
163:
164: static $installed = array();
165:
166: if(!count($installed)) {
167: $result = $this->fuel('db')->query("SELECT id, class, flags, data FROM modules ORDER BY class");
168: while($row = $result->fetch_assoc()) {
169: if($row['flags'] & self::flagsAutoload) {
170:
171: $this->configData[$row['id']] = wireDecodeJSON($row['data']);
172: }
173: unset($row['data']);
174: $installed[$row['class']] = $row;
175: }
176: $result->free();
177: }
178:
179: $files = $this->findModuleFiles($path, true);
180:
181: foreach($files as $pathname) {
182:
183: $pathname = $path . $pathname;
184: $dirname = dirname($pathname);
185: $filename = basename($pathname);
186: $basename = basename($filename, '.module');
187:
188:
189: if(!strpos($filename, '.module') || substr($filename, -7) !== '.module') continue;
190:
191:
192: if(strpos($pathname, $path) !== 0) continue;
193:
194:
195: if(!is_file($pathname)) continue;
196:
197:
198: if(!array_key_exists($basename, $installed)) {
199: $this->installable[$basename] = $pathname;
200: continue;
201: }
202:
203: $info = $installed[$basename];
204: $this->setConfigPaths($basename, $dirname);
205:
206: if($info['flags'] & self::flagsAutoload) {
207:
208:
209:
210: include_once($pathname);
211: $module = new $basename();
212:
213: } else {
214:
215: $module = new ModulePlaceholder();
216: $module->setClass($basename);
217: $module->singular = $info['flags'] & self::flagsSingular;
218: $module->file = $pathname;
219: }
220:
221: $this->moduleIDs[$basename] = $info['id'];
222: $this->set($basename, $module);
223: }
224:
225: }
226:
227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237:
238: protected function findModuleFiles($path, $readCache = false, $level = 0) {
239:
240: static $startPath;
241:
242: $config = $this->fuel('config');
243:
244: if($level == 0) {
245: $startPath = $path;
246: $cacheFilename = $config->paths->cache . "Modules." . md5($path) . ".cache";
247: if($readCache && is_file($cacheFilename)) return explode("\n", file_get_contents($cacheFilename));
248: }
249:
250: $files = array();
251: $dir = new DirectoryIterator($path);
252:
253: foreach($dir as $file) {
254:
255: if($file->isDot()) continue;
256:
257: $filename = $file->getFilename();
258: $pathname = $file->getPathname();
259:
260: if(DIRECTORY_SEPARATOR != '/') {
261: $pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname);
262: $filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
263: }
264:
265:
266: if($file->isDir() && ($level < 1 || is_file("$pathname/$filename.module"))) {
267: $files = array_merge($files, $this->findModuleFiles($pathname, false, $level + 1));
268: }
269:
270:
271: if(!strpos($filename, '.module') || substr($filename, -7) !== '.module') continue;
272:
273: $files[] = str_replace($startPath, '', $pathname);
274: }
275:
276: if($level == 0) @file_put_contents($cacheFilename, implode("\n", $files), LOCK_EX);
277: if($config->chmodFile) @chmod($cacheFilename, octdec($config->chmodFile));
278:
279: return $files;
280: }
281:
282:
283: 284: 285: 286: 287: 288: 289:
290: protected function setConfigPaths($moduleName, $path) {
291: $config = $this->fuel('config');
292: $path = rtrim($path, '/');
293: $path = substr($path, strlen($config->paths->root)) . '/';
294: $config->paths->set($moduleName, $path);
295: $config->urls->set($moduleName, $path);
296: }
297:
298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308:
309: public function get($key) {
310:
311: $module = null;
312: $justInstalled = false;
313:
314:
315: if(ctype_digit("$key")) {
316: if(!$key = array_search($key, $this->moduleIDs)) return null;
317: }
318:
319: if($module = parent::get($key)) {
320:
321:
322:
323: if($module instanceof ModulePlaceholder || !$this->isSingular($module)) {
324: $placeholder = $module;
325: $class = $this->getModuleClass($placeholder);
326: if($module instanceof ModulePlaceholder) $this->includeModule($module);
327: $module = new $class();
328: if($this->isSingular($placeholder)) $this->set($key, $module);
329: }
330:
331: } else if(array_key_exists($key, $this->getInstallable())) {
332:
333:
334: $module = $this->install($key);
335: $justInstalled = true;
336: }
337:
338:
339:
340: if($module && (!$this->isAutoload($module) || $justInstalled)) {
341:
342:
343: $this->setModuleConfigData($module);
344: $module->init();
345: }
346:
347: return $module;
348: }
349:
350: 351: 352: 353: 354: 355: 356:
357: public function includeModule($module) {
358:
359: if(is_string($module)) $module = parent::get($module);
360: if(!$module) return false;
361:
362: if($module instanceof ModulePlaceholder) {
363: include_once($module->file);
364: } else {
365:
366: }
367: return true;
368: }
369:
370: 371: 372: 373: 374: 375: 376:
377: public function find($selector) {
378: $a = parent::find($selector);
379: if($a) {
380: foreach($a as $key => $value) {
381: $a[$key] = $this->get($value->className());
382: }
383: }
384: return $a;
385: }
386:
387: 388: 389: 390: 391: 392:
393: public function getInstallable() {
394: return $this->installable;
395: }
396:
397: 398: 399: 400: 401: 402: 403:
404: public function isInstalled($class) {
405: return (parent::get($class) !== null);
406: }
407:
408:
409: 410: 411: 412: 413: 414: 415:
416: public function isInstallable($class) {
417: return array_key_exists($class, $this->installable);
418: }
419:
420: 421: 422: 423: 424: 425: 426:
427: public function ___install($class) {
428:
429: if(!$this->isInstallable($class)) return null;
430: $pathname = $this->installable[$class];
431: require_once($pathname);
432: $this->setConfigPaths($class, dirname($pathname));
433:
434: $requires = $this->getRequiresForInstall($class);
435: if(count($requires)) throw new WireException("Module $class requires: " . implode(", ", $requires));
436:
437: $module = new $class();
438:
439: $flags = 0;
440: if($this->isSingular($module)) $flags = $flags | self::flagsSingular;
441: if($this->isAutoload($module)) $flags = $flags | self::flagsAutoload;
442:
443: $sql = "INSERT INTO modules SET " .
444: "class='" . $this->fuel('db')->escape_string($class) . "', " .
445: "flags=$flags, " .
446: "data='' ";
447:
448: $result = $this->fuel('db')->query($sql);
449: $moduleID = $this->fuel('db')->insert_id;
450: $this->moduleIDs[$class] = $moduleID;
451:
452: $this->add($module);
453: unset($this->installable[$class]);
454:
455:
456: if(method_exists($module, '___install') || method_exists($module, 'install')) {
457: try {
458: $module->install();
459:
460: } catch(Exception $e) {
461:
462: $this->fuel('db')->query("DELETE FROM modules WHERE id='$moduleID' LIMIT 1");
463: throw new WireException("Unable to install module '$class': " . $e->getMessage());
464: }
465: }
466:
467: $info = $this->getModuleInfo($class);
468:
469:
470: $label = "Module Auto Install:";
471: foreach($info['installs'] as $name) {
472: if(!$this->isInstalled($name)) {
473: try {
474: $this->install($name);
475: $this->message("$label $name");
476: } catch(Exception $e) {
477: $this->error("$label $name - " . $e->getMessage());
478: }
479: }
480: }
481:
482: return $module;
483: }
484:
485: 486: 487: 488: 489: 490: 491: 492:
493: public function isUninstallable($class, $returnReason = false) {
494:
495: $reason = '';
496: $reason1 = "Module is not already installed";
497: $class = $this->getModuleClass($class);
498:
499: if(!$this->isInstalled($class)) {
500: $reason = $reason1;
501:
502: } else {
503: $this->includeModule($class);
504: if(!class_exists($class)) $reason = $reason1;
505: }
506:
507: if(!$reason) {
508:
509:
510: $info = $this->getModuleInfo($class);
511: if(!empty($info['permanent'])) {
512: $reason = "Module is permanent";
513: } else {
514: $dependents = $this->getRequiresForUninstall($class);
515: if(count($dependents)) $reason = "Module is required by other modules that must be uninstalled first";
516: }
517: }
518:
519: if(!$reason && in_array('Fieldtype', class_parents($class))) {
520: foreach(wire('fields') as $field) {
521: $fieldtype = get_class($field->type);
522: if($fieldtype == $class) {
523: $reason = "This module is a Fieldtype currently in use by one or more fields";
524: break;
525: }
526: }
527: }
528:
529: if($returnReason && $reason) return $reason;
530:
531: return $reason ? false : true;
532: }
533:
534: 535: 536: 537: 538: 539: 540:
541: public function ___uninstall($class) {
542:
543: $class = $this->getModuleClass($class);
544: $reason = $this->isUninstallable($class, true);
545: if($reason !== true) throw new WireException("$class - Can't Uninstall - $reason");
546:
547: $info = $this->getModuleInfo($class);
548: $module = $this->get($class);
549:
550: if(method_exists($module, '___uninstall') || method_exists($module, 'uninstall')) {
551:
552: $module->uninstall();
553: }
554:
555: $result = $this->fuel('db')->query("DELETE FROM modules WHERE class='" . $this->fuel('db')->escape_string($class) . "' LIMIT 1");
556: if(!$result) return false;
557:
558:
559: foreach($info['installs'] as $name) {
560:
561:
562: if(!$this->isInstalled($name)) continue;
563:
564:
565: $i = $this->getModuleInfo($name);
566: if(!in_array($class, $i['requires'])) continue;
567:
568:
569: $label = "Module Auto Uninstall";
570: try {
571: $this->uninstall($name);
572: $this->message("$label - $name");
573:
574: } catch(Exception $e) {
575: $this->error("$label - $name - " . $e->getMessage());
576: }
577: }
578:
579: unset($this->moduleIDs[$class]);
580: $this->remove($module);
581:
582: return true;
583: }
584:
585: 586: 587: 588: 589: 590: 591:
592: public function getModuleID($class) {
593:
594: if(is_object($class)) {
595: if($class instanceof Module) $class = $this->getModuleClass($class);
596: else throw new WireException("Unknown module type");
597: }
598:
599: return isset($this->moduleIDs[$class]) ? (int) $this->moduleIDs[$class] : 0;
600: }
601:
602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618:
619: public function getModuleClass($module) {
620:
621: if($module instanceof Module) {
622: if(method_exists($module, 'className')) return $module->className();
623: return get_class($module);
624:
625: } else if(is_int($module) || ctype_digit("$module")) {
626: return array_search((int) $module, $this->moduleIDs);
627:
628: } else if(is_string($module)) {
629: if(array_key_exists($module, $this->moduleIDs)) return $module;
630: if(array_key_exists($module, $this->installable)) return $module;
631: }
632:
633: return false;
634: }
635:
636:
637: 638: 639: 640: 641: 642: 643:
644: public function getModuleInfo($module) {
645:
646: $info = array(
647: 'title' => '',
648: 'version' => 0,
649: 'author' => '',
650: 'summary' => '',
651: 'href' => '',
652: 'requires' => array(),
653: 'installs' => array(),
654: );
655:
656: if($module instanceof Module || ctype_digit("$module")) {
657: $module = $this->getModuleClass($module);
658: }
659:
660: if(!class_exists($module)) {
661:
662: if(isset($this->installable[$module])) {
663: $filename = $this->installable[$module];
664: include_once($filename);
665: }
666:
667: if(!class_exists($module)) {
668: $info['title'] = $module;
669: $info['summary'] = 'Inactive';
670: return $info;
671: }
672: }
673:
674:
675:
676: $info = array_merge($info, call_user_func(array($module, 'getModuleInfo')));
677:
678:
679: if(!is_array($info['requires'])) $info['requires'] = array($info['requires']);
680: if(!is_array($info['installs'])) $info['installs'] = array($info['installs']);
681:
682: return $info;
683: }
684:
685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695:
696: public function getModuleConfigData($className) {
697:
698: if(is_object($className)) $className = $className->className();
699: if(!$id = $this->moduleIDs[$className]) return array();
700: if(isset($this->configData[$id])) return $this->configData[$id];
701:
702:
703: if(!in_array('ConfigurableModule', class_implements($className))) return array();
704:
705: $result = $this->fuel('db')->query("SELECT data FROM modules WHERE id=$id");
706: list($data) = $result->fetch_array();
707: if(empty($data)) $data = array();
708: else $data = wireDecodeJSON($data);
709: $this->configData[$id] = $data;
710: $result->free();
711:
712: return $data;
713: }
714:
715: 716: 717: 718: 719: 720: 721: 722: 723: 724:
725: protected function setModuleConfigData(Module $module, $data = null) {
726:
727: if(!$module instanceof ConfigurableModule) return;
728: if(!is_array($data)) $data = $this->getModuleConfigData($module);
729:
730: if(method_exists($module, 'setConfigData') || method_exists($module, '___setConfigData')) {
731: $module->setConfigData($data);
732: return;
733: }
734:
735: foreach($data as $key => $value) {
736: $module->$key = $value;
737: }
738: }
739:
740: 741: 742: 743: 744: 745: 746: 747:
748: public function ___saveModuleConfigData($className, array $configData) {
749: if(is_object($className)) $className = $className->className();
750: if(!$id = $this->moduleIDs[$className]) throw new WireException("Unable to find ID for Module '$className'");
751: $this->configData[$id] = $configData;
752: $json = count($configData) ? wireEncodeJSON($configData, true) : '';
753: return $this->fuel('db')->query("UPDATE modules SET data='" . $this->fuel('db')->escape_string($json) . "' WHERE id=$id");
754: }
755:
756: 757: 758: 759: 760: 761: 762: 763: 764: 765: 766: 767: 768: 769:
770: public function isSingular(Module $module) {
771: $info = $module->getModuleInfo();
772: if(isset($info['singular'])) return $info['singular'];
773: if(method_exists($module, 'isSingular')) return $module->isSingular();
774:
775: return false;
776: }
777:
778: 779: 780: 781: 782: 783: 784:
785: public function isAutoload(Module $module) {
786: $info = $module->getModuleInfo();
787: if(isset($info['autoload'])) return $info['autoload'];
788: if(method_exists($module, 'isAutoload')) return $module->isAutoload();
789: return false;
790: }
791:
792: 793: 794: 795: 796: 797:
798: public function isInitialized() {
799: return $this->initialized;
800: }
801:
802: 803: 804: 805:
806: public function resetCache() {
807: $this->findModuleFiles($this->modulePath);
808: if($this->modulePath2) $this->findModuleFiles($this->modulePath2);
809: }
810:
811: 812: 813: 814: 815: 816: 817: 818: 819:
820: public function getRequiredBy($class, $uninstalled = false, $installs = false) {
821:
822: $class = $this->getModuleClass($class);
823: $info = $this->getModuleInfo($class);
824: $dependents = array();
825:
826: foreach($this as $module) {
827: $c = $this->getModuleClass($module);
828: if(!$uninstalled && !$this->isInstalled($c)) continue;
829: $i = $this->getModuleInfo($c);
830: if(!count($i['requires'])) continue;
831: if($installs && in_array($c, $info['installs'])) continue;
832: if(in_array($class, $i['requires'])) $dependents[] = $c;
833: }
834:
835: return $dependents;
836: }
837:
838: 839: 840: 841: 842: 843: 844: 845:
846: public function getRequires($class, $uninstalled = false) {
847:
848: $class = $this->getModuleClass($class);
849: $info = $this->getModuleInfo($class);
850: $requires = $info['requires'];
851:
852:
853: if(!$uninstalled) return $requires;
854:
855: foreach($requires as $key => $module) {
856: $c = $this->getModuleClass($module);
857: if($this->isInstalled($c) || in_array($c, $info['installs'])) {
858: unset($requires[$key]);
859: }
860: }
861:
862: return $requires;
863: }
864:
865: 866: 867: 868: 869: 870: 871: 872: 873: 874:
875: public function getRequiresForInstall($class) {
876: return $this->getRequires($class, true);
877: }
878:
879: 880: 881: 882: 883: 884: 885: 886: 887:
888: public function getRequiresForUninstall($class) {
889: return $this->getRequiredBy($class, false, true);
890: }
891:
892: }
893:
894: