Dotclear

source: inc/core/class.dc.modules.php @ 2997:8f7c065a8639

Revision 2997:8f7c065a8639, 16.4 KB checked in by Dsls, 10 years ago (diff)

First step in modules dependencies management

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

Sites map