Merge branch '2016.11' into 2016.11-develop
[tine20] / tine20 / Setup / Update / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Setup
6  * @subpackage  Update
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2016 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Matthias Greiling <m.greiling@metaways.de>
10  */
11
12 /**
13  * Common class for a Tine 2.0 Update
14  * 
15  * @package     Setup
16  * @subpackage  Update
17  */
18 class Setup_Update_Abstract
19 {
20     /**
21      * backend for databse handling and extended database queries
22      *
23      * @var Setup_Backend_Mysql
24      */
25     protected $_backend;
26     
27     /**
28      * @var Zend_Db_Adapter_Abstract
29      */
30     protected $_db;
31     
32     /** 
33     * the constructor
34     */
35     public function __construct($_backend)
36     {
37         $this->_backend = $_backend;
38         $this->_db = Tinebase_Core::getDb();
39     }
40     
41     /**
42      * get version number of a given application 
43      * version is stored in database table "applications"
44      *
45      * @param string application
46      * @return string version number major.minor release 
47      */
48     public function getApplicationVersion($_application)
49     {
50         $select = $this->_db->select()
51                 ->from(SQL_TABLE_PREFIX . 'applications')
52                 ->where($this->_db->quoteIdentifier('name') . ' = ?', $_application);
53
54         $stmt = $select->query();
55         $version = $stmt->fetchAll();
56         
57         return $version[0]['version'];
58     }
59
60     /**
61      * set version number of a given application 
62      * version is stored in database table "applications"
63      *
64      * @param string $_applicationName
65      * @param string $_version new version number
66      * @return Tinebase_Model_Application
67      */    
68     public function setApplicationVersion($_applicationName, $_version)
69     {
70         $application = Tinebase_Application::getInstance()->getApplicationByName($_applicationName);
71         $application->version = $_version;
72         
73         return Tinebase_Application::getInstance()->updateApplication($application);
74     }
75     
76     /**
77      * get version number of a given table
78      * version is stored in database table "applications_tables"
79      *
80      * @param Tinebase_Application application
81      * @return int version number 
82      */
83     public function getTableVersion($_tableName)
84     {
85         $select = $this->_db->select()
86                 ->from(SQL_TABLE_PREFIX . 'application_tables')
87                 ->where(    $this->_db->quoteIdentifier('name') . ' = ?', $_tableName)
88                 ->orwhere(  $this->_db->quoteIdentifier('name') . ' = ?', SQL_TABLE_PREFIX . $_tableName);
89
90         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
91             ' ' . $select->__toString());
92
93         $stmt = $select->query();
94         $rows = $stmt->fetchAll();
95
96         $result = (count($rows) > 0 && isset($rows[0]['version'])) ? $rows[0]['version'] : 0;
97         
98         return $result;
99     }
100     
101     /**
102      * set version number of a given table
103      * version is stored in database table "applications_tables"
104      *
105      * @param string tableName
106      * @param int|string $_version
107      * @param boolean $_createIfNotExist
108      * @param string $_application
109      * @return void
110      * @throws Setup_Exception_NotFound
111      */     
112     public function setTableVersion($_tableName, $_version, $_createIfNotExist = TRUE, $_application = 'Tinebase')
113     {
114         if ($this->getTableVersion($_tableName) == 0) {
115             if ($_createIfNotExist) {
116                 Tinebase_Application::getInstance()->addApplicationTable(
117                     Tinebase_Application::getInstance()->getApplicationByName($_application), 
118                     $_tableName,
119                     $_version
120                 );
121             } else {
122                 throw new Setup_Exception_NotFound('Table ' . $_tableName . ' not found in application tables or previous version number invalid.');
123             }
124         } else {
125             $applicationsTables = new Tinebase_Db_Table(array('name' =>  SQL_TABLE_PREFIX . 'application_tables'));
126             $where  = array(
127                 $this->_db->quoteInto($this->_db->quoteIdentifier('name') . ' = ?', $_tableName),
128             );
129             $result = $applicationsTables->update(array('version' => $_version), $where);
130         }
131     }
132     
133     /**
134      * set version number of a given table
135      * version is stored in database table "applications_tables"
136      *
137      * @param string tableName
138      * @return int version number 
139      */  
140     public function increaseTableVersion($_tableName)
141     {
142         $currentVersion = $this->getTableVersion($_tableName);
143
144         $version = ++$currentVersion;
145         
146         $applicationsTables = new Tinebase_Db_Table(array('name' =>  SQL_TABLE_PREFIX . 'application_tables'));
147         $where  = array(
148             $this->_db->quoteInto($this->_db->quoteIdentifier('name') . ' = ?', $_tableName),
149         );
150         $result = $applicationsTables->update(array('version' => $version), $where);
151     }
152     
153     /**
154      * compares version numbers of given table and given number
155      *
156      * @param  string $_tableName
157      * @param  int version number
158      * @throws Setup_Exception
159      */     
160     public function validateTableVersion($_tableName, $_version)
161     {
162         $currentVersion = $this->getTableVersion($_tableName);
163         if($_version != $currentVersion) {
164             throw new Setup_Exception("Wrong table version for $_tableName. expected $_version got $currentVersion");
165         }
166     }
167     
168     /**
169      * create new table and add it to application tables
170      * 
171      * @param string $_tableName
172      * @param Setup_Backend_Schema_Table_Abstract $_table
173      * @param string $_application
174      */
175     public function createTable($_tableName, Setup_Backend_Schema_Table_Abstract $_table, $_application = 'Tinebase', $_version = 1)
176     {
177         $app = Tinebase_Application::getInstance()->getApplicationByName($_application);
178         Tinebase_Application::getInstance()->removeApplicationTable($app, $_tableName);
179         
180         if (false === $this->_backend->createTable($_table)) {
181             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Creation of table ' . $_tableName . ' gracefully failed');
182             return false;
183         }
184         
185         Tinebase_Application::getInstance()->addApplicationTable($app, $_tableName, $_version);
186         
187         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Created new table ' . $_tableName);
188     }
189     
190     /**
191      * rename table in applications table
192      *
193      * @param string $_oldTableName
194      * @param string $_newTableName
195      */  
196     public function renameTable($_oldTableName, $_newTableName)
197     {
198         $this->_backend->renameTable($_oldTableName, $_newTableName);
199         $this->renameTableInAppTables($_oldTableName, $_newTableName);
200     }
201
202     /**
203      * @param $_oldTableName
204      * @param $_newTableName
205      * @return int
206      */
207     public function renameTableInAppTables($_oldTableName, $_newTableName)
208     {
209         $applicationsTables = new Tinebase_Db_Table(array('name' =>  SQL_TABLE_PREFIX . 'application_tables'));
210         $where  = array(
211             $this->_db->quoteInto($this->_db->quoteIdentifier('name') . ' = ?', $_oldTableName),
212         );
213         $result = $applicationsTables->update(array('name' => $_newTableName), $where);
214         return $result;
215     }
216     
217     /**
218      * drop table
219      *
220      * @param string $_tableName
221      * @param string $_application
222      */  
223     public function dropTable($_tableName, $_application = 'Tinebase')
224     {
225         Tinebase_Application::getInstance()->removeApplicationTable(Tinebase_Application::getInstance()->getApplicationByName($_application), $_tableName);
226         $result = $this->_backend->dropTable($_tableName);
227     }
228     
229     /**
230      * prompts for a username to set as active user on performing updates. this must be an admin user.
231      * the user account will be returned. this method can be called by cli only, so a exception will 
232      * be thrown if not running on cli
233      * 
234      * @throws Tinebase_Exception
235      * @return Tinebase_Model_FullUser
236      */
237     public function promptForUsername()
238     {
239         if (php_sapi_name() == 'cli') {
240             
241             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
242                 . ' Prompting for username on CLI');
243             
244             $userFound = NULL;
245             
246             do {
247                 try {
248                     if ($userFound === FALSE) {
249                         echo PHP_EOL;
250                         echo 'The user could not be found!' . PHP_EOL . PHP_EOL;
251                     }
252                     
253                     $user = Tinebase_Server_Cli::promptInput('Please enter an admin username to perform updates ');
254                     $userAccount = Tinebase_User::getInstance()->getFullUserByLoginName($user);
255                     
256                     if (! $userAccount->hasRight('Tinebase', Tinebase_Acl_Rights::ADMIN)) {
257                         $userFound = NULL;
258                         echo PHP_EOL;
259                         echo 'The user "' . $user . '" could be found, but this is not an admin user!' . PHP_EOL . PHP_EOL;
260                     } else {
261                         Tinebase_Core::set(Tinebase_Core::USER, $userAccount);
262                         $userFound = TRUE;
263                     }
264                     
265                 } catch (Tinebase_Exception_NotFound $e) {
266                     $userFound = FALSE;
267                 }
268                 
269             } while (! $userFound);
270             
271         } else {
272             throw new Setup_Exception_PromptUser('no CLI call');
273         }
274         
275         return $userAccount;
276     }
277
278     /**
279      * get db adapter
280      * 
281      * @return Zend_Db_Adapter_Abstract
282      */
283     public function getDb()
284     {
285         return $this->_db;
286     }
287     
288     /**
289      * Search for text fields that contain a string longer as a specific length and truncate it to this length
290      * 
291      * @param string $table
292      * @param string $field
293      * @param int $length
294      */
295     public function shortenTextValues($table, $field, $length)
296     {
297         $select = $this->_db->select()
298             ->from(array($table => SQL_TABLE_PREFIX . $table), array($field))
299             ->where("CHAR_LENGTH(" . $this->_db->quoteIdentifier($field) . ") > ?", $length);
300         
301         $stmt = $this->_db->query($select);
302         $results = $stmt->fetchAll();
303         $stmt->closeCursor();
304         
305         foreach ($results as $result) {
306             $where = array(
307                 array($this->_db->quoteIdentifier($field) . ' = ?' => $result[$field])
308             );
309             
310             $newContent = array(
311                 $field => iconv_substr($result[$field], 0 , $length, 'UTF-8')
312             );
313             
314             try {
315                 $this->_db->update(SQL_TABLE_PREFIX . $table, $newContent, $where);
316                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
317                     . ' Field was shortend: ' . print_r($result, true));
318             } catch (Tinebase_Exception_Record_Validation $terv) {
319                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
320                     . ' Failed to shorten field: ' . print_r($result, true));
321                 Tinebase_Exception::log($terv);
322             }
323         }
324     }
325     
326     /**
327      * truncate text fields to a specific length
328      * Array needs to contain the table name, field name, and a config option for "<notnull>" ("true", "false")
329      * or use "null" to set default to NULL
330      * 
331      * @param array $columns
332      * @param int $length
333      */
334     public function truncateTextColumn($columns, $length)
335     {
336         foreach ($columns as $table => $fields) {
337             foreach ($fields as $field => $config) {
338                 try {
339                     $this->shortenTextValues($table, $field, $length);
340                     if (isset($config)) {
341                         $config = ($config == 'null' ? '<default>NULL</default>': '<notnull>' . $config . '</notnull>');
342                     }
343                     $declaration = new Setup_Backend_Schema_Field_Xml('
344                         <field>
345                             <name>' . $field . '</name>
346                             <type>text</type>
347                             <length>' . $length . '</length>'
348                             . $config .
349                         '</field>
350                     ');
351                     
352                     $this->_backend->alterCol($table, $declaration);
353                 } catch (Zend_Db_Statement_Exception $zdse) {
354                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
355                         . ' Could not truncate text column ' . $field . ' in table ' . $table);
356                     Tinebase_Exception::log($zdse);
357                 }
358             }
359         }
360     }
361
362     protected function _addModlogFields($table)
363     {
364         $fields = array('<field>
365                 <name>created_by</name>
366                 <type>text</type>
367                 <length>40</length>
368             </field>','
369             <field>
370                 <name>creation_time</name>
371                 <type>datetime</type>
372             </field> ','
373             <field>
374                 <name>last_modified_by</name>
375                 <type>text</type>
376                 <length>40</length>
377             </field>','
378             <field>
379                 <name>last_modified_time</name>
380                 <type>datetime</type>
381             </field>','
382             <field>
383                 <name>is_deleted</name>
384                 <type>boolean</type>
385                 <default>false</default>
386             </field>','
387             <field>
388                 <name>deleted_by</name>
389                 <type>text</type>
390                 <length>40</length>
391             </field>','
392             <field>
393                 <name>deleted_time</name>
394                 <type>datetime</type>
395             </field>','
396             <field>
397                 <name>seq</name>
398                 <type>integer</type>
399                 <notnull>true</notnull>
400                 <default>0</default>
401             </field>');
402         
403         foreach ($fields as $field) {
404             $declaration = new Setup_Backend_Schema_Field_Xml($field);
405             try {
406                 $this->_backend->addCol($table, $declaration);
407             } catch (Zend_Db_Statement_Exception $zdse) {
408                 Tinebase_Exception::log($zdse);
409             }
410         }
411     }
412
413     /**
414      * try to get user for setup tasks from config
415      *
416      * @return Tinebase_Model_FullUser
417      */
418     static public function getSetupFromConfigOrCreateOnTheFly()
419     {
420         try {
421             $setupId = Tinebase_Config::getInstance()->get(Tinebase_Config::SETUPUSERID);
422             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Setting user with id ' . $setupId . ' as setupuser.');
423             $setupUser = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $setupId, 'Tinebase_Model_FullUser');
424         } catch (Tinebase_Exception_NotFound $tenf) {
425             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $tenf->getMessage());
426
427             $setupUser = Tinebase_User::createSystemUser('setupuser');
428             if ($setupUser) {
429                 Tinebase_Config::getInstance()->set(Tinebase_Config::SETUPUSERID, null);
430                 Tinebase_Config::getInstance()->set(Tinebase_Config::SETUPUSERID, $setupUser->getId());
431             }
432         }
433
434         return $setupUser;
435     }
436
437     /**
438      * update schema of modelconfig enabled app
439      *
440      * @param string $appName
441      * @param array $modelNames
442      * @return boolean success
443      * @throws Setup_Exception_NotFound
444      */
445     public function updateSchema($appName, $modelNames)
446     {
447         if (! Setup_Core::isDoctrineAvailable()) {
448
449             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
450                 ' No doctrine ORM available -> disabling app ' . $appName);
451
452             Tinebase_Application::getInstance()->setApplicationState(
453                 array(Tinebase_Application::getInstance()->getApplicationByName($appName)->getId()),
454                 Tinebase_Application::DISABLED
455             );
456             return false;
457         }
458
459         $updateRequired = false;
460         $setNewVersions = array();
461         foreach($modelNames as $modelName) {
462             $modelConfig = $modelName::getConfiguration();
463             $tableName = Tinebase_Helper::array_value('name', $modelConfig->getTable());
464             $currentVersion = $this->getTableVersion($tableName);
465             $schemaVersion = $modelConfig->getVersion();
466             if ($currentVersion < $schemaVersion) {
467                 $updateRequired = true;
468                 $setNewVersions[$tableName] = $schemaVersion;
469             }
470         }
471
472         if ($updateRequired) {
473             Setup_SchemaTool::updateSchema($appName, $modelNames);
474
475             foreach($setNewVersions as $table => $version) {
476                 $this->setTableVersion($table, $version);
477             }
478         }
479
480         return true;
481     }
482 }