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