]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Plugins.php
ec93e147ab260f0459cca2eb8c9b85a4c776c3bd
[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 (!file_exists($full_entry . '/_define.php')
114 || !file_exists($full_entry . '/_routes.php')
115 ) {
116 //plugin is not compatible with that version of galette.
117 Analog::log(
118 'Plugin ' . $entry . ' is missing a _define.php and/or _routes.php ' .
119 'files that are required.',
120 Analog::WARNING
121 );
122 $this->setDisabled(self::DISABLED_MISS);
123 } elseif (!file_exists($full_entry.'/_disabled')) {
124 include $full_entry . '/_define.php';
125 $this->id = null;
126 $this->mroot = null;
127 if ($this->autoload == true) {
128 //set autoloader to PluginName.
129 if (file_exists($full_entry . '/lib') && isset($this->modules[$entry])) {
130 $varname = $entry . 'Loader';
131 $$varname = new ClassLoader(
132 $this->getNamespace($entry),
133 $full_entry . '/lib'
134 );
135 $$varname->register();
136 }
137 }
138 } else {
139 //plugin is not compatible with that version of galette.
140 Analog::log(
141 'Plugin ' . $entry . ' is explicitely disabled',
142 Analog::INFO
143 );
144 $this->setDisabled(self::DISABLED_EXPLICIT);
145 }
146 }
147 }
148 $d->close();
149 }
150 }
151
152 /**
153 * Loads modules.
154 *
155 * @param Preferences $preferences Galette's Preferences
156 * @param string $path could be a separated list of paths
157 * (path separator depends on your OS).
158 * @param string $lang Indicates if we need to load a lang file on plugin
159 * loading.
160 *
161 * @return void
162 */
163 public function loadModules(Preferences $preferences, $path, $lang = null)
164 {
165 $this->preferences = $preferences;
166 $this->path = explode(PATH_SEPARATOR, $path);
167
168 $this->parseModules();
169
170 // Sort plugins
171 uasort($this->modules, array($this, 'sortModules'));
172
173 // Load translation, _prepend and ns_file
174 foreach ($this->modules as $id => $m) {
175 $this->loadModuleL10N($id, $lang);
176 $this->loadSmarties($id);
177 $this->loadEventProviders($id);
178 $this->overridePrefs($id);
179 }
180 }
181
182 /**
183 * This method registers a module in modules list. You should use this to
184 * register a new module.
185 *
186 * <var>$permissions</var> is a comma separated list of permissions for your
187 * module. If <var>$permissions</var> is null, only super admin has access to
188 * this module.
189 *
190 * <var>$priority</var> is an integer. Modules are sorted by priority and name.
191 * Lowest priority comes first.
192 *
193 * @param string $name Module name
194 * @param string $desc Module description
195 * @param string $author Module author name
196 * @param string $version Module version
197 * @param string $compver Galette version compatibility
198 * @param string $route Module route name
199 * @param string $date Module release date
200 * @param string $acls Module routes ACLs
201 * @param integer $priority Module priority
202 *
203 * @return void
204 */
205 public function register(
206 $name,
207 $desc,
208 $author,
209 $version,
210 $compver = null,
211 $route = null,
212 $date = null,
213 $acls = null,
214 $priority = 1000
215 ) {
216 if ($compver === null) {
217 //plugin compatibility missing!
218 Analog::log(
219 'Plugin ' . $name . ' does not contains mandatory version ' .
220 'compatiblity information. Please contact the author.',
221 Analog::ERROR
222 );
223 $this->setDisabled(self::DISABLED_COMPAT);
224 } elseif (version_compare($compver, GALETTE_COMPAT_VERSION, '<')) {
225 //plugin is not compatible with that version of galette.
226 Analog::log(
227 'Plugin ' . $name . ' is known to be compatible with Galette ' .
228 $compver . ' only, but you current installation require a ' .
229 'plugin compatible with at least ' . GALETTE_COMPAT_VERSION,
230 Analog::WARNING
231 );
232 $this->setDisabled(self::DISABLED_COMPAT);
233 } else {
234 if ($this->id) {
235 $release_date = $date;
236 if ($date !== null && $this->autoload === false) {
237 //try to localize release date
238 try {
239 $release_date = new \DateTime($date);
240 $release_date = $release_date->format(__("Y-m-d"));
241 } catch (\Exception $e) {
242 Analog::log(
243 'Unable to localize release date for plugin ' . $name,
244 Analog::WARNING
245 );
246 }
247 }
248
249 $this->modules[$this->id] = array(
250 'root' => $this->mroot,
251 'name' => $name,
252 'desc' => $desc,
253 'author' => $author,
254 'version' => $version,
255 'acls' => $acls,
256 'date' => $release_date,
257 'priority' => $priority === null ?
258 1000 :
259 (integer) $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 (class_exists($providerClassName)
392 && method_exists($providerClassName, 'provideListeners')
393 ) {
394 $emitter->useListenerProvider(new $providerClassName());
395 }
396 }
397
398 /**
399 * Returns all modules associative array or only one module if <var>$id</var>
400 * is present.
401 *
402 * @param string $id Optionnal module ID
403 *
404 * @return <b>array</b>
405 */
406 public function getModules($id = null)
407 {
408 if ($id && isset($this->modules[$id])) {
409 return $this->modules[$id];
410 }
411 return $this->modules;
412 }
413
414 /**
415 * Returns true if the module with ID <var>$id</var> exists.
416 *
417 * @param string $id Module ID
418 *
419 * @return <b>boolean</b>
420 */
421 public function moduleExists($id)
422 {
423 return isset($this->modules[$id]);
424 }
425
426 /**
427 * Returns all disabled modules in an array
428 *
429 * @return <b>array</b>
430 */
431 public function getDisabledModules()
432 {
433 return $this->disabled;
434 }
435
436 /**
437 * Returns root path for module with ID <var>$id</var>.
438 *
439 * @param string $id Module ID
440 *
441 * @return <b>string</b>
442 */
443 public function moduleRoot($id)
444 {
445 return $this->moduleInfo($id, 'root');
446 }
447
448 /**
449 * Returns a module information that could be:
450 * - root
451 * - name
452 * - desc
453 * - author
454 * - version
455 * - date
456 * - permissions
457 * - priority
458 *
459 * @param string $id Module ID
460 * @param string $info Information to retrieve
461 *
462 * @return module's information
463 */
464 public function moduleInfo($id, $info)
465 {
466 return isset($this->modules[$id][$info]) ? $this->modules[$id][$info] : null;
467 }
468
469 /**
470 * Search and load menu templates from plugins.
471 * Also sets the web path to the plugin with the var "galette_[plugin-name]_path"
472 *
473 * @param Smarty $tpl Smarty template
474 *
475 * @return void
476 */
477 public function getMenus($tpl)
478 {
479 $modules = $this->getModules();
480 foreach (array_keys($this->getModules()) as $r) {
481 $menu_path = $this->getTemplatesPath($r) . '/menu.tpl';
482 if ($tpl->templateExists($menu_path)) {
483 $name2path = strtolower(
484 str_replace(' ', '_', $modules[$r]['name'])
485 );
486 $tpl->assign(
487 'galette_' . $name2path . '_path',
488 'plugins/' . $r . '/'
489 );
490 $tpl->display($menu_path);
491 }
492 }
493 }
494
495 /**
496 * Search and load public menu templates from plugins.
497 * Also sets the web path to the plugin with the var "galette_[plugin-name]_path"
498 *
499 * @param Smarty $tpl Smarty template
500 * @param boolean $public_page Called from a public page
501 *
502 * @return void
503 */
504 public function getPublicMenus($tpl, $public_page = false)
505 {
506 $modules = $this->getModules();
507 foreach (array_keys($this->getModules()) as $r) {
508 $menu_path = $this->getTemplatesPath($r) . '/public_menu.tpl';
509 if ($tpl->templateExists($menu_path)) {
510 $name2path = strtolower(
511 str_replace(' ', '_', $modules[$r]['name'])
512 );
513 $tpl->assign(
514 'galette_' . $name2path . '_path',
515 'plugins/' . $r . '/'
516 );
517 $tpl->assign(
518 'public_page',
519 $public_page
520 );
521 $tpl->display($menu_path);
522 }
523 }
524 }
525
526 /**
527 * Get plugins dashboard entries.
528 *
529 * @param Smarty $tpl Smarty template
530 *
531 * @return void
532 */
533 public function getDashboard($tpl)
534 {
535 $modules = $this->getModules();
536 foreach (array_keys($this->getModules()) as $r) {
537 $dash_path = $this->getTemplatesPath($r) . '/dashboard.tpl';
538 if ($tpl->templateExists($dash_path)) {
539 $name2path = strtolower(
540 str_replace(' ', '_', $modules[$r]['name'])
541 );
542 $tpl->display($dash_path);
543 }
544 }
545 }
546
547 /**
548 * Get plugins single member dashboard entries.
549 *
550 * @param Smarty $tpl Smarty template
551 *
552 * @return void
553 */
554 public function getMemberDashboard($tpl)
555 {
556 $modules = $this->getModules();
557 foreach (array_keys($this->getModules()) as $r) {
558 $dash_path = $this->getTemplatesPath($r) . '/dashboard_member.tpl';
559 if ($tpl->templateExists($dash_path)) {
560 $name2path = strtolower(
561 str_replace(' ', '_', $modules[$r]['name'])
562 );
563 $tpl->display($dash_path);
564 }
565 }
566 }
567
568 /**
569 * Sort modules
570 *
571 * @param array $a A module
572 * @param array $b Another module
573 *
574 * @return 1 if a has the highest priority, -1 otherwise
575 */
576 private function sortModules($a, $b)
577 {
578 if ($a['priority'] == $b['priority']) {
579 return strcasecmp($a['name'], $b['name']);
580 }
581
582 return ($a['priority'] < $b['priority']) ? -1 : 1;
583 }
584
585 /**
586 * Get the templates path for a specified module
587 *
588 * @param string $id Module's ID
589 *
590 * @return Concatenated templates path for requested module
591 */
592 public function getTemplatesPath($id)
593 {
594 return $this->moduleRoot($id) . '/templates/' . $this->preferences->pref_theme;
595 }
596
597 /**
598 * Get the templates path for a specified module name
599 *
600 * @param string $name Module's name
601 *
602 * @return Concatenated templates path for requested module
603 */
604 public function getTemplatesPathFromName($name)
605 {
606 $id = null;
607 foreach (array_keys($this->getModules()) as $r) {
608 $mod = $this->getModules($r);
609 if ($mod['name'] === $name) {
610 return $this->getTemplatesPath($r);
611 }
612 }
613 }
614
615 /**
616 * For each module, returns the headers.tpl full path, if present.
617 *
618 * @return array of headers to include for all modules
619 */
620 public function getTplHeaders()
621 {
622 $_headers = array();
623 foreach (array_keys($this->modules) as $key) {
624 $headers_path = $this->getTemplatesPath($key) . '/headers.tpl';
625 if (file_exists($headers_path)) {
626 $_headers[$key] = $headers_path;
627 }
628 }
629 return $_headers;
630 }
631
632 /**
633 * For each module, return the adh_actions.tpl full path, if present.
634 *
635 * @return array of adherent actions to include on member list for all modules
636 */
637 public function getTplAdhActions()
638 {
639 $_actions = array();
640 foreach (array_keys($this->modules) as $key) {
641 $actions_path = $this->getTemplatesPath($key) . '/adh_actions.tpl';
642 if (file_exists($actions_path)) {
643 $_actions['actions_' . $key] = $actions_path;
644 }
645 }
646 return $_actions;
647 }
648
649 /**
650 * For each module, return the adh_batch_action.tpl full path, if present.
651 *
652 * @return array of adherents batch actions to include on members list
653 * all modules
654 */
655 public function getTplAdhBatchActions()
656 {
657 $_actions = array();
658 foreach (array_keys($this->modules) as $key) {
659 $actions_path = $this->getTemplatesPath($key) . '/adh_batch_action.tpl';
660 if (file_exists($actions_path)) {
661 $_actions['batch_action_' . $key] = $actions_path;
662 }
663 }
664 return $_actions;
665 }
666
667 /**
668 * For each module, return the adh_fiche_action.tpl full path, if present.
669 *
670 * @return array of adherent actions to include on member detailled view for
671 * all modules
672 */
673 public function getTplAdhDetailledActions()
674 {
675 $_actions = array();
676 foreach (array_keys($this->modules) as $key) {
677 $actions_path = $this->getTemplatesPath($key) . '/adh_fiche_action.tpl';
678 if (file_exists($actions_path)) {
679 $_actions['det_actions_' . $key] = $actions_path;
680 }
681 }
682 return $_actions;
683 }
684
685 /**
686 * For each module, gets templates assignements ; and replace some path variables
687 *
688 * @return array of Smarty templates assignement for all modules
689 */
690 public function getTplAssignments()
691 {
692 $_assign = array();
693 foreach ($this->modules as $key => $module) {
694 if (isset($module['tpl_assignments'])) {
695 foreach ($module['tpl_assignments'] as $k => $v) {
696 $v = str_replace(
697 '__plugin_dir__',
698 'plugins/' . $key . '/',
699 $v
700 );
701 $v = str_replace(
702 '__plugin_include_dir__',
703 'plugins/' . $key . '/includes/',
704 $v
705 );
706 $v = str_replace(
707 '__plugin_templates_dir__',
708 'plugins/' . $key . '/templates/' .
709 $this->preferences->pref_theme . '/',
710 $v
711 );
712 $_assign[$k] = $v;
713 }
714 }
715 }
716 return $_assign;
717 }
718
719 /**
720 * Does module needs a database?
721 *
722 * @param string $id Module's ID
723 *
724 * @return boolean
725 */
726 public function needsDatabase($id)
727 {
728 if (isset($this->modules[$id])) {
729 $d = $this->modules[$id]['root'] . '/scripts/';
730 if (file_exists($d)) {
731 return true;
732 } else {
733 return false;
734 }
735 } else {
736 throw new \Exception(_T("Module does not exists!"));
737 }
738 }
739
740 /**
741 * Override preferences from plugin
742 *
743 * @param string $id Module ID
744 *
745 * @return void
746 */
747 public function overridePrefs($id)
748 {
749 $overridables = ['pref_adhesion_form'];
750
751 $f = $this->modules[$id]['root'] . '/_preferences.php';
752 if (file_exists($f)) {
753 include_once $f;
754 if (isset($_preferences)) {
755 foreach ($_preferences as $k => $v) {
756 if (in_array($k, $overridables)) {
757 $this->preferences->$k = $v;
758 }
759 }
760 }
761 }
762 }
763
764 /**
765 * Get plugins routes ACLs
766 *
767 * @return array
768 */
769 public function getAcls()
770 {
771 $acls = [];
772 foreach ($this->modules as $module) {
773 $acls = array_merge($acls, $module['acls']);
774 }
775 return $acls;
776 }
777
778 /**
779 * Retrieve a file that should be publically exposed
780 *
781 * @param int $id Module id
782 * @param string $path File path
783 *
784 * @return string
785 */
786 public function getFile($id, $path)
787 {
788 if (isset($this->modules[$id])) {
789 $file = $this->modules[$id]['root'] . '/webroot/' . $path;
790 if (file_exists($file)) {
791 return $file;
792 } else {
793 throw new \RuntimeException(_T("File not found!"));
794 }
795 } else {
796 throw new \Exception(_T("Module does not exists!"));
797 }
798 }
799
800 /**
801 * Set a module as disabled
802 *
803 * @param integer $cause Cause (one of Plugins::DISABLED_* constants)
804 *
805 * @return void
806 */
807 private function setDisabled($cause)
808 {
809 $this->disabled[$this->id] = array(
810 'root' => $this->mroot,
811 'root_writable' => is_writable($this->mroot),
812 'cause' => $cause
813 );
814 $this->id = null;
815 $this->mroot = null;
816 }
817
818 /**
819 * Get module namespace
820 *
821 * @param integer $id Module ID
822 *
823 * @return string
824 */
825 public function getNamespace($id)
826 {
827 return str_replace(' ', '', $this->modules[$id]['name']);
828 }
829 }