Dotclear

source: inc/core/class.dc.modules.php @ 2954:85e45ee8f77e

Revision 2954:85e45ee8f77e, 15.3 KB checked in by Dsls, 11 years ago (diff)

Take disabled module information into consideration, enrich plugins.php disabled plugins list. Fixes #2066

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

Sites map