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