Dotclear

source: inc/core/class.dc.modules.php @ 3008:dfca115b70dd

Revision 3008:dfca115b70dd, 17.5 KB checked in by Dsls, 10 years ago (diff)

oops (c)...

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

Sites map