]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Plugins.php
79885bdceafbcb7970d8cb7ce63da4aba2069370
[galette.git] / galette / lib / Galette / Core / Plugins.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Plugins handling
7 *
8 * PHP version 5
9 *
10 * Copyright © 2009-2014 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Core
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2014 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7 - 2009-03-09
35 */
36
37 namespace Galette\Core;
38
39 use Throwable;
40 use Analog\Analog;
41 use Galette\Common\ClassLoader;
42 use Galette\Core\Preferences;
43
44 /**
45 * Plugins class for galette
46 *
47 * @category Core
48 * @name Plugins
49 * @package Galette
50 * @author Johan Cwiklinski <johan@x-tnd.be>
51 * @copyright 2009-2014 The Galette Team
52 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
53 * @link http://galette.tuxfamily.org
54 * @since Available since 0.7 - 2009-03-09
55 */
56
57 class Plugins
58 {
59 public const DISABLED_COMPAT = 0;
60 public const DISABLED_MISS = 1;
61 public const DISABLED_EXPLICIT = 2;
62
63 protected $path;
64 protected $modules = array();
65 protected $disabled = array();
66
67 protected $id;
68 protected $mroot;
69
70 protected $preferences;
71 protected $autoload = false;
72
73
74 /**
75 * Register autoloaders for all plugins
76 *
77 * @param string $path could be a separated list of paths
78 * (path separator depends on your OS).
79 *
80 * @return void
81 */
82 public function autoload($path)
83 {
84 $this->path = explode(PATH_SEPARATOR, $path);
85 $this->autoload = true;
86 $this->parseModules();
87 }
88
89 /**
90 * Parse modules in current path
91 *
92 * @return void
93 */
94 protected function parseModules()
95 {
96 foreach ($this->path as $root) {
97 if (!is_dir($root) || !is_readable($root)) {
98 continue;
99 }
100
101 if (substr($root, -1) != '/') {
102 $root .= '/';
103 }
104
105 if (($d = @dir($root)) === false) {
106 continue;
107 }
108
109 while (($entry = $d->read()) !== false) {
110 $full_entry = realpath($root . $entry);
111 if ($entry != '.' && $entry != '..' && is_dir($full_entry)) {
112 $this->id = $entry;
113 $this->mroot = $full_entry;
114 if ($this->autoload === true) {
115 if (
116 !file_exists($full_entry . '/_define.php')
117 || !file_exists($full_entry . '/_routes.php')
118 ) {
119 //plugin is not compatible with that version of galette.
120 Analog::log(
121 'Plugin ' . $entry . ' is missing a _define.php and/or _routes.php ' .
122 'files that are required.',
123 Analog::WARNING
124 );
125 $this->setDisabled(self::DISABLED_MISS);
126 } elseif (!file_exists($full_entry . '/_disabled')) {
127 include $full_entry . '/_define.php';
128 $this->id = null;
129 $this->mroot = null;
130 //set autoloader to PluginName.
131 if (isset($this->modules[$entry]) && file_exists($full_entry . '/lib')) {
132 $varname = $entry . 'Loader';
133 $$varname = new ClassLoader(
134 $this->getNamespace($entry),
135 $full_entry . '/lib'
136 );
137 $$varname->register();
138 }
139 } else {
140 //plugin is not compatible with that version of galette.
141 Analog::log(
142 'Plugin ' . $entry . ' is explicitely disabled',
143 Analog::INFO
144 );
145 $this->setDisabled(self::DISABLED_EXPLICIT);
146 }
147 }
148 }
149 }
150 $d->close();
151 }
152 }
153
154 /**
155 * Loads modules.
156 *
157 * @param Preferences $preferences Galette's Preferences
158 * @param string $path could be a separated list of paths
159 * (path separator depends on your OS).
160 * @param string $lang Indicates if we need to load a lang file on plugin
161 * loading.
162 *
163 * @return void
164 */
165 public function loadModules(Preferences $preferences, $path, $lang = null)
166 {
167 $this->preferences = $preferences;
168 $this->path = explode(PATH_SEPARATOR, $path);
169
170 $this->parseModules();
171
172 // Sort plugins
173 uasort($this->modules, array($this, 'sortModules'));
174
175 // Load translation, _prepend and ns_file
176 foreach ($this->modules as $id => $m) {
177 $this->loadModuleL10N($id, $lang);
178 $this->loadSmarties($id);
179 $this->loadEventProviders($id);
180 $this->overridePrefs($id);
181 }
182 }
183
184 /**
185 * This method registers a module in modules list. You should use this to
186 * register a new module.
187 *
188 * <var>$permissions</var> is a comma separated list of permissions for your
189 * module. If <var>$permissions</var> is null, only super admin has access to
190 * this module.
191 *
192 * <var>$priority</var> is an integer. Modules are sorted by priority and name.
193 * Lowest priority comes first.
194 *
195 * @param string $name Module name
196 * @param string $desc Module description
197 * @param string $author Module author name
198 * @param string $version Module version
199 * @param string $compver Galette version compatibility
200 * @param string $route Module route name
201 * @param string $date Module release date
202 * @param string $acls Module routes ACLs
203 * @param integer $priority Module priority
204 *
205 * @return void
206 */
207 public function register(
208 $name,
209 $desc,
210 $author,
211 $version,
212 $compver = null,
213 $route = null,
214 $date = null,
215 $acls = null,
216 $priority = 1000
217 ) {
218 if ($compver === null) {
219 //plugin compatibility missing!
220 Analog::log(
221 'Plugin ' . $name . ' does not contains mandatory version ' .
222 'compatiblity information. Please contact the author.',
223 Analog::ERROR
224 );
225 $this->setDisabled(self::DISABLED_COMPAT);
226 } elseif (version_compare($compver, GALETTE_COMPAT_VERSION, '<')) {
227 //plugin is not compatible with that version of galette.
228 Analog::log(
229 'Plugin ' . $name . ' is known to be compatible with Galette ' .
230 $compver . ' only, but you current installation require a ' .
231 'plugin compatible with at least ' . GALETTE_COMPAT_VERSION,
232 Analog::WARNING
233 );
234 $this->setDisabled(self::DISABLED_COMPAT);
235 } else if ($this->id) {
236 $this->modules[$this->id] = array(
237 'root' => $this->mroot,
238 'name' => $name,
239 'desc' => $desc,
240 'author' => $author,
241 'version' => $version,
242 'acls' => $acls,
243 'date' => $date,
244 'priority' => $priority === null ?
245 1000 : (int)$priority,
246 'root_writable' => is_writable($this->mroot),
247 'route' => $route
248 );
249 }
250 }
251
252 /**
253 * Reset modules list
254 *
255 * @return void
256 */
257 public function resetModulesList()
258 {
259 $this->modules = array();
260 }
261
262 /**
263 * Deactivate specified module
264 *
265 * @param string $id Module's ID
266 *
267 * @return void|exception
268 */
269 public function deactivateModule($id)
270 {
271 if (!isset($this->modules[$id])) {
272 throw new \Exception(_T("No such module."));
273 }
274
275 if (!$this->modules[$id]['root_writable']) {
276 throw new \Exception(_T("Cannot deactivate plugin."));
277 }
278
279 if (@file_put_contents($this->modules[$id]['root'] . '/_disabled', '')) {
280 throw new \Exception(_T("Cannot deactivate plugin."));
281 }
282 }
283
284 /**
285 * Activate specified module
286 *
287 * @param string $id Module's ID
288 *
289 * @return void|exception
290 */
291 public function activateModule($id)
292 {
293 if (!isset($this->disabled[$id])) {
294 throw new \Exception(_T("No such module."));
295 }
296
297 if (!$this->disabled[$id]['root_writable']) {
298 throw new \Exception(_T("Cannot activate plugin."));
299 }
300
301 if (@unlink($this->disabled[$id]['root'] . '/_disabled') === false) {
302 throw new \Exception(_T("Cannot activate plugin."));
303 }
304 }
305
306 /**
307 * This method will search for file <var>$file</var> in language
308 * <var>$lang</var> for module <var>$id</var>.
309 * <var>$file</var> should not have any extension.
310 *
311 * @param string $id Module ID
312 * @param string $language Language code
313 *
314 * @return void
315 */
316 public function loadModuleL10N($id, $language)
317 {
318 global $translator;
319
320 if (!$language || !isset($this->modules[$id])) {
321 return;
322 }
323
324 $domains = [
325 $this->modules[$id]['route']
326 ];
327 foreach ($domains as $domain) {
328 //load translation file for domain
329 $translator->addTranslationFilePattern(
330 'gettext',
331 $this->modules[$id]['root'] . '/lang/',
332 '/%s/LC_MESSAGES/' . $domain . '.mo',
333 $domain
334 );
335
336 //check if a local lang file exists and load it
337 $translator->addTranslationFilePattern(
338 'phparray',
339 $this->modules[$id]['root'] . '/lang/',
340 $domain . '_%s_local_lang.php',
341 $domain
342 );
343 }
344 }
345
346 /**
347 * Loads smarties specific (headers, assigments and so on)
348 *
349 * @param string $id Module ID
350 *
351 * @return void
352 */
353 public function loadSmarties($id)
354 {
355 $f = $this->modules[$id]['root'] . '/_smarties.php';
356 if (file_exists($f)) {
357 include_once $f;
358 if (isset($_tpl_assignments)) {
359 $this->modules[$id]['tpl_assignments'] = $_tpl_assignments;
360 }
361 }
362 }
363
364 /**
365 * Loads event provider
366 *
367 * @param string $id Module ID
368 *
369 * @return void
370 */
371 public function loadEventProviders($id)
372 {
373 global $emitter;
374
375 $providerClassName = '\\' . $this->getNamespace($id) . '\\' . 'PluginEventProvider';
376 if (
377 class_exists($providerClassName)
378 && method_exists($providerClassName, 'provideListeners')
379 ) {
380 $emitter->useListenerProvider(new $providerClassName());
381 }
382 }
383
384 /**
385 * Returns all modules associative array or only one module if <var>$id</var>
386 * is present.
387 *
388 * @param string $id Optionnal module ID
389 *
390 * @return array
391 */
392 public function getModules($id = null)
393 {
394 if ($id && isset($this->modules[$id])) {
395 return $this->modules[$id];
396 }
397 return $this->modules;
398 }
399
400 /**
401 * Returns true if the module with ID <var>$id</var> exists.
402 *
403 * @param string $id Module ID
404 *
405 * @return boolean
406 */
407 public function moduleExists($id)
408 {
409 return isset($this->modules[$id]);
410 }
411
412 /**
413 * Returns all disabled modules in an array
414 *
415 * @return array
416 */
417 public function getDisabledModules()
418 {
419 return $this->disabled;
420 }
421
422 /**
423 * Returns root path for module with ID <var>$id</var>.
424 *
425 * @param string $id Module ID
426 *
427 * @return string
428 */
429 public function moduleRoot($id)
430 {
431 return $this->moduleInfo($id, 'root');
432 }
433
434 /**
435 * Returns a module information that could be:
436 * - root
437 * - name
438 * - desc
439 * - author
440 * - version
441 * - date
442 * - permissions
443 * - priority
444 *
445 * @param string $id Module ID
446 * @param string $info Information to retrieve
447 *
448 * @return module's information
449 */
450 public function moduleInfo($id, $info)
451 {
452 return isset($this->modules[$id][$info]) ? $this->modules[$id][$info] : null;
453 }
454
455 /**
456 * Search and load menu templates from plugins.
457 * Also sets the web path to the plugin with the var "galette_[plugin-name]_path"
458 *
459 * @param Smarty $tpl Smarty template
460 *
461 * @return void
462 */
463 public function getMenus($tpl)
464 {
465 $modules = $this->getModules();
466 foreach (array_keys($this->getModules()) as $r) {
467 $menu_path = $this->getTemplatesPath($r) . '/menu.tpl';
468 if ($tpl->templateExists($menu_path)) {
469 $name2path = strtolower(
470 str_replace(' ', '_', $modules[$r]['name'])
471 );
472 $tpl->assign(
473 'galette_' . $name2path . '_path',
474 'plugins/' . $r . '/'
475 );
476 $tpl->display($menu_path);
477 }
478 }
479 }
480
481 /**
482 * Search and load public menu templates from plugins.
483 * Also sets the web path to the plugin with the var "galette_[plugin-name]_path"
484 *
485 * @param Smarty $tpl Smarty template
486 * @param boolean $public_page Called from a public page
487 *
488 * @return void
489 */
490 public function getPublicMenus($tpl, $public_page = false)
491 {
492 $modules = $this->getModules();
493 foreach (array_keys($this->getModules()) as $r) {
494 $menu_path = $this->getTemplatesPath($r) . '/public_menu.tpl';
495 if ($tpl->templateExists($menu_path)) {
496 $name2path = strtolower(
497 str_replace(' ', '_', $modules[$r]['name'])
498 );
499 $tpl->assign(
500 'galette_' . $name2path . '_path',
501 'plugins/' . $r . '/'
502 );
503 $tpl->assign(
504 'public_page',
505 $public_page
506 );
507 $tpl->display($menu_path);
508 }
509 }
510 }
511
512 /**
513 * Get plugins dashboard entries.
514 *
515 * @param Smarty $tpl Smarty template
516 *
517 * @return void
518 */
519 public function getDashboard($tpl)
520 {
521 $modules = $this->getModules();
522 foreach (array_keys($this->getModules()) as $r) {
523 $dash_path = $this->getTemplatesPath($r) . '/dashboard.tpl';
524 if ($tpl->templateExists($dash_path)) {
525 $name2path = strtolower(
526 str_replace(' ', '_', $modules[$r]['name'])
527 );
528 $tpl->display($dash_path);
529 }
530 }
531 }
532
533 /**
534 * Get plugins single member dashboard entries.
535 *
536 * @param Smarty $tpl Smarty template
537 *
538 * @return void
539 */
540 public function getMemberDashboard($tpl)
541 {
542 $modules = $this->getModules();
543 foreach (array_keys($this->getModules()) as $r) {
544 $dash_path = $this->getTemplatesPath($r) . '/dashboard_member.tpl';
545 if ($tpl->templateExists($dash_path)) {
546 $name2path = strtolower(
547 str_replace(' ', '_', $modules[$r]['name'])
548 );
549 $tpl->display($dash_path);
550 }
551 }
552 }
553
554 /**
555 * Sort modules
556 *
557 * @param array $a A module
558 * @param array $b Another module
559 *
560 * @return 1|-1 1 if a has the highest priority, -1 otherwise
561 */
562 private function sortModules($a, $b)
563 {
564 if ($a['priority'] == $b['priority']) {
565 return strcasecmp($a['name'], $b['name']);
566 }
567
568 return ($a['priority'] < $b['priority']) ? -1 : 1;
569 }
570
571 /**
572 * Get the templates path for a specified module
573 *
574 * @param string $id Module's ID
575 *
576 * @return string Concatenated templates path for requested module
577 */
578 public function getTemplatesPath($id)
579 {
580 return $this->moduleRoot($id) . '/templates/' . $this->preferences->pref_theme;
581 }
582
583 /**
584 * Get the templates path for a specified module name
585 *
586 * @param string $name Module's name
587 *
588 * @return string Concatenated templates path for requested module
589 */
590 public function getTemplatesPathFromName($name)
591 {
592 $id = null;
593 foreach (array_keys($this->getModules()) as $r) {
594 $mod = $this->getModules($r);
595 if ($mod['name'] === $name) {
596 return $this->getTemplatesPath($r);
597 }
598 }
599 }
600
601 /**
602 * For each module, returns the headers.tpl full path, if present.
603 *
604 * @return array of headers to include for all modules
605 */
606 public function getTplHeaders()
607 {
608 $_headers = array();
609 foreach (array_keys($this->modules) as $key) {
610 $headers_path = $this->getTemplatesPath($key) . '/headers.tpl';
611 if (file_exists($headers_path)) {
612 $_headers[$key] = $headers_path;
613 }
614 }
615 return $_headers;
616 }
617
618 /**
619 * For each module, return the adh_actions.tpl full path, if present.
620 *
621 * @return array of adherent actions to include on member list for all modules
622 */
623 public function getTplAdhActions()
624 {
625 $_actions = array();
626 foreach (array_keys($this->modules) as $key) {
627 $actions_path = $this->getTemplatesPath($key) . '/adh_actions.tpl';
628 if (file_exists($actions_path)) {
629 $_actions['actions_' . $key] = $actions_path;
630 }
631 }
632 return $_actions;
633 }
634
635 /**
636 * For each module, return the adh_batch_action.tpl full path, if present.
637 *
638 * @return array of adherents batch actions to include on members list
639 * all modules
640 */
641 public function getTplAdhBatchActions()
642 {
643 $_actions = array();
644 foreach (array_keys($this->modules) as $key) {
645 $actions_path = $this->getTemplatesPath($key) . '/adh_batch_action.tpl';
646 if (file_exists($actions_path)) {
647 $_actions['batch_action_' . $key] = $actions_path;
648 }
649 }
650 return $_actions;
651 }
652
653 /**
654 * For each module, return the adh_fiche_action.tpl full path, if present.
655 *
656 * @return array of adherent actions to include on member detailled view for
657 * all modules
658 */
659 public function getTplAdhDetailledActions()
660 {
661 $_actions = array();
662 foreach (array_keys($this->modules) as $key) {
663 $actions_path = $this->getTemplatesPath($key) . '/adh_fiche_action.tpl';
664 if (file_exists($actions_path)) {
665 $_actions['det_actions_' . $key] = $actions_path;
666 }
667 }
668 return $_actions;
669 }
670
671 /**
672 * For each module, gets templates assignements ; and replace some path variables
673 *
674 * @return array of Smarty templates assignement for all modules
675 */
676 public function getTplAssignments()
677 {
678 $_assign = array();
679 foreach ($this->modules as $key => $module) {
680 if (isset($module['tpl_assignments'])) {
681 foreach ($module['tpl_assignments'] as $k => $v) {
682 $v = str_replace(
683 '__plugin_dir__',
684 'plugins/' . $key . '/',
685 $v
686 );
687 $v = str_replace(
688 '__plugin_include_dir__',
689 'plugins/' . $key . '/includes/',
690 $v
691 );
692 $v = str_replace(
693 '__plugin_templates_dir__',
694 'plugins/' . $key . '/templates/' .
695 $this->preferences->pref_theme . '/',
696 $v
697 );
698 $_assign[$k] = $v;
699 }
700 }
701 }
702 return $_assign;
703 }
704
705 /**
706 * Does module needs a database?
707 *
708 * @param string $id Module's ID
709 *
710 * @return boolean
711 */
712 public function needsDatabase($id)
713 {
714 if (isset($this->modules[$id])) {
715 $d = $this->modules[$id]['root'] . '/scripts/';
716 if (file_exists($d)) {
717 return true;
718 } else {
719 return false;
720 }
721 } else {
722 throw new \Exception(_T("Module does not exists!"));
723 }
724 }
725
726 /**
727 * Override preferences from plugin
728 *
729 * @param string $id Module ID
730 *
731 * @return void
732 */
733 public function overridePrefs($id)
734 {
735 $overridables = ['pref_adhesion_form'];
736
737 $f = $this->modules[$id]['root'] . '/_preferences.php';
738 if (file_exists($f)) {
739 include_once $f;
740 if (isset($_preferences)) {
741 foreach ($_preferences as $k => $v) {
742 if (in_array($k, $overridables)) {
743 $this->preferences->$k = $v;
744 }
745 }
746 }
747 }
748 }
749
750 /**
751 * Get plugins routes ACLs
752 *
753 * @return array
754 */
755 public function getAcls()
756 {
757 $acls = [];
758 foreach ($this->modules as $module) {
759 $acls = array_merge($acls, $module['acls']);
760 }
761 return $acls;
762 }
763
764 /**
765 * Retrieve a file that should be publically exposed
766 *
767 * @param int $id Module id
768 * @param string $path File path
769 *
770 * @return string
771 */
772 public function getFile($id, $path)
773 {
774 if (isset($this->modules[$id])) {
775 $file = $this->modules[$id]['root'] . '/webroot/' . $path;
776 if (file_exists($file)) {
777 return $file;
778 } else {
779 throw new \RuntimeException(_T("File not found!"));
780 }
781 } else {
782 throw new \Exception(_T("Module does not exists!"));
783 }
784 }
785
786 /**
787 * Set a module as disabled
788 *
789 * @param integer $cause Cause (one of Plugins::DISABLED_* constants)
790 *
791 * @return void
792 */
793 private function setDisabled($cause)
794 {
795 $this->disabled[$this->id] = array(
796 'root' => $this->mroot,
797 'root_writable' => is_writable($this->mroot),
798 'cause' => $cause
799 );
800 $this->id = null;
801 $this->mroot = null;
802 }
803
804 /**
805 * Get module namespace
806 *
807 * @param integer $id Module ID
808 *
809 * @return string
810 */
811 public function getNamespace($id)
812 {
813 return str_replace(' ', '', $this->modules[$id]['name']);
814 }
815 }