[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Joomla! Content Management System 5 * 6 * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> 7 * @license GNU General Public License version 2 or later; see LICENSE.txt 8 */ 9 10 namespace Joomla\CMS\Installer; 11 12 use Joomla\CMS\Adapter\Adapter; 13 use Joomla\CMS\Application\ApplicationHelper; 14 use Joomla\CMS\Factory; 15 use Joomla\CMS\Filesystem\File; 16 use Joomla\CMS\Filesystem\Folder; 17 use Joomla\CMS\Filesystem\Path; 18 use Joomla\CMS\Language\Text; 19 use Joomla\CMS\Log\Log; 20 use Joomla\CMS\Plugin\PluginHelper; 21 use Joomla\CMS\Table\Extension; 22 use Joomla\CMS\Table\Table; 23 use Joomla\Database\DatabaseAwareInterface; 24 use Joomla\Database\DatabaseAwareTrait; 25 use Joomla\Database\DatabaseDriver; 26 use Joomla\Database\DatabaseInterface; 27 use Joomla\Database\Exception\ExecutionFailureException; 28 use Joomla\Database\Exception\PrepareStatementFailureException; 29 use Joomla\Database\ParameterType; 30 use Joomla\DI\ContainerAwareInterface; 31 32 // phpcs:disable PSR1.Files.SideEffects 33 \defined('JPATH_PLATFORM') or die; 34 // phpcs:enable PSR1.Files.SideEffects 35 36 /** 37 * Joomla base installer class 38 * 39 * @since 3.1 40 */ 41 class Installer extends Adapter implements DatabaseAwareInterface 42 { 43 use DatabaseAwareTrait; 44 45 /** 46 * Array of paths needed by the installer 47 * 48 * @var array 49 * @since 3.1 50 */ 51 protected $paths = array(); 52 53 /** 54 * True if package is an upgrade 55 * 56 * @var boolean 57 * @since 3.1 58 */ 59 protected $upgrade = null; 60 61 /** 62 * The manifest trigger class 63 * 64 * @var object 65 * @since 3.1 66 */ 67 public $manifestClass = null; 68 69 /** 70 * True if existing files can be overwritten 71 * 72 * @var boolean 73 * @since 3.0.0 74 */ 75 protected $overwrite = false; 76 77 /** 78 * Stack of installation steps 79 * - Used for installation rollback 80 * 81 * @var array 82 * @since 3.1 83 */ 84 protected $stepStack = array(); 85 86 /** 87 * Extension Table Entry 88 * 89 * @var Extension 90 * @since 3.1 91 */ 92 public $extension = null; 93 94 /** 95 * The output from the install/uninstall scripts 96 * 97 * @var string 98 * @since 3.1 99 * */ 100 public $message = null; 101 102 /** 103 * The installation manifest XML object 104 * 105 * @var object 106 * @since 3.1 107 */ 108 public $manifest = null; 109 110 /** 111 * The extension message that appears 112 * 113 * @var string 114 * @since 3.1 115 */ 116 protected $extension_message = null; 117 118 /** 119 * The redirect URL if this extension (can be null if no redirect) 120 * 121 * @var string 122 * @since 3.1 123 */ 124 protected $redirect_url = null; 125 126 /** 127 * Flag if the uninstall process was triggered by uninstalling a package 128 * 129 * @var boolean 130 * @since 3.7.0 131 */ 132 protected $packageUninstall = false; 133 134 /** 135 * Backup extra_query during update_sites rebuild 136 * 137 * @var string 138 * @since 3.9.26 139 */ 140 public $extraQuery = ''; 141 142 /** 143 * JInstaller instances container. 144 * 145 * @var Installer[] 146 * @since 3.4 147 */ 148 protected static $instances; 149 150 /** 151 * A comment marker to indicate that an update SQL query may fail without triggering an update error. 152 * 153 * @since 4.2.0 154 */ 155 protected const CAN_FAIL_MARKER = '/** CAN FAIL **/'; 156 157 /** 158 * The length of the CAN_FAIL_MARKER string 159 * 160 * @since 4.2.0 161 */ 162 protected const CAN_FAIL_MARKER_LENGTH = 16; 163 164 /** 165 * Constructor 166 * 167 * @param string $basepath Base Path of the adapters 168 * @param string $classprefix Class prefix of adapters 169 * @param string $adapterfolder Name of folder to append to base path 170 * 171 * @since 3.1 172 */ 173 public function __construct($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter') 174 { 175 parent::__construct($basepath, $classprefix, $adapterfolder); 176 177 $this->extension = Table::getInstance('extension'); 178 } 179 180 /** 181 * Returns the global Installer object, only creating it if it doesn't already exist. 182 * 183 * @param string $basepath Base Path of the adapters 184 * @param string $classprefix Class prefix of adapters 185 * @param string $adapterfolder Name of folder to append to base path 186 * 187 * @return Installer An installer object 188 * 189 * @since 3.1 190 */ 191 public static function getInstance($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter') 192 { 193 if (!isset(self::$instances[$basepath])) { 194 self::$instances[$basepath] = new static($basepath, $classprefix, $adapterfolder); 195 self::$instances[$basepath]->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); 196 } 197 198 return self::$instances[$basepath]; 199 } 200 201 /** 202 * Splits a string of multiple queries into an array of individual queries. 203 * 204 * This is different than DatabaseDriver::splitSql. It supports the special CAN FAIL comment 205 * marker which indicates that a SQL statement could fail without raising an error during the 206 * installation. 207 * 208 * @param string|null $sql Input SQL string with which to split into individual queries. 209 * 210 * @return array 211 * 212 * @since 4.2.0 213 */ 214 public static function splitSql(?string $sql): array 215 { 216 if (empty($sql)) { 217 return []; 218 } 219 220 $start = 0; 221 $open = false; 222 $comment = false; 223 $endString = ''; 224 $end = \strlen($sql); 225 $queries = []; 226 $query = ''; 227 228 for ($i = 0; $i < $end; $i++) { 229 $current = substr($sql, $i, 1); 230 $current2 = substr($sql, $i, 2); 231 $current3 = substr($sql, $i, 3); 232 $lenEndString = \strlen($endString); 233 $testEnd = substr($sql, $i, $lenEndString); 234 235 if ( 236 $current === '"' || $current === "'" || $current2 === '--' 237 || ($current2 === '/*' && $current3 !== '/*!' && $current3 !== '/*+') 238 || ($current === '#' && $current3 !== '#__') 239 || ($comment && $testEnd === $endString) 240 ) { 241 // Check if quoted with previous backslash 242 $n = 2; 243 244 while (substr($sql, $i - $n + 1, 1) === '\\' && $n < $i) { 245 $n++; 246 } 247 248 // Not quoted 249 if ($n % 2 === 0) { 250 if ($open) { 251 if ($testEnd === $endString) { 252 if ($comment) { 253 $comment = false; 254 255 if ($lenEndString > 1) { 256 $i += ($lenEndString - 1); 257 $current = substr($sql, $i, 1); 258 } 259 260 $start = $i + 1; 261 } 262 263 $open = false; 264 $endString = ''; 265 } 266 } else { 267 $open = true; 268 269 if ($current2 === '--') { 270 $endString = "\n"; 271 $comment = true; 272 } elseif ($current2 === '/*') { 273 $endString = '*/'; 274 $comment = true; 275 } elseif ($current === '#') { 276 $endString = "\n"; 277 $comment = true; 278 } else { 279 $endString = $current; 280 } 281 282 if ($comment && $start < $i) { 283 $query .= substr($sql, $start, $i - $start); 284 } 285 } 286 } 287 } 288 289 if ($comment) { 290 $start = $i + 1; 291 } 292 293 if (($current === ';' && !$open) || $i === $end - 1) { 294 if ($current === ';' && !$open && $start <= $i && $start > self::CAN_FAIL_MARKER_LENGTH) { 295 $possibleMarker = substr($sql, $start - self::CAN_FAIL_MARKER_LENGTH, $i - $start + self::CAN_FAIL_MARKER_LENGTH); 296 297 if (strtoupper($possibleMarker) === self::CAN_FAIL_MARKER) { 298 $start -= self::CAN_FAIL_MARKER_LENGTH; 299 } 300 } 301 302 if ($start <= $i) { 303 $query .= substr($sql, $start, $i - $start + 1); 304 } 305 306 $query = trim($query); 307 308 if ($query) { 309 if (($i === $end - 1) && ($current !== ';')) { 310 $query .= ';'; 311 } 312 313 $queries[] = $query; 314 } 315 316 $query = ''; 317 $start = $i + 1; 318 } 319 320 $endComment = false; 321 } 322 323 return $queries; 324 } 325 326 /** 327 * Get the allow overwrite switch 328 * 329 * @return boolean Allow overwrite switch 330 * 331 * @since 3.1 332 */ 333 public function isOverwrite() 334 { 335 return $this->overwrite; 336 } 337 338 /** 339 * Set the allow overwrite switch 340 * 341 * @param boolean $state Overwrite switch state 342 * 343 * @return boolean True it state is set, false if it is not 344 * 345 * @since 3.1 346 */ 347 public function setOverwrite($state = false) 348 { 349 $tmp = $this->overwrite; 350 351 if ($state) { 352 $this->overwrite = true; 353 } else { 354 $this->overwrite = false; 355 } 356 357 return $tmp; 358 } 359 360 /** 361 * Get the redirect location 362 * 363 * @return string Redirect location (or null) 364 * 365 * @since 3.1 366 */ 367 public function getRedirectUrl() 368 { 369 return $this->redirect_url; 370 } 371 372 /** 373 * Set the redirect location 374 * 375 * @param string $newurl New redirect location 376 * 377 * @return void 378 * 379 * @since 3.1 380 */ 381 public function setRedirectUrl($newurl) 382 { 383 $this->redirect_url = $newurl; 384 } 385 386 /** 387 * Get whether this installer is uninstalling extensions which are part of a package 388 * 389 * @return boolean 390 * 391 * @since 3.7.0 392 */ 393 public function isPackageUninstall() 394 { 395 return $this->packageUninstall; 396 } 397 398 /** 399 * Set whether this installer is uninstalling extensions which are part of a package 400 * 401 * @param boolean $uninstall True if a package triggered the uninstall, false otherwise 402 * 403 * @return void 404 * 405 * @since 3.7.0 406 */ 407 public function setPackageUninstall($uninstall) 408 { 409 $this->packageUninstall = $uninstall; 410 } 411 412 /** 413 * Get the upgrade switch 414 * 415 * @return boolean 416 * 417 * @since 3.1 418 */ 419 public function isUpgrade() 420 { 421 return $this->upgrade; 422 } 423 424 /** 425 * Set the upgrade switch 426 * 427 * @param boolean $state Upgrade switch state 428 * 429 * @return boolean True if upgrade, false otherwise 430 * 431 * @since 3.1 432 */ 433 public function setUpgrade($state = false) 434 { 435 $tmp = $this->upgrade; 436 437 if ($state) { 438 $this->upgrade = true; 439 } else { 440 $this->upgrade = false; 441 } 442 443 return $tmp; 444 } 445 446 /** 447 * Get the installation manifest object 448 * 449 * @return \SimpleXMLElement Manifest object 450 * 451 * @since 3.1 452 */ 453 public function getManifest() 454 { 455 if (!\is_object($this->manifest)) { 456 $this->findManifest(); 457 } 458 459 return $this->manifest; 460 } 461 462 /** 463 * Get an installer path by name 464 * 465 * @param string $name Path name 466 * @param string $default Default value 467 * 468 * @return string Path 469 * 470 * @since 3.1 471 */ 472 public function getPath($name, $default = null) 473 { 474 return (!empty($this->paths[$name])) ? $this->paths[$name] : $default; 475 } 476 477 /** 478 * Sets an installer path by name 479 * 480 * @param string $name Path name 481 * @param string $value Path 482 * 483 * @return void 484 * 485 * @since 3.1 486 */ 487 public function setPath($name, $value) 488 { 489 $this->paths[$name] = $value; 490 } 491 492 /** 493 * Pushes a step onto the installer stack for rolling back steps 494 * 495 * @param array $step Installer step 496 * 497 * @return void 498 * 499 * @since 3.1 500 */ 501 public function pushStep($step) 502 { 503 $this->stepStack[] = $step; 504 } 505 506 /** 507 * Installation abort method 508 * 509 * @param string $msg Abort message from the installer 510 * @param string $type Package type if defined 511 * 512 * @return boolean True if successful 513 * 514 * @since 3.1 515 */ 516 public function abort($msg = null, $type = null) 517 { 518 $retval = true; 519 $step = array_pop($this->stepStack); 520 521 // Raise abort warning 522 if ($msg) { 523 Log::add($msg, Log::WARNING, 'jerror'); 524 } 525 526 while ($step != null) { 527 switch ($step['type']) { 528 case 'file': 529 // Remove the file 530 $stepval = File::delete($step['path']); 531 break; 532 533 case 'folder': 534 // Remove the folder 535 $stepval = Folder::delete($step['path']); 536 break; 537 538 case 'query': 539 // Execute the query. 540 $stepval = $this->parseSQLFiles($step['script']); 541 break; 542 543 case 'extension': 544 // Get database connector object 545 $db = $this->getDatabase(); 546 $query = $db->getQuery(true); 547 $stepId = (int) $step['id']; 548 549 // Remove the entry from the #__extensions table 550 $query->delete($db->quoteName('#__extensions')) 551 ->where($db->quoteName('extension_id') . ' = :step_id') 552 ->bind(':step_id', $stepId, ParameterType::INTEGER); 553 $db->setQuery($query); 554 555 try { 556 $db->execute(); 557 558 $stepval = true; 559 } catch (ExecutionFailureException $e) { 560 // The database API will have already logged the error it caught, we just need to alert the user to the issue 561 Log::add(Text::_('JLIB_INSTALLER_ABORT_ERROR_DELETING_EXTENSIONS_RECORD'), Log::WARNING, 'jerror'); 562 563 $stepval = false; 564 } 565 566 break; 567 568 default: 569 if ($type && \is_object($this->_adapters[$type])) { 570 // Build the name of the custom rollback method for the type 571 $method = '_rollback_' . $step['type']; 572 573 // Custom rollback method handler 574 if (method_exists($this->_adapters[$type], $method)) { 575 $stepval = $this->_adapters[$type]->$method($step); 576 } 577 } else { 578 // Set it to false 579 $stepval = false; 580 } 581 break; 582 } 583 584 // Only set the return value if it is false 585 if ($stepval === false) { 586 $retval = false; 587 } 588 589 // Get the next step and continue 590 $step = array_pop($this->stepStack); 591 } 592 593 return $retval; 594 } 595 596 // Adapter functions 597 598 /** 599 * Package installation method 600 * 601 * @param string $path Path to package source folder 602 * 603 * @return boolean True if successful 604 * 605 * @since 3.1 606 */ 607 public function install($path = null) 608 { 609 if ($path && Folder::exists($path)) { 610 $this->setPath('source', $path); 611 } else { 612 $this->abort(Text::_('JLIB_INSTALLER_ABORT_NOINSTALLPATH')); 613 614 return false; 615 } 616 617 if (!$adapter = $this->setupInstall('install', true)) { 618 $this->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); 619 620 return false; 621 } 622 623 if (!\is_object($adapter)) { 624 return false; 625 } 626 627 // Add the languages from the package itself 628 if (method_exists($adapter, 'loadLanguage')) { 629 $adapter->loadLanguage($path); 630 } 631 632 // Fire the onExtensionBeforeInstall event. 633 PluginHelper::importPlugin('extension'); 634 Factory::getApplication()->triggerEvent( 635 'onExtensionBeforeInstall', 636 array( 637 'method' => 'install', 638 'type' => $this->manifest->attributes()->type, 639 'manifest' => $this->manifest, 640 'extension' => 0, 641 ) 642 ); 643 644 // Run the install 645 $result = $adapter->install(); 646 647 // Make sure Joomla can figure out what has changed 648 clearstatcache(); 649 650 // Fire the onExtensionAfterInstall 651 Factory::getApplication()->triggerEvent( 652 'onExtensionAfterInstall', 653 array('installer' => clone $this, 'eid' => $result) 654 ); 655 656 if ($result !== false) { 657 // Refresh versionable assets cache 658 Factory::getApplication()->flushAssets(); 659 660 return true; 661 } 662 663 return false; 664 } 665 666 /** 667 * Discovered package installation method 668 * 669 * @param integer $eid Extension ID 670 * 671 * @return boolean True if successful 672 * 673 * @since 3.1 674 */ 675 public function discover_install($eid = null) 676 { 677 if (!$eid) { 678 $this->abort(Text::_('JLIB_INSTALLER_ABORT_EXTENSIONNOTVALID')); 679 680 return false; 681 } 682 683 if (!$this->extension->load($eid)) { 684 $this->abort(Text::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS')); 685 686 return false; 687 } 688 689 if ($this->extension->state != -1) { 690 $this->abort(Text::_('JLIB_INSTALLER_ABORT_ALREADYINSTALLED')); 691 692 return false; 693 } 694 695 // Load the adapter(s) for the install manifest 696 $type = $this->extension->type; 697 $params = array('extension' => $this->extension, 'route' => 'discover_install'); 698 699 $adapter = $this->loadAdapter($type, $params); 700 701 if (!\is_object($adapter)) { 702 return false; 703 } 704 705 if (!method_exists($adapter, 'discover_install') || !$adapter->getDiscoverInstallSupported()) { 706 $this->abort(Text::sprintf('JLIB_INSTALLER_ERROR_DISCOVER_INSTALL_UNSUPPORTED', $type)); 707 708 return false; 709 } 710 711 // The adapter needs to prepare itself 712 if (method_exists($adapter, 'prepareDiscoverInstall')) { 713 try { 714 $adapter->prepareDiscoverInstall(); 715 } catch (\RuntimeException $e) { 716 $this->abort($e->getMessage()); 717 718 return false; 719 } 720 } 721 722 // Add the languages from the package itself 723 if (method_exists($adapter, 'loadLanguage')) { 724 $adapter->loadLanguage(); 725 } 726 727 // Fire the onExtensionBeforeInstall event. 728 PluginHelper::importPlugin('extension'); 729 Factory::getApplication()->triggerEvent( 730 'onExtensionBeforeInstall', 731 array( 732 'method' => 'discover_install', 733 'type' => $this->extension->get('type'), 734 'manifest' => null, 735 'extension' => $this->extension->get('extension_id'), 736 ) 737 ); 738 739 // Run the install 740 $result = $adapter->discover_install(); 741 742 // Fire the onExtensionAfterInstall 743 Factory::getApplication()->triggerEvent( 744 'onExtensionAfterInstall', 745 array('installer' => clone $this, 'eid' => $result) 746 ); 747 748 if ($result !== false) { 749 // Refresh versionable assets cache 750 Factory::getApplication()->flushAssets(); 751 752 return true; 753 } 754 755 return false; 756 } 757 758 /** 759 * Extension discover method 760 * 761 * Asks each adapter to find extensions 762 * 763 * @return InstallerExtension[] 764 * 765 * @since 3.1 766 */ 767 public function discover() 768 { 769 $results = array(); 770 771 foreach ($this->getAdapters() as $adapter) { 772 $instance = $this->loadAdapter($adapter); 773 774 // Joomla! 1.5 installation adapter legacy support 775 if (method_exists($instance, 'discover')) { 776 $tmp = $instance->discover(); 777 778 // If its an array and has entries 779 if (\is_array($tmp) && \count($tmp)) { 780 // Merge it into the system 781 $results = array_merge($results, $tmp); 782 } 783 } 784 } 785 786 return $results; 787 } 788 789 /** 790 * Package update method 791 * 792 * @param string $path Path to package source folder 793 * 794 * @return boolean True if successful 795 * 796 * @since 3.1 797 */ 798 public function update($path = null) 799 { 800 if ($path && Folder::exists($path)) { 801 $this->setPath('source', $path); 802 } else { 803 $this->abort(Text::_('JLIB_INSTALLER_ABORT_NOUPDATEPATH')); 804 805 return false; 806 } 807 808 if (!$adapter = $this->setupInstall('update', true)) { 809 $this->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); 810 811 return false; 812 } 813 814 if (!\is_object($adapter)) { 815 return false; 816 } 817 818 // Add the languages from the package itself 819 if (method_exists($adapter, 'loadLanguage')) { 820 $adapter->loadLanguage($path); 821 } 822 823 // Fire the onExtensionBeforeUpdate event. 824 PluginHelper::importPlugin('extension'); 825 Factory::getApplication()->triggerEvent( 826 'onExtensionBeforeUpdate', 827 array('type' => $this->manifest->attributes()->type, 'manifest' => $this->manifest) 828 ); 829 830 // Run the update 831 $result = $adapter->update(); 832 833 // Fire the onExtensionAfterUpdate 834 Factory::getApplication()->triggerEvent( 835 'onExtensionAfterUpdate', 836 array('installer' => clone $this, 'eid' => $result) 837 ); 838 839 if ($result !== false) { 840 return true; 841 } 842 843 return false; 844 } 845 846 /** 847 * Package uninstallation method 848 * 849 * @param string $type Package type 850 * @param mixed $identifier Package identifier for adapter 851 * 852 * @return boolean True if successful 853 * 854 * @since 3.1 855 */ 856 public function uninstall($type, $identifier) 857 { 858 $params = array('extension' => $this->extension, 'route' => 'uninstall'); 859 860 $adapter = $this->loadAdapter($type, $params); 861 862 if (!\is_object($adapter)) { 863 return false; 864 } 865 866 // We don't load languages here, we get the extension adapter to work it out 867 // Fire the onExtensionBeforeUninstall event. 868 PluginHelper::importPlugin('extension'); 869 Factory::getApplication()->triggerEvent( 870 'onExtensionBeforeUninstall', 871 array('eid' => $identifier) 872 ); 873 874 // Run the uninstall 875 $result = $adapter->uninstall($identifier); 876 877 // Fire the onExtensionAfterInstall 878 Factory::getApplication()->triggerEvent( 879 'onExtensionAfterUninstall', 880 array('installer' => clone $this, 'eid' => $identifier, 'removed' => $result) 881 ); 882 883 // Refresh versionable assets cache 884 Factory::getApplication()->flushAssets(); 885 886 return $result; 887 } 888 889 /** 890 * Refreshes the manifest cache stored in #__extensions 891 * 892 * @param integer $eid Extension ID 893 * 894 * @return boolean 895 * 896 * @since 3.1 897 */ 898 public function refreshManifestCache($eid) 899 { 900 if ($eid) { 901 if (!$this->extension->load($eid)) { 902 $this->abort(Text::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS')); 903 904 return false; 905 } 906 907 if ($this->extension->state == -1) { 908 $this->abort(Text::sprintf('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE', $this->extension->name)); 909 910 return false; 911 } 912 913 // Fetch the adapter 914 $adapter = $this->loadAdapter($this->extension->type); 915 916 if (!\is_object($adapter)) { 917 return false; 918 } 919 920 if (!method_exists($adapter, 'refreshManifestCache')) { 921 $this->abort(Text::sprintf('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED_TYPE', $this->extension->type)); 922 923 return false; 924 } 925 926 $result = $adapter->refreshManifestCache(); 927 928 if ($result !== false) { 929 return true; 930 } else { 931 return false; 932 } 933 } 934 935 $this->abort(Text::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE_VALID')); 936 937 return false; 938 } 939 940 // Utility functions 941 942 /** 943 * Prepare for installation: this method sets the installation directory, finds 944 * and checks the installation file and verifies the installation type. 945 * 946 * @param string $route The install route being followed 947 * @param boolean $returnAdapter Flag to return the instantiated adapter 948 * 949 * @return boolean|InstallerAdapter InstallerAdapter object if explicitly requested otherwise boolean 950 * 951 * @since 3.1 952 */ 953 public function setupInstall($route = 'install', $returnAdapter = false) 954 { 955 // We need to find the installation manifest file 956 if (!$this->findManifest()) { 957 return false; 958 } 959 960 // Load the adapter(s) for the install manifest 961 $type = (string) $this->manifest->attributes()->type; 962 $params = array('route' => $route, 'manifest' => $this->getManifest()); 963 964 // Load the adapter 965 $adapter = $this->loadAdapter($type, $params); 966 967 if ($returnAdapter) { 968 return $adapter; 969 } 970 971 return true; 972 } 973 974 /** 975 * Backward compatible method to parse through a queries element of the 976 * installation manifest file and take appropriate action. 977 * 978 * @param \SimpleXMLElement $element The XML node to process 979 * 980 * @return mixed Number of queries processed or False on error 981 * 982 * @since 3.1 983 */ 984 public function parseQueries(\SimpleXMLElement $element) 985 { 986 // Get the database connector object 987 $db = & $this->_db; 988 989 if (!$element || !\count($element->children())) { 990 // Either the tag does not exist or has no children therefore we return zero files processed. 991 return 0; 992 } 993 994 // Get the array of query nodes to process 995 $queries = $element->children(); 996 997 if (\count($queries) === 0) { 998 // No queries to process 999 return 0; 1000 } 1001 1002 $update_count = 0; 1003 1004 // Process each query in the $queries array (children of $tagName). 1005 foreach ($queries as $query) { 1006 try { 1007 $db->setQuery($query)->execute(); 1008 } catch (ExecutionFailureException $e) { 1009 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); 1010 1011 return false; 1012 } 1013 1014 $update_count++; 1015 } 1016 1017 return $update_count; 1018 } 1019 1020 /** 1021 * Method to extract the name of a discreet installation sql file from the installation manifest file. 1022 * 1023 * @param object $element The XML node to process 1024 * 1025 * @return mixed Number of queries processed or False on error 1026 * 1027 * @since 3.1 1028 */ 1029 public function parseSQLFiles($element) 1030 { 1031 if (!$element || !\count($element->children())) { 1032 // The tag does not exist. 1033 return 0; 1034 } 1035 1036 $db = &$this->_db; 1037 $dbDriver = $db->getServerType(); 1038 $updateCount = 0; 1039 1040 // Get the name of the sql file to process 1041 foreach ($element->children() as $file) { 1042 $fCharset = strtolower($file->attributes()->charset) === 'utf8' ? 'utf8' : ''; 1043 $fDriver = strtolower($file->attributes()->driver); 1044 1045 if ($fDriver === 'mysqli' || $fDriver === 'pdomysql') { 1046 $fDriver = 'mysql'; 1047 } elseif ($fDriver === 'pgsql') { 1048 $fDriver = 'postgresql'; 1049 } 1050 1051 if ($fCharset !== 'utf8' || $fDriver != $dbDriver) { 1052 continue; 1053 } 1054 1055 $sqlfile = $this->getPath('extension_root') . '/' . trim($file); 1056 1057 // Check that sql files exists before reading. Otherwise raise error for rollback 1058 if (!file_exists($sqlfile)) { 1059 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_FILENOTFOUND', $sqlfile), Log::WARNING, 'jerror'); 1060 1061 return false; 1062 } 1063 1064 $buffer = file_get_contents($sqlfile); 1065 1066 // Graceful exit and rollback if read not successful 1067 if ($buffer === false) { 1068 Log::add(Text::_('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), Log::WARNING, 'jerror'); 1069 1070 return false; 1071 } 1072 1073 // Create an array of queries from the sql file 1074 $queries = self::splitSql($buffer); 1075 1076 if (\count($queries) === 0) { 1077 // No queries to process 1078 continue; 1079 } 1080 1081 // Process each query in the $queries array (split out of sql file). 1082 foreach ($queries as $query) { 1083 $canFail = strlen($query) > self::CAN_FAIL_MARKER_LENGTH + 1 && 1084 strtoupper(substr($query, -self::CAN_FAIL_MARKER_LENGTH - 1)) === (self::CAN_FAIL_MARKER . ';'); 1085 $query = $canFail ? (substr($query, 0, -self::CAN_FAIL_MARKER_LENGTH - 1) . ';') : $query; 1086 1087 try { 1088 $db->setQuery($query)->execute(); 1089 } catch (ExecutionFailureException $e) { 1090 if (!$canFail) { 1091 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); 1092 1093 return false; 1094 } 1095 } 1096 1097 $updateCount++; 1098 } 1099 } 1100 1101 return $updateCount; 1102 } 1103 1104 /** 1105 * Set the schema version for an extension by looking at its latest update 1106 * 1107 * @param \SimpleXMLElement $schema Schema Tag 1108 * @param integer $eid Extension ID 1109 * 1110 * @return void 1111 * 1112 * @since 3.1 1113 */ 1114 public function setSchemaVersion(\SimpleXMLElement $schema, $eid) 1115 { 1116 if ($eid && $schema) { 1117 $db = $this->getDatabase(); 1118 $schemapaths = $schema->children(); 1119 1120 if (!$schemapaths) { 1121 return; 1122 } 1123 1124 if (\count($schemapaths)) { 1125 $dbDriver = $db->getServerType(); 1126 1127 $schemapath = ''; 1128 1129 foreach ($schemapaths as $entry) { 1130 $attrs = $entry->attributes(); 1131 1132 if ($attrs['type'] == $dbDriver) { 1133 $schemapath = $entry; 1134 break; 1135 } 1136 } 1137 1138 if ($schemapath !== '') { 1139 $files = str_replace('.sql', '', Folder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$')); 1140 usort($files, 'version_compare'); 1141 1142 // Update the database 1143 $query = $db->getQuery(true) 1144 ->delete('#__schemas') 1145 ->where('extension_id = :extension_id') 1146 ->bind(':extension_id', $eid, ParameterType::INTEGER); 1147 $db->setQuery($query); 1148 1149 if ($db->execute()) { 1150 $schemaVersion = end($files); 1151 1152 $query->clear() 1153 ->insert($db->quoteName('#__schemas')) 1154 ->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id'))) 1155 ->values(':extension_id, :version_id') 1156 ->bind(':extension_id', $eid, ParameterType::INTEGER) 1157 ->bind(':version_id', $schemaVersion); 1158 $db->setQuery($query); 1159 $db->execute(); 1160 } 1161 } 1162 } 1163 } 1164 } 1165 1166 /** 1167 * Method to process the updates for an item 1168 * 1169 * @param \SimpleXMLElement $schema The XML node to process 1170 * @param integer $eid Extension Identifier 1171 * 1172 * @return boolean|int Number of SQL updates executed; false on failure. 1173 * 1174 * @since 3.1 1175 */ 1176 public function parseSchemaUpdates(\SimpleXMLElement $schema, $eid) 1177 { 1178 $updateCount = 0; 1179 1180 // Ensure we have an XML element and a valid extension id 1181 if (!$eid || !$schema) { 1182 return $updateCount; 1183 } 1184 1185 $db = $this->getDatabase(); 1186 $schemapaths = $schema->children(); 1187 1188 if (!\count($schemapaths)) { 1189 return $updateCount; 1190 } 1191 1192 $dbDriver = $db->getServerType(); 1193 1194 $schemapath = ''; 1195 1196 foreach ($schemapaths as $entry) { 1197 $attrs = $entry->attributes(); 1198 1199 // Assuming that the type is a mandatory attribute but if it is not mandatory then there should be a discussion for it. 1200 $uDriver = strtolower($attrs['type']); 1201 1202 if ($uDriver === 'mysqli' || $uDriver === 'pdomysql') { 1203 $uDriver = 'mysql'; 1204 } elseif ($uDriver === 'pgsql') { 1205 $uDriver = 'postgresql'; 1206 } 1207 1208 if ($uDriver == $dbDriver) { 1209 $schemapath = $entry; 1210 break; 1211 } 1212 } 1213 1214 if ($schemapath === '') { 1215 return $updateCount; 1216 } 1217 1218 $files = Folder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$'); 1219 1220 if (empty($files)) { 1221 return $updateCount; 1222 } 1223 1224 Log::add(Text::_('JLIB_INSTALLER_SQL_BEGIN'), Log::INFO, 'Update'); 1225 1226 $files = str_replace('.sql', '', $files); 1227 usort($files, 'version_compare'); 1228 1229 $query = $db->getQuery(true) 1230 ->select('version_id') 1231 ->from('#__schemas') 1232 ->where('extension_id = :extension_id') 1233 ->bind(':extension_id', $eid, ParameterType::INTEGER); 1234 $db->setQuery($query); 1235 1236 $hasVersion = true; 1237 1238 try { 1239 $version = $db->loadResult(); 1240 1241 // No version - use initial version. 1242 if (!$version) { 1243 $version = '0.0.0'; 1244 $hasVersion = false; 1245 } 1246 } catch (ExecutionFailureException $e) { 1247 $version = '0.0.0'; 1248 } 1249 1250 Log::add(Text::sprintf('JLIB_INSTALLER_SQL_BEGIN_SCHEMA', $version), Log::INFO, 'Update'); 1251 1252 foreach ($files as $file) { 1253 // Skip over files earlier or equal to the latest schema version recorded for this extension. 1254 if (version_compare($file, $version) <= 0) { 1255 continue; 1256 } 1257 1258 $buffer = file_get_contents(sprintf("%s/%s/%s.sql", $this->getPath('extension_root'), $schemapath, $file)); 1259 1260 // Graceful exit and rollback if read not successful 1261 if ($buffer === false) { 1262 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), Log::WARNING, 'jerror'); 1263 1264 return false; 1265 } 1266 1267 // Create an array of queries from the sql file 1268 $queries = self::splitSql($buffer); 1269 1270 // Process each query in the $queries array (split out of sql file). 1271 foreach ($queries as $query) { 1272 $canFail = strlen($query) > self::CAN_FAIL_MARKER_LENGTH + 1 && 1273 strtoupper(substr($query, -self::CAN_FAIL_MARKER_LENGTH - 1)) === (self::CAN_FAIL_MARKER . ';'); 1274 $query = $canFail ? (substr($query, 0, -self::CAN_FAIL_MARKER_LENGTH - 1) . ';') : $query; 1275 1276 $queryString = (string) $query; 1277 $queryString = str_replace(["\r", "\n"], ['', ' '], substr($queryString, 0, 80)); 1278 1279 try { 1280 $db->setQuery($query)->execute(); 1281 } catch (ExecutionFailureException | PrepareStatementFailureException $e) { 1282 if (!$canFail) { 1283 $errorMessage = Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()); 1284 1285 // Log the error in the update log file 1286 Log::add(Text::sprintf('JLIB_INSTALLER_UPDATE_LOG_QUERY', $file, $queryString), Log::INFO, 'Update'); 1287 Log::add($errorMessage, Log::INFO, 'Update'); 1288 Log::add(Text::_('JLIB_INSTALLER_SQL_END_NOT_COMPLETE'), Log::INFO, 'Update'); 1289 1290 // Show the error message to the user 1291 Log::add($errorMessage, Log::WARNING, 'jerror'); 1292 1293 return false; 1294 } 1295 } 1296 1297 Log::add(Text::sprintf('JLIB_INSTALLER_UPDATE_LOG_QUERY', $file, $queryString), Log::INFO, 'Update'); 1298 1299 $updateCount++; 1300 } 1301 1302 // Update the schema version for this extension 1303 try { 1304 $this->updateSchemaTable($eid, $file, $hasVersion); 1305 $hasVersion = true; 1306 } catch (ExecutionFailureException $e) { 1307 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); 1308 1309 return false; 1310 } 1311 } 1312 1313 Log::add(Text::_('JLIB_INSTALLER_SQL_END'), Log::INFO, 'Update'); 1314 1315 return $updateCount; 1316 } 1317 1318 /** 1319 * Update the schema table with the latest version 1320 * 1321 * @param int $eid Extension ID. 1322 * @param string $version Latest schema version ID. 1323 * @param boolean $update Should I run an update against an existing record or insert a new one? 1324 * 1325 * @return void 1326 * 1327 * @since 4.2.0 1328 */ 1329 protected function updateSchemaTable(int $eid, string $version, bool $update = false): void 1330 { 1331 /** @var DatabaseDriver $db */ 1332 $db = Factory::getContainer()->get('DatabaseDriver'); 1333 1334 $o = (object) [ 1335 'extension_id' => $eid, 1336 'version_id' => $version, 1337 ]; 1338 1339 try { 1340 if ($update) { 1341 $db->updateObject('#__schemas', $o, 'extension_id'); 1342 } else { 1343 $db->insertObject('#__schemas', $o); 1344 } 1345 } catch (ExecutionFailureException $e) { 1346 /** 1347 * Safe fallback: delete any existing record and insert afresh. 1348 * 1349 * It is possible that the schema version may be populated after we detected it does not 1350 * exist (or removed after we detected it exists) and before we finish executing the SQL 1351 * update script. This could happen e.g. if the update SQL script messes with it, or if 1352 * another process is also tinkering with the #__schemas table. 1353 * 1354 * The safe fallback below even runs inside a transaction to prevent interference from 1355 * another process. 1356 */ 1357 $db->transactionStart(); 1358 1359 $query = $db->getQuery(true) 1360 ->delete('#__schemas') 1361 ->where('extension_id = :extension_id') 1362 ->bind(':extension_id', $eid, ParameterType::INTEGER); 1363 1364 $db->setQuery($query)->execute(); 1365 1366 $db->insertObject('#__schemas', $o); 1367 1368 $db->transactionCommit(); 1369 } 1370 } 1371 1372 /** 1373 * Method to parse through a files element of the installation manifest and take appropriate 1374 * action. 1375 * 1376 * @param \SimpleXMLElement $element The XML node to process 1377 * @param integer $cid Application ID of application to install to 1378 * @param array $oldFiles List of old files (SimpleXMLElement's) 1379 * @param array $oldMD5 List of old MD5 sums (indexed by filename with value as MD5) 1380 * 1381 * @return boolean True on success 1382 * 1383 * @since 3.1 1384 */ 1385 public function parseFiles(\SimpleXMLElement $element, $cid = 0, $oldFiles = null, $oldMD5 = null) 1386 { 1387 // Get the array of file nodes to process; we checked whether this had children above. 1388 if (!$element || !\count($element->children())) { 1389 // Either the tag does not exist or has no children (hence no files to process) therefore we return zero files processed. 1390 return 0; 1391 } 1392 1393 $copyfiles = array(); 1394 1395 // Get the client info 1396 $client = ApplicationHelper::getClientInfo($cid); 1397 1398 /* 1399 * Here we set the folder we are going to remove the files from. 1400 */ 1401 if ($client) { 1402 $pathname = 'extension_' . $client->name; 1403 $destination = $this->getPath($pathname); 1404 } else { 1405 $pathname = 'extension_root'; 1406 $destination = $this->getPath($pathname); 1407 } 1408 1409 /* 1410 * Here we set the folder we are going to copy the files from. 1411 * 1412 * Does the element have a folder attribute? 1413 * 1414 * If so this indicates that the files are in a subdirectory of the source 1415 * folder and we should append the folder attribute to the source path when 1416 * copying files. 1417 */ 1418 1419 $folder = (string) $element->attributes()->folder; 1420 1421 if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { 1422 $source = $this->getPath('source') . '/' . $folder; 1423 } else { 1424 $source = $this->getPath('source'); 1425 } 1426 1427 // Work out what files have been deleted 1428 if ($oldFiles && ($oldFiles instanceof \SimpleXMLElement)) { 1429 $oldEntries = $oldFiles->children(); 1430 1431 if (\count($oldEntries)) { 1432 $deletions = $this->findDeletedFiles($oldEntries, $element->children()); 1433 1434 foreach ($deletions['folders'] as $deleted_folder) { 1435 Folder::delete($destination . '/' . $deleted_folder); 1436 } 1437 1438 foreach ($deletions['files'] as $deleted_file) { 1439 File::delete($destination . '/' . $deleted_file); 1440 } 1441 } 1442 } 1443 1444 $path = array(); 1445 1446 // Copy the MD5SUMS file if it exists 1447 if (file_exists($source . '/MD5SUMS')) { 1448 $path['src'] = $source . '/MD5SUMS'; 1449 $path['dest'] = $destination . '/MD5SUMS'; 1450 $path['type'] = 'file'; 1451 $copyfiles[] = $path; 1452 } 1453 1454 // Process each file in the $files array (children of $tagName). 1455 foreach ($element->children() as $file) { 1456 $path['src'] = $source . '/' . $file; 1457 $path['dest'] = $destination . '/' . $file; 1458 1459 // Is this path a file or folder? 1460 $path['type'] = $file->getName() === 'folder' ? 'folder' : 'file'; 1461 1462 /* 1463 * Before we can add a file to the copyfiles array we need to ensure 1464 * that the folder we are copying our file to exists and if it doesn't, 1465 * we need to create it. 1466 */ 1467 1468 if (basename($path['dest']) !== $path['dest']) { 1469 $newdir = \dirname($path['dest']); 1470 1471 if (!Folder::create($newdir)) { 1472 Log::add( 1473 Text::sprintf( 1474 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 1475 Text::_('JLIB_INSTALLER_INSTALL'), 1476 $newdir 1477 ), 1478 Log::WARNING, 1479 'jerror' 1480 ); 1481 1482 return false; 1483 } 1484 } 1485 1486 // Add the file to the copyfiles array 1487 $copyfiles[] = $path; 1488 } 1489 1490 return $this->copyFiles($copyfiles); 1491 } 1492 1493 /** 1494 * Method to parse through a languages element of the installation manifest and take appropriate 1495 * action. 1496 * 1497 * @param \SimpleXMLElement $element The XML node to process 1498 * @param integer $cid Application ID of application to install to 1499 * 1500 * @return boolean True on success 1501 * 1502 * @since 3.1 1503 */ 1504 public function parseLanguages(\SimpleXMLElement $element, $cid = 0) 1505 { 1506 // TODO: work out why the below line triggers 'node no longer exists' errors with files 1507 if (!$element || !\count($element->children())) { 1508 // Either the tag does not exist or has no children therefore we return zero files processed. 1509 return 0; 1510 } 1511 1512 $copyfiles = array(); 1513 1514 // Get the client info 1515 $client = ApplicationHelper::getClientInfo($cid); 1516 1517 // Here we set the folder we are going to copy the files to. 1518 // 'languages' Files are copied to JPATH_BASE/language/ folder 1519 1520 $destination = $client->path . '/language'; 1521 1522 /* 1523 * Here we set the folder we are going to copy the files from. 1524 * 1525 * Does the element have a folder attribute? 1526 * 1527 * If so this indicates that the files are in a subdirectory of the source 1528 * folder and we should append the folder attribute to the source path when 1529 * copying files. 1530 */ 1531 1532 $folder = (string) $element->attributes()->folder; 1533 1534 if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { 1535 $source = $this->getPath('source') . '/' . $folder; 1536 } else { 1537 $source = $this->getPath('source'); 1538 } 1539 1540 // Process each file in the $files array (children of $tagName). 1541 foreach ($element->children() as $file) { 1542 /* 1543 * Language files go in a subfolder based on the language code, ie. 1544 * <language tag="en-US">en-US.mycomponent.ini</language> 1545 * would go in the en-US subdirectory of the language folder. 1546 */ 1547 1548 // We will only install language files where a core language pack 1549 // already exists. 1550 1551 if ((string) $file->attributes()->tag !== '') { 1552 $path['src'] = $source . '/' . $file; 1553 1554 if ((string) $file->attributes()->client !== '') { 1555 // Override the client 1556 $langclient = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true); 1557 $path['dest'] = $langclient->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file); 1558 } else { 1559 // Use the default client 1560 $path['dest'] = $destination . '/' . $file->attributes()->tag . '/' . basename((string) $file); 1561 } 1562 1563 // If the language folder is not present, then the core pack hasn't been installed... ignore 1564 if (!Folder::exists(\dirname($path['dest']))) { 1565 continue; 1566 } 1567 } else { 1568 $path['src'] = $source . '/' . $file; 1569 $path['dest'] = $destination . '/' . $file; 1570 } 1571 1572 /* 1573 * Before we can add a file to the copyfiles array we need to ensure 1574 * that the folder we are copying our file to exists and if it doesn't, 1575 * we need to create it. 1576 */ 1577 1578 if (basename($path['dest']) !== $path['dest']) { 1579 $newdir = \dirname($path['dest']); 1580 1581 if (!Folder::create($newdir)) { 1582 Log::add( 1583 Text::sprintf( 1584 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 1585 Text::_('JLIB_INSTALLER_INSTALL'), 1586 $newdir 1587 ), 1588 Log::WARNING, 1589 'jerror' 1590 ); 1591 1592 return false; 1593 } 1594 } 1595 1596 // Add the file to the copyfiles array 1597 $copyfiles[] = $path; 1598 } 1599 1600 return $this->copyFiles($copyfiles); 1601 } 1602 1603 /** 1604 * Method to parse through a media element of the installation manifest and take appropriate 1605 * action. 1606 * 1607 * @param \SimpleXMLElement $element The XML node to process 1608 * @param integer $cid Application ID of application to install to 1609 * 1610 * @return boolean True on success 1611 * 1612 * @since 3.1 1613 */ 1614 public function parseMedia(\SimpleXMLElement $element, $cid = 0) 1615 { 1616 if (!$element || !\count($element->children())) { 1617 // Either the tag does not exist or has no children therefore we return zero files processed. 1618 return 0; 1619 } 1620 1621 $copyfiles = array(); 1622 1623 // Here we set the folder we are going to copy the files to. 1624 // Default 'media' Files are copied to the JPATH_BASE/media folder 1625 1626 $folder = ((string) $element->attributes()->destination) ? '/' . $element->attributes()->destination : null; 1627 $destination = Path::clean(JPATH_ROOT . '/media' . $folder); 1628 1629 // Here we set the folder we are going to copy the files from. 1630 1631 /* 1632 * Does the element have a folder attribute? 1633 * If so this indicates that the files are in a subdirectory of the source 1634 * folder and we should append the folder attribute to the source path when 1635 * copying files. 1636 */ 1637 1638 $folder = (string) $element->attributes()->folder; 1639 1640 if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { 1641 $source = $this->getPath('source') . '/' . $folder; 1642 } else { 1643 $source = $this->getPath('source'); 1644 } 1645 1646 // Process each file in the $files array (children of $tagName). 1647 foreach ($element->children() as $file) { 1648 $path['src'] = $source . '/' . $file; 1649 $path['dest'] = $destination . '/' . $file; 1650 1651 // Is this path a file or folder? 1652 $path['type'] = $file->getName() === 'folder' ? 'folder' : 'file'; 1653 1654 /* 1655 * Before we can add a file to the copyfiles array we need to ensure 1656 * that the folder we are copying our file to exists and if it doesn't, 1657 * we need to create it. 1658 */ 1659 1660 if (basename($path['dest']) !== $path['dest']) { 1661 $newdir = \dirname($path['dest']); 1662 1663 if (!Folder::create($newdir)) { 1664 Log::add( 1665 Text::sprintf( 1666 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 1667 Text::_('JLIB_INSTALLER_INSTALL'), 1668 $newdir 1669 ), 1670 Log::WARNING, 1671 'jerror' 1672 ); 1673 1674 return false; 1675 } 1676 } 1677 1678 // Add the file to the copyfiles array 1679 $copyfiles[] = $path; 1680 } 1681 1682 return $this->copyFiles($copyfiles); 1683 } 1684 1685 /** 1686 * Method to parse the parameters of an extension, build the JSON string for its default parameters, and return the JSON string. 1687 * 1688 * @return string JSON string of parameter values 1689 * 1690 * @since 3.1 1691 * @note This method must always return a JSON compliant string 1692 */ 1693 public function getParams() 1694 { 1695 // Validate that we have a fieldset to use 1696 if (!isset($this->manifest->config->fields->fieldset)) { 1697 return '{}'; 1698 } 1699 1700 // Getting the fieldset tags 1701 $fieldsets = $this->manifest->config->fields->fieldset; 1702 1703 // Creating the data collection variable: 1704 $ini = array(); 1705 1706 // Iterating through the fieldsets: 1707 foreach ($fieldsets as $fieldset) { 1708 if (!\count($fieldset->children())) { 1709 // Either the tag does not exist or has no children therefore we return zero files processed. 1710 return '{}'; 1711 } 1712 1713 // Iterating through the fields and collecting the name/default values: 1714 foreach ($fieldset as $field) { 1715 // Check against the null value since otherwise default values like "0" 1716 // cause entire parameters to be skipped. 1717 1718 if (($name = $field->attributes()->name) === null) { 1719 continue; 1720 } 1721 1722 if (($value = $field->attributes()->default) === null) { 1723 continue; 1724 } 1725 1726 $ini[(string) $name] = (string) $value; 1727 } 1728 } 1729 1730 return json_encode($ini); 1731 } 1732 1733 /** 1734 * Copyfiles 1735 * 1736 * Copy files from source directory to the target directory 1737 * 1738 * @param array $files Array with filenames 1739 * @param boolean $overwrite True if existing files can be replaced 1740 * 1741 * @return boolean True on success 1742 * 1743 * @since 3.1 1744 */ 1745 public function copyFiles($files, $overwrite = null) 1746 { 1747 /* 1748 * To allow for manual override on the overwriting flag, we check to see if 1749 * the $overwrite flag was set and is a boolean value. If not, use the object 1750 * allowOverwrite flag. 1751 */ 1752 1753 if ($overwrite === null || !\is_bool($overwrite)) { 1754 $overwrite = $this->overwrite; 1755 } 1756 1757 /* 1758 * $files must be an array of filenames. Verify that it is an array with 1759 * at least one file to copy. 1760 */ 1761 if (\is_array($files) && \count($files) > 0) { 1762 foreach ($files as $file) { 1763 // Get the source and destination paths 1764 $filesource = Path::clean($file['src']); 1765 $filedest = Path::clean($file['dest']); 1766 $filetype = \array_key_exists('type', $file) ? $file['type'] : 'file'; 1767 1768 if (!file_exists($filesource)) { 1769 /* 1770 * The source file does not exist. Nothing to copy so set an error 1771 * and return false. 1772 */ 1773 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_NO_FILE', $filesource), Log::WARNING, 'jerror'); 1774 1775 return false; 1776 } elseif (($exists = file_exists($filedest)) && !$overwrite) { 1777 // It's okay if the manifest already exists 1778 if ($this->getPath('manifest') === $filesource) { 1779 continue; 1780 } 1781 1782 // The destination file already exists and the overwrite flag is false. 1783 // Set an error and return false. 1784 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_EXISTS', $filedest), Log::WARNING, 'jerror'); 1785 1786 return false; 1787 } else { 1788 // Copy the folder or file to the new location. 1789 if ($filetype === 'folder') { 1790 if (!Folder::copy($filesource, $filedest, null, $overwrite)) { 1791 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER', $filesource, $filedest), Log::WARNING, 'jerror'); 1792 1793 return false; 1794 } 1795 1796 $step = array('type' => 'folder', 'path' => $filedest); 1797 } else { 1798 if (!File::copy($filesource, $filedest, null)) { 1799 Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FILE', $filesource, $filedest), Log::WARNING, 'jerror'); 1800 1801 // In 3.2, TinyMCE language handling changed. Display a special notice in case an older language pack is installed. 1802 if (strpos($filedest, 'media/editors/tinymce/jscripts/tiny_mce/langs')) { 1803 Log::add(Text::_('JLIB_INSTALLER_NOT_ERROR'), Log::WARNING, 'jerror'); 1804 } 1805 1806 return false; 1807 } 1808 1809 $step = array('type' => 'file', 'path' => $filedest); 1810 } 1811 1812 /* 1813 * Since we copied a file/folder, we want to add it to the installation step stack so that 1814 * in case we have to roll back the installation we can remove the files copied. 1815 */ 1816 if (!$exists) { 1817 $this->stepStack[] = $step; 1818 } 1819 } 1820 } 1821 } else { 1822 // The $files variable was either not an array or an empty array 1823 return false; 1824 } 1825 1826 return \count($files); 1827 } 1828 1829 /** 1830 * Method to parse through a files element of the installation manifest and remove 1831 * the files that were installed 1832 * 1833 * @param object $element The XML node to process 1834 * @param integer $cid Application ID of application to remove from 1835 * 1836 * @return boolean True on success 1837 * 1838 * @since 3.1 1839 */ 1840 public function removeFiles($element, $cid = 0) 1841 { 1842 if (!$element || !\count($element->children())) { 1843 // Either the tag does not exist or has no children therefore we return zero files processed. 1844 return true; 1845 } 1846 1847 $retval = true; 1848 1849 // Get the client info if we're using a specific client 1850 if ($cid > -1) { 1851 $client = ApplicationHelper::getClientInfo($cid); 1852 } else { 1853 $client = null; 1854 } 1855 1856 // Get the array of file nodes to process 1857 $files = $element->children(); 1858 1859 if (\count($files) === 0) { 1860 // No files to process 1861 return true; 1862 } 1863 1864 $folder = ''; 1865 1866 /* 1867 * Here we set the folder we are going to remove the files from. There are a few 1868 * special cases that need to be considered for certain reserved tags. 1869 */ 1870 switch ($element->getName()) { 1871 case 'media': 1872 if ((string) $element->attributes()->destination) { 1873 $folder = (string) $element->attributes()->destination; 1874 } else { 1875 $folder = ''; 1876 } 1877 1878 $source = $client->path . '/media/' . $folder; 1879 1880 break; 1881 1882 case 'languages': 1883 $lang_client = (string) $element->attributes()->client; 1884 1885 if ($lang_client) { 1886 $client = ApplicationHelper::getClientInfo($lang_client, true); 1887 $source = $client->path . '/language'; 1888 } else { 1889 if ($client) { 1890 $source = $client->path . '/language'; 1891 } else { 1892 $source = ''; 1893 } 1894 } 1895 1896 break; 1897 1898 default: 1899 if ($client) { 1900 $pathname = 'extension_' . $client->name; 1901 $source = $this->getPath($pathname); 1902 } else { 1903 $pathname = 'extension_root'; 1904 $source = $this->getPath($pathname); 1905 } 1906 1907 break; 1908 } 1909 1910 // Process each file in the $files array (children of $tagName). 1911 foreach ($files as $file) { 1912 /* 1913 * If the file is a language, we must handle it differently. Language files 1914 * go in a subdirectory based on the language code, ie. 1915 * <language tag="en_US">en_US.mycomponent.ini</language> 1916 * would go in the en_US subdirectory of the languages directory. 1917 */ 1918 1919 if ($file->getName() === 'language' && (string) $file->attributes()->tag !== '') { 1920 if ($source) { 1921 $path = $source . '/' . $file->attributes()->tag . '/' . basename((string) $file); 1922 } else { 1923 $target_client = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true); 1924 $path = $target_client->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file); 1925 } 1926 1927 // If the language folder is not present, then the core pack hasn't been installed... ignore 1928 if (!Folder::exists(\dirname($path))) { 1929 continue; 1930 } 1931 } else { 1932 $path = $source . '/' . $file; 1933 } 1934 1935 // Actually delete the files/folders 1936 1937 if (is_dir($path)) { 1938 $val = Folder::delete($path); 1939 } else { 1940 $val = File::delete($path); 1941 } 1942 1943 if ($val === false) { 1944 Log::add('Failed to delete ' . $path, Log::WARNING, 'jerror'); 1945 $retval = false; 1946 } 1947 } 1948 1949 if (!empty($folder)) { 1950 Folder::delete($source); 1951 } 1952 1953 return $retval; 1954 } 1955 1956 /** 1957 * Copies the installation manifest file to the extension folder in the given client 1958 * 1959 * @param integer $cid Where to copy the installfile [optional: defaults to 1 (admin)] 1960 * 1961 * @return boolean True on success, False on error 1962 * 1963 * @since 3.1 1964 */ 1965 public function copyManifest($cid = 1) 1966 { 1967 // Get the client info 1968 $client = ApplicationHelper::getClientInfo($cid); 1969 1970 $path['src'] = $this->getPath('manifest'); 1971 1972 if ($client) { 1973 $pathname = 'extension_' . $client->name; 1974 $path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest')); 1975 } else { 1976 $pathname = 'extension_root'; 1977 $path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest')); 1978 } 1979 1980 return $this->copyFiles(array($path), true); 1981 } 1982 1983 /** 1984 * Tries to find the package manifest file 1985 * 1986 * @return boolean True on success, False on error 1987 * 1988 * @since 3.1 1989 */ 1990 public function findManifest() 1991 { 1992 // Do nothing if folder does not exist for some reason 1993 if (!Folder::exists($this->getPath('source'))) { 1994 return false; 1995 } 1996 1997 // Main folder manifests (higher priority) 1998 $parentXmlfiles = Folder::files($this->getPath('source'), '.xml$', false, true); 1999 2000 // Search for children manifests (lower priority) 2001 $allXmlFiles = Folder::files($this->getPath('source'), '.xml$', 1, true); 2002 2003 // Create an unique array of files ordered by priority 2004 $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); 2005 2006 // If at least one XML file exists 2007 if (!empty($xmlfiles)) { 2008 foreach ($xmlfiles as $file) { 2009 // Is it a valid Joomla installation manifest file? 2010 $manifest = $this->isManifest($file); 2011 2012 if ($manifest !== null) { 2013 // If the root method attribute is set to upgrade, allow file overwrite 2014 if ((string) $manifest->attributes()->method === 'upgrade') { 2015 $this->upgrade = true; 2016 $this->overwrite = true; 2017 } 2018 2019 // If the overwrite option is set, allow file overwriting 2020 if ((string) $manifest->attributes()->overwrite === 'true') { 2021 $this->overwrite = true; 2022 } 2023 2024 // Set the manifest object and path 2025 $this->manifest = $manifest; 2026 $this->setPath('manifest', $file); 2027 2028 // Set the installation source path to that of the manifest file 2029 $this->setPath('source', \dirname($file)); 2030 2031 return true; 2032 } 2033 } 2034 2035 // None of the XML files found were valid install files 2036 Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); 2037 2038 return false; 2039 } else { 2040 // No XML files were found in the install folder 2041 Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); 2042 2043 return false; 2044 } 2045 } 2046 2047 /** 2048 * Is the XML file a valid Joomla installation manifest file. 2049 * 2050 * @param string $file An xmlfile path to check 2051 * 2052 * @return \SimpleXMLElement|null A \SimpleXMLElement, or null if the file failed to parse 2053 * 2054 * @since 3.1 2055 */ 2056 public function isManifest($file) 2057 { 2058 $xml = simplexml_load_file($file); 2059 2060 // If we cannot load the XML file return null 2061 if (!$xml) { 2062 return; 2063 } 2064 2065 // Check for a valid XML root tag. 2066 if ($xml->getName() !== 'extension') { 2067 return; 2068 } 2069 2070 // Valid manifest file return the object 2071 return $xml; 2072 } 2073 2074 /** 2075 * Generates a manifest cache 2076 * 2077 * @return string serialised manifest data 2078 * 2079 * @since 3.1 2080 */ 2081 public function generateManifestCache() 2082 { 2083 return json_encode(self::parseXMLInstallFile($this->getPath('manifest'))); 2084 } 2085 2086 /** 2087 * Cleans up discovered extensions if they're being installed some other way 2088 * 2089 * @param string $type The type of extension (component, etc) 2090 * @param string $element Unique element identifier (e.g. com_content) 2091 * @param string $folder The folder of the extension (plugins; e.g. system) 2092 * @param integer $client The client application (administrator or site) 2093 * 2094 * @return object Result of query 2095 * 2096 * @since 3.1 2097 */ 2098 public function cleanDiscoveredExtension($type, $element, $folder = '', $client = 0) 2099 { 2100 $db = $this->getDatabase(); 2101 $query = $db->getQuery(true) 2102 ->delete($db->quoteName('#__extensions')) 2103 ->where('type = :type') 2104 ->where('element = :element') 2105 ->where('folder = :folder') 2106 ->where('client_id = :client_id') 2107 ->where('state = -1') 2108 ->bind(':type', $type) 2109 ->bind(':element', $element) 2110 ->bind(':folder', $folder) 2111 ->bind(':client_id', $client, ParameterType::INTEGER); 2112 $db->setQuery($query); 2113 2114 return $db->execute(); 2115 } 2116 2117 /** 2118 * Compares two "files" entries to find deleted files/folders 2119 * 2120 * @param array $oldFiles An array of \SimpleXMLElement objects that are the old files 2121 * @param array $newFiles An array of \SimpleXMLElement objects that are the new files 2122 * 2123 * @return array An array with the delete files and folders in findDeletedFiles[files] and findDeletedFiles[folders] respectively 2124 * 2125 * @since 3.1 2126 */ 2127 public function findDeletedFiles($oldFiles, $newFiles) 2128 { 2129 // The magic find deleted files function! 2130 // The files that are new 2131 $files = array(); 2132 2133 // The folders that are new 2134 $folders = array(); 2135 2136 // The folders of the files that are new 2137 $containers = array(); 2138 2139 // A list of files to delete 2140 $files_deleted = array(); 2141 2142 // A list of folders to delete 2143 $folders_deleted = array(); 2144 2145 foreach ($newFiles as $file) { 2146 switch ($file->getName()) { 2147 case 'folder': 2148 // Add any folders to the list 2149 $folders[] = (string) $file; 2150 break; 2151 2152 case 'file': 2153 default: 2154 // Add any files to the list 2155 $files[] = (string) $file; 2156 2157 // Now handle the folder part of the file to ensure we get any containers 2158 // Break up the parts of the directory 2159 $container_parts = explode('/', \dirname((string) $file)); 2160 2161 // Make sure this is clean and empty 2162 $container = ''; 2163 2164 foreach ($container_parts as $part) { 2165 // Iterate through each part 2166 // Add a slash if its not empty 2167 if (!empty($container)) { 2168 $container .= '/'; 2169 } 2170 2171 // Append the folder part 2172 $container .= $part; 2173 2174 if (!\in_array($container, $containers)) { 2175 // Add the container if it doesn't already exist 2176 $containers[] = $container; 2177 } 2178 } 2179 break; 2180 } 2181 } 2182 2183 foreach ($oldFiles as $file) { 2184 switch ($file->getName()) { 2185 case 'folder': 2186 if (!\in_array((string) $file, $folders)) { 2187 // See whether the folder exists in the new list 2188 if (!\in_array((string) $file, $containers)) { 2189 // Check if the folder exists as a container in the new list 2190 // If it's not in the new list or a container then delete it 2191 $folders_deleted[] = (string) $file; 2192 } 2193 } 2194 break; 2195 2196 case 'file': 2197 default: 2198 if (!\in_array((string) $file, $files)) { 2199 // Look if the file exists in the new list 2200 if (!\in_array(\dirname((string) $file), $folders)) { 2201 // Look if the file is now potentially in a folder 2202 $files_deleted[] = (string) $file; 2203 } 2204 } 2205 break; 2206 } 2207 } 2208 2209 return array('files' => $files_deleted, 'folders' => $folders_deleted); 2210 } 2211 2212 /** 2213 * Loads an MD5SUMS file into an associative array 2214 * 2215 * @param string $filename Filename to load 2216 * 2217 * @return array Associative array with filenames as the index and the MD5 as the value 2218 * 2219 * @since 3.1 2220 */ 2221 public function loadMD5Sum($filename) 2222 { 2223 if (!file_exists($filename)) { 2224 // Bail if the file doesn't exist 2225 return false; 2226 } 2227 2228 $data = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 2229 $retval = array(); 2230 2231 foreach ($data as $row) { 2232 // Split up the data 2233 $results = explode(' ', $row); 2234 2235 // Cull any potential prefix 2236 $results[1] = str_replace('./', '', $results[1]); 2237 2238 // Throw into the array 2239 $retval[$results[1]] = $results[0]; 2240 } 2241 2242 return $retval; 2243 } 2244 2245 /** 2246 * Parse a XML install manifest file. 2247 * 2248 * XML Root tag should be 'install' except for languages which use meta file. 2249 * 2250 * @param string $path Full path to XML file. 2251 * 2252 * @return array XML metadata. 2253 * 2254 * @since 3.0.0 2255 */ 2256 public static function parseXMLInstallFile($path) 2257 { 2258 // Check if xml file exists. 2259 if (!file_exists($path)) { 2260 return false; 2261 } 2262 2263 // Read the file to see if it's a valid component XML file 2264 $xml = simplexml_load_file($path); 2265 2266 if (!$xml) { 2267 return false; 2268 } 2269 2270 // Check for a valid XML root tag. 2271 2272 // Extensions use 'extension' as the root tag. Languages use 'metafile' instead 2273 2274 $name = $xml->getName(); 2275 2276 if ($name !== 'extension' && $name !== 'metafile') { 2277 unset($xml); 2278 2279 return false; 2280 } 2281 2282 $data = array(); 2283 2284 $data['name'] = (string) $xml->name; 2285 2286 // Check if we're a language. If so use metafile. 2287 $data['type'] = $xml->getName() === 'metafile' ? 'language' : (string) $xml->attributes()->type; 2288 2289 $data['creationDate'] = ((string) $xml->creationDate) ?: Text::_('JLIB_UNKNOWN'); 2290 $data['author'] = ((string) $xml->author) ?: Text::_('JLIB_UNKNOWN'); 2291 2292 $data['copyright'] = (string) $xml->copyright; 2293 $data['authorEmail'] = (string) $xml->authorEmail; 2294 $data['authorUrl'] = (string) $xml->authorUrl; 2295 $data['version'] = (string) $xml->version; 2296 $data['description'] = (string) $xml->description; 2297 $data['group'] = (string) $xml->group; 2298 2299 // Child template specific fields. 2300 if (isset($xml->inheritable)) { 2301 $data['inheritable'] = (string) $xml->inheritable === '0' ? false : true; 2302 } 2303 2304 if (isset($xml->parent) && (string) $xml->parent !== '') { 2305 $data['parent'] = (string) $xml->parent; 2306 } 2307 2308 if ($xml->files && \count($xml->files->children())) { 2309 $filename = basename($path); 2310 $data['filename'] = File::stripExt($filename); 2311 2312 foreach ($xml->files->children() as $oneFile) { 2313 if ((string) $oneFile->attributes()->plugin) { 2314 $data['filename'] = (string) $oneFile->attributes()->plugin; 2315 break; 2316 } 2317 } 2318 } 2319 2320 return $data; 2321 } 2322 2323 /** 2324 * Gets a list of available install adapters. 2325 * 2326 * @param array $options An array of options to inject into the adapter 2327 * @param array $custom Array of custom install adapters 2328 * 2329 * @return string[] An array of the class names of available install adapters. 2330 * 2331 * @since 3.4 2332 */ 2333 public function getAdapters($options = array(), array $custom = array()) 2334 { 2335 $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); 2336 2337 // Process the core adapters 2338 foreach ($files as $file) { 2339 $fileName = $file->getFilename(); 2340 2341 // Only load for php files. 2342 if (!$file->isFile() || $file->getExtension() !== 'php') { 2343 continue; 2344 } 2345 2346 // Derive the class name from the filename. 2347 $name = str_ireplace('.php', '', trim($fileName)); 2348 $name = str_ireplace('adapter', '', trim($name)); 2349 $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; 2350 2351 if (!class_exists($class)) { 2352 // Not namespaced 2353 $class = $this->_classprefix . ucfirst($name); 2354 } 2355 2356 // Core adapters should autoload based on classname, keep this fallback just in case 2357 if (!class_exists($class)) { 2358 // Try to load the adapter object 2359 \JLoader::register($class, $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName); 2360 2361 if (!class_exists($class)) { 2362 // Skip to next one 2363 continue; 2364 } 2365 } 2366 2367 $adapters[] = $name; 2368 } 2369 2370 // Add any custom adapters if specified 2371 if (\count($custom) >= 1) { 2372 foreach ($custom as $adapter) { 2373 // Setup the class name 2374 // TODO - Can we abstract this to not depend on the Joomla class namespace without PHP namespaces? 2375 $class = $this->_classprefix . ucfirst(trim($adapter)); 2376 2377 // If the class doesn't exist we have nothing left to do but look at the next type. We did our best. 2378 if (!class_exists($class)) { 2379 continue; 2380 } 2381 2382 $adapters[] = str_ireplace('.php', '', $fileName); 2383 } 2384 } 2385 2386 return $adapters; 2387 } 2388 2389 /** 2390 * Method to load an adapter instance 2391 * 2392 * @param string $adapter Adapter name 2393 * @param array $options Adapter options 2394 * 2395 * @return InstallerAdapter 2396 * 2397 * @since 3.4 2398 * @throws \InvalidArgumentException 2399 */ 2400 public function loadAdapter($adapter, $options = array()) 2401 { 2402 $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($adapter) . 'Adapter'; 2403 2404 if (!class_exists($class)) { 2405 // Not namespaced 2406 $class = $this->_classprefix . ucfirst($adapter); 2407 } 2408 2409 if (!class_exists($class)) { 2410 throw new \InvalidArgumentException(sprintf('The %s install adapter does not exist.', $adapter)); 2411 } 2412 2413 // Ensure the adapter type is part of the options array 2414 $options['type'] = $adapter; 2415 2416 // Check for a possible service from the container otherwise manually instantiate the class 2417 if (Factory::getContainer()->has($class)) { 2418 return Factory::getContainer()->get($class); 2419 } 2420 2421 $adapter = new $class($this, $this->getDatabase(), $options); 2422 2423 if ($adapter instanceof ContainerAwareInterface) { 2424 $adapter->setContainer(Factory::getContainer()); 2425 } 2426 2427 return $adapter; 2428 } 2429 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |