1<?php 2/** 3 * PEAR_Downloader, the PEAR Installer's download utility class 4 * 5 * PHP versions 4 and 5 6 * 7 * @category pear 8 * @package PEAR 9 * @author Greg Beaver <cellog@php.net> 10 * @author Stig Bakken <ssb@php.net> 11 * @author Tomas V. V. Cox <cox@idecnet.com> 12 * @author Martin Jansen <mj@php.net> 13 * @copyright 1997-2009 The Authors 14 * @license http://opensource.org/licenses/bsd-license.php New BSD License 15 * @link http://pear.php.net/package/PEAR 16 * @since File available since Release 1.3.0 17 */ 18 19/** 20 * Needed for constants, extending 21 */ 22require_once 'PEAR/Common.php'; 23require_once 'PEAR/Proxy.php'; 24 25define('PEAR_INSTALLER_OK', 1); 26define('PEAR_INSTALLER_FAILED', 0); 27define('PEAR_INSTALLER_SKIPPED', -1); 28define('PEAR_INSTALLER_ERROR_NO_PREF_STATE', 2); 29 30/** 31 * Administration class used to download anything from the internet (PEAR Packages, 32 * static URLs, xml files) 33 * 34 * @category pear 35 * @package PEAR 36 * @author Greg Beaver <cellog@php.net> 37 * @author Stig Bakken <ssb@php.net> 38 * @author Tomas V. V. Cox <cox@idecnet.com> 39 * @author Martin Jansen <mj@php.net> 40 * @copyright 1997-2009 The Authors 41 * @license http://opensource.org/licenses/bsd-license.php New BSD License 42 * @version Release: @package_version@ 43 * @link http://pear.php.net/package/PEAR 44 * @since Class available since Release 1.3.0 45 */ 46class PEAR_Downloader extends PEAR_Common 47{ 48 /** 49 * @var PEAR_Registry 50 * @access private 51 */ 52 var $_registry; 53 54 /** 55 * Preferred Installation State (snapshot, devel, alpha, beta, stable) 56 * @var string|null 57 * @access private 58 */ 59 var $_preferredState; 60 61 /** 62 * Options from command-line passed to Install. 63 * 64 * Recognized options:<br /> 65 * - onlyreqdeps : install all required dependencies as well 66 * - alldeps : install all dependencies, including optional 67 * - installroot : base relative path to install files in 68 * - force : force a download even if warnings would prevent it 69 * - nocompress : download uncompressed tarballs 70 * @see PEAR_Command_Install 71 * @access private 72 * @var array 73 */ 74 var $_options; 75 76 /** 77 * Downloaded Packages after a call to download(). 78 * 79 * Format of each entry: 80 * 81 * <code> 82 * array('pkg' => 'package_name', 'file' => '/path/to/local/file', 83 * 'info' => array() // parsed package.xml 84 * ); 85 * </code> 86 * @access private 87 * @var array 88 */ 89 var $_downloadedPackages = array(); 90 91 /** 92 * Packages slated for download. 93 * 94 * This is used to prevent downloading a package more than once should it be a dependency 95 * for two packages to be installed. 96 * Format of each entry: 97 * 98 * <pre> 99 * array('package_name1' => parsed package.xml, 'package_name2' => parsed package.xml, 100 * ); 101 * </pre> 102 * @access private 103 * @var array 104 */ 105 var $_toDownload = array(); 106 107 /** 108 * Array of every package installed, with names lower-cased. 109 * 110 * Format: 111 * <code> 112 * array('package1' => 0, 'package2' => 1, ); 113 * </code> 114 * @var array 115 */ 116 var $_installed = array(); 117 118 /** 119 * @var array 120 * @access private 121 */ 122 var $_errorStack = array(); 123 124 /** 125 * @var boolean 126 * @access private 127 */ 128 var $_internalDownload = false; 129 130 /** 131 * Temporary variable used in sorting packages by dependency in {@link sortPkgDeps()} 132 * @var array 133 * @access private 134 */ 135 var $_packageSortTree; 136 137 /** 138 * Temporary directory, or configuration value where downloads will occur 139 * @var string 140 */ 141 var $_downloadDir; 142 143 /** 144 * List of methods that can be called both statically and non-statically. 145 * @var array 146 */ 147 protected static $bivalentMethods = array( 148 'setErrorHandling' => true, 149 'raiseError' => true, 150 'throwError' => true, 151 'pushErrorHandling' => true, 152 'popErrorHandling' => true, 153 'downloadHttp' => true, 154 ); 155 156 /** 157 * @param PEAR_Frontend_* 158 * @param array 159 * @param PEAR_Config 160 */ 161 function __construct($ui = null, $options = array(), $config = null) 162 { 163 parent::__construct(); 164 $this->_options = $options; 165 if ($config !== null) { 166 $this->config = &$config; 167 $this->_preferredState = $this->config->get('preferred_state'); 168 } 169 $this->ui = &$ui; 170 if (!$this->_preferredState) { 171 // don't inadvertently use a non-set preferred_state 172 $this->_preferredState = null; 173 } 174 175 if ($config !== null) { 176 if (isset($this->_options['installroot'])) { 177 $this->config->setInstallRoot($this->_options['installroot']); 178 } 179 $this->_registry = &$config->getRegistry(); 180 } 181 182 if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) { 183 $this->_installed = $this->_registry->listAllPackages(); 184 foreach ($this->_installed as $key => $unused) { 185 if (!count($unused)) { 186 continue; 187 } 188 $strtolower = function($a) { return strtolower($a); }; 189 array_walk($this->_installed[$key], $strtolower); 190 } 191 } 192 } 193 194 /** 195 * Attempt to discover a channel's remote capabilities from 196 * its server name 197 * @param string 198 * @return boolean 199 */ 200 function discover($channel) 201 { 202 $this->log(1, 'Attempting to discover channel "' . $channel . '"...'); 203 PEAR::pushErrorHandling(PEAR_ERROR_RETURN); 204 $callback = $this->ui ? array(&$this, '_downloadCallback') : null; 205 if (!class_exists('System')) { 206 require_once 'System.php'; 207 } 208 209 $tmpdir = $this->config->get('temp_dir'); 210 $tmp = System::mktemp('-d -t "' . $tmpdir . '"'); 211 $a = $this->downloadHttp('http://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false); 212 PEAR::popErrorHandling(); 213 if (PEAR::isError($a)) { 214 // Attempt to fallback to https automatically. 215 PEAR::pushErrorHandling(PEAR_ERROR_RETURN); 216 $this->log(1, 'Attempting fallback to https instead of http on channel "' . $channel . '"...'); 217 $a = $this->downloadHttp('https://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false); 218 PEAR::popErrorHandling(); 219 if (PEAR::isError($a)) { 220 return false; 221 } 222 } 223 224 list($a, $lastmodified) = $a; 225 if (!class_exists('PEAR_ChannelFile')) { 226 require_once 'PEAR/ChannelFile.php'; 227 } 228 229 $b = new PEAR_ChannelFile; 230 if ($b->fromXmlFile($a)) { 231 unlink($a); 232 if ($this->config->get('auto_discover')) { 233 $this->_registry->addChannel($b, $lastmodified); 234 $alias = $b->getName(); 235 if ($b->getName() == $this->_registry->channelName($b->getAlias())) { 236 $alias = $b->getAlias(); 237 } 238 239 $this->log(1, 'Auto-discovered channel "' . $channel . 240 '", alias "' . $alias . '", adding to registry'); 241 } 242 243 return true; 244 } 245 246 unlink($a); 247 return false; 248 } 249 250 /** 251 * For simpler unit-testing 252 * @param PEAR_Downloader 253 * @return PEAR_Downloader_Package 254 */ 255 function newDownloaderPackage(&$t) 256 { 257 if (!class_exists('PEAR_Downloader_Package')) { 258 require_once 'PEAR/Downloader/Package.php'; 259 } 260 $a = new PEAR_Downloader_Package($t); 261 return $a; 262 } 263 264 /** 265 * For simpler unit-testing 266 * @param PEAR_Config 267 * @param array 268 * @param array 269 * @param int 270 */ 271 function &getDependency2Object(&$c, $i, $p, $s) 272 { 273 if (!class_exists('PEAR_Dependency2')) { 274 require_once 'PEAR/Dependency2.php'; 275 } 276 $z = new PEAR_Dependency2($c, $i, $p, $s); 277 return $z; 278 } 279 280 function &download($params) 281 { 282 if (!count($params)) { 283 $a = array(); 284 return $a; 285 } 286 287 if (!isset($this->_registry)) { 288 $this->_registry = &$this->config->getRegistry(); 289 } 290 291 $channelschecked = array(); 292 // convert all parameters into PEAR_Downloader_Package objects 293 foreach ($params as $i => $param) { 294 $params[$i] = $this->newDownloaderPackage($this); 295 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 296 $err = $params[$i]->initialize($param); 297 PEAR::staticPopErrorHandling(); 298 if (!$err) { 299 // skip parameters that were missed by preferred_state 300 continue; 301 } 302 303 if (PEAR::isError($err)) { 304 if (!isset($this->_options['soft']) && $err->getMessage() !== '') { 305 $this->log(0, $err->getMessage()); 306 } 307 308 $params[$i] = false; 309 if (is_object($param)) { 310 $param = $param->getChannel() . '/' . $param->getPackage(); 311 } 312 313 if (!isset($this->_options['soft'])) { 314 $this->log(2, 'Package "' . $param . '" is not valid'); 315 } 316 317 // Message logged above in a specific verbose mode, passing null to not show up on CLI 318 $this->pushError(null, PEAR_INSTALLER_SKIPPED); 319 } else { 320 do { 321 if ($params[$i] && $params[$i]->getType() == 'local') { 322 // bug #7090 skip channel.xml check for local packages 323 break; 324 } 325 326 if ($params[$i] && !isset($channelschecked[$params[$i]->getChannel()]) && 327 !isset($this->_options['offline']) 328 ) { 329 $channelschecked[$params[$i]->getChannel()] = true; 330 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 331 if (!class_exists('System')) { 332 require_once 'System.php'; 333 } 334 335 $curchannel = $this->_registry->getChannel($params[$i]->getChannel()); 336 if (PEAR::isError($curchannel)) { 337 PEAR::staticPopErrorHandling(); 338 return $this->raiseError($curchannel); 339 } 340 341 if (PEAR::isError($dir = $this->getDownloadDir())) { 342 PEAR::staticPopErrorHandling(); 343 break; 344 } 345 346 $mirror = $this->config->get('preferred_mirror', null, $params[$i]->getChannel()); 347 $url = 'http://' . $mirror . '/channel.xml'; 348 $a = $this->downloadHttp($url, $this->ui, $dir, null, $curchannel->lastModified()); 349 350 PEAR::staticPopErrorHandling(); 351 if ($a === false) { 352 //channel.xml not modified 353 break; 354 } else if (PEAR::isError($a)) { 355 // Attempt fallback to https automatically 356 PEAR::pushErrorHandling(PEAR_ERROR_RETURN); 357 $a = $this->downloadHttp('https://' . $mirror . 358 '/channel.xml', $this->ui, $dir, null, $curchannel->lastModified()); 359 360 PEAR::staticPopErrorHandling(); 361 if (PEAR::isError($a) || !$a) { 362 break; 363 } 364 } 365 $this->log(0, 'WARNING: channel "' . $params[$i]->getChannel() . '" has ' . 366 'updated its protocols, use "' . PEAR_RUNTYPE . ' channel-update ' . $params[$i]->getChannel() . 367 '" to update'); 368 } 369 } while (false); 370 371 if ($params[$i] && !isset($this->_options['downloadonly'])) { 372 if (isset($this->_options['packagingroot'])) { 373 $checkdir = $this->_prependPath( 374 $this->config->get('php_dir', null, $params[$i]->getChannel()), 375 $this->_options['packagingroot']); 376 } else { 377 $checkdir = $this->config->get('php_dir', 378 null, $params[$i]->getChannel()); 379 } 380 381 while ($checkdir && $checkdir != '/' && !file_exists($checkdir)) { 382 $checkdir = dirname($checkdir); 383 } 384 385 if ($checkdir == '.') { 386 $checkdir = '/'; 387 } 388 389 if (!is_writeable($checkdir)) { 390 return PEAR::raiseError('Cannot install, php_dir for channel "' . 391 $params[$i]->getChannel() . '" is not writeable by the current user'); 392 } 393 } 394 } 395 } 396 397 unset($channelschecked); 398 PEAR_Downloader_Package::removeDuplicates($params); 399 if (!count($params)) { 400 $a = array(); 401 return $a; 402 } 403 404 if (!isset($this->_options['nodeps']) && !isset($this->_options['offline'])) { 405 $reverify = true; 406 while ($reverify) { 407 $reverify = false; 408 foreach ($params as $i => $param) { 409 //PHP Bug 40768 / PEAR Bug #10944 410 //Nested foreaches fail in PHP 5.2.1 411 key($params); 412 $ret = $params[$i]->detectDependencies($params); 413 if (PEAR::isError($ret)) { 414 $reverify = true; 415 $params[$i] = false; 416 PEAR_Downloader_Package::removeDuplicates($params); 417 if (!isset($this->_options['soft'])) { 418 $this->log(0, $ret->getMessage()); 419 } 420 continue 2; 421 } 422 } 423 } 424 } 425 426 if (isset($this->_options['offline'])) { 427 $this->log(3, 'Skipping dependency download check, --offline specified'); 428 } 429 430 if (!count($params)) { 431 $a = array(); 432 return $a; 433 } 434 435 while (PEAR_Downloader_Package::mergeDependencies($params)); 436 PEAR_Downloader_Package::removeDuplicates($params, true); 437 $errorparams = array(); 438 if (PEAR_Downloader_Package::detectStupidDuplicates($params, $errorparams)) { 439 if (count($errorparams)) { 440 foreach ($errorparams as $param) { 441 $name = $this->_registry->parsedPackageNameToString($param->getParsedPackage()); 442 $this->pushError('Duplicate package ' . $name . ' found', PEAR_INSTALLER_FAILED); 443 } 444 $a = array(); 445 return $a; 446 } 447 } 448 449 PEAR_Downloader_Package::removeInstalled($params); 450 if (!count($params)) { 451 $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED); 452 $a = array(); 453 return $a; 454 } 455 456 PEAR::pushErrorHandling(PEAR_ERROR_RETURN); 457 $err = $this->analyzeDependencies($params); 458 PEAR::popErrorHandling(); 459 if (!count($params)) { 460 $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED); 461 $a = array(); 462 return $a; 463 } 464 465 $ret = array(); 466 $newparams = array(); 467 if (isset($this->_options['pretend'])) { 468 return $params; 469 } 470 471 $somefailed = false; 472 foreach ($params as $i => $package) { 473 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 474 $pf = &$params[$i]->download(); 475 PEAR::staticPopErrorHandling(); 476 if (PEAR::isError($pf)) { 477 if (!isset($this->_options['soft'])) { 478 $this->log(1, $pf->getMessage()); 479 $this->log(0, 'Error: cannot download "' . 480 $this->_registry->parsedPackageNameToString($package->getParsedPackage(), 481 true) . 482 '"'); 483 } 484 $somefailed = true; 485 continue; 486 } 487 488 $newparams[] = &$params[$i]; 489 $ret[] = array( 490 'file' => $pf->getArchiveFile(), 491 'info' => &$pf, 492 'pkg' => $pf->getPackage() 493 ); 494 } 495 496 if ($somefailed) { 497 // remove params that did not download successfully 498 PEAR::pushErrorHandling(PEAR_ERROR_RETURN); 499 $err = $this->analyzeDependencies($newparams, true); 500 PEAR::popErrorHandling(); 501 if (!count($newparams)) { 502 $this->pushError('Download failed', PEAR_INSTALLER_FAILED); 503 $a = array(); 504 return $a; 505 } 506 } 507 508 $this->_downloadedPackages = $ret; 509 return $newparams; 510 } 511 512 /** 513 * @param array all packages to be installed 514 */ 515 function analyzeDependencies(&$params, $force = false) 516 { 517 if (isset($this->_options['downloadonly'])) { 518 return; 519 } 520 521 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 522 $redo = true; 523 $reset = $hasfailed = $failed = false; 524 while ($redo) { 525 $redo = false; 526 foreach ($params as $i => $param) { 527 $deps = $param->getDeps(); 528 if (!$deps) { 529 $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(), 530 $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING); 531 $send = $param->getPackageFile(); 532 533 $installcheck = $depchecker->validatePackage($send, $this, $params); 534 if (PEAR::isError($installcheck)) { 535 if (!isset($this->_options['soft'])) { 536 $this->log(0, $installcheck->getMessage()); 537 } 538 $hasfailed = true; 539 $params[$i] = false; 540 $reset = true; 541 $redo = true; 542 $failed = false; 543 PEAR_Downloader_Package::removeDuplicates($params); 544 continue 2; 545 } 546 continue; 547 } 548 549 if (!$reset && $param->alreadyValidated() && !$force) { 550 continue; 551 } 552 553 if (count($deps)) { 554 $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(), 555 $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING); 556 $send = $param->getPackageFile(); 557 if ($send === null) { 558 $send = $param->getDownloadURL(); 559 } 560 561 $installcheck = $depchecker->validatePackage($send, $this, $params); 562 if (PEAR::isError($installcheck)) { 563 if (!isset($this->_options['soft'])) { 564 $this->log(0, $installcheck->getMessage()); 565 } 566 $hasfailed = true; 567 $params[$i] = false; 568 $reset = true; 569 $redo = true; 570 $failed = false; 571 PEAR_Downloader_Package::removeDuplicates($params); 572 continue 2; 573 } 574 575 $failed = false; 576 if (isset($deps['required']) && is_array($deps['required'])) { 577 foreach ($deps['required'] as $type => $dep) { 578 // note: Dependency2 will never return a PEAR_Error if ignore-errors 579 // is specified, so soft is needed to turn off logging 580 if (!isset($dep[0])) { 581 if (PEAR::isError($e = $depchecker->{"validate{$type}Dependency"}($dep, 582 true, $params))) { 583 $failed = true; 584 if (!isset($this->_options['soft'])) { 585 $this->log(0, $e->getMessage()); 586 } 587 } elseif (is_array($e) && !$param->alreadyValidated()) { 588 if (!isset($this->_options['soft'])) { 589 $this->log(0, $e[0]); 590 } 591 } 592 } else { 593 foreach ($dep as $d) { 594 if (PEAR::isError($e = 595 $depchecker->{"validate{$type}Dependency"}($d, 596 true, $params))) { 597 $failed = true; 598 if (!isset($this->_options['soft'])) { 599 $this->log(0, $e->getMessage()); 600 } 601 } elseif (is_array($e) && !$param->alreadyValidated()) { 602 if (!isset($this->_options['soft'])) { 603 $this->log(0, $e[0]); 604 } 605 } 606 } 607 } 608 } 609 610 if (isset($deps['optional']) && is_array($deps['optional'])) { 611 foreach ($deps['optional'] as $type => $dep) { 612 if (!isset($dep[0])) { 613 if (PEAR::isError($e = 614 $depchecker->{"validate{$type}Dependency"}($dep, 615 false, $params))) { 616 $failed = true; 617 if (!isset($this->_options['soft'])) { 618 $this->log(0, $e->getMessage()); 619 } 620 } elseif (is_array($e) && !$param->alreadyValidated()) { 621 if (!isset($this->_options['soft'])) { 622 $this->log(0, $e[0]); 623 } 624 } 625 } else { 626 foreach ($dep as $d) { 627 if (PEAR::isError($e = 628 $depchecker->{"validate{$type}Dependency"}($d, 629 false, $params))) { 630 $failed = true; 631 if (!isset($this->_options['soft'])) { 632 $this->log(0, $e->getMessage()); 633 } 634 } elseif (is_array($e) && !$param->alreadyValidated()) { 635 if (!isset($this->_options['soft'])) { 636 $this->log(0, $e[0]); 637 } 638 } 639 } 640 } 641 } 642 } 643 644 $groupname = $param->getGroup(); 645 if (isset($deps['group']) && $groupname) { 646 if (!isset($deps['group'][0])) { 647 $deps['group'] = array($deps['group']); 648 } 649 650 $found = false; 651 foreach ($deps['group'] as $group) { 652 if ($group['attribs']['name'] == $groupname) { 653 $found = true; 654 break; 655 } 656 } 657 658 if ($found) { 659 unset($group['attribs']); 660 foreach ($group as $type => $dep) { 661 if (!isset($dep[0])) { 662 if (PEAR::isError($e = 663 $depchecker->{"validate{$type}Dependency"}($dep, 664 false, $params))) { 665 $failed = true; 666 if (!isset($this->_options['soft'])) { 667 $this->log(0, $e->getMessage()); 668 } 669 } elseif (is_array($e) && !$param->alreadyValidated()) { 670 if (!isset($this->_options['soft'])) { 671 $this->log(0, $e[0]); 672 } 673 } 674 } else { 675 foreach ($dep as $d) { 676 if (PEAR::isError($e = 677 $depchecker->{"validate{$type}Dependency"}($d, 678 false, $params))) { 679 $failed = true; 680 if (!isset($this->_options['soft'])) { 681 $this->log(0, $e->getMessage()); 682 } 683 } elseif (is_array($e) && !$param->alreadyValidated()) { 684 if (!isset($this->_options['soft'])) { 685 $this->log(0, $e[0]); 686 } 687 } 688 } 689 } 690 } 691 } 692 } 693 } else { 694 foreach ($deps as $dep) { 695 if (PEAR::isError($e = $depchecker->validateDependency1($dep, $params))) { 696 $failed = true; 697 if (!isset($this->_options['soft'])) { 698 $this->log(0, $e->getMessage()); 699 } 700 } elseif (is_array($e) && !$param->alreadyValidated()) { 701 if (!isset($this->_options['soft'])) { 702 $this->log(0, $e[0]); 703 } 704 } 705 } 706 } 707 $params[$i]->setValidated(); 708 } 709 710 if ($failed) { 711 $hasfailed = true; 712 $params[$i] = false; 713 $reset = true; 714 $redo = true; 715 $failed = false; 716 PEAR_Downloader_Package::removeDuplicates($params); 717 continue 2; 718 } 719 } 720 } 721 722 PEAR::staticPopErrorHandling(); 723 if ($hasfailed && (isset($this->_options['ignore-errors']) || 724 isset($this->_options['nodeps']))) { 725 // this is probably not needed, but just in case 726 if (!isset($this->_options['soft'])) { 727 $this->log(0, 'WARNING: dependencies failed'); 728 } 729 } 730 } 731 732 /** 733 * Retrieve the directory that downloads will happen in 734 * @access private 735 * @return string 736 */ 737 function getDownloadDir() 738 { 739 if (isset($this->_downloadDir)) { 740 return $this->_downloadDir; 741 } 742 743 $downloaddir = $this->config->get('download_dir'); 744 if (empty($downloaddir) || (is_dir($downloaddir) && !is_writable($downloaddir))) { 745 if (is_dir($downloaddir) && !is_writable($downloaddir)) { 746 $this->log(0, 'WARNING: configuration download directory "' . $downloaddir . 747 '" is not writeable. Change download_dir config variable to ' . 748 'a writeable dir to avoid this warning'); 749 } 750 751 if (!class_exists('System')) { 752 require_once 'System.php'; 753 } 754 755 if (PEAR::isError($downloaddir = System::mktemp('-d'))) { 756 return $downloaddir; 757 } 758 $this->log(3, '+ tmp dir created at ' . $downloaddir); 759 } 760 761 if (!is_writable($downloaddir)) { 762 if (PEAR::isError(System::mkdir(array('-p', $downloaddir))) || 763 !is_writable($downloaddir)) { 764 return PEAR::raiseError('download directory "' . $downloaddir . 765 '" is not writeable. Change download_dir config variable to ' . 766 'a writeable dir'); 767 } 768 } 769 770 return $this->_downloadDir = $downloaddir; 771 } 772 773 function setDownloadDir($dir) 774 { 775 if (!@is_writable($dir)) { 776 if (PEAR::isError(System::mkdir(array('-p', $dir)))) { 777 return PEAR::raiseError('download directory "' . $dir . 778 '" is not writeable. Change download_dir config variable to ' . 779 'a writeable dir'); 780 } 781 } 782 $this->_downloadDir = $dir; 783 } 784 785 function configSet($key, $value, $layer = 'user', $channel = false) 786 { 787 $this->config->set($key, $value, $layer, $channel); 788 $this->_preferredState = $this->config->get('preferred_state', null, $channel); 789 if (!$this->_preferredState) { 790 // don't inadvertently use a non-set preferred_state 791 $this->_preferredState = null; 792 } 793 } 794 795 function setOptions($options) 796 { 797 $this->_options = $options; 798 } 799 800 function getOptions() 801 { 802 return $this->_options; 803 } 804 805 806 /** 807 * @param array output of {@link parsePackageName()} 808 * @access private 809 */ 810 function _getPackageDownloadUrl($parr) 811 { 812 $curchannel = $this->config->get('default_channel'); 813 $this->configSet('default_channel', $parr['channel']); 814 // getDownloadURL returns an array. On error, it only contains information 815 // on the latest release as array(version, info). On success it contains 816 // array(version, info, download url string) 817 $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state'); 818 if (!$this->_registry->channelExists($parr['channel'])) { 819 do { 820 if ($this->config->get('auto_discover') && $this->discover($parr['channel'])) { 821 break; 822 } 823 824 $this->configSet('default_channel', $curchannel); 825 return PEAR::raiseError('Unknown remote channel: ' . $parr['channel']); 826 } while (false); 827 } 828 829 $chan = $this->_registry->getChannel($parr['channel']); 830 if (PEAR::isError($chan)) { 831 return $chan; 832 } 833 834 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 835 $version = $this->_registry->packageInfo($parr['package'], 'version', $parr['channel']); 836 $stability = $this->_registry->packageInfo($parr['package'], 'stability', $parr['channel']); 837 // package is installed - use the installed release stability level 838 if (!isset($parr['state']) && $stability !== null) { 839 $state = $stability['release']; 840 } 841 PEAR::staticPopErrorHandling(); 842 $base2 = false; 843 844 $preferred_mirror = $this->config->get('preferred_mirror'); 845 if (!$chan->supportsREST($preferred_mirror) || 846 ( 847 !($base2 = $chan->getBaseURL('REST1.3', $preferred_mirror)) 848 && 849 !($base = $chan->getBaseURL('REST1.0', $preferred_mirror)) 850 ) 851 ) { 852 return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.'); 853 } 854 855 if ($base2) { 856 $rest = &$this->config->getREST('1.3', $this->_options); 857 $base = $base2; 858 } else { 859 $rest = &$this->config->getREST('1.0', $this->_options); 860 } 861 862 $downloadVersion = false; 863 if (!isset($parr['version']) && !isset($parr['state']) && $version 864 && !PEAR::isError($version) 865 && !isset($this->_options['downloadonly']) 866 ) { 867 $downloadVersion = $version; 868 } 869 870 $url = $rest->getDownloadURL($base, $parr, $state, $downloadVersion, $chan->getName()); 871 if (PEAR::isError($url)) { 872 $this->configSet('default_channel', $curchannel); 873 return $url; 874 } 875 876 if ($parr['channel'] != $curchannel) { 877 $this->configSet('default_channel', $curchannel); 878 } 879 880 if (!is_array($url)) { 881 return $url; 882 } 883 884 $url['raw'] = false; // no checking is necessary for REST 885 if (!is_array($url['info'])) { 886 return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' . 887 'this should never happen'); 888 } 889 890 if (!isset($this->_options['force']) && 891 !isset($this->_options['downloadonly']) && 892 $version && 893 !PEAR::isError($version) && 894 !isset($parr['group']) 895 ) { 896 if (version_compare($version, $url['version'], '=')) { 897 return PEAR::raiseError($this->_registry->parsedPackageNameToString( 898 $parr, true) . ' is already installed and is the same as the ' . 899 'released version ' . $url['version'], -976); 900 } 901 902 if (version_compare($version, $url['version'], '>')) { 903 return PEAR::raiseError($this->_registry->parsedPackageNameToString( 904 $parr, true) . ' is already installed and is newer than detected ' . 905 'released version ' . $url['version'], -976); 906 } 907 } 908 909 if (isset($url['info']['required']) || $url['compatible']) { 910 require_once 'PEAR/PackageFile/v2.php'; 911 $pf = new PEAR_PackageFile_v2; 912 $pf->setRawChannel($parr['channel']); 913 if ($url['compatible']) { 914 $pf->setRawCompatible($url['compatible']); 915 } 916 } else { 917 require_once 'PEAR/PackageFile/v1.php'; 918 $pf = new PEAR_PackageFile_v1; 919 } 920 921 $pf->setRawPackage($url['package']); 922 $pf->setDeps($url['info']); 923 if ($url['compatible']) { 924 $pf->setCompatible($url['compatible']); 925 } 926 927 $pf->setRawState($url['stability']); 928 $url['info'] = &$pf; 929 if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { 930 $ext = '.tar'; 931 } else { 932 $ext = '.tgz'; 933 } 934 935 if (is_array($url) && isset($url['url'])) { 936 $url['url'] .= $ext; 937 } 938 939 return $url; 940 } 941 942 /** 943 * @param array dependency array 944 * @access private 945 */ 946 function _getDepPackageDownloadUrl($dep, $parr) 947 { 948 $xsdversion = isset($dep['rel']) ? '1.0' : '2.0'; 949 $curchannel = $this->config->get('default_channel'); 950 if (isset($dep['uri'])) { 951 $xsdversion = '2.0'; 952 $chan = $this->_registry->getChannel('__uri'); 953 if (PEAR::isError($chan)) { 954 return $chan; 955 } 956 957 $version = $this->_registry->packageInfo($dep['name'], 'version', '__uri'); 958 $this->configSet('default_channel', '__uri'); 959 } else { 960 if (isset($dep['channel'])) { 961 $remotechannel = $dep['channel']; 962 } else { 963 $remotechannel = 'pear.php.net'; 964 } 965 966 if (!$this->_registry->channelExists($remotechannel)) { 967 do { 968 if ($this->config->get('auto_discover')) { 969 if ($this->discover($remotechannel)) { 970 break; 971 } 972 } 973 return PEAR::raiseError('Unknown remote channel: ' . $remotechannel); 974 } while (false); 975 } 976 977 $chan = $this->_registry->getChannel($remotechannel); 978 if (PEAR::isError($chan)) { 979 return $chan; 980 } 981 982 $version = $this->_registry->packageInfo($dep['name'], 'version', $remotechannel); 983 $this->configSet('default_channel', $remotechannel); 984 } 985 986 $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state'); 987 if (isset($parr['state']) && isset($parr['version'])) { 988 unset($parr['state']); 989 } 990 991 if (isset($dep['uri'])) { 992 $info = $this->newDownloaderPackage($this); 993 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); 994 $err = $info->initialize($dep); 995 PEAR::staticPopErrorHandling(); 996 if (!$err) { 997 // skip parameters that were missed by preferred_state 998 return PEAR::raiseError('Cannot initialize dependency'); 999 } 1000 1001 if (PEAR::isError($err)) { 1002 if (!isset($this->_options['soft'])) { 1003 $this->log(0, $err->getMessage()); 1004 } 1005 1006 if (is_object($info)) { 1007 $param = $info->getChannel() . '/' . $info->getPackage(); 1008 } 1009 return PEAR::raiseError('Package "' . $param . '" is not valid'); 1010 } 1011 return $info; 1012 } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) 1013 && 1014 ( 1015 ($base2 = $chan->getBaseURL('REST1.3', $this->config->get('preferred_mirror'))) 1016 || 1017 ($base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) 1018 ) 1019 ) { 1020 if ($base2) { 1021 $base = $base2; 1022 $rest = &$this->config->getREST('1.3', $this->_options); 1023 } else { 1024 $rest = &$this->config->getREST('1.0', $this->_options); 1025 } 1026 1027 $url = $rest->getDepDownloadURL($base, $xsdversion, $dep, $parr, 1028 $state, $version, $chan->getName()); 1029 if (PEAR::isError($url)) { 1030 return $url; 1031 } 1032 1033 if ($parr['channel'] != $curchannel) { 1034 $this->configSet('default_channel', $curchannel); 1035 } 1036 1037 if (!is_array($url)) { 1038 return $url; 1039 } 1040 1041 $url['raw'] = false; // no checking is necessary for REST 1042 if (!is_array($url['info'])) { 1043 return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' . 1044 'this should never happen'); 1045 } 1046 1047 if (isset($url['info']['required'])) { 1048 if (!class_exists('PEAR_PackageFile_v2')) { 1049 require_once 'PEAR/PackageFile/v2.php'; 1050 } 1051 $pf = new PEAR_PackageFile_v2; 1052 $pf->setRawChannel($remotechannel); 1053 } else { 1054 if (!class_exists('PEAR_PackageFile_v1')) { 1055 require_once 'PEAR/PackageFile/v1.php'; 1056 } 1057 $pf = new PEAR_PackageFile_v1; 1058 1059 } 1060 $pf->setRawPackage($url['package']); 1061 $pf->setDeps($url['info']); 1062 if ($url['compatible']) { 1063 $pf->setCompatible($url['compatible']); 1064 } 1065 1066 $pf->setRawState($url['stability']); 1067 $url['info'] = &$pf; 1068 if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) { 1069 $ext = '.tar'; 1070 } else { 1071 $ext = '.tgz'; 1072 } 1073 1074 if (is_array($url) && isset($url['url'])) { 1075 $url['url'] .= $ext; 1076 } 1077 1078 return $url; 1079 } 1080 1081 return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.'); 1082 } 1083 1084 /** 1085 * @deprecated in favor of _getPackageDownloadUrl 1086 */ 1087 function getPackageDownloadUrl($package, $version = null, $channel = false) 1088 { 1089 if ($version) { 1090 $package .= "-$version"; 1091 } 1092 if ($this === null || $this->_registry === null) { 1093 $package = "http://pear.php.net/get/$package"; 1094 } else { 1095 $chan = $this->_registry->getChannel($channel); 1096 if (PEAR::isError($chan)) { 1097 return ''; 1098 } 1099 $package = "http://" . $chan->getServer() . "/get/$package"; 1100 } 1101 if (!extension_loaded("zlib")) { 1102 $package .= '?uncompress=yes'; 1103 } 1104 return $package; 1105 } 1106 1107 /** 1108 * Retrieve a list of downloaded packages after a call to {@link download()}. 1109 * 1110 * Also resets the list of downloaded packages. 1111 * @return array 1112 */ 1113 function getDownloadedPackages() 1114 { 1115 $ret = $this->_downloadedPackages; 1116 $this->_downloadedPackages = array(); 1117 $this->_toDownload = array(); 1118 return $ret; 1119 } 1120 1121 function _downloadCallback($msg, $params = null) 1122 { 1123 switch ($msg) { 1124 case 'saveas': 1125 $this->log(1, "downloading $params ..."); 1126 break; 1127 case 'done': 1128 $this->log(1, '...done: ' . number_format($params, 0, '', ',') . ' bytes'); 1129 break; 1130 case 'bytesread': 1131 static $bytes; 1132 if (empty($bytes)) { 1133 $bytes = 0; 1134 } 1135 if (!($bytes % 10240)) { 1136 $this->log(1, '.', false); 1137 } 1138 $bytes += $params; 1139 break; 1140 case 'start': 1141 if($params[1] == -1) { 1142 $length = "Unknown size"; 1143 } else { 1144 $length = number_format($params[1], 0, '', ',')." bytes"; 1145 } 1146 $this->log(1, "Starting to download {$params[0]} ($length)"); 1147 break; 1148 } 1149 if (method_exists($this->ui, '_downloadCallback')) 1150 $this->ui->_downloadCallback($msg, $params); 1151 } 1152 1153 function _prependPath($path, $prepend) 1154 { 1155 if (strlen($prepend) > 0) { 1156 if (OS_WINDOWS && preg_match('/^[a-z]:/i', $path)) { 1157 if (preg_match('/^[a-z]:/i', $prepend)) { 1158 $prepend = substr($prepend, 2); 1159 } elseif ($prepend{0} != '\\') { 1160 $prepend = "\\$prepend"; 1161 } 1162 $path = substr($path, 0, 2) . $prepend . substr($path, 2); 1163 } else { 1164 $path = $prepend . $path; 1165 } 1166 } 1167 return $path; 1168 } 1169 1170 /** 1171 * @param string 1172 * @param integer 1173 */ 1174 function pushError($errmsg, $code = -1) 1175 { 1176 array_push($this->_errorStack, array($errmsg, $code)); 1177 } 1178 1179 function getErrorMsgs() 1180 { 1181 $msgs = array(); 1182 $errs = $this->_errorStack; 1183 foreach ($errs as $err) { 1184 $msgs[] = $err[0]; 1185 } 1186 $this->_errorStack = array(); 1187 return $msgs; 1188 } 1189 1190 /** 1191 * for BC 1192 * 1193 * @deprecated 1194 */ 1195 function sortPkgDeps(&$packages, $uninstall = false) 1196 { 1197 $uninstall ? 1198 $this->sortPackagesForUninstall($packages) : 1199 $this->sortPackagesForInstall($packages); 1200 } 1201 1202 /** 1203 * Sort a list of arrays of array(downloaded packagefilename) by dependency. 1204 * 1205 * This uses the topological sort method from graph theory, and the 1206 * Structures_Graph package to properly sort dependencies for installation. 1207 * @param array an array of downloaded PEAR_Downloader_Packages 1208 * @return array array of array(packagefilename, package.xml contents) 1209 */ 1210 function sortPackagesForInstall(&$packages) 1211 { 1212 require_once 'Structures/Graph.php'; 1213 require_once 'Structures/Graph/Node.php'; 1214 require_once 'Structures/Graph/Manipulator/TopologicalSorter.php'; 1215 $depgraph = new Structures_Graph(true); 1216 $nodes = array(); 1217 $reg = &$this->config->getRegistry(); 1218 foreach ($packages as $i => $package) { 1219 $pname = $reg->parsedPackageNameToString( 1220 array( 1221 'channel' => $package->getChannel(), 1222 'package' => strtolower($package->getPackage()), 1223 )); 1224 $nodes[$pname] = new Structures_Graph_Node; 1225 $nodes[$pname]->setData($packages[$i]); 1226 $depgraph->addNode($nodes[$pname]); 1227 } 1228 1229 $deplinks = array(); 1230 foreach ($nodes as $package => $node) { 1231 $pf = &$node->getData(); 1232 $pdeps = $pf->getDeps(true); 1233 if (!$pdeps) { 1234 continue; 1235 } 1236 1237 if ($pf->getPackagexmlVersion() == '1.0') { 1238 foreach ($pdeps as $dep) { 1239 if ($dep['type'] != 'pkg' || 1240 (isset($dep['optional']) && $dep['optional'] == 'yes')) { 1241 continue; 1242 } 1243 1244 $dname = $reg->parsedPackageNameToString( 1245 array( 1246 'channel' => 'pear.php.net', 1247 'package' => strtolower($dep['name']), 1248 )); 1249 1250 if (isset($nodes[$dname])) { 1251 if (!isset($deplinks[$dname])) { 1252 $deplinks[$dname] = array(); 1253 } 1254 1255 $deplinks[$dname][$package] = 1; 1256 // dependency is in installed packages 1257 continue; 1258 } 1259 1260 $dname = $reg->parsedPackageNameToString( 1261 array( 1262 'channel' => 'pecl.php.net', 1263 'package' => strtolower($dep['name']), 1264 )); 1265 1266 if (isset($nodes[$dname])) { 1267 if (!isset($deplinks[$dname])) { 1268 $deplinks[$dname] = array(); 1269 } 1270 1271 $deplinks[$dname][$package] = 1; 1272 // dependency is in installed packages 1273 continue; 1274 } 1275 } 1276 } else { 1277 // the only ordering we care about is: 1278 // 1) subpackages must be installed before packages that depend on them 1279 // 2) required deps must be installed before packages that depend on them 1280 if (isset($pdeps['required']['subpackage'])) { 1281 $t = $pdeps['required']['subpackage']; 1282 if (!isset($t[0])) { 1283 $t = array($t); 1284 } 1285 1286 $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); 1287 } 1288 1289 if (isset($pdeps['group'])) { 1290 if (!isset($pdeps['group'][0])) { 1291 $pdeps['group'] = array($pdeps['group']); 1292 } 1293 1294 foreach ($pdeps['group'] as $group) { 1295 if (isset($group['subpackage'])) { 1296 $t = $group['subpackage']; 1297 if (!isset($t[0])) { 1298 $t = array($t); 1299 } 1300 1301 $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); 1302 } 1303 } 1304 } 1305 1306 if (isset($pdeps['optional']['subpackage'])) { 1307 $t = $pdeps['optional']['subpackage']; 1308 if (!isset($t[0])) { 1309 $t = array($t); 1310 } 1311 1312 $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); 1313 } 1314 1315 if (isset($pdeps['required']['package'])) { 1316 $t = $pdeps['required']['package']; 1317 if (!isset($t[0])) { 1318 $t = array($t); 1319 } 1320 1321 $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); 1322 } 1323 1324 if (isset($pdeps['group'])) { 1325 if (!isset($pdeps['group'][0])) { 1326 $pdeps['group'] = array($pdeps['group']); 1327 } 1328 1329 foreach ($pdeps['group'] as $group) { 1330 if (isset($group['package'])) { 1331 $t = $group['package']; 1332 if (!isset($t[0])) { 1333 $t = array($t); 1334 } 1335 1336 $this->_setupGraph($t, $reg, $deplinks, $nodes, $package); 1337 } 1338 } 1339 } 1340 } 1341 } 1342 1343 $this->_detectDepCycle($deplinks); 1344 foreach ($deplinks as $dependent => $parents) { 1345 foreach ($parents as $parent => $unused) { 1346 $nodes[$dependent]->connectTo($nodes[$parent]); 1347 } 1348 } 1349 1350 $installOrder = Structures_Graph_Manipulator_TopologicalSorter::sort($depgraph); 1351 $ret = array(); 1352 for ($i = 0, $count = count($installOrder); $i < $count; $i++) { 1353 foreach ($installOrder[$i] as $index => $sortedpackage) { 1354 $data = &$installOrder[$i][$index]->getData(); 1355 $ret[] = &$nodes[$reg->parsedPackageNameToString( 1356 array( 1357 'channel' => $data->getChannel(), 1358 'package' => strtolower($data->getPackage()), 1359 ))]->getData(); 1360 } 1361 } 1362 1363 $packages = $ret; 1364 return; 1365 } 1366 1367 /** 1368 * Detect recursive links between dependencies and break the cycles 1369 * 1370 * @param array 1371 * @access private 1372 */ 1373 function _detectDepCycle(&$deplinks) 1374 { 1375 do { 1376 $keepgoing = false; 1377 foreach ($deplinks as $dep => $parents) { 1378 foreach ($parents as $parent => $unused) { 1379 // reset the parent cycle detector 1380 $this->_testCycle(null, null, null); 1381 if ($this->_testCycle($dep, $deplinks, $parent)) { 1382 $keepgoing = true; 1383 unset($deplinks[$dep][$parent]); 1384 if (count($deplinks[$dep]) == 0) { 1385 unset($deplinks[$dep]); 1386 } 1387 1388 continue 3; 1389 } 1390 } 1391 } 1392 } while ($keepgoing); 1393 } 1394 1395 function _testCycle($test, $deplinks, $dep) 1396 { 1397 static $visited = array(); 1398 if ($test === null) { 1399 $visited = array(); 1400 return; 1401 } 1402 1403 // this happens when a parent has a dep cycle on another dependency 1404 // but the child is not part of the cycle 1405 if (isset($visited[$dep])) { 1406 return false; 1407 } 1408 1409 $visited[$dep] = 1; 1410 if ($test == $dep) { 1411 return true; 1412 } 1413 1414 if (isset($deplinks[$dep])) { 1415 if (in_array($test, array_keys($deplinks[$dep]), true)) { 1416 return true; 1417 } 1418 1419 foreach ($deplinks[$dep] as $parent => $unused) { 1420 if ($this->_testCycle($test, $deplinks, $parent)) { 1421 return true; 1422 } 1423 } 1424 } 1425 1426 return false; 1427 } 1428 1429 /** 1430 * Set up the dependency for installation parsing 1431 * 1432 * @param array $t dependency information 1433 * @param PEAR_Registry $reg 1434 * @param array $deplinks list of dependency links already established 1435 * @param array $nodes all existing package nodes 1436 * @param string $package parent package name 1437 * @access private 1438 */ 1439 function _setupGraph($t, $reg, &$deplinks, &$nodes, $package) 1440 { 1441 foreach ($t as $dep) { 1442 $depchannel = !isset($dep['channel']) ? '__uri': $dep['channel']; 1443 $dname = $reg->parsedPackageNameToString( 1444 array( 1445 'channel' => $depchannel, 1446 'package' => strtolower($dep['name']), 1447 )); 1448 1449 if (isset($nodes[$dname])) { 1450 if (!isset($deplinks[$dname])) { 1451 $deplinks[$dname] = array(); 1452 } 1453 $deplinks[$dname][$package] = 1; 1454 } 1455 } 1456 } 1457 1458 function _dependsOn($a, $b) 1459 { 1460 return $this->_checkDepTree(strtolower($a->getChannel()), strtolower($a->getPackage()), $b); 1461 } 1462 1463 function _checkDepTree($channel, $package, $b, $checked = array()) 1464 { 1465 $checked[$channel][$package] = true; 1466 if (!isset($this->_depTree[$channel][$package])) { 1467 return false; 1468 } 1469 1470 if (isset($this->_depTree[$channel][$package][strtolower($b->getChannel())] 1471 [strtolower($b->getPackage())])) { 1472 return true; 1473 } 1474 1475 foreach ($this->_depTree[$channel][$package] as $ch => $packages) { 1476 foreach ($packages as $pa => $true) { 1477 if ($this->_checkDepTree($ch, $pa, $b, $checked)) { 1478 return true; 1479 } 1480 } 1481 } 1482 1483 return false; 1484 } 1485 1486 function _sortInstall($a, $b) 1487 { 1488 if (!$a->getDeps() && !$b->getDeps()) { 1489 return 0; // neither package has dependencies, order is insignificant 1490 } 1491 if ($a->getDeps() && !$b->getDeps()) { 1492 return 1; // $a must be installed after $b because $a has dependencies 1493 } 1494 if (!$a->getDeps() && $b->getDeps()) { 1495 return -1; // $b must be installed after $a because $b has dependencies 1496 } 1497 // both packages have dependencies 1498 if ($this->_dependsOn($a, $b)) { 1499 return 1; 1500 } 1501 if ($this->_dependsOn($b, $a)) { 1502 return -1; 1503 } 1504 return 0; 1505 } 1506 1507 /** 1508 * Download a file through HTTP. Considers suggested file name in 1509 * Content-disposition: header and can run a callback function for 1510 * different events. The callback will be called with two 1511 * parameters: the callback type, and parameters. The implemented 1512 * callback types are: 1513 * 1514 * 'setup' called at the very beginning, parameter is a UI object 1515 * that should be used for all output 1516 * 'message' the parameter is a string with an informational message 1517 * 'saveas' may be used to save with a different file name, the 1518 * parameter is the filename that is about to be used. 1519 * If a 'saveas' callback returns a non-empty string, 1520 * that file name will be used as the filename instead. 1521 * Note that $save_dir will not be affected by this, only 1522 * the basename of the file. 1523 * 'start' download is starting, parameter is number of bytes 1524 * that are expected, or -1 if unknown 1525 * 'bytesread' parameter is the number of bytes read so far 1526 * 'done' download is complete, parameter is the total number 1527 * of bytes read 1528 * 'connfailed' if the TCP/SSL connection fails, this callback is called 1529 * with array(host,port,errno,errmsg) 1530 * 'writefailed' if writing to disk fails, this callback is called 1531 * with array(destfile,errmsg) 1532 * 1533 * If an HTTP proxy has been configured (http_proxy PEAR_Config 1534 * setting), the proxy will be used. 1535 * 1536 * @param string $url the URL to download 1537 * @param object $ui PEAR_Frontend_* instance 1538 * @param object $config PEAR_Config instance 1539 * @param string $save_dir directory to save file in 1540 * @param mixed $callback function/method to call for status 1541 * updates 1542 * @param false|string|array $lastmodified header values to check against for caching 1543 * use false to return the header values from this download 1544 * @param false|array $accept Accept headers to send 1545 * @param false|string $channel Channel to use for retrieving authentication 1546 * @return mixed Returns the full path of the downloaded file or a PEAR 1547 * error on failure. If the error is caused by 1548 * socket-related errors, the error object will 1549 * have the fsockopen error code available through 1550 * getCode(). If caching is requested, then return the header 1551 * values. 1552 * If $lastmodified was given and the there are no changes, 1553 * boolean false is returned. 1554 * 1555 * @access public 1556 */ 1557 public static function _downloadHttp( 1558 $object, $url, &$ui, $save_dir = '.', $callback = null, $lastmodified = null, 1559 $accept = false, $channel = false 1560 ) { 1561 static $redirect = 0; 1562 // always reset , so we are clean case of error 1563 $wasredirect = $redirect; 1564 $redirect = 0; 1565 if ($callback) { 1566 call_user_func($callback, 'setup', array(&$ui)); 1567 } 1568 1569 $info = parse_url($url); 1570 if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) { 1571 return PEAR::raiseError('Cannot download non-http URL "' . $url . '"'); 1572 } 1573 1574 if (!isset($info['host'])) { 1575 return PEAR::raiseError('Cannot download from non-URL "' . $url . '"'); 1576 } 1577 1578 $host = isset($info['host']) ? $info['host'] : null; 1579 $port = isset($info['port']) ? $info['port'] : null; 1580 $path = isset($info['path']) ? $info['path'] : null; 1581 1582 if ($object !== null) { 1583 $config = $object->config; 1584 } else { 1585 $config = &PEAR_Config::singleton(); 1586 } 1587 1588 $proxy = new PEAR_Proxy($config); 1589 1590 if ($proxy->isProxyConfigured() && $callback) { 1591 call_user_func($callback, 'message', "Using HTTP proxy $host:$port"); 1592 } 1593 1594 if (empty($port)) { 1595 $port = (isset($info['scheme']) && $info['scheme'] == 'https') ? 443 : 80; 1596 } 1597 1598 $scheme = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http'; 1599 $secure = ($scheme == 'https'); 1600 1601 $fp = $proxy->openSocket($host, $port, $secure); 1602 if (PEAR::isError($fp)) { 1603 if ($callback) { 1604 $errno = $fp->getCode(); 1605 $errstr = $fp->getMessage(); 1606 call_user_func($callback, 'connfailed', array($host, $port, 1607 $errno, $errstr)); 1608 } 1609 return $fp; 1610 } 1611 1612 $requestPath = $path; 1613 if ($proxy->isProxyConfigured()) { 1614 $requestPath = $url; 1615 } 1616 1617 if ($lastmodified === false || $lastmodified) { 1618 $request = "GET $requestPath HTTP/1.1\r\n"; 1619 } else { 1620 $request = "GET $requestPath HTTP/1.0\r\n"; 1621 } 1622 $request .= "Host: $host\r\n"; 1623 1624 $ifmodifiedsince = ''; 1625 if (is_array($lastmodified)) { 1626 if (isset($lastmodified['Last-Modified'])) { 1627 $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n"; 1628 } 1629 1630 if (isset($lastmodified['ETag'])) { 1631 $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n"; 1632 } 1633 } else { 1634 $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : ''); 1635 } 1636 1637 $request .= $ifmodifiedsince . 1638 "User-Agent: PEAR/@package_version@/PHP/" . PHP_VERSION . "\r\n"; 1639 1640 if ($object !== null) { // only pass in authentication for non-static calls 1641 $username = $config->get('username', null, $channel); 1642 $password = $config->get('password', null, $channel); 1643 if ($username && $password) { 1644 $tmp = base64_encode("$username:$password"); 1645 $request .= "Authorization: Basic $tmp\r\n"; 1646 } 1647 } 1648 1649 $proxyAuth = $proxy->getProxyAuth(); 1650 if ($proxyAuth) { 1651 $request .= 'Proxy-Authorization: Basic ' . 1652 $proxyAuth . "\r\n"; 1653 } 1654 1655 if ($accept) { 1656 $request .= 'Accept: ' . implode(', ', $accept) . "\r\n"; 1657 } 1658 1659 $request .= "Connection: close\r\n"; 1660 $request .= "\r\n"; 1661 fwrite($fp, $request); 1662 $headers = array(); 1663 $reply = 0; 1664 while (trim($line = fgets($fp, 1024))) { 1665 if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) { 1666 $headers[strtolower($matches[1])] = trim($matches[2]); 1667 } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) { 1668 $reply = (int)$matches[1]; 1669 if ($reply == 304 && ($lastmodified || ($lastmodified === false))) { 1670 return false; 1671 } 1672 1673 if (!in_array($reply, array(200, 301, 302, 303, 305, 307))) { 1674 return PEAR::raiseError("File $scheme://$host:$port$path not valid (received: $line)"); 1675 } 1676 } 1677 } 1678 1679 if ($reply != 200) { 1680 if (!isset($headers['location'])) { 1681 return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirected but no location)"); 1682 } 1683 1684 if ($wasredirect > 4) { 1685 return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirection looped more than 5 times)"); 1686 } 1687 1688 $redirect = $wasredirect + 1; 1689 return static::_downloadHttp($object, $headers['location'], 1690 $ui, $save_dir, $callback, $lastmodified, $accept); 1691 } 1692 1693 if (isset($headers['content-disposition']) && 1694 preg_match('/\sfilename=\"([^;]*\S)\"\s*(;|\\z)/', $headers['content-disposition'], $matches)) { 1695 $save_as = basename($matches[1]); 1696 } else { 1697 $save_as = basename($url); 1698 } 1699 1700 if ($callback) { 1701 $tmp = call_user_func($callback, 'saveas', $save_as); 1702 if ($tmp) { 1703 $save_as = $tmp; 1704 } 1705 } 1706 1707 $dest_file = $save_dir . DIRECTORY_SEPARATOR . $save_as; 1708 if (is_link($dest_file)) { 1709 return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $dest_file . ' as it is symlinked to ' . readlink($dest_file) . ' - Possible symlink attack'); 1710 } 1711 1712 if (!$wp = @fopen($dest_file, 'wb')) { 1713 fclose($fp); 1714 if ($callback) { 1715 call_user_func($callback, 'writefailed', 1716 array($dest_file, error_get_last()["message"])); 1717 } 1718 return PEAR::raiseError("could not open $dest_file for writing"); 1719 } 1720 1721 $length = isset($headers['content-length']) ? $headers['content-length'] : -1; 1722 1723 $bytes = 0; 1724 if ($callback) { 1725 call_user_func($callback, 'start', array(basename($dest_file), $length)); 1726 } 1727 1728 while ($data = fread($fp, 1024)) { 1729 $bytes += strlen($data); 1730 if ($callback) { 1731 call_user_func($callback, 'bytesread', $bytes); 1732 } 1733 if (!@fwrite($wp, $data)) { 1734 fclose($fp); 1735 if ($callback) { 1736 call_user_func($callback, 'writefailed', 1737 array($dest_file, error_get_last()["message"])); 1738 } 1739 return PEAR::raiseError( 1740 "$dest_file: write failed (" . error_get_last()["message"] . ")"); 1741 } 1742 } 1743 1744 fclose($fp); 1745 fclose($wp); 1746 if ($callback) { 1747 call_user_func($callback, 'done', $bytes); 1748 } 1749 1750 if ($lastmodified === false || $lastmodified) { 1751 if (isset($headers['etag'])) { 1752 $lastmodified = array('ETag' => $headers['etag']); 1753 } 1754 1755 if (isset($headers['last-modified'])) { 1756 if (is_array($lastmodified)) { 1757 $lastmodified['Last-Modified'] = $headers['last-modified']; 1758 } else { 1759 $lastmodified = $headers['last-modified']; 1760 } 1761 } 1762 return array($dest_file, $lastmodified, $headers); 1763 } 1764 return $dest_file; 1765 } 1766} 1767