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