Dotclear

source: inc/core/class.dc.modules.php @ 3066:aa297059d2a0

Revision 3066:aa297059d2a0, 18.5 KB checked in by Dsls, 10 years ago (diff)

Disable plugins with unmetdependencies, in admin home and plugins.php admin page. See #1842, fixed partially (need to see how to cope with themes now)

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

Sites map