Dotclear

source: inc/core/class.dc.modules.php @ 3108:9ca47e752bde

Revision 3108:9ca47e752bde, 18.7 KB checked in by franck <carnet.franck.paul@…>, 10 years ago (diff)

Register default admin URLs after loading define/prepend of modules and before loading context (admin) part

Line 
1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
7# Licensed under the GPL version 2.0 license.
8# See LICENSE file or
9# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10#
11# -- END LICENSE BLOCK -----------------------------------------
12if (!defined('DC_RC_PATH')) { return; }
13
14/**
15@ingroup DC_CORE
16@brief Modules handler
17
18Provides an object to handle modules (themes or plugins).
19*/
20class dcModules
21{
22     protected $path;
23     protected $ns;
24     protected $modules = array();
25     protected $disabled = array();
26     protected $errors = array();
27     protected $modules_names = array();
28     protected $all_modules = array();
29     protected $disabled_mode = false;
30     protected $disabled_meta = array();
31     protected $to_disable = array();
32
33     protected $id;
34     protected $mroot;
35
36     # Inclusion variables
37     protected static $superglobals = array('GLOBALS','_SERVER','_GET','_POST','_COOKIE','_FILES','_ENV','_REQUEST','_SESSION');
38     protected static $_k;
39     protected static $_n;
40
41     protected static $type = null;
42
43     public $core;  ///< <b>dcCore</b>  dcCore instance
44
45     /**
46     Object constructor.
47
48     @param    core      <b>dcCore</b>  dcCore instance
49     */
50     public function __construct($core)
51     {
52          $this->core =& $core;
53     }
54
55     /**
56      * Checks all modules dependencies
57      *   Fills in the following information in module :
58      *     * cannot_enable : list reasons why module cannot be enabled. Not set if module can be enabled
59      *     * cannot_disable : list reasons why module cannot be disabled. Not set if module can be disabled
60      *     * implies : reverse dependencies
61      * @return array list of enabled modules with unmet dependencies, and that must be disabled.
62      */
63     public function checkDependencies()
64     {
65          $this->to_disable = array();
66          foreach ($this->all_modules as $k => &$m) {
67               if (isset($m['requires'])) {
68                    $missing = array();
69                    foreach ($m['requires'] as &$dep) {
70                         if (!is_array($dep)) {
71                              $dep = array($dep);
72                         }
73                         // grab missing dependencies
74                         if (!isset($this->all_modules[$dep[0]])) {
75                              // module not present
76                              $missing[$dep[0]] = sprintf(__("Requires module %s which is not installed"), $dep[0]);
77                         } elseif ((count($dep)>1) && version_compare($this->all_modules[$dep[0]]['version'],$dep[1])==-1) {
78                              // module present, but version missing
79                              $missing[$dep[0]] = sprintf(__("Requires module %s version %s, but version %s is installed"), $dep[0],$dep[1],$m['version']);
80                         } elseif (!$this->all_modules[$dep[0]]['enabled']) {
81                              // module disabled
82                              $missing[$dep[0]] = sprintf(__("Requires module %s which is disabled"), $dep[0]);
83                         }
84                         $this->all_modules[$dep[0]]['implies'][]=$k;
85                    }
86                    if (count($missing)) {
87                         $m['cannot_enable']=$missing;
88                         if ($m['enabled']) {
89                              $this->to_disable[]=array('name' => $k,'reason'=> $missing);
90                         }
91                    }
92               }
93          }
94          // Check modules that cannot be disabled
95          foreach ($this->modules as $k => &$m) {
96               if (isset($m['implies']) && $m['enabled']) {
97                    foreach ($m['implies'] as $im) {
98                         if (isset($this->all_modules[$im]) && $this->all_modules[$im]['enabled']) {
99                              $m['cannot_disable'][]=$im;
100                         }
101                    }
102               }
103          }
104     }
105
106     /**
107      * Checks all modules dependencies, and disable unmet dependencies
108      * @param  string $redir_url URL to redirect if modules are to disable
109      * @return boolean, true if a redirection has been performed
110      */
111     public function disableDepModules($redir_url)
112     {
113          if (isset($_GET['dep'])) {
114               // Avoid infinite redirects
115               return false;
116          }
117          $reason = array();
118          foreach ($this->to_disable as $module) {
119               try{
120                    $this->deactivateModule($module['name']);
121                    $reason[] = sprintf("<li>%s : %s</li>",$module['name'],join(',',$module['reason']));
122               } catch (Exception $e) {
123               }
124          }
125          if (count($reason)) {
126               $message = sprintf ("<p>%s</p><ul>%s</ul>",
127                    __('The following extensions have been disabled :'),
128                    join('',$reason)
129               );
130               dcPage::addWarningNotice($message,array('divtag' => true,'with_ts' => false));
131               $url = $redir_url.(strpos($redir_url,"?") ? '&' : '?').'dep=1';
132               http::redirect($url);
133               return true;
134          }
135          return false;
136     }
137
138     /**
139     Loads modules. <var>$path</var> could be a separated list of paths
140     (path separator depends on your OS).
141
142     <var>$ns</var> indicates if an additionnal file needs to be loaded on plugin
143     load, value could be:
144     - admin (loads module's _admin.php)
145     - public (loads module's _public.php)
146     - xmlrpc (loads module's _xmlrpc.php)
147
148     <var>$lang</var> indicates if we need to load a lang file on plugin
149     loading.
150     */
151     public function loadModules($path,$ns=null,$lang=null)
152     {
153          $this->path = explode(PATH_SEPARATOR,$path);
154          $this->ns = $ns;
155
156          $disabled = isset($_SESSION['sess_safe_mode']) && $_SESSION['sess_safe_mode'];
157          $disabled = $disabled && !get_parent_class($this) ? true : false;
158
159          $ignored = array();
160
161          foreach ($this->path as $root)
162          {
163               if (!is_dir($root) || !is_readable($root)) {
164                    continue;
165               }
166
167               if (substr($root,-1) != '/') {
168                    $root .= '/';
169               }
170
171               if (($d = @dir($root)) === false) {
172                    continue;
173               }
174
175               while (($entry = $d->read()) !== false)
176               {
177                    $full_entry = $root.$entry;
178
179                    if ($entry != '.' && $entry != '..' && is_dir($full_entry)
180                    && file_exists($full_entry.'/_define.php'))
181                    {
182                         if (!file_exists($full_entry.'/_disabled') && !$disabled)
183                         {
184                              $this->id = $entry;
185                              $this->mroot = $full_entry;
186                              require $full_entry.'/_define.php';
187                              $this->all_modules[$entry] =& $this->modules[$entry];
188                              $this->id = null;
189                              $this->mroot = null;
190                         }
191                         else
192                         {
193                              if (file_exists($full_entry.'/_define.php')) {
194                                   $this->id = $entry;
195                                   $this->mroot = $full_entry;
196                                   $this->disabled_mode=true;
197                                   require $full_entry.'/_define.php';
198                                   $this->disabled_mode=false;
199                                   $this->disabled[$entry] =  $this->disabled_meta;
200                                   $this->all_modules[$entry] =& $this->disabled[$entry];
201                                   $this->id = null;
202                                   $this->mroot = null;
203                              }
204                         }
205                    }
206               }
207               $d->close();
208          }
209          $this->checkDependencies();
210          # Sort plugins
211          uasort($this->modules,array($this,'sortModules'));
212
213          foreach ($this->modules as $id => $m)
214          {
215               # Load translation and _prepend
216               if (file_exists($m['root'].'/_prepend.php'))
217               {
218                    $r = $this->loadModuleFile($m['root'].'/_prepend.php');
219
220                    # If _prepend.php file returns null (ie. it has a void return statement)
221                    if (is_null($r)) {
222                         $ignored[] = $id;
223                         continue;
224                    }
225                    unset($r);
226               }
227
228               $this->loadModuleL10N($id,$lang,'main');
229               if ($ns == 'admin') {
230                    $this->loadModuleL10Nresources($id,$lang);
231                    $this->core->adminurl->register('admin.plugin.'.$id,'plugin.php',array('p'=>$id));
232               }
233          }
234
235          // Give opportunity to do something before loading context (admin,public,xmlrpc) files
236          // Admin: should register default URLs with this behavior
237          $this->core->callBehavior('coreBeforeLoadingNsFiles',$this->core,$this,$lang);
238
239          foreach ($this->modules as $id => $m)
240          {
241               # If _prepend.php file returns null (ie. it has a void return statement)
242               if (in_array($id,$ignored)) {
243                    continue;
244               }
245               # Load ns_file
246               $this->loadNsFile($id,$ns);
247          }
248     }
249
250     public function requireDefine($dir,$id)
251     {
252          if (file_exists($dir.'/_define.php')) {
253               $this->id = $id;
254               require $dir.'/_define.php';
255               $this->id = null;
256          }
257     }
258
259     /**
260     This method registers a module in modules list. You should use this to
261     register a new module.
262
263     <var>$permissions</var> is a comma separated list of permissions for your
264     module. If <var>$permissions</var> is null, only super admin has access to
265     this module.
266
267     <var>$priority</var> is an integer. Modules are sorted by priority and name.
268     Lowest priority comes first.
269
270     @param    name           <b>string</b>       Module name
271     @param    desc           <b>string</b>       Module description
272     @param    author         <b>string</b>       Module author name
273     @param    version        <b>string</b>       Module version
274     @param    properties     <b>array</b>        extra properties
275     (currently available keys : permissions, priority, type)
276     */
277     public function registerModule($name,$desc,$author,$version, $properties = array())
278     {
279          if ($this->disabled_mode) {
280               $this->disabled_meta = array_merge(
281                         $properties,
282                         array(
283                              'root' => $this->mroot,
284                              'name' => $name,
285                              'desc' => $desc,
286                              'author' => $author,
287                              'version' => $version,
288                              'enabled' => false,
289                              'root_writable' => is_writable($this->mroot)
290                         )
291                    );
292               return;
293          }
294          # Fallback to legacy registerModule parameters
295          if (!is_array($properties)) {
296               $args = func_get_args();
297               $properties = array();
298               if (isset($args[4])) {
299                    $properties['permissions']=$args[4];
300               }
301               if (isset($args[5])) {
302                    $properties['priority']= (integer)$args[5];
303               }
304          }
305
306          # Default module properties
307          $properties = array_merge(
308               array(
309                    'permissions' => null,
310                    'priority' => 1000,
311                    'standalone_config' => false,
312                    'type' => null,
313                    'enabled' => true,
314                    'requires' => array()
315               ), $properties
316          );
317
318          # Check module type
319          if (self::$type !== null && $properties['type'] !== null && $properties['type'] != self::$type) {
320               $this->errors[] = sprintf(
321                    __('Module "%s" has type "%s" that mismatch required module type "%s".'),
322                    '<strong>'.html::escapeHTML($name).'</strong>',
323                    '<em>'.html::escapeHTML($properties['type']).'</em>',
324                    '<em>'.html::escapeHTML(self::$type).'</em>'
325               );
326               return;
327          }
328
329          # Check module perms on admin side
330          $permissions = $properties['permissions'];
331          if ($this->ns == 'admin') {
332               if ($permissions == '' && !$this->core->auth->isSuperAdmin()) {
333                    return;
334               } elseif (!$this->core->auth->check($permissions,$this->core->blog->id)) {
335                    return;
336               }
337          }
338
339          # Check module install on multiple path
340          if ($this->id) {
341               $module_exists = array_key_exists($name,$this->modules_names);
342               $module_overwrite = $module_exists ? version_compare($this->modules_names[$name],$version,'<') : false;
343               if (!$module_exists || ($module_exists && $module_overwrite)) {
344                    $this->modules_names[$name] = $version;
345                    $this->modules[$this->id] = array_merge(
346                         $properties,
347                         array(
348                              'root' => $this->mroot,
349                              'name' => $name,
350                              'desc' => $desc,
351                              'author' => $author,
352                              'version' => $version,
353                              'root_writable' => is_writable($this->mroot)
354                         )
355                    );
356               }
357               else {
358                    $path1 = path::real($this->moduleInfo($name,'root'));
359                    $path2 = path::real($this->mroot);
360                    $this->errors[] = sprintf(
361                         __('Module "%s" is installed twice in "%s" and "%s".'),
362                         '<strong>'.$name.'</strong>',
363                         '<em>'.$path1.'</em>',
364                         '<em>'.$path2.'</em>'
365                    );
366               }
367          }
368     }
369
370     public function resetModulesList()
371     {
372          $this->modules = array();
373          $this->modules_names = array();
374          $this->errors = array();
375     }
376
377     public static function installPackage($zip_file,dcModules &$modules)
378     {
379          $zip = new fileUnzip($zip_file);
380          $zip->getList(false,'#(^|/)(__MACOSX|\.svn|\.hg|\.git|\.DS_Store|\.directory|Thumbs\.db)(/|$)#');
381
382          $zip_root_dir = $zip->getRootDir();
383          $define = '';
384          if ($zip_root_dir != false) {
385               $target = dirname($zip_file);
386               $destination = $target.'/'.$zip_root_dir;
387               $define = $zip_root_dir.'/_define.php';
388               $has_define = $zip->hasFile($define);
389          } else {
390               $target = dirname($zip_file).'/'.preg_replace('/\.([^.]+)$/','',basename($zip_file));
391               $destination = $target;
392               $define = '_define.php';
393               $has_define = $zip->hasFile($define);
394          }
395
396          if ($zip->isEmpty()) {
397               $zip->close();
398               unlink($zip_file);
399               throw new Exception(__('Empty module zip file.'));
400          }
401
402          if (!$has_define) {
403               $zip->close();
404               unlink($zip_file);
405               throw new Exception(__('The zip file does not appear to be a valid Dotclear module.'));
406          }
407
408          $ret_code = 1;
409
410          if (!is_dir($destination))
411          {
412               try {
413                    files::makeDir($destination,true);
414
415                    $sandbox = clone $modules;
416                    $zip->unzip($define, $target.'/_define.php');
417
418                    $sandbox->resetModulesList();
419                    $sandbox->requireDefine($target,basename($destination));
420                    unlink($target.'/_define.php');
421
422                    $new_errors = $sandbox->getErrors();
423                    if (!empty($new_errors)) {
424                         $new_errors = is_array($new_errors) ? implode(" \n",$new_errors) : $new_errors;
425                         throw new Exception($new_errors);
426                    }
427
428                    files::deltree($destination);
429               }
430               catch(Exception $e)
431               {
432                    $zip->close();
433                    unlink($zip_file);
434                    files::deltree($destination);
435                    throw new Exception($e->getMessage());
436               }
437          }
438          else
439          {
440               # test for update
441               $sandbox = clone $modules;
442               $zip->unzip($define, $target.'/_define.php');
443
444               $sandbox->resetModulesList();
445               $sandbox->requireDefine($target,basename($destination));
446               unlink($target.'/_define.php');
447               $new_modules = $sandbox->getModules();
448
449               if (!empty($new_modules))
450               {
451                    $tmp = array_keys($new_modules);
452                    $id = $tmp[0];
453                    $cur_module = $modules->getModules($id);
454                    if (!empty($cur_module) && (defined('DC_DEV') && DC_DEV === true || dcUtils::versionsCompare($new_modules[$id]['version'], $cur_module['version'], '>', true)))
455                    {
456                         # delete old module
457                         if (!files::deltree($destination)) {
458                              throw new Exception(__('An error occurred during module deletion.'));
459                         }
460                         $ret_code = 2;
461                    }
462                    else
463                    {
464                         $zip->close();
465                         unlink($zip_file);
466                         throw new Exception(sprintf(__('Unable to upgrade "%s". (older or same version)'),basename($destination)));
467                    }
468               }
469               else
470               {
471                    $zip->close();
472                    unlink($zip_file);
473                    throw new Exception(sprintf(__('Unable to read new _define.php file')));
474               }
475          }
476          $zip->unzipAll($target);
477          $zip->close();
478          unlink($zip_file);
479          return $ret_code;
480     }
481
482     /**
483     This method installs all modules having a _install file.
484
485     @see dcModules::installModule
486     */
487     public function installModules()
488     {
489          $res = array('success'=>array(),'failure'=>array());
490          foreach ($this->modules as $id => &$m)
491          {
492               $i = $this->installModule($id,$msg);
493               if ($i === true) {
494                    $res['success'][$id] = true;
495               } elseif ($i === false) {
496                    $res['failure'][$id] = $msg;
497               }
498          }
499
500          return $res;
501     }
502
503     /**
504     This method installs module with ID <var>$id</var> and having a _install
505     file. This file should throw exception on failure or true if it installs
506     successfully.
507
508     <var>$msg</var> is an out parameter that handle installer message.
509
510     @param    id        <b>string</b>       Module ID
511     @param    msg       <b>string</b>       Module installer message
512     @return   <b>boolean</b>
513     */
514     public function installModule($id,&$msg)
515     {
516          try {
517               $i = $this->loadModuleFile($this->modules[$id]['root'].'/_install.php');
518               if ($i === true) {
519                    return true;
520               }
521          } catch (Exception $e) {
522               $msg = $e->getMessage();
523               return false;
524          }
525
526          return null;
527     }
528
529     public function deleteModule($id,$disabled=false)
530     {
531          if ($disabled) {
532               $p =& $this->disabled;
533          } else {
534               $p =& $this->modules;
535          }
536
537          if (!isset($p[$id])) {
538               throw new Exception(__('No such module.'));
539          }
540
541          if (!files::deltree($p[$id]['root'])) {
542               throw new Exception(__('Cannot remove module files'));
543          }
544     }
545
546     public function deactivateModule($id)
547     {
548          if (!isset($this->modules[$id])) {
549               throw new Exception(__('No such module.'));
550          }
551
552          if (!$this->modules[$id]['root_writable']) {
553               throw new Exception(__('Cannot deactivate plugin.'));
554          }
555
556          if (@file_put_contents($this->modules[$id]['root'].'/_disabled','')) {
557               throw new Exception(__('Cannot deactivate plugin.'));
558          }
559     }
560
561     public function activateModule($id)
562     {
563          if (!isset($this->disabled[$id])) {
564               throw new Exception(__('No such module.'));
565          }
566
567          if (!$this->disabled[$id]['root_writable']) {
568               throw new Exception(__('Cannot activate plugin.'));
569          }
570
571          if (@unlink($this->disabled[$id]['root'].'/_disabled') === false) {
572               throw new Exception(__('Cannot activate plugin.'));
573          }
574     }
575
576     /**
577     This method will search for file <var>$file</var> in language
578     <var>$lang</var> for module <var>$id</var>.
579
580     <var>$file</var> should not have any extension.
581
582     @param    id        <b>string</b>       Module ID
583     @param    lang      <b>string</b>       Language code
584     @param    file      <b>string</b>       File name (without extension)
585     */
586     public function loadModuleL10N($id,$lang,$file)
587     {
588          if (!$lang || !isset($this->modules[$id])) {
589               return;
590          }
591
592          $lfile = $this->modules[$id]['root'].'/locales/%s/%s';
593          if (l10n::set(sprintf($lfile,$lang,$file)) === false && $lang != 'en') {
594               l10n::set(sprintf($lfile,'en',$file));
595          }
596     }
597
598     public function loadModuleL10Nresources($id,$lang)
599     {
600          if (!$lang || !isset($this->modules[$id])) {
601               return;
602          }
603
604          $f = l10n::getFilePath($this->modules[$id]['root'].'/locales','resources.php',$lang);
605          if ($f) {
606               $this->loadModuleFile($f);
607          }
608     }
609
610     /**
611     Returns all modules associative array or only one module if <var>$id</var>
612     is present.
613
614     @param    id        <b>string</b>       Optionnal module ID
615     @return   <b>array</b>
616     */
617     public function getModules($id=null)
618     {
619          if ($id && isset($this->modules[$id])) {
620               return $this->modules[$id];
621          }
622          return $this->modules;
623     }
624
625     /**
626     Returns true if the module with ID <var>$id</var> exists.
627
628     @param    id        <b>string</b>       Module ID
629     @return   <b>boolean</b>
630     */
631     public function moduleExists($id)
632     {
633          return isset($this->modules[$id]);
634     }
635
636     /**
637     Returns all disabled modules in an array
638
639     @return   <b>array</b>
640     */
641     public function getDisabledModules()
642     {
643          return $this->disabled;
644     }
645
646     /**
647     Returns root path for module with ID <var>$id</var>.
648
649     @param    id        <b>string</b>       Module ID
650     @return   <b>string</b>
651     */
652     public function moduleRoot($id)
653     {
654          return $this->moduleInfo($id,'root');
655     }
656
657     /**
658     Returns a module information that could be:
659     - root
660     - name
661     - desc
662     - author
663     - version
664     - permissions
665     - priority
666
667     @param    id        <b>string</b>       Module ID
668     @param    info      <b>string</b>       Information to retrieve
669     @return   <b>string</b>
670     */
671     public function moduleInfo($id,$info)
672     {
673          return isset($this->modules[$id][$info]) ? $this->modules[$id][$info] : null;
674     }
675
676     /**
677     Loads namespace <var>$ns</var> specific files for all modules.
678
679     @param    ns        <b>string</b>       Namespace name
680     */
681     public function loadNsFiles($ns=null)
682     {
683          foreach ($this->modules as $k => $v) {
684               $this->loadNsFile($k,$ns);
685          }
686     }
687
688     /**
689     Loads namespace <var>$ns</var> specific file for module with ID
690     <var>$id</var>
691
692     @param    id        <b>string</b>       Module ID
693     @param    ns        <b>string</b>       Namespace name
694     */
695     public function loadNsFile($id,$ns=null)
696     {
697          switch ($ns) {
698               case 'admin':
699                    $this->loadModuleFile($this->modules[$id]['root'].'/_admin.php');
700                    break;
701               case 'public':
702                    $this->loadModuleFile($this->modules[$id]['root'].'/_public.php');
703                    break;
704               case 'xmlrpc':
705                    $this->loadModuleFile($this->modules[$id]['root'].'/_xmlrpc.php');
706                    break;
707          }
708     }
709
710     public function getErrors()
711     {
712          return $this->errors;
713     }
714
715     protected function loadModuleFile($________)
716     {
717          if (!file_exists($________)) {
718               return;
719          }
720
721          self::$_k = array_keys($GLOBALS);
722
723          foreach (self::$_k as self::$_n) {
724               if (!in_array(self::$_n,self::$superglobals)) {
725                    global ${self::$_n};
726               }
727          }
728
729          return require $________;
730     }
731
732     private function sortModules($a,$b)
733     {
734          if ($a['priority'] == $b['priority']) {
735               return strcasecmp($a['name'],$b['name']);
736          }
737
738          return ($a['priority'] < $b['priority']) ? -1 : 1;
739     }
740}
Note: See TracBrowser for help on using the repository browser.

Sites map