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\Schema; 10 11defined('JPATH_PLATFORM') or die; 12 13jimport('joomla.filesystem.folder'); 14 15/** 16 * Contains a set of JSchemaChange objects for a particular instance of Joomla. 17 * Each of these objects contains a DDL query that should have been run against 18 * the database when this database was created or updated. This enables the 19 * Installation Manager to check that the current database schema is up to date. 20 * 21 * @since 2.5 22 */ 23class ChangeSet 24{ 25 /** 26 * Array of ChangeItem objects 27 * 28 * @var ChangeItem[] 29 * @since 2.5 30 */ 31 protected $changeItems = array(); 32 33 /** 34 * \JDatabaseDriver object 35 * 36 * @var \JDatabaseDriver 37 * @since 2.5 38 */ 39 protected $db = null; 40 41 /** 42 * Folder where SQL update files will be found 43 * 44 * @var string 45 * @since 2.5 46 */ 47 protected $folder = null; 48 49 /** 50 * The singleton instance of this object 51 * 52 * @var ChangeSet 53 * @since 3.5.1 54 */ 55 protected static $instance; 56 57 /** 58 * Constructor: builds array of $changeItems by processing the .sql files in a folder. 59 * The folder for the Joomla core updates is `administrator/components/com_admin/sql/updates/<database>`. 60 * 61 * @param \JDatabaseDriver $db The current database object 62 * @param string $folder The full path to the folder containing the update queries 63 * 64 * @since 2.5 65 */ 66 public function __construct($db, $folder = null) 67 { 68 $this->db = $db; 69 $this->folder = $folder; 70 $updateFiles = $this->getUpdateFiles(); 71 $updateQueries = $this->getUpdateQueries($updateFiles); 72 73 foreach ($updateQueries as $obj) 74 { 75 $changeItem = ChangeItem::getInstance($db, $obj->file, $obj->updateQuery); 76 77 if ($changeItem->queryType === 'UTF8CNV') 78 { 79 // Execute the special update query for utf8mb4 conversion status reset 80 try 81 { 82 $this->db->setQuery($changeItem->updateQuery)->execute(); 83 } 84 catch (\RuntimeException $e) 85 { 86 \JFactory::getApplication()->enqueueMessage($e->getMessage(), 'error'); 87 } 88 } 89 else 90 { 91 // Normal change item 92 $this->changeItems[] = $changeItem; 93 } 94 } 95 96 // If on mysql, add a query at the end to check for utf8mb4 conversion status 97 if ($this->db->getServerType() === 'mysql') 98 { 99 // Let the update query do nothing when being executed 100 $tmpSchemaChangeItem = ChangeItem::getInstance( 101 $db, 102 'database.php', 103 'UPDATE ' . $this->db->quoteName('#__utf8_conversion') 104 . ' SET ' . $this->db->quoteName('converted') . ' = ' 105 . $this->db->quoteName('converted') . ';'); 106 107 // Set to not skipped 108 $tmpSchemaChangeItem->checkStatus = 0; 109 110 // Set the check query 111 if ($this->db->hasUTF8mb4Support()) 112 { 113 $converted = 5; 114 $tmpSchemaChangeItem->queryType = 'UTF8_CONVERSION_UTF8MB4'; 115 } 116 else 117 { 118 $converted = 3; 119 $tmpSchemaChangeItem->queryType = 'UTF8_CONVERSION_UTF8'; 120 } 121 122 $tmpSchemaChangeItem->checkQuery = 'SELECT ' 123 . $this->db->quoteName('converted') 124 . ' FROM ' . $this->db->quoteName('#__utf8_conversion') 125 . ' WHERE ' . $this->db->quoteName('converted') . ' = ' . $converted; 126 127 // Set expected records from check query 128 $tmpSchemaChangeItem->checkQueryExpected = 1; 129 130 $tmpSchemaChangeItem->msgElements = array(); 131 132 $this->changeItems[] = $tmpSchemaChangeItem; 133 } 134 } 135 136 /** 137 * Returns a reference to the ChangeSet object, only creating it if it doesn't already exist. 138 * 139 * @param \JDatabaseDriver $db The current database object 140 * @param string $folder The full path to the folder containing the update queries 141 * 142 * @return ChangeSet 143 * 144 * @since 2.5 145 */ 146 public static function getInstance($db, $folder = null) 147 { 148 if (!is_object(static::$instance)) 149 { 150 static::$instance = new ChangeSet($db, $folder); 151 } 152 153 return static::$instance; 154 } 155 156 /** 157 * Checks the database and returns an array of any errors found. 158 * Note these are not database errors but rather situations where 159 * the current schema is not up to date. 160 * 161 * @return array Array of errors if any. 162 * 163 * @since 2.5 164 */ 165 public function check() 166 { 167 $errors = array(); 168 169 foreach ($this->changeItems as $item) 170 { 171 if ($item->check() === -2) 172 { 173 // Error found 174 $errors[] = $item; 175 } 176 } 177 178 return $errors; 179 } 180 181 /** 182 * Runs the update query to apply the change to the database 183 * 184 * @return void 185 * 186 * @since 2.5 187 */ 188 public function fix() 189 { 190 $this->check(); 191 192 foreach ($this->changeItems as $item) 193 { 194 $item->fix(); 195 } 196 } 197 198 /** 199 * Returns an array of results for this set 200 * 201 * @return array associative array of changeitems grouped by unchecked, ok, error, and skipped 202 * 203 * @since 2.5 204 */ 205 public function getStatus() 206 { 207 $result = array('unchecked' => array(), 'ok' => array(), 'error' => array(), 'skipped' => array()); 208 209 foreach ($this->changeItems as $item) 210 { 211 switch ($item->checkStatus) 212 { 213 case 0: 214 $result['unchecked'][] = $item; 215 break; 216 case 1: 217 $result['ok'][] = $item; 218 break; 219 case -2: 220 $result['error'][] = $item; 221 break; 222 case -1: 223 $result['skipped'][] = $item; 224 break; 225 } 226 } 227 228 return $result; 229 } 230 231 /** 232 * Gets the current database schema, based on the highest version number. 233 * Note that the .sql files are named based on the version and date, so 234 * the file name of the last file should match the database schema version 235 * in the #__schemas table. 236 * 237 * @return string the schema version for the database 238 * 239 * @since 2.5 240 */ 241 public function getSchema() 242 { 243 $updateFiles = $this->getUpdateFiles(); 244 $result = new \SplFileInfo(array_pop($updateFiles)); 245 246 return $result->getBasename('.sql'); 247 } 248 249 /** 250 * Get list of SQL update files for this database 251 * 252 * @return array list of sql update full-path names 253 * 254 * @since 2.5 255 */ 256 private function getUpdateFiles() 257 { 258 // Get the folder from the database name 259 $sqlFolder = $this->db->getServerType(); 260 261 // For `mssql` server types, convert the type to `sqlazure` 262 if ($sqlFolder === 'mssql') 263 { 264 $sqlFolder = 'sqlazure'; 265 } 266 267 // Default folder to core com_admin 268 if (!$this->folder) 269 { 270 $this->folder = JPATH_ADMINISTRATOR . '/components/com_admin/sql/updates/'; 271 } 272 273 return \JFolder::files( 274 $this->folder . '/' . $sqlFolder, '\.sql$', 1, true, array('.svn', 'CVS', '.DS_Store', '__MACOSX'), array('^\..*', '.*~'), true 275 ); 276 } 277 278 /** 279 * Get array of SQL queries 280 * 281 * @param array $sqlfiles Array of .sql update filenames. 282 * 283 * @return array Array of \stdClass objects where: 284 * file=filename, 285 * update_query = text of SQL update query 286 * 287 * @since 2.5 288 */ 289 private function getUpdateQueries(array $sqlfiles) 290 { 291 // Hold results as array of objects 292 $result = array(); 293 294 foreach ($sqlfiles as $file) 295 { 296 $buffer = file_get_contents($file); 297 298 // Create an array of queries from the sql file 299 $queries = \JDatabaseDriver::splitSql($buffer); 300 301 foreach ($queries as $query) 302 { 303 $fileQueries = new \stdClass; 304 $fileQueries->file = $file; 305 $fileQueries->updateQuery = $query; 306 $result[] = $fileQueries; 307 } 308 } 309 310 return $result; 311 } 312} 313