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