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