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