Dotclear

source: inc/core/class.dc.modules.php @ 2918:8c110613c240

Revision 2918:8c110613c240, 14.6 KB checked in by franck <carnet.franck.paul@…>, 11 years ago (diff)

Load all modules _prepend.php before loading their _admin.php or _public.php, addresses #1296

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

Sites map