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