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