1<?php
2/**
3 * PEAR_DependencyDB, advanced installed packages dependency database
4 *
5 * PHP versions 4 and 5
6 *
7 * @category   pear
8 * @package    PEAR
9 * @author     Tomas V. V. Cox <cox@idecnet.com>
10 * @author     Greg Beaver <cellog@php.net>
11 * @copyright  1997-2009 The Authors
12 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
13 * @link       http://pear.php.net/package/PEAR
14 * @since      File available since Release 1.4.0a1
15 */
16
17/**
18 * Needed for error handling
19 */
20require_once 'PEAR.php';
21require_once 'PEAR/Config.php';
22
23$GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] = array();
24/**
25 * Track dependency relationships between installed packages
26 * @category   pear
27 * @package    PEAR
28 * @author     Greg Beaver <cellog@php.net>
29 * @author     Tomas V.V.Cox <cox@idec.net.com>
30 * @copyright  1997-2009 The Authors
31 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
32 * @version    Release: @package_version@
33 * @link       http://pear.php.net/package/PEAR
34 * @since      Class available since Release 1.4.0a1
35 */
36class PEAR_DependencyDB
37{
38    // {{{ properties
39
40    /**
41     * This is initialized by {@link setConfig()}
42     * @var PEAR_Config
43     * @access private
44     */
45    var $_config;
46    /**
47     * This is initialized by {@link setConfig()}
48     * @var PEAR_Registry
49     * @access private
50     */
51    var $_registry;
52    /**
53     * Filename of the dependency DB (usually .depdb)
54     * @var string
55     * @access private
56     */
57    var $_depdb = false;
58    /**
59     * File name of the lockfile (usually .depdblock)
60     * @var string
61     * @access private
62     */
63    var $_lockfile = false;
64    /**
65     * Open file resource for locking the lockfile
66     * @var resource|false
67     * @access private
68     */
69    var $_lockFp = false;
70    /**
71     * API version of this class, used to validate a file on-disk
72     * @var string
73     * @access private
74     */
75    var $_version = '1.0';
76    /**
77     * Cached dependency database file
78     * @var array|null
79     * @access private
80     */
81    var $_cache;
82
83    // }}}
84    // {{{ & singleton()
85
86    /**
87     * Get a raw dependency database.  Calls setConfig() and assertDepsDB()
88     * @param PEAR_Config
89     * @param string|false full path to the dependency database, or false to use default
90     * @return PEAR_DependencyDB|PEAR_Error
91     */
92    public static function &singleton(&$config, $depdb = false)
93    {
94        $phpdir = $config->get('php_dir', null, 'pear.php.net');
95        if (!isset($GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir])) {
96            $a = new PEAR_DependencyDB;
97            $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir] = &$a;
98            $a->setConfig($config, $depdb);
99            $e = $a->assertDepsDB();
100            if (PEAR::isError($e)) {
101                return $e;
102            }
103        }
104
105        return $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir];
106    }
107
108    /**
109     * Set up the registry/location of dependency DB
110     * @param PEAR_Config|false
111     * @param string|false full path to the dependency database, or false to use default
112     */
113    function setConfig(&$config, $depdb = false)
114    {
115        if (!$config) {
116            $this->_config = &PEAR_Config::singleton();
117        } else {
118            $this->_config = &$config;
119        }
120
121        $this->_registry = &$this->_config->getRegistry();
122        if (!$depdb) {
123            $dir = $this->_config->get('metadata_dir', null, 'pear.php.net');
124            if (!$dir) {
125                $dir = $this->_config->get('php_dir', null, 'pear.php.net');
126            }
127            $this->_depdb =  $dir . DIRECTORY_SEPARATOR . '.depdb';
128        } else {
129            $this->_depdb = $depdb;
130        }
131
132        $this->_lockfile = dirname($this->_depdb) . DIRECTORY_SEPARATOR . '.depdblock';
133    }
134    // }}}
135
136    function hasWriteAccess()
137    {
138        if (!file_exists($this->_depdb)) {
139            $dir = $this->_depdb;
140            while ($dir && $dir != '.') {
141                $dir = dirname($dir); // cd ..
142                if ($dir != '.' && file_exists($dir)) {
143                    if (is_writeable($dir)) {
144                        return true;
145                    }
146
147                    return false;
148                }
149            }
150
151            return false;
152        }
153
154        return is_writeable($this->_depdb);
155    }
156
157    // {{{ assertDepsDB()
158
159    /**
160     * Create the dependency database, if it doesn't exist.  Error if the database is
161     * newer than the code reading it.
162     * @return void|PEAR_Error
163     */
164    function assertDepsDB()
165    {
166        if (!is_file($this->_depdb)) {
167            $this->rebuildDB();
168            return;
169        }
170
171        $depdb = $this->_getDepDB();
172        // Datatype format has been changed, rebuild the Deps DB
173        if ($depdb['_version'] < $this->_version) {
174            $this->rebuildDB();
175        }
176
177        if ($depdb['_version']{0} > $this->_version{0}) {
178            return PEAR::raiseError('Dependency database is version ' .
179                $depdb['_version'] . ', and we are version ' .
180                $this->_version . ', cannot continue');
181        }
182    }
183
184    /**
185     * Get a list of installed packages that depend on this package
186     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
187     * @return array|false
188     */
189    function getDependentPackages(&$pkg)
190    {
191        $data = $this->_getDepDB();
192        if (is_object($pkg)) {
193            $channel = strtolower($pkg->getChannel());
194            $package = strtolower($pkg->getPackage());
195        } else {
196            $channel = strtolower($pkg['channel']);
197            $package = strtolower($pkg['package']);
198        }
199
200        if (isset($data['packages'][$channel][$package])) {
201            return $data['packages'][$channel][$package];
202        }
203
204        return false;
205    }
206
207    /**
208     * Get a list of the actual dependencies of installed packages that depend on
209     * a package.
210     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
211     * @return array|false
212     */
213    function getDependentPackageDependencies(&$pkg)
214    {
215        $data = $this->_getDepDB();
216        if (is_object($pkg)) {
217            $channel = strtolower($pkg->getChannel());
218            $package = strtolower($pkg->getPackage());
219        } else {
220            $channel = strtolower($pkg['channel']);
221            $package = strtolower($pkg['package']);
222        }
223
224        $depend = $this->getDependentPackages($pkg);
225        if (!$depend) {
226            return false;
227        }
228
229        $dependencies = array();
230        foreach ($depend as $info) {
231            $temp = $this->getDependencies($info);
232            foreach ($temp as $dep) {
233                if (
234                    isset($dep['dep'], $dep['dep']['channel'], $dep['dep']['name']) &&
235                    strtolower($dep['dep']['channel']) == $channel &&
236                    strtolower($dep['dep']['name']) == $package
237                ) {
238                    if (!isset($dependencies[$info['channel']])) {
239                        $dependencies[$info['channel']] = array();
240                    }
241
242                    if (!isset($dependencies[$info['channel']][$info['package']])) {
243                        $dependencies[$info['channel']][$info['package']] = array();
244                    }
245                    $dependencies[$info['channel']][$info['package']][] = $dep;
246                }
247            }
248        }
249
250        return $dependencies;
251    }
252
253    /**
254     * Get a list of dependencies of this installed package
255     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
256     * @return array|false
257     */
258    function getDependencies(&$pkg)
259    {
260        if (is_object($pkg)) {
261            $channel = strtolower($pkg->getChannel());
262            $package = strtolower($pkg->getPackage());
263        } else {
264            $channel = strtolower($pkg['channel']);
265            $package = strtolower($pkg['package']);
266        }
267
268        $data = $this->_getDepDB();
269        if (isset($data['dependencies'][$channel][$package])) {
270            return $data['dependencies'][$channel][$package];
271        }
272
273        return false;
274    }
275
276    /**
277     * Determine whether $parent depends on $child, near or deep
278     * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
279     * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
280     */
281    function dependsOn($parent, $child)
282    {
283        $c = array();
284        $this->_getDepDB();
285        return $this->_dependsOn($parent, $child, $c);
286    }
287
288    function _dependsOn($parent, $child, &$checked)
289    {
290        if (is_object($parent)) {
291            $channel = strtolower($parent->getChannel());
292            $package = strtolower($parent->getPackage());
293        } else {
294            $channel = strtolower($parent['channel']);
295            $package = strtolower($parent['package']);
296        }
297
298        if (is_object($child)) {
299            $depchannel = strtolower($child->getChannel());
300            $deppackage = strtolower($child->getPackage());
301        } else {
302            $depchannel = strtolower($child['channel']);
303            $deppackage = strtolower($child['package']);
304        }
305
306        if (isset($checked[$channel][$package][$depchannel][$deppackage])) {
307            return false; // avoid endless recursion
308        }
309
310        $checked[$channel][$package][$depchannel][$deppackage] = true;
311        if (!isset($this->_cache['dependencies'][$channel][$package])) {
312            return false;
313        }
314
315        foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
316            if (isset($info['dep']['uri'])) {
317                if (is_object($child)) {
318                    if ($info['dep']['uri'] == $child->getURI()) {
319                        return true;
320                    }
321                } elseif (isset($child['uri'])) {
322                    if ($info['dep']['uri'] == $child['uri']) {
323                        return true;
324                    }
325                }
326                return false;
327            }
328
329            if (strtolower($info['dep']['channel']) == $depchannel &&
330                  strtolower($info['dep']['name']) == $deppackage) {
331                return true;
332            }
333        }
334
335        foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
336            if (isset($info['dep']['uri'])) {
337                if ($this->_dependsOn(array(
338                        'uri' => $info['dep']['uri'],
339                        'package' => $info['dep']['name']), $child, $checked)) {
340                    return true;
341                }
342            } else {
343                if ($this->_dependsOn(array(
344                        'channel' => $info['dep']['channel'],
345                        'package' => $info['dep']['name']), $child, $checked)) {
346                    return true;
347                }
348            }
349        }
350
351        return false;
352    }
353
354    /**
355     * Register dependencies of a package that is being installed or upgraded
356     * @param PEAR_PackageFile_v2|PEAR_PackageFile_v2
357     */
358    function installPackage(&$package)
359    {
360        $data = $this->_getDepDB();
361        unset($this->_cache);
362        $this->_setPackageDeps($data, $package);
363        $this->_writeDepDB($data);
364    }
365
366    /**
367     * Remove dependencies of a package that is being uninstalled, or upgraded.
368     *
369     * Upgraded packages first uninstall, then install
370     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array If an array, then it must have
371     *        indices 'channel' and 'package'
372     */
373    function uninstallPackage(&$pkg)
374    {
375        $data = $this->_getDepDB();
376        unset($this->_cache);
377        if (is_object($pkg)) {
378            $channel = strtolower($pkg->getChannel());
379            $package = strtolower($pkg->getPackage());
380        } else {
381            $channel = strtolower($pkg['channel']);
382            $package = strtolower($pkg['package']);
383        }
384
385        if (!isset($data['dependencies'][$channel][$package])) {
386            return true;
387        }
388
389        foreach ($data['dependencies'][$channel][$package] as $dep) {
390            $found      = false;
391            $depchannel = isset($dep['dep']['uri']) ? '__uri' : strtolower($dep['dep']['channel']);
392            $depname    = strtolower($dep['dep']['name']);
393            if (isset($data['packages'][$depchannel][$depname])) {
394                foreach ($data['packages'][$depchannel][$depname] as $i => $info) {
395                    if ($info['channel'] == $channel && $info['package'] == $package) {
396                        $found = true;
397                        break;
398                    }
399                }
400            }
401
402            if ($found) {
403                unset($data['packages'][$depchannel][$depname][$i]);
404                if (!count($data['packages'][$depchannel][$depname])) {
405                    unset($data['packages'][$depchannel][$depname]);
406                    if (!count($data['packages'][$depchannel])) {
407                        unset($data['packages'][$depchannel]);
408                    }
409                } else {
410                    $data['packages'][$depchannel][$depname] =
411                        array_values($data['packages'][$depchannel][$depname]);
412                }
413            }
414        }
415
416        unset($data['dependencies'][$channel][$package]);
417        if (!count($data['dependencies'][$channel])) {
418            unset($data['dependencies'][$channel]);
419        }
420
421        if (!count($data['dependencies'])) {
422            unset($data['dependencies']);
423        }
424
425        if (!count($data['packages'])) {
426            unset($data['packages']);
427        }
428
429        $this->_writeDepDB($data);
430    }
431
432    /**
433     * Rebuild the dependency DB by reading registry entries.
434     * @return true|PEAR_Error
435     */
436    function rebuildDB()
437    {
438        $depdb = array('_version' => $this->_version);
439        if (!$this->hasWriteAccess()) {
440            // allow startup for read-only with older Registry
441            return $depdb;
442        }
443
444        $packages = $this->_registry->listAllPackages();
445        if (PEAR::isError($packages)) {
446            return $packages;
447        }
448
449        foreach ($packages as $channel => $ps) {
450            foreach ($ps as $package) {
451                $package = $this->_registry->getPackage($package, $channel);
452                if (PEAR::isError($package)) {
453                    return $package;
454                }
455                $this->_setPackageDeps($depdb, $package);
456            }
457        }
458
459        $error = $this->_writeDepDB($depdb);
460        if (PEAR::isError($error)) {
461            return $error;
462        }
463
464        $this->_cache = $depdb;
465        return true;
466    }
467
468    /**
469     * Register usage of the dependency DB to prevent race conditions
470     * @param int one of the LOCK_* constants
471     * @return true|PEAR_Error
472     * @access private
473     */
474    function _lock($mode = LOCK_EX)
475    {
476        if (stristr(php_uname(), 'Windows 9')) {
477            return true;
478        }
479
480        if ($mode != LOCK_UN && is_resource($this->_lockFp)) {
481            // XXX does not check type of lock (LOCK_SH/LOCK_EX)
482            return true;
483        }
484
485        $open_mode = 'w';
486        // XXX People reported problems with LOCK_SH and 'w'
487        if ($mode === LOCK_SH) {
488            if (!file_exists($this->_lockfile)) {
489                touch($this->_lockfile);
490            } elseif (!is_file($this->_lockfile)) {
491                return PEAR::raiseError('could not create Dependency lock file, ' .
492                    'it exists and is not a regular file');
493            }
494            $open_mode = 'r';
495        }
496
497        if (!is_resource($this->_lockFp)) {
498            $this->_lockFp = @fopen($this->_lockfile, $open_mode);
499        }
500
501        if (!is_resource($this->_lockFp)) {
502            return PEAR::raiseError("could not create Dependency lock file" .
503                                     (isset($php_errormsg) ? ": " . $php_errormsg : ""));
504        }
505
506        if (!(int)flock($this->_lockFp, $mode)) {
507            switch ($mode) {
508                case LOCK_SH: $str = 'shared';    break;
509                case LOCK_EX: $str = 'exclusive'; break;
510                case LOCK_UN: $str = 'unlock';    break;
511                default:      $str = 'unknown';   break;
512            }
513
514            return PEAR::raiseError("could not acquire $str lock ($this->_lockfile)");
515        }
516
517        return true;
518    }
519
520    /**
521     * Release usage of dependency DB
522     * @return true|PEAR_Error
523     * @access private
524     */
525    function _unlock()
526    {
527        $ret = $this->_lock(LOCK_UN);
528        if (is_resource($this->_lockFp)) {
529            fclose($this->_lockFp);
530        }
531        $this->_lockFp = null;
532        return $ret;
533    }
534
535    /**
536     * Load the dependency database from disk, or return the cache
537     * @return array|PEAR_Error
538     */
539    function _getDepDB()
540    {
541        if (!$this->hasWriteAccess()) {
542            return array('_version' => $this->_version);
543        }
544
545        if (isset($this->_cache)) {
546            return $this->_cache;
547        }
548
549        if (!$fp = fopen($this->_depdb, 'r')) {
550            $err = PEAR::raiseError("Could not open dependencies file `".$this->_depdb."'");
551            return $err;
552        }
553
554        clearstatcache();
555        fclose($fp);
556        $data = unserialize(file_get_contents($this->_depdb));
557        $this->_cache = $data;
558        return $data;
559    }
560
561    /**
562     * Write out the dependency database to disk
563     * @param array the database
564     * @return true|PEAR_Error
565     * @access private
566     */
567    function _writeDepDB(&$deps)
568    {
569        if (PEAR::isError($e = $this->_lock(LOCK_EX))) {
570            return $e;
571        }
572
573        if (!$fp = fopen($this->_depdb, 'wb')) {
574            $this->_unlock();
575            return PEAR::raiseError("Could not open dependencies file `".$this->_depdb."' for writing");
576        }
577
578        fwrite($fp, serialize($deps));
579        fclose($fp);
580        $this->_unlock();
581        $this->_cache = $deps;
582        return true;
583    }
584
585    /**
586     * Register all dependencies from a package in the dependencies database, in essence
587     * "installing" the package's dependency information
588     * @param array the database
589     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
590     * @access private
591     */
592    function _setPackageDeps(&$data, &$pkg)
593    {
594        $pkg->setConfig($this->_config);
595        if ($pkg->getPackagexmlVersion() == '1.0') {
596            $gen = &$pkg->getDefaultGenerator();
597            $deps = $gen->dependenciesToV2();
598        } else {
599            $deps = $pkg->getDeps(true);
600        }
601
602        if (!$deps) {
603            return;
604        }
605
606        if (!is_array($data)) {
607            $data = array();
608        }
609
610        if (!isset($data['dependencies'])) {
611            $data['dependencies'] = array();
612        }
613
614        $channel = strtolower($pkg->getChannel());
615        $package = strtolower($pkg->getPackage());
616
617        if (!isset($data['dependencies'][$channel])) {
618            $data['dependencies'][$channel] = array();
619        }
620
621        $data['dependencies'][$channel][$package] = array();
622        if (isset($deps['required']['package'])) {
623            if (!isset($deps['required']['package'][0])) {
624                $deps['required']['package'] = array($deps['required']['package']);
625            }
626
627            foreach ($deps['required']['package'] as $dep) {
628                $this->_registerDep($data, $pkg, $dep, 'required');
629            }
630        }
631
632        if (isset($deps['optional']['package'])) {
633            if (!isset($deps['optional']['package'][0])) {
634                $deps['optional']['package'] = array($deps['optional']['package']);
635            }
636
637            foreach ($deps['optional']['package'] as $dep) {
638                $this->_registerDep($data, $pkg, $dep, 'optional');
639            }
640        }
641
642        if (isset($deps['required']['subpackage'])) {
643            if (!isset($deps['required']['subpackage'][0])) {
644                $deps['required']['subpackage'] = array($deps['required']['subpackage']);
645            }
646
647            foreach ($deps['required']['subpackage'] as $dep) {
648                $this->_registerDep($data, $pkg, $dep, 'required');
649            }
650        }
651
652        if (isset($deps['optional']['subpackage'])) {
653            if (!isset($deps['optional']['subpackage'][0])) {
654                $deps['optional']['subpackage'] = array($deps['optional']['subpackage']);
655            }
656
657            foreach ($deps['optional']['subpackage'] as $dep) {
658                $this->_registerDep($data, $pkg, $dep, 'optional');
659            }
660        }
661
662        if (isset($deps['group'])) {
663            if (!isset($deps['group'][0])) {
664                $deps['group'] = array($deps['group']);
665            }
666
667            foreach ($deps['group'] as $group) {
668                if (isset($group['package'])) {
669                    if (!isset($group['package'][0])) {
670                        $group['package'] = array($group['package']);
671                    }
672
673                    foreach ($group['package'] as $dep) {
674                        $this->_registerDep($data, $pkg, $dep, 'optional',
675                            $group['attribs']['name']);
676                    }
677                }
678
679                if (isset($group['subpackage'])) {
680                    if (!isset($group['subpackage'][0])) {
681                        $group['subpackage'] = array($group['subpackage']);
682                    }
683
684                    foreach ($group['subpackage'] as $dep) {
685                        $this->_registerDep($data, $pkg, $dep, 'optional',
686                            $group['attribs']['name']);
687                    }
688                }
689            }
690        }
691
692        if ($data['dependencies'][$channel][$package] == array()) {
693            unset($data['dependencies'][$channel][$package]);
694            if (!count($data['dependencies'][$channel])) {
695                unset($data['dependencies'][$channel]);
696            }
697        }
698    }
699
700    /**
701     * @param array the database
702     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
703     * @param array the specific dependency
704     * @param required|optional whether this is a required or an optional dep
705     * @param string|false dependency group this dependency is from, or false for ordinary dep
706     */
707    function _registerDep(&$data, &$pkg, $dep, $type, $group = false)
708    {
709        $info = array(
710            'dep'   => $dep,
711            'type'  => $type,
712            'group' => $group
713        );
714
715        $dep  = array_map('strtolower', $dep);
716        $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri';
717        if (!isset($data['dependencies'])) {
718            $data['dependencies'] = array();
719        }
720
721        $channel = strtolower($pkg->getChannel());
722        $package = strtolower($pkg->getPackage());
723
724        if (!isset($data['dependencies'][$channel])) {
725            $data['dependencies'][$channel] = array();
726        }
727
728        if (!isset($data['dependencies'][$channel][$package])) {
729            $data['dependencies'][$channel][$package] = array();
730        }
731
732        $data['dependencies'][$channel][$package][] = $info;
733        if (isset($data['packages'][$depchannel][$dep['name']])) {
734            $found = false;
735            foreach ($data['packages'][$depchannel][$dep['name']] as $i => $p) {
736                if ($p['channel'] == $channel && $p['package'] == $package) {
737                    $found = true;
738                    break;
739                }
740            }
741        } else {
742            if (!isset($data['packages'])) {
743                $data['packages'] = array();
744            }
745
746            if (!isset($data['packages'][$depchannel])) {
747                $data['packages'][$depchannel] = array();
748            }
749
750            if (!isset($data['packages'][$depchannel][$dep['name']])) {
751                $data['packages'][$depchannel][$dep['name']] = array();
752            }
753
754            $found = false;
755        }
756
757        if (!$found) {
758            $data['packages'][$depchannel][$dep['name']][] = array(
759                'channel' => $channel,
760                'package' => $package
761            );
762        }
763    }
764}
765