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