Dotclear

source: inc/core/class.dc.modules.php @ 3007:f9d013776723

Revision 3007:f9d013776723, 17.6 KB checked in by Dsls, 10 years ago (diff)

Update dependencies management, add notices and prevent enabling/disabling modules when dependencies are unmet.

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

Sites map