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