[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package Joomla.Administrator 5 * @subpackage com_installer 6 * 7 * @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 */ 10 11 namespace Joomla\Component\Installer\Administrator\Model; 12 13 \defined('_JEXEC') or die; 14 15 use Joomla\CMS\Component\ComponentHelper; 16 use Joomla\CMS\Language\Text; 17 use Joomla\CMS\MVC\Factory\MVCFactoryInterface; 18 use Joomla\CMS\Schema\ChangeSet; 19 use Joomla\CMS\Table\Extension; 20 use Joomla\CMS\Version; 21 use Joomla\Component\Installer\Administrator\Helper\InstallerHelper; 22 use Joomla\Database\DatabaseQuery; 23 use Joomla\Database\Exception\ExecutionFailureException; 24 use Joomla\Database\ParameterType; 25 use Joomla\Registry\Registry; 26 27 \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); 28 29 /** 30 * Installer Database Model 31 * 32 * @since 1.6 33 */ 34 class DatabaseModel extends InstallerModel 35 { 36 /** 37 * Set the model context 38 * 39 * @var string 40 * 41 * @since 4.0.0 42 */ 43 protected $_context = 'com_installer.discover'; 44 45 /** 46 * ChangeSet of all extensions 47 * 48 * @var array 49 * 50 * @since 4.0.0 51 */ 52 private $changeSetList = array(); 53 54 /** 55 * Total of errors 56 * 57 * @var integer 58 * 59 * @since 4.0.0 60 */ 61 private $errorCount = 0; 62 63 /** 64 * Constructor. 65 * 66 * @param array $config An optional associative array of configuration settings. 67 * @param MVCFactoryInterface $factory The factory. 68 * 69 * @see ListModel 70 * @since 4.0.0 71 */ 72 public function __construct($config = array(), MVCFactoryInterface $factory = null) 73 { 74 if (empty($config['filter_fields'])) { 75 $config['filter_fields'] = array( 76 'update_site_name', 77 'name', 78 'client_id', 79 'client', 'client_translated', 80 'status', 81 'type', 'type_translated', 82 'folder', 'folder_translated', 83 'extension_id' 84 ); 85 } 86 87 parent::__construct($config, $factory); 88 } 89 90 /** 91 * Method to return the total number of errors in all the extensions, saved in cache. 92 * 93 * @return integer 94 * 95 * @throws \Exception 96 * 97 * @since 4.0.0 98 */ 99 public function getErrorCount() 100 { 101 return $this->errorCount; 102 } 103 104 /** 105 * Method to populate the schema cache. 106 * 107 * @param integer $cid The extension ID to get the schema for 108 * 109 * @return void 110 * 111 * @throws \Exception 112 * 113 * @since 4.0.0 114 */ 115 private function fetchSchemaCache($cid = 0) 116 { 117 // We already have it 118 if (array_key_exists($cid, $this->changeSetList)) { 119 return; 120 } 121 122 // Add the ID to the state so it can be used for filtering 123 if ($cid) { 124 $this->setState('filter.extension_id', $cid); 125 } 126 127 // With the parent::save it can get the limit and we need to make sure it gets all extensions 128 $results = $this->_getList($this->getListQuery()); 129 130 foreach ($results as $result) { 131 $errorMessages = array(); 132 $errorCount = 0; 133 134 if (strcmp($result->element, 'joomla') === 0) { 135 $result->element = 'com_admin'; 136 137 if (!$this->getDefaultTextFilters()) { 138 $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); 139 $errorCount++; 140 } 141 } 142 143 $db = $this->getDatabase(); 144 145 if ($result->type === 'component') { 146 $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; 147 } elseif ($result->type === 'plugin') { 148 $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; 149 } elseif ($result->type === 'module') { 150 // Typehint to integer to normalise some DBs returning strings and others integers 151 if ((int) $result->client_id === 1) { 152 $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; 153 } elseif ((int) $result->client_id === 0) { 154 $basePath = JPATH_SITE . '/modules/' . $result->element; 155 } else { 156 // Module with unknown client id!? - bail 157 continue; 158 } 159 } elseif ($result->type === 'file' && $result->element === 'com_admin') { 160 // Specific bodge for the Joomla CMS special database check which points to com_admin 161 $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; 162 } else { 163 // Unknown extension type (library, files etc which don't have known SQL paths right now) 164 continue; 165 } 166 167 // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML 168 // file. This just gives us a small performance win of not parsing the XML every time. 169 $folderTmp = $basePath . '/sql/updates/'; 170 171 if (!file_exists($folderTmp)) { 172 $installationXML = InstallerHelper::getInstallationXML( 173 $result->element, 174 $result->type, 175 $result->client_id, 176 $result->type === 'plugin' ? $result->folder : null 177 ); 178 179 if ($installationXML !== null) { 180 $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; 181 $a = explode('/', $folderTmp); 182 array_pop($a); 183 $folderTmp = $basePath . '/' . implode('/', $a); 184 } 185 } 186 187 // Can't find the folder still - give up now and move on. 188 if (!file_exists($folderTmp)) { 189 continue; 190 } 191 192 $changeSet = new ChangeSet($db, $folderTmp); 193 194 // If the version in the #__schemas is different 195 // than the update files, add to problems message 196 $schema = $changeSet->getSchema(); 197 198 // If the schema is empty we couldn't find any update files. Just ignore the extension. 199 if (empty($schema)) { 200 continue; 201 } 202 203 if ($result->version_id !== $schema) { 204 $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); 205 $errorCount++; 206 } 207 208 // If the version in the manifest_cache is different than the 209 // version in the installation xml, add to problems message 210 $compareUpdateMessage = $this->compareUpdateVersion($result); 211 212 if ($compareUpdateMessage) { 213 $errorMessages[] = $compareUpdateMessage; 214 $errorCount++; 215 } 216 217 // If there are errors in the database, add to the problems message 218 $errors = $changeSet->check(); 219 220 $errorsMessage = $this->getErrorsMessage($errors); 221 222 if ($errorsMessage) { 223 $errorMessages = array_merge($errorMessages, $errorsMessage); 224 $errorCount++; 225 } 226 227 // Number of database tables Checked and Skipped 228 $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); 229 230 // Set the total number of errors 231 $this->errorCount += $errorCount; 232 233 // Collect the extension details 234 $this->changeSetList[$result->extension_id] = array( 235 'folderTmp' => $folderTmp, 236 'errorsMessage' => $errorMessages, 237 'errorsCount' => $errorCount, 238 'results' => $changeSet->getStatus(), 239 'schema' => $schema, 240 'extension' => $result 241 ); 242 } 243 } 244 245 /** 246 * Method to auto-populate the model state. 247 * 248 * Note. Calling getState in this method will result in recursion. 249 * 250 * @param string $ordering An optional ordering field. 251 * @param string $direction An optional direction (asc|desc). 252 * 253 * @return void 254 * 255 * @since 1.6 256 */ 257 protected function populateState($ordering = 'name', $direction = 'asc') 258 { 259 $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); 260 $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); 261 $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); 262 $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); 263 264 parent::populateState($ordering, $direction); 265 } 266 267 /** 268 * Fixes database problems. 269 * 270 * @param array $cids List of the selected extensions to fix 271 * 272 * @return void|boolean 273 * 274 * @throws \Exception 275 * 276 * @since 4.0.0 277 */ 278 public function fix($cids = array()) 279 { 280 $db = $this->getDatabase(); 281 282 foreach ($cids as $i => $cid) { 283 // Load the database issues 284 $this->fetchSchemaCache($cid); 285 286 $changeSet = $this->changeSetList[$cid]; 287 $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); 288 $changeSet['changeset']->fix(); 289 290 $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); 291 $this->fixUpdateVersion($changeSet['extension']->extension_id); 292 293 if ($changeSet['extension']->element === 'com_admin') { 294 $installer = new \JoomlaInstallerScript(); 295 $installer->deleteUnexistingFiles(); 296 $this->fixDefaultTextFilters(); 297 298 /* 299 * Finally, if the schema updates succeeded, make sure the database table is 300 * converted to utf8mb4 or, if not supported by the server, compatible to it. 301 */ 302 $statusArray = $changeSet['changeset']->getStatus(); 303 304 if (count($statusArray['error']) == 0) { 305 $installer->convertTablesToUtf8mb4(false); 306 } 307 } 308 } 309 } 310 311 /** 312 * Gets the changeset array. 313 * 314 * @return array Array with the information of the versions problems, errors and the extensions itself 315 * 316 * @throws \Exception 317 * 318 * @since 4.0.0 319 */ 320 public function getItems() 321 { 322 $this->fetchSchemaCache(); 323 324 $results = parent::getItems(); 325 $results = $this->mergeSchemaCache($results); 326 327 return $results; 328 } 329 330 /** 331 * Method to get the database query 332 * 333 * @return DatabaseQuery The database query 334 * 335 * @since 4.0.0 336 */ 337 protected function getListQuery() 338 { 339 $db = $this->getDatabase(); 340 $query = $db->getQuery(true) 341 ->select( 342 $db->quoteName( 343 [ 344 'extensions.client_id', 345 'extensions.element', 346 'extensions.extension_id', 347 'extensions.folder', 348 'extensions.manifest_cache', 349 'extensions.name', 350 'extensions.type', 351 'schemas.version_id' 352 ] 353 ) 354 ) 355 ->from( 356 $db->quoteName( 357 '#__schemas', 358 'schemas' 359 ) 360 ) 361 ->join( 362 'INNER', 363 $db->quoteName('#__extensions', 'extensions'), 364 $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') 365 ); 366 367 $type = $this->getState('filter.type'); 368 $clientId = $this->getState('filter.client_id'); 369 $extensionId = $this->getState('filter.extension_id'); 370 $folder = $this->getState('filter.folder'); 371 372 if ($type) { 373 $query->where($db->quoteName('extensions.type') . ' = :type') 374 ->bind(':type', $type); 375 } 376 377 if ($clientId != '') { 378 $clientId = (int) $clientId; 379 $query->where($db->quoteName('extensions.client_id') . ' = :clientid') 380 ->bind(':clientid', $clientId, ParameterType::INTEGER); 381 } 382 383 if ($extensionId != '') { 384 $extensionId = (int) $extensionId; 385 $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') 386 ->bind(':extensionid', $extensionId, ParameterType::INTEGER); 387 } 388 389 if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { 390 $folder = $folder === '*' ? '' : $folder; 391 $query->where($db->quoteName('extensions.folder') . ' = :folder') 392 ->bind(':folder', $folder); 393 } 394 395 // Process search filter (update site id). 396 $search = $this->getState('filter.search'); 397 398 if (!empty($search) && stripos($search, 'id:') === 0) { 399 $ids = (int) substr($search, 3); 400 $query->where($db->quoteName('schemas.extension_id') . ' = :eid') 401 ->bind(':eid', $ids, ParameterType::INTEGER); 402 } 403 404 return $query; 405 } 406 407 /** 408 * Merge the items that will be visible with the changeSet information in cache 409 * 410 * @param array $results extensions returned from parent::getItems(). 411 * 412 * @return array the changeSetList of the merged items 413 * 414 * @since 4.0.0 415 */ 416 protected function mergeSchemaCache($results) 417 { 418 $changeSetList = $this->changeSetList; 419 $finalResults = array(); 420 421 foreach ($results as $result) { 422 if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) { 423 $finalResults[] = $changeSetList[$result->extension_id]; 424 } 425 } 426 427 return $finalResults; 428 } 429 430 /** 431 * Get version from #__schemas table. 432 * 433 * @param integer $extensionId id of the extensions. 434 * 435 * @return mixed the return value from the query, or null if the query fails. 436 * 437 * @throws \Exception 438 * 439 * @since 4.0.0 440 */ 441 public function getSchemaVersion($extensionId) 442 { 443 $db = $this->getDatabase(); 444 $extensionId = (int) $extensionId; 445 $query = $db->getQuery(true) 446 ->select($db->quoteName('version_id')) 447 ->from($db->quoteName('#__schemas')) 448 ->where($db->quoteName('extension_id') . ' = :extensionid') 449 ->bind(':extensionid', $extensionId, ParameterType::INTEGER); 450 $db->setQuery($query); 451 452 return $db->loadResult(); 453 } 454 455 /** 456 * Fix schema version if wrong. 457 * 458 * @param ChangeSet $changeSet Schema change set. 459 * @param integer $extensionId ID of the extensions. 460 * 461 * @return mixed string schema version if success, false if fail. 462 * 463 * @throws \Exception 464 * 465 * @since 4.0.0 466 */ 467 public function fixSchemaVersion($changeSet, $extensionId) 468 { 469 // Get correct schema version -- last file in array. 470 $schema = $changeSet->getSchema(); 471 472 // Check value. If ok, don't do update. 473 if ($schema == $this->getSchemaVersion($extensionId)) { 474 return $schema; 475 } 476 477 // Delete old row. 478 $extensionId = (int) $extensionId; 479 $db = $this->getDatabase(); 480 $query = $db->getQuery(true) 481 ->delete($db->quoteName('#__schemas')) 482 ->where($db->quoteName('extension_id') . ' = :extensionid') 483 ->bind(':extensionid', $extensionId, ParameterType::INTEGER); 484 $db->setQuery($query)->execute(); 485 486 // Add new row. 487 $query->clear() 488 ->insert($db->quoteName('#__schemas')) 489 ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) 490 ->values(':extensionid, :schema') 491 ->bind(':extensionid', $extensionId, ParameterType::INTEGER) 492 ->bind(':schema', $schema); 493 $db->setQuery($query); 494 495 try { 496 $db->execute(); 497 } catch (ExecutionFailureException $e) { 498 return false; 499 } 500 501 return $schema; 502 } 503 504 /** 505 * Get current version from #__extensions table. 506 * 507 * @param object $extension data from #__extensions of a single extension. 508 * 509 * @return mixed string message with the errors with the update version or null if none 510 * 511 * @since 4.0.0 512 */ 513 public function compareUpdateVersion($extension) 514 { 515 $updateVersion = json_decode($extension->manifest_cache)->version; 516 517 if ($extension->element === 'com_admin') { 518 $extensionVersion = JVERSION; 519 } else { 520 $installationXML = InstallerHelper::getInstallationXML( 521 $extension->element, 522 $extension->type, 523 $extension->client_id, 524 $extension->type === 'plugin' ? $extension->folder : null 525 ); 526 527 $extensionVersion = (string) $installationXML->version; 528 } 529 530 if (version_compare($extensionVersion, $updateVersion) != 0) { 531 return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); 532 } 533 534 return null; 535 } 536 537 /** 538 * Get a message of the tables skipped and checked 539 * 540 * @param array $status status of of the update files 541 * 542 * @return array Messages with the errors with the update version 543 * 544 * @since 4.0.0 545 */ 546 private function getOtherInformationMessage($status) 547 { 548 $problemsMessage = array(); 549 $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); 550 $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); 551 552 return $problemsMessage; 553 } 554 555 /** 556 * Get a message with all errors found in a given extension 557 * 558 * @param array $errors data from #__extensions of a single extension. 559 * 560 * @return array List of messages with the errors in the database 561 * 562 * @since 4.0.0 563 */ 564 private function getErrorsMessage($errors) 565 { 566 $errorMessages = array(); 567 568 foreach ($errors as $line => $error) { 569 $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; 570 $messages = $error->msgElements; 571 $file = basename($error->file); 572 $message0 = isset($messages[0]) ? $messages[0] : ' '; 573 $message1 = isset($messages[1]) ? $messages[1] : ' '; 574 $message2 = isset($messages[2]) ? $messages[2] : ' '; 575 $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); 576 } 577 578 return $errorMessages; 579 } 580 581 /** 582 * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). 583 * 584 * @param integer $extensionId id of the extension 585 * 586 * @return mixed string update version if success, false if fail. 587 * 588 * @since 4.0.0 589 */ 590 public function fixUpdateVersion($extensionId) 591 { 592 $table = new Extension($this->getDatabase()); 593 $table->load($extensionId); 594 $cache = new Registry($table->manifest_cache); 595 $updateVersion = $cache->get('version'); 596 597 if ($table->get('type') === 'file' && $table->get('element') === 'joomla') { 598 $extensionVersion = new Version(); 599 $extensionVersion = $extensionVersion->getShortVersion(); 600 } else { 601 $installationXML = InstallerHelper::getInstallationXML( 602 $table->get('element'), 603 $table->get('type'), 604 $table->get('client_id'), 605 $table->get('type') === 'plugin' ? $table->get('folder') : null 606 ); 607 $extensionVersion = (string) $installationXML->version; 608 } 609 610 if ($updateVersion === $extensionVersion) { 611 return $updateVersion; 612 } 613 614 $cache->set('version', $extensionVersion); 615 $table->set('manifest_cache', $cache->toString()); 616 617 if ($table->store()) { 618 return $extensionVersion; 619 } 620 621 return false; 622 } 623 624 /** 625 * For version 2.5.x only 626 * Check if com_config parameters are blank. 627 * 628 * @return string default text filters (if any). 629 * 630 * @since 4.0.0 631 */ 632 public function getDefaultTextFilters() 633 { 634 $table = new Extension($this->getDatabase()); 635 $table->load($table->find(array('name' => 'com_config'))); 636 637 return $table->params; 638 } 639 640 /** 641 * For version 2.5.x only 642 * Check if com_config parameters are blank. If so, populate with com_content text filters. 643 * 644 * @return void 645 * 646 * @since 4.0.0 647 */ 648 private function fixDefaultTextFilters() 649 { 650 $table = new Extension($this->getDatabase()); 651 $table->load($table->find(array('name' => 'com_config'))); 652 653 // Check for empty $config and non-empty content filters. 654 if (!$table->params) { 655 // Get filters from com_content and store if you find them. 656 $contentParams = ComponentHelper::getComponent('com_content')->getParams(); 657 658 if ($contentParams->get('filters')) { 659 $newParams = new Registry(); 660 $newParams->set('filters', $contentParams->get('filters')); 661 $table->params = (string) $newParams; 662 $table->store(); 663 } 664 } 665 } 666 }
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 |