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 13/** 14 * Each object represents one query, which is one line from a DDL SQL query. 15 * This class is used to check the site's database to see if the DDL query has been run. 16 * If not, it provides the ability to fix the database by re-running the DDL query. 17 * The queries are parsed from the update files in the folder 18 * `administrator/components/com_admin/sql/updates/<database>`. 19 * These updates are run automatically if the site was updated using com_installer. 20 * However, it is possible that the program files could be updated without udpating 21 * the database (for example, if a user just copies the new files over the top of an 22 * existing installation). 23 * 24 * This is an abstract class. We need to extend it for each database and add a 25 * buildCheckQuery() method that creates the query to check that a DDL query has been run. 26 * 27 * @since 2.5 28 */ 29abstract class ChangeItem 30{ 31 /** 32 * Update file: full path file name where query was found 33 * 34 * @var string 35 * @since 2.5 36 */ 37 public $file = null; 38 39 /** 40 * Update query: query used to change the db schema (one line from the file) 41 * 42 * @var string 43 * @since 2.5 44 */ 45 public $updateQuery = null; 46 47 /** 48 * Check query: query used to check the db schema 49 * 50 * @var string 51 * @since 2.5 52 */ 53 public $checkQuery = null; 54 55 /** 56 * Check query result: expected result of check query if database is up to date 57 * 58 * @var string 59 * @since 2.5 60 */ 61 public $checkQueryExpected = 1; 62 63 /** 64 * \JDatabaseDriver object 65 * 66 * @var \JDatabaseDriver 67 * @since 2.5 68 */ 69 public $db = null; 70 71 /** 72 * Query type: To be used in building a language key for a 73 * message to tell user what was checked / changed 74 * Possible values: ADD_TABLE, ADD_COLUMN, CHANGE_COLUMN_TYPE, ADD_INDEX 75 * 76 * @var string 77 * @since 2.5 78 */ 79 public $queryType = null; 80 81 /** 82 * Array with values for use in a \JText::sprintf statment indicating what was checked 83 * 84 * Tells you what the message should be, based on which elements are defined, as follows: 85 * For ADD_TABLE: table 86 * For ADD_COLUMN: table, column 87 * For CHANGE_COLUMN_TYPE: table, column, type 88 * For ADD_INDEX: table, index 89 * 90 * @var array 91 * @since 2.5 92 */ 93 public $msgElements = array(); 94 95 /** 96 * Checked status 97 * 98 * @var integer 0=not checked, -1=skipped, -2=failed, 1=succeeded 99 * @since 2.5 100 */ 101 public $checkStatus = 0; 102 103 /** 104 * Rerun status 105 * 106 * @var int 0=not rerun, -1=skipped, -2=failed, 1=succeeded 107 * @since 2.5 108 */ 109 public $rerunStatus = 0; 110 111 /** 112 * Constructor: builds check query and message from $updateQuery 113 * 114 * @param \JDatabaseDriver $db Database connector object 115 * @param string $file Full path name of the sql file 116 * @param string $query Text of the sql query (one line of the file) 117 * 118 * @since 2.5 119 */ 120 public function __construct($db, $file, $query) 121 { 122 $this->updateQuery = $query; 123 $this->file = $file; 124 $this->db = $db; 125 $this->buildCheckQuery(); 126 } 127 128 /** 129 * Returns a reference to the ChangeItem object. 130 * 131 * @param \JDatabaseDriver $db Database connector object 132 * @param string $file Full path name of the sql file 133 * @param string $query Text of the sql query (one line of the file) 134 * 135 * @return ChangeItem instance based on the database driver 136 * 137 * @since 2.5 138 * @throws \RuntimeException if class for database driver not found 139 */ 140 public static function getInstance($db, $file, $query) 141 { 142 // Get the class name 143 $serverType = $db->getServerType(); 144 145 // For `mssql` server types, convert the type to `sqlsrv` 146 if ($serverType === 'mssql') 147 { 148 $serverType = 'sqlsrv'; 149 } 150 151 $class = '\\Joomla\\CMS\\Schema\\ChangeItem\\' . ucfirst($serverType) . 'ChangeItem'; 152 153 // If the class exists, return it. 154 if (class_exists($class)) 155 { 156 return new $class($db, $file, $query); 157 } 158 159 throw new \RuntimeException(sprintf('ChangeItem child class not found for the %s database driver', $serverType), 500); 160 } 161 162 /** 163 * Checks a DDL query to see if it is a known type 164 * If yes, build a check query to see if the DDL has been run on the database. 165 * If successful, the $msgElements, $queryType, $checkStatus and $checkQuery fields are populated. 166 * The $msgElements contains the text to create the user message. 167 * The $checkQuery contains the SQL query to check whether the schema change has 168 * been run against the current database. The $queryType contains the type of 169 * DDL query that was run (for example, CREATE_TABLE, ADD_COLUMN, CHANGE_COLUMN_TYPE, ADD_INDEX). 170 * The $checkStatus field is set to zero if the query is created 171 * 172 * If not successful, $checkQuery is empty and , and $checkStatus is -1. 173 * For example, this will happen if the current line is a non-DDL statement. 174 * 175 * @return void 176 * 177 * @since 2.5 178 */ 179 abstract protected function buildCheckQuery(); 180 181 /** 182 * Runs the check query and checks that 1 row is returned 183 * If yes, return true, otherwise return false 184 * 185 * @return boolean true on success, false otherwise 186 * 187 * @since 2.5 188 */ 189 public function check() 190 { 191 $this->checkStatus = -1; 192 193 if ($this->checkQuery) 194 { 195 $this->db->setQuery($this->checkQuery); 196 197 try 198 { 199 $rows = $this->db->loadRowList(0); 200 } 201 catch (\RuntimeException $e) 202 { 203 // Still render the error message from the Exception object 204 \JFactory::getApplication()->enqueueMessage($e->getMessage(), 'error'); 205 $this->checkStatus = -2; 206 207 return $this->checkStatus; 208 } 209 210 if (count($rows) === $this->checkQueryExpected) 211 { 212 $this->checkStatus = 1; 213 214 return $this->checkStatus; 215 } 216 217 $this->checkStatus = -2; 218 } 219 220 return $this->checkStatus; 221 } 222 223 /** 224 * Runs the update query to apply the change to the database 225 * 226 * @return void 227 * 228 * @since 2.5 229 */ 230 public function fix() 231 { 232 if ($this->checkStatus === -2) 233 { 234 // At this point we have a failed query 235 $query = $this->db->convertUtf8mb4QueryToUtf8($this->updateQuery); 236 $this->db->setQuery($query); 237 238 if ($this->db->execute()) 239 { 240 if ($this->check()) 241 { 242 $this->checkStatus = 1; 243 $this->rerunStatus = 1; 244 } 245 else 246 { 247 $this->rerunStatus = -2; 248 } 249 } 250 else 251 { 252 $this->rerunStatus = -2; 253 } 254 } 255 } 256} 257