[ 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\Adapter; 11 12 use Joomla\CMS\Application\ApplicationHelper; 13 use Joomla\CMS\Factory; 14 use Joomla\CMS\Filesystem\Folder; 15 use Joomla\CMS\Filesystem\Path; 16 use Joomla\CMS\Installer\Installer; 17 use Joomla\CMS\Installer\InstallerAdapter; 18 use Joomla\CMS\Language\Text; 19 use Joomla\CMS\Log\Log; 20 use Joomla\CMS\Table\Asset; 21 use Joomla\CMS\Table\Extension; 22 use Joomla\CMS\Table\Table; 23 use Joomla\CMS\Table\Update; 24 use Joomla\Database\ParameterType; 25 use Joomla\Registry\Registry; 26 27 // phpcs:disable PSR1.Files.SideEffects 28 \defined('JPATH_PLATFORM') or die; 29 // phpcs:enable PSR1.Files.SideEffects 30 31 /** 32 * Component installer 33 * 34 * @since 3.1 35 */ 36 class ComponentAdapter extends InstallerAdapter 37 { 38 /** 39 * The list of current files for the Joomla! CMS administrator that are installed and is read 40 * from the manifest on disk in the update area to handle doing a diff 41 * and deleting files that are in the old files list and not in the new 42 * files list. 43 * 44 * @var array 45 * @since 3.1 46 * */ 47 protected $oldAdminFiles = null; 48 49 /** 50 * The list of current files for the Joomla! CMS API that are installed and is read 51 * from the manifest on disk in the update area to handle doing a diff 52 * and deleting files that are in the old files list and not in the new 53 * files list. 54 * 55 * @var array 56 * @since 4.0.0 57 * */ 58 protected $oldApiFiles = null; 59 60 /** 61 * The list of current files that are installed and is read 62 * from the manifest on disk in the update area to handle doing a diff 63 * and deleting files that are in the old files list and not in the new 64 * files list. 65 * 66 * @var array 67 * @since 3.1 68 * */ 69 protected $oldFiles = null; 70 71 /** 72 * A path to the PHP file that the scriptfile declaration in 73 * the manifest refers to. 74 * 75 * @var string 76 * @since 3.1 77 * */ 78 protected $manifest_script = null; 79 80 /** 81 * For legacy installations this is a path to the PHP file that the scriptfile declaration in the 82 * manifest refers to. 83 * 84 * @var string 85 * @since 3.1 86 * */ 87 protected $install_script = null; 88 89 /** 90 * Method to check if the extension is present in the filesystem 91 * 92 * @return boolean 93 * 94 * @since 3.4 95 * @throws \RuntimeException 96 */ 97 protected function checkExtensionInFilesystem() 98 { 99 /* 100 * If the component site or admin directory already exists, then we will assume that the component is already 101 * installed or another component is using that directory. 102 */ 103 if ( 104 file_exists($this->parent->getPath('extension_site')) 105 || file_exists($this->parent->getPath('extension_administrator')) 106 || file_exists($this->parent->getPath('extension_api')) 107 ) { 108 // Look for an update function or update tag 109 $updateElement = $this->getManifest()->update; 110 111 // Upgrade manually set or update function available or update tag detected 112 if ( 113 $updateElement || $this->parent->isUpgrade() 114 || ($this->parent->manifestClass && method_exists($this->parent->manifestClass, 'update')) 115 ) { 116 // If there is a matching extension mark this as an update 117 $this->setRoute('update'); 118 } elseif (!$this->parent->isOverwrite()) { 119 // We didn't have overwrite set, find an update function or find an update tag so lets call it safe 120 if (file_exists($this->parent->getPath('extension_site'))) { 121 // If the site exists say so. 122 throw new \RuntimeException( 123 Text::sprintf( 124 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_SITE', 125 $this->parent->getPath('extension_site') 126 ) 127 ); 128 } 129 130 if (file_exists($this->parent->getPath('extension_administrator'))) { 131 // If the admin exists say so 132 throw new \RuntimeException( 133 Text::sprintf( 134 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_ADMIN', 135 $this->parent->getPath('extension_administrator') 136 ) 137 ); 138 } 139 140 // If the API exists say so 141 throw new \RuntimeException( 142 Text::sprintf( 143 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_API', 144 $this->parent->getPath('extension_api') 145 ) 146 ); 147 } 148 } 149 150 return false; 151 } 152 153 /** 154 * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file 155 * 156 * @return void 157 * 158 * @since 3.4 159 * @throws \RuntimeException 160 */ 161 protected function copyBaseFiles() 162 { 163 // Copy site files 164 if ($this->getManifest()->files) { 165 if ($this->route === 'update') { 166 $result = $this->parent->parseFiles($this->getManifest()->files, 0, $this->oldFiles); 167 } else { 168 $result = $this->parent->parseFiles($this->getManifest()->files); 169 } 170 171 if ($result === false) { 172 throw new \RuntimeException( 173 Text::sprintf( 174 'JLIB_INSTALLER_ABORT_COMP_FAIL_SITE_FILES', 175 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) 176 ) 177 ); 178 } 179 } 180 181 // Copy admin files 182 if ($this->getManifest()->administration->files) { 183 if ($this->route === 'update') { 184 $result = $this->parent->parseFiles($this->getManifest()->administration->files, 1, $this->oldAdminFiles); 185 } else { 186 $result = $this->parent->parseFiles($this->getManifest()->administration->files, 1); 187 } 188 189 if ($result === false) { 190 throw new \RuntimeException( 191 Text::sprintf( 192 'JLIB_INSTALLER_ABORT_COMP_FAIL_ADMIN_FILES', 193 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) 194 ) 195 ); 196 } 197 } 198 199 // Copy API files 200 if ($this->getManifest()->api->files) { 201 if ($this->route === 'update') { 202 $result = $this->parent->parseFiles($this->getManifest()->api->files, 3, $this->oldApiFiles); 203 } else { 204 $result = $this->parent->parseFiles($this->getManifest()->api->files, 3); 205 } 206 207 if ($result === false) { 208 throw new \RuntimeException( 209 Text::sprintf( 210 'JLIB_INSTALLER_ABORT_COMP_FAIL_API_FILES', 211 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) 212 ) 213 ); 214 } 215 } 216 217 // If there is a manifest script, let's copy it. 218 if ($this->manifest_script) { 219 $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; 220 $path['dest'] = $this->parent->getPath('extension_administrator') . '/' . $this->manifest_script; 221 222 if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { 223 if (!$this->parent->copyFiles(array($path))) { 224 throw new \RuntimeException( 225 Text::sprintf( 226 'JLIB_INSTALLER_ABORT_MANIFEST', 227 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) 228 ) 229 ); 230 } 231 } 232 } 233 } 234 235 /** 236 * Method to create the extension root path if necessary 237 * 238 * @return void 239 * 240 * @since 3.4 241 * @throws \RuntimeException 242 */ 243 protected function createExtensionRoot() 244 { 245 // If the component directory does not exist, let's create it 246 $created = false; 247 248 if (!file_exists($this->parent->getPath('extension_site'))) { 249 if (!$created = Folder::create($this->parent->getPath('extension_site'))) { 250 throw new \RuntimeException( 251 Text::sprintf( 252 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 253 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), 254 $this->parent->getPath('extension_site') 255 ) 256 ); 257 } 258 } 259 260 /* 261 * Since we created the component directory and we will want to remove it if we have to roll back 262 * the installation, let's add it to the installation step stack 263 */ 264 if ($created) { 265 $this->parent->pushStep( 266 array( 267 'type' => 'folder', 268 'path' => $this->parent->getPath('extension_site'), 269 ) 270 ); 271 } 272 273 // If the component admin directory does not exist, let's create it 274 $created = false; 275 276 if (!file_exists($this->parent->getPath('extension_administrator'))) { 277 if (!$created = Folder::create($this->parent->getPath('extension_administrator'))) { 278 throw new \RuntimeException( 279 Text::sprintf( 280 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 281 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), 282 $this->parent->getPath('extension_administrator') 283 ) 284 ); 285 } 286 } 287 288 /* 289 * Since we created the component admin directory and we will want to remove it if we have to roll 290 * back the installation, let's add it to the installation step stack 291 */ 292 if ($created) { 293 $this->parent->pushStep( 294 array( 295 'type' => 'folder', 296 'path' => $this->parent->getPath('extension_administrator'), 297 ) 298 ); 299 } 300 301 // If the component API directory does not exist, let's create it 302 $created = false; 303 304 if (!file_exists($this->parent->getPath('extension_api'))) { 305 if (!$created = Folder::create($this->parent->getPath('extension_api'))) { 306 throw new \RuntimeException( 307 Text::sprintf( 308 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', 309 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), 310 $this->parent->getPath('extension_api') 311 ) 312 ); 313 } 314 } 315 316 /* 317 * Since we created the component API directory and we will want to remove it if we have to roll 318 * back the installation, let's add it to the installation step stack 319 */ 320 if ($created) { 321 $this->parent->pushStep( 322 array( 323 'type' => 'folder', 324 'path' => $this->parent->getPath('extension_api'), 325 ) 326 ); 327 } 328 } 329 330 /** 331 * Method to finalise the installation processing 332 * 333 * @return void 334 * 335 * @since 3.4 336 * @throws \RuntimeException 337 */ 338 protected function finaliseInstall() 339 { 340 /** @var Update $update */ 341 $update = Table::getInstance('update'); 342 343 // Clobber any possible pending updates 344 $uid = $update->find( 345 array( 346 'element' => $this->element, 347 'type' => $this->extension->type, 348 'client_id' => 1, 349 ) 350 ); 351 352 if ($uid) { 353 $update->delete($uid); 354 } 355 356 // We will copy the manifest file to its appropriate place. 357 if ($this->route !== 'discover_install') { 358 if (!$this->parent->copyManifest()) { 359 // Install failed, roll back changes 360 throw new \RuntimeException( 361 Text::sprintf( 362 'JLIB_INSTALLER_ABORT_COPY_SETUP', 363 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) 364 ) 365 ); 366 } 367 } 368 369 // Time to build the admin menus 370 if (!$this->_buildAdminMenus($this->extension->extension_id)) { 371 Log::add(Text::_('JLIB_INSTALLER_ABORT_COMP_BUILDADMINMENUS_FAILED'), Log::WARNING, 'jerror'); 372 } 373 374 // Make sure that menu items pointing to the component have correct component id assigned to them. 375 // Prevents message "Component 'com_extension' does not exist." after uninstalling / re-installing component. 376 if (!$this->_updateMenus($this->extension->extension_id)) { 377 Log::add(Text::_('JLIB_INSTALLER_ABORT_COMP_UPDATESITEMENUS_FAILED'), Log::WARNING, 'jerror'); 378 } 379 380 /** @var Asset $asset */ 381 $asset = Table::getInstance('Asset'); 382 383 // Check if an asset already exists for this extension and create it if not 384 if (!$asset->loadByName($this->extension->element)) { 385 // Register the component container just under root in the assets table. 386 $asset->name = $this->extension->element; 387 $asset->parent_id = 1; 388 $asset->rules = '{}'; 389 $asset->title = $this->extension->name; 390 $asset->setLocation(1, 'last-child'); 391 392 if (!$asset->store()) { 393 // Install failed, roll back changes 394 throw new \RuntimeException( 395 Text::sprintf( 396 'JLIB_INSTALLER_ABORT_ROLLBACK', 397 Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), 398 $this->extension->getError() 399 ) 400 ); 401 } 402 } 403 } 404 405 /** 406 * Method to finalise the uninstallation processing 407 * 408 * @return boolean 409 * 410 * @since 4.0.0 411 * @throws \RuntimeException 412 */ 413 protected function finaliseUninstall(): bool 414 { 415 $extensionId = $this->extension->extension_id; 416 417 $db = $this->getDatabase(); 418 419 // Remove the schema version 420 $query = $db->getQuery(true) 421 ->delete($db->quoteName('#__schemas')) 422 ->where($db->quoteName('extension_id') . ' = :extension_id') 423 ->bind(':extension_id', $extensionId, ParameterType::INTEGER); 424 $db->setQuery($query); 425 $db->execute(); 426 427 // Remove the component container in the assets table. 428 $asset = Table::getInstance('Asset'); 429 430 if ($asset->loadByName($this->getElement())) { 431 $asset->delete(); 432 } 433 434 $extensionName = $this->element; 435 $extensionNameWithWildcard = $extensionName . '.%'; 436 437 // Remove categories for this component 438 $query = $db->getQuery(true) 439 ->delete($db->quoteName('#__categories')) 440 ->where( 441 [ 442 $db->quoteName('extension') . ' = :extension', 443 $db->quoteName('extension') . ' LIKE :wildcard', 444 ], 445 'OR' 446 ) 447 ->bind(':extension', $extensionName) 448 ->bind(':wildcard', $extensionNameWithWildcard); 449 $db->setQuery($query); 450 $db->execute(); 451 452 // Rebuild the categories for correct lft/rgt 453 Table::getInstance('category')->rebuild(); 454 455 // Clobber any possible pending updates 456 $update = Table::getInstance('update'); 457 $uid = $update->find( 458 array( 459 'element' => $this->extension->element, 460 'type' => 'component', 461 'client_id' => 1, 462 'folder' => '', 463 ) 464 ); 465 466 if ($uid) { 467 $update->delete($uid); 468 } 469 470 // Now we need to delete the installation directories. This is the final step in uninstalling the component. 471 if (trim($this->extension->element)) { 472 $retval = true; 473 474 // Delete the component site directory 475 if (is_dir($this->parent->getPath('extension_site'))) { 476 if (!Folder::delete($this->parent->getPath('extension_site'))) { 477 Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_SITE'), Log::WARNING, 'jerror'); 478 $retval = false; 479 } 480 } 481 482 // Delete the component admin directory 483 if (is_dir($this->parent->getPath('extension_administrator'))) { 484 if (!Folder::delete($this->parent->getPath('extension_administrator'))) { 485 Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_ADMIN'), Log::WARNING, 'jerror'); 486 $retval = false; 487 } 488 } 489 490 // Delete the component API directory 491 if (is_dir($this->parent->getPath('extension_api'))) { 492 if (!Folder::delete($this->parent->getPath('extension_api'))) { 493 Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_API'), Log::WARNING, 'jerror'); 494 $retval = false; 495 } 496 } 497 498 // Now we will no longer need the extension object, so let's delete it 499 $this->extension->delete($this->extension->extension_id); 500 501 return $retval; 502 } 503 504 // No component option defined... cannot delete what we don't know about 505 Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_NO_OPTION'), Log::WARNING, 'jerror'); 506 507 return false; 508 } 509 510 /** 511 * Get the filtered extension element from the manifest 512 * 513 * @param string $element Optional element name to be converted 514 * 515 * @return string The filtered element 516 * 517 * @since 3.4 518 */ 519 public function getElement($element = null) 520 { 521 $element = parent::getElement($element); 522 523 if (strpos($element, 'com_') !== 0) { 524 $element = 'com_' . $element; 525 } 526 527 return $element; 528 } 529 530 /** 531 * Custom loadLanguage method 532 * 533 * @param string $path The path language files are on. 534 * 535 * @return void 536 * 537 * @since 3.1 538 */ 539 public function loadLanguage($path = null) 540 { 541 $source = $this->parent->getPath('source'); 542 543 switch ($this->parent->extension->client_id) { 544 case 0: 545 $client = JPATH_SITE; 546 547 break; 548 549 case 1: 550 $client = JPATH_ADMINISTRATOR; 551 552 break; 553 554 case 3: 555 $client = JPATH_API; 556 557 break; 558 559 default: 560 throw new \InvalidArgumentException( 561 sprintf( 562 'Unsupported client ID %d for component %s', 563 $this->parent->extension->client_id, 564 $this->parent->extension->element 565 ) 566 ); 567 } 568 569 if (!$source) { 570 $this->parent->setPath('source', $client . '/components/' . $this->parent->extension->element); 571 } 572 573 $extension = $this->getElement(); 574 $source = $path ?: $client . '/components/' . $extension; 575 576 if ($this->getManifest()->administration->files) { 577 $element = $this->getManifest()->administration->files; 578 } elseif ($this->getManifest()->api->files) { 579 $element = $this->getManifest()->api->files; 580 } elseif ($this->getManifest()->files) { 581 $element = $this->getManifest()->files; 582 } else { 583 $element = null; 584 } 585 586 if ($element) { 587 $folder = (string) $element->attributes()->folder; 588 589 if ($folder && file_exists($path . '/' . $folder)) { 590 $source = $path . '/' . $folder; 591 } 592 } 593 594 $this->doLoadLanguage($extension, $source); 595 } 596 597 /** 598 * Method to parse optional tags in the manifest 599 * 600 * @return void 601 * 602 * @since 3.4 603 */ 604 protected function parseOptionalTags() 605 { 606 // Parse optional tags 607 $this->parent->parseMedia($this->getManifest()->media); 608 $this->parent->parseLanguages($this->getManifest()->languages); 609 $this->parent->parseLanguages($this->getManifest()->administration->languages, 1); 610 } 611 612 /** 613 * Method to parse the queries specified in the `<sql>` tags 614 * 615 * @return void 616 * 617 * @since 4.0.0 618 * @throws \RuntimeException 619 */ 620 protected function parseQueries() 621 { 622 parent::parseQueries(); 623 624 // We have extra tasks to run for the uninstall path 625 if ($this->route === 'uninstall') { 626 $this->_removeAdminMenus($this->extension->extension_id); 627 } 628 } 629 630 /** 631 * Prepares the adapter for a discover_install task 632 * 633 * @return void 634 * 635 * @since 3.4 636 * @throws \RuntimeException 637 */ 638 public function prepareDiscoverInstall() 639 { 640 // Need to find to find where the XML file is since we don't store this normally 641 $client = ApplicationHelper::getClientInfo($this->extension->client_id); 642 $short_element = str_replace('com_', '', $this->extension->element); 643 $manifestPath = $client->path . '/components/' . $this->extension->element . '/' . $short_element . '.xml'; 644 $this->parent->manifest = $this->parent->isManifest($manifestPath); 645 $this->parent->setPath('manifest', $manifestPath); 646 $this->parent->setPath('source', $client->path . '/components/' . $this->extension->element); 647 $this->parent->setPath('extension_root', $this->parent->getPath('source')); 648 $this->setManifest($this->parent->getManifest()); 649 650 $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); 651 $this->extension->manifest_cache = json_encode($manifest_details); 652 $this->extension->state = 0; 653 $this->extension->name = $manifest_details['name']; 654 $this->extension->enabled = 1; 655 $this->extension->params = $this->parent->getParams(); 656 657 $stored = false; 658 659 try { 660 $this->extension->store(); 661 $stored = true; 662 } catch (\RuntimeException $e) { 663 $name = $this->extension->name; 664 $type = $this->extension->type; 665 $element = $this->extension->element; 666 667 // Try to delete existing failed records before retrying 668 $db = $this->getDatabase(); 669 670 $query = $db->getQuery(true) 671 ->select($db->quoteName('extension_id')) 672 ->from($db->quoteName('#__extensions')) 673 ->where( 674 [ 675 $db->quoteName('name') . ' = :name', 676 $db->quoteName('type') . ' = :type', 677 $db->quoteName('element') . ' = :element', 678 ] 679 ) 680 ->bind(':name', $name) 681 ->bind(':type', $type) 682 ->bind(':element', $element); 683 684 $db->setQuery($query); 685 686 $extension_ids = $db->loadColumn(); 687 688 if (!empty($extension_ids)) { 689 foreach ($extension_ids as $eid) { 690 // Remove leftover admin menus for this extension ID 691 $this->_removeAdminMenus($eid); 692 693 // Remove the extension record itself 694 /** @var Extension $extensionTable */ 695 $extensionTable = Table::getInstance('extension'); 696 $extensionTable->delete($eid); 697 } 698 } 699 } 700 701 if (!$stored) { 702 try { 703 $this->extension->store(); 704 } catch (\RuntimeException $e) { 705 throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_DISCOVER_STORE_DETAILS'), $e->getCode(), $e); 706 } 707 } 708 } 709 710 /** 711 * Removes this extension's files 712 * 713 * @return void 714 * 715 * @since 4.0.0 716 * @throws \RuntimeException 717 */ 718 protected function removeExtensionFiles() 719 { 720 // Let's remove those language files and media in the JROOT/images/ folder that are associated with the component we are uninstalling 721 $this->parent->removeFiles($this->getManifest()->media); 722 $this->parent->removeFiles($this->getManifest()->languages); 723 $this->parent->removeFiles($this->getManifest()->administration->languages, 1); 724 } 725 726 /** 727 * Method to do any prechecks and setup the install paths for the extension 728 * 729 * @return void 730 * 731 * @since 3.4 732 * @throws \RuntimeException 733 */ 734 protected function setupInstallPaths() 735 { 736 // Set the installation target paths 737 $this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->element)); 738 $this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->element)); 739 $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->element)); 740 741 // Copy the admin path as it's used as a common base 742 $this->parent->setPath('extension_root', $this->parent->getPath('extension_administrator')); 743 744 // Make sure that we have an admin element 745 if (!$this->getManifest()->administration) { 746 throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_INSTALL_ADMIN_ELEMENT')); 747 } 748 } 749 750 /** 751 * Method to do any prechecks and setup the uninstall job 752 * 753 * @return void 754 * 755 * @since 4.0.0 756 */ 757 protected function setupUninstall() 758 { 759 // Get the admin and site paths for the component 760 $this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->extension->element)); 761 $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->extension->element)); 762 $this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->extension->element)); 763 764 // Copy the admin path as it's used as a common base 765 $this->parent->setPath('extension_root', $this->parent->getPath('extension_administrator')); 766 767 // Find and load the XML install file for the component 768 $this->parent->setPath('source', $this->parent->getPath('extension_administrator')); 769 770 // Get the package manifest object 771 // We do findManifest to avoid problem when uninstalling a list of extension: getManifest cache its manifest file 772 $this->parent->findManifest(); 773 $this->setManifest($this->parent->getManifest()); 774 775 if (!$this->getManifest()) { 776 // Make sure we delete the folders if no manifest exists 777 Folder::delete($this->parent->getPath('extension_administrator')); 778 Folder::delete($this->parent->getPath('extension_api')); 779 Folder::delete($this->parent->getPath('extension_site')); 780 781 // Remove the menu 782 $this->_removeAdminMenus($this->extension->extension_id); 783 784 // Raise a warning 785 throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_ERRORREMOVEMANUALLY')); 786 } 787 788 // Attempt to load the admin language file; might have uninstall strings 789 $this->loadLanguage(JPATH_ADMINISTRATOR . '/components/' . $this->extension->element); 790 } 791 792 /** 793 * Method to setup the update routine for the adapter 794 * 795 * @return void 796 * 797 * @since 3.4 798 */ 799 protected function setupUpdates() 800 { 801 // Hunt for the original XML file 802 $old_manifest = null; 803 804 // Use a temporary instance due to side effects; start in the administrator first 805 $tmpInstaller = new Installer(); 806 $tmpInstaller->setDatabase($this->getDatabase()); 807 $tmpInstaller->setPath('source', $this->parent->getPath('extension_administrator')); 808 809 if (!$tmpInstaller->findManifest()) { 810 // Then the site 811 $tmpInstaller->setPath('source', $this->parent->getPath('extension_site')); 812 813 if ($tmpInstaller->findManifest()) { 814 $old_manifest = $tmpInstaller->getManifest(); 815 } 816 } else { 817 $old_manifest = $tmpInstaller->getManifest(); 818 } 819 820 if ($old_manifest) { 821 $this->oldAdminFiles = $old_manifest->administration->files; 822 $this->oldApiFiles = $old_manifest->api->files; 823 $this->oldFiles = $old_manifest->files; 824 } 825 } 826 827 /** 828 * Method to store the extension to the database 829 * 830 * @param bool $deleteExisting Should I try to delete existing records of the same component? 831 * 832 * @return void 833 * 834 * @since 3.4 835 * @throws \RuntimeException 836 */ 837 protected function storeExtension($deleteExisting = false) 838 { 839 // The extension is stored during prepareDiscoverInstall for discover installs 840 if ($this->route === 'discover_install') { 841 return; 842 } 843 844 // Add or update an entry to the extension table 845 $this->extension->name = $this->name; 846 $this->extension->type = 'component'; 847 $this->extension->element = $this->element; 848 $this->extension->changelogurl = $this->changelogurl; 849 850 // If we are told to delete existing extension entries then do so. 851 if ($deleteExisting) { 852 $name = $this->extension->name; 853 $type = $this->extension->type; 854 $element = $this->extension->element; 855 856 // Try to delete existing failed records before retrying 857 $db = $this->getDatabase(); 858 859 $query = $db->getQuery(true) 860 ->select($db->quoteName('extension_id')) 861 ->from($db->quoteName('#__extensions')) 862 ->where( 863 [ 864 $db->quoteName('name') . ' = :name', 865 $db->quoteName('type') . ' = :type', 866 $db->quoteName('element') . ' = :element', 867 ] 868 ) 869 ->bind(':name', $name) 870 ->bind(':type', $type) 871 ->bind(':element', $element); 872 873 $db->setQuery($query); 874 875 $extension_ids = $db->loadColumn(); 876 877 if (!empty($extension_ids)) { 878 foreach ($extension_ids as $eid) { 879 // Remove leftover admin menus for this extension ID 880 $this->_removeAdminMenus($eid); 881 882 // Remove the extension record itself 883 /** @var Extension $extensionTable */ 884 $extensionTable = Table::getInstance('extension'); 885 $extensionTable->delete($eid); 886 } 887 } 888 } 889 890 // Namespace is optional 891 if (isset($this->manifest->namespace)) { 892 $this->extension->namespace = (string) $this->manifest->namespace; 893 } 894 895 // If there is not already a row, generate a heap of defaults 896 if (!$this->currentExtensionId) { 897 $this->extension->folder = ''; 898 $this->extension->enabled = 1; 899 $this->extension->protected = 0; 900 $this->extension->access = 0; 901 $this->extension->client_id = 1; 902 $this->extension->params = $this->parent->getParams(); 903 } 904 905 $this->extension->manifest_cache = $this->parent->generateManifestCache(); 906 907 $couldStore = $this->extension->store(); 908 909 if (!$couldStore && $deleteExisting) { 910 // Install failed, roll back changes 911 throw new \RuntimeException( 912 Text::sprintf( 913 'JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', 914 $this->extension->getError() 915 ) 916 ); 917 } 918 919 if (!$couldStore && !$deleteExisting) { 920 // Maybe we have a failed installation (e.g. timeout). Let's retry after deleting old records. 921 $this->storeExtension(true); 922 } 923 } 924 925 /** 926 * Method to build menu database entries for a component 927 * 928 * @param int|null $componentId The component ID for which I'm building menus 929 * 930 * @return boolean True if successful 931 * 932 * @since 3.1 933 */ 934 protected function _buildAdminMenus($componentId = null) 935 { 936 $db = $this->getDatabase(); 937 $option = $this->element; 938 939 // If a component exists with this option in the table within the protected menutype 'main' then we don't need to add menus 940 $query = $db->getQuery(true) 941 ->select( 942 [ 943 $db->quoteName('m.id'), 944 $db->quoteName('e.extension_id'), 945 ] 946 ) 947 ->from($db->quoteName('#__menu', 'm')) 948 ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) 949 ->where( 950 [ 951 $db->quoteName('m.parent_id') . ' = 1', 952 $db->quoteName('m.client_id') . ' = 1', 953 $db->quoteName('m.menutype') . ' = ' . $db->quote('main'), 954 $db->quoteName('e.element') . ' = :element', 955 ] 956 ) 957 ->bind(':element', $option); 958 959 $db->setQuery($query); 960 961 // In case of a failed installation (e.g. timeout error) we may have duplicate menu item and extension records. 962 $componentrows = $db->loadObjectList(); 963 964 // Check if menu items exist 965 if (!empty($componentrows)) { 966 // Don't do anything if overwrite has not been enabled 967 if (!$this->parent->isOverwrite()) { 968 return true; 969 } 970 971 // Remove all menu items 972 foreach ($componentrows as $componentrow) { 973 // Remove existing menu items if overwrite has been enabled 974 if ($option) { 975 // If something goes wrong, there's no way to rollback @todo: Search for better solution 976 $this->_removeAdminMenus($componentrow->extension_id); 977 } 978 } 979 } 980 981 // Only try to detect the component ID if it's not provided 982 if (empty($componentId)) { 983 // Lets find the extension id 984 $query->clear() 985 ->select($db->quoteName('e.extension_id')) 986 ->from($db->quoteName('#__extensions', 'e')) 987 ->where( 988 [ 989 $db->quoteName('e.type') . ' = ' . $db->quote('component'), 990 $db->quoteName('e.element') . ' = :element', 991 ] 992 ) 993 ->bind(':element', $option); 994 995 $db->setQuery($query); 996 $componentId = $db->loadResult(); 997 } 998 999 // Ok, now its time to handle the menus. Start with the component root menu, then handle submenus. 1000 $menuElement = $this->getManifest()->administration->menu; 1001 1002 // Just do not create the menu if $menuElement not exist 1003 if (!$menuElement) { 1004 return true; 1005 } 1006 1007 // If the menu item is hidden do nothing more, just return 1008 if (\in_array((string) $menuElement['hidden'], array('true', 'hidden'))) { 1009 return true; 1010 } 1011 1012 // Let's figure out what the menu item data should look like 1013 $data = array(); 1014 1015 // I have a menu element, use this information 1016 $data['menutype'] = 'main'; 1017 $data['client_id'] = 1; 1018 $data['title'] = (string) trim($menuElement); 1019 $data['alias'] = (string) $menuElement; 1020 $data['type'] = 'component'; 1021 $data['published'] = 1; 1022 $data['parent_id'] = 1; 1023 $data['component_id'] = $componentId; 1024 $data['img'] = ((string) $menuElement->attributes()->img) ?: 'class:component'; 1025 $data['home'] = 0; 1026 $data['path'] = ''; 1027 $data['params'] = ''; 1028 1029 if ($params = $menuElement->params) { 1030 // Pass $params through Registry to convert to JSON. 1031 $params = new Registry($params); 1032 $data['params'] = $params->toString(); 1033 } 1034 1035 // Set the menu link 1036 $request = []; 1037 1038 if ((string) $menuElement->attributes()->task) { 1039 $request[] = 'task=' . $menuElement->attributes()->task; 1040 } 1041 1042 if ((string) $menuElement->attributes()->view) { 1043 $request[] = 'view=' . $menuElement->attributes()->view; 1044 } 1045 1046 $qstring = \count($request) ? '&' . implode('&', $request) : ''; 1047 $data['link'] = 'index.php?option=' . $option . $qstring; 1048 1049 // Try to create the menu item in the database 1050 $parent_id = $this->_createAdminMenuItem($data, 1); 1051 1052 if ($parent_id === false) { 1053 return false; 1054 } 1055 1056 /* 1057 * Process SubMenus 1058 */ 1059 1060 if (!$this->getManifest()->administration->submenu) { 1061 // No submenu? We're done. 1062 return true; 1063 } 1064 1065 foreach ($this->getManifest()->administration->submenu->menu as $child) { 1066 $data = array(); 1067 $data['menutype'] = 'main'; 1068 $data['client_id'] = 1; 1069 $data['title'] = (string) trim($child); 1070 $data['alias'] = (string) $child; 1071 $data['type'] = 'component'; 1072 $data['published'] = 1; 1073 $data['parent_id'] = $parent_id; 1074 $data['component_id'] = $componentId; 1075 $data['img'] = ((string) $child->attributes()->img) ?: 'class:component'; 1076 $data['home'] = 0; 1077 $data['params'] = ''; 1078 1079 if ($params = $child->params) { 1080 // Pass $params through Registry to convert to JSON. 1081 $params = new Registry($params); 1082 $data['params'] = $params->toString(); 1083 } 1084 1085 // Set the sub menu link 1086 if ((string) $child->attributes()->link) { 1087 $data['link'] = 'index.php?' . $child->attributes()->link; 1088 } else { 1089 $request = array(); 1090 1091 if ((string) $child->attributes()->act) { 1092 $request[] = 'act=' . $child->attributes()->act; 1093 } 1094 1095 if ((string) $child->attributes()->task) { 1096 $request[] = 'task=' . $child->attributes()->task; 1097 } 1098 1099 if ((string) $child->attributes()->controller) { 1100 $request[] = 'controller=' . $child->attributes()->controller; 1101 } 1102 1103 if ((string) $child->attributes()->view) { 1104 $request[] = 'view=' . $child->attributes()->view; 1105 } 1106 1107 if ((string) $child->attributes()->layout) { 1108 $request[] = 'layout=' . $child->attributes()->layout; 1109 } 1110 1111 if ((string) $child->attributes()->sub) { 1112 $request[] = 'sub=' . $child->attributes()->sub; 1113 } 1114 1115 $qstring = \count($request) ? '&' . implode('&', $request) : ''; 1116 $data['link'] = 'index.php?option=' . $option . $qstring; 1117 } 1118 1119 $submenuId = $this->_createAdminMenuItem($data, $parent_id); 1120 1121 if ($submenuId === false) { 1122 return false; 1123 } 1124 1125 /* 1126 * Since we have created a menu item, we add it to the installation step stack 1127 * so that if we have to rollback the changes we can undo it. 1128 */ 1129 $this->parent->pushStep(array('type' => 'menu', 'id' => $componentId)); 1130 } 1131 1132 return true; 1133 } 1134 1135 /** 1136 * Method to remove admin menu references to a component 1137 * 1138 * @param int $id The ID of the extension whose admin menus will be removed 1139 * 1140 * @return boolean True if successful. 1141 * 1142 * @throws \Exception 1143 * 1144 * @since 3.1 1145 */ 1146 protected function _removeAdminMenus($id) 1147 { 1148 $db = $this->getDatabase(); 1149 1150 /** @var \Joomla\CMS\Table\Menu $table */ 1151 $table = Table::getInstance('menu'); 1152 1153 // Get the ids of the menu items 1154 $query = $db->getQuery(true) 1155 ->select($db->quoteName('id')) 1156 ->from($db->quoteName('#__menu')) 1157 ->where( 1158 [ 1159 $db->quoteName('client_id') . ' = 1', 1160 $db->quoteName('menutype') . ' = ' . $db->quote('main'), 1161 $db->quoteName('component_id') . ' = :id', 1162 ] 1163 ) 1164 ->bind(':id', $id, ParameterType::INTEGER); 1165 1166 $db->setQuery($query); 1167 1168 $ids = $db->loadColumn(); 1169 $result = true; 1170 1171 // Check for error 1172 if (!empty($ids)) { 1173 // Iterate the items to delete each one. 1174 foreach ($ids as $menuid) { 1175 if (!$table->delete((int) $menuid, false)) { 1176 Factory::getApplication()->enqueueMessage($table->getError(), 'error'); 1177 1178 $result = false; 1179 } 1180 } 1181 1182 // Rebuild the whole tree 1183 $table->rebuild(); 1184 } 1185 1186 return $result; 1187 } 1188 1189 /** 1190 * Method to update menu database entries for a component in case the component has been uninstalled before. 1191 * NOTE: This will not update admin menus. Use _updateMenus() instead to update admin menus ase well. 1192 * 1193 * @param int|null $componentId The component ID. 1194 * 1195 * @return boolean True if successful 1196 * 1197 * @since 3.4.2 1198 */ 1199 protected function _updateSiteMenus($componentId = null) 1200 { 1201 return $this->_updateMenus($componentId, 0); 1202 } 1203 1204 /** 1205 * Method to update menu database entries for a component in case if the component has been uninstalled before. 1206 * 1207 * @param int|null $componentId The component ID. 1208 * @param int $clientId The client id 1209 * 1210 * @return boolean True if successful 1211 * 1212 * @since 3.7.0 1213 */ 1214 protected function _updateMenus($componentId, $clientId = null) 1215 { 1216 $db = $this->getDatabase(); 1217 $option = $this->element; 1218 $link = 'index.php?option=' . $option; 1219 $linkMatch = 'index.php?option=' . $option . '&%'; 1220 1221 // Update all menu items which contain 'index.php?option=com_extension' or 'index.php?option=com_extension&...' 1222 // to use the new component id. 1223 $query = $db->getQuery(true) 1224 ->update($db->quoteName('#__menu')) 1225 ->set($db->quoteName('component_id') . ' = :componentId') 1226 ->where($db->quoteName('type') . ' = ' . $db->quote('component')) 1227 ->extendWhere( 1228 'AND', 1229 [ 1230 $db->quoteName('link') . ' LIKE :link', 1231 $db->quoteName('link') . ' LIKE :linkMatch', 1232 ], 1233 'OR' 1234 ) 1235 ->bind(':componentId', $componentId, ParameterType::INTEGER) 1236 ->bind(':link', $link) 1237 ->bind(':linkMatch', $linkMatch); 1238 1239 if (isset($clientId)) { 1240 $query->where($db->quoteName('client_id') . ' = :clientId') 1241 ->bind(':clientId', $clientId, ParameterType::INTEGER); 1242 } 1243 1244 try { 1245 $db->setQuery($query); 1246 $db->execute(); 1247 } catch (\RuntimeException $e) { 1248 return false; 1249 } 1250 1251 return true; 1252 } 1253 1254 /** 1255 * Custom rollback method 1256 * - Roll back the component menu item 1257 * 1258 * @param array $step Installation step to rollback. 1259 * 1260 * @return boolean True on success 1261 * 1262 * @throws \Exception 1263 * 1264 * @since 3.1 1265 */ 1266 protected function _rollback_menu($step) 1267 { 1268 return $this->_removeAdminMenus($step['id']); 1269 } 1270 1271 /** 1272 * Discover unregistered extensions. 1273 * 1274 * @return array A list of extensions. 1275 * 1276 * @since 3.1 1277 */ 1278 public function discover() 1279 { 1280 $results = array(); 1281 $site_components = Folder::folders(JPATH_SITE . '/components'); 1282 $admin_components = Folder::folders(JPATH_ADMINISTRATOR . '/components'); 1283 $api_components = Folder::folders(JPATH_API . '/components'); 1284 1285 foreach ($site_components as $component) { 1286 if (file_exists(JPATH_SITE . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { 1287 $manifest_details = Installer::parseXMLInstallFile( 1288 JPATH_SITE . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' 1289 ); 1290 $extension = Table::getInstance('extension'); 1291 $extension->set('type', 'component'); 1292 $extension->set('client_id', 0); 1293 $extension->set('element', $component); 1294 $extension->set('folder', ''); 1295 $extension->set('name', $component); 1296 $extension->set('state', -1); 1297 $extension->set('manifest_cache', json_encode($manifest_details)); 1298 $extension->set('params', '{}'); 1299 1300 $results[] = $extension; 1301 } 1302 } 1303 1304 foreach ($admin_components as $component) { 1305 if (file_exists(JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { 1306 $manifest_details = Installer::parseXMLInstallFile( 1307 JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' 1308 ); 1309 $extension = Table::getInstance('extension'); 1310 $extension->set('type', 'component'); 1311 $extension->set('client_id', 1); 1312 $extension->set('element', $component); 1313 $extension->set('folder', ''); 1314 $extension->set('name', $component); 1315 $extension->set('state', -1); 1316 $extension->set('manifest_cache', json_encode($manifest_details)); 1317 $extension->set('params', '{}'); 1318 $results[] = $extension; 1319 } 1320 } 1321 1322 foreach ($api_components as $component) { 1323 if (file_exists(JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { 1324 $manifest_details = Installer::parseXMLInstallFile( 1325 JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' 1326 ); 1327 $extension = Table::getInstance('extension'); 1328 $extension->set('type', 'component'); 1329 $extension->set('client_id', 3); 1330 $extension->set('element', $component); 1331 $extension->set('folder', ''); 1332 $extension->set('name', $component); 1333 $extension->set('state', -1); 1334 $extension->set('manifest_cache', json_encode($manifest_details)); 1335 $extension->set('params', '{}'); 1336 $results[] = $extension; 1337 } 1338 } 1339 1340 return $results; 1341 } 1342 1343 /** 1344 * Refreshes the extension table cache 1345 * 1346 * @return boolean Result of operation, true if updated, false on failure 1347 * 1348 * @since 3.1 1349 */ 1350 public function refreshManifestCache() 1351 { 1352 // Need to find to find where the XML file is since we don't store this normally 1353 $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); 1354 $short_element = str_replace('com_', '', $this->parent->extension->element); 1355 $manifestPath = $client->path . '/components/' . $this->parent->extension->element . '/' . $short_element . '.xml'; 1356 $this->parent->manifest = $this->parent->isManifest($manifestPath); 1357 $this->parent->setPath('manifest', $manifestPath); 1358 1359 $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); 1360 $this->parent->extension->manifest_cache = json_encode($manifest_details); 1361 $this->parent->extension->name = $manifest_details['name']; 1362 1363 // Namespace is optional 1364 if (isset($manifest_details['namespace'])) { 1365 $this->parent->extension->namespace = $manifest_details['namespace']; 1366 } 1367 1368 try { 1369 return $this->parent->extension->store(); 1370 } catch (\RuntimeException $e) { 1371 Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); 1372 1373 return false; 1374 } 1375 } 1376 1377 /** 1378 * Creates the menu item in the database. If the item already exists it tries to remove it and create it afresh. 1379 * 1380 * @param array &$data The menu item data to create 1381 * @param integer $parentId The parent menu item ID 1382 * 1383 * @return boolean|integer Menu item ID on success, false on failure 1384 * 1385 * @throws \Exception 1386 * 1387 * @since 3.1 1388 */ 1389 protected function _createAdminMenuItem(array &$data, $parentId) 1390 { 1391 $db = $this->getDatabase(); 1392 1393 /** @var \Joomla\CMS\Table\Menu $table */ 1394 $table = Table::getInstance('menu'); 1395 1396 try { 1397 $table->setLocation($parentId, 'last-child'); 1398 } catch (\InvalidArgumentException $e) { 1399 Log::add($e->getMessage(), Log::WARNING, 'jerror'); 1400 1401 return false; 1402 } 1403 1404 if (!$table->bind($data) || !$table->check() || !$table->store()) { 1405 $menutype = $data['menutype']; 1406 $link = $data['link']; 1407 $type = $data['type']; 1408 $menuParentId = $data['parent_id']; 1409 $home = $data['home']; 1410 1411 // The menu item already exists. Delete it and retry instead of throwing an error. 1412 $query = $db->getQuery(true) 1413 ->select($db->quoteName('id')) 1414 ->from($db->quoteName('#__menu')) 1415 ->where( 1416 [ 1417 $db->quoteName('menutype') . ' = :menutype', 1418 $db->quoteName('client_id') . ' = 1', 1419 $db->quoteName('link') . ' = :link', 1420 $db->quoteName('type') . ' = :type', 1421 $db->quoteName('parent_id') . ' = :parent_id', 1422 $db->quoteName('home') . ' = :home', 1423 ] 1424 ) 1425 ->bind(':menutype', $menutype) 1426 ->bind(':link', $link) 1427 ->bind(':type', $type) 1428 ->bind(':parent_id', $menuParentId, ParameterType::INTEGER) 1429 ->bind(':home', $home, ParameterType::BOOLEAN); 1430 1431 $db->setQuery($query); 1432 $menu_id = $db->loadResult(); 1433 1434 if (!$menu_id) { 1435 // Oops! Could not get the menu ID. Go back and rollback changes. 1436 Factory::getApplication()->enqueueMessage($table->getError(), 'error'); 1437 1438 return false; 1439 } else { 1440 /** @var \Joomla\CMS\Table\Menu $temporaryTable */ 1441 $temporaryTable = Table::getInstance('menu'); 1442 $temporaryTable->delete($menu_id, true); 1443 $temporaryTable->load($parentId); 1444 $temporaryTable->rebuild($parentId, $temporaryTable->lft, $temporaryTable->level, $temporaryTable->path); 1445 1446 // Retry creating the menu item 1447 $table->setLocation($parentId, 'last-child'); 1448 1449 if (!$table->bind($data) || !$table->check() || !$table->store()) { 1450 // Install failed, warn user and rollback changes 1451 Factory::getApplication()->enqueueMessage($table->getError(), 'error'); 1452 1453 return false; 1454 } 1455 } 1456 } 1457 1458 return $table->id; 1459 } 1460 }
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 |