[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_installer/src/Model/ -> DatabaseModel.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer