prevent problems with getApplicationByName
[tine20] / tine20 / Tinebase / Application.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Application
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  *
11  * @todo        add 'getTitleTranslation' function?
12  */
13
14 /**
15  * the class provides functions to handle applications
16  * 
17  * @package     Tinebase
18  * @subpackage  Application
19  */
20 class Tinebase_Application
21 {
22     /**
23      * application enabled
24      *
25      */
26     const ENABLED  = 'enabled';
27     
28     /**
29      * application disabled
30      *
31      */
32     const DISABLED = 'disabled';
33     
34     /**
35      * Table name
36      *
37      * @var string
38      */
39     protected $_tableName = 'applications';
40     
41     /**
42      * the db adapter
43      *
44      * @var Zend_Db_Adapter_Abstract
45      */
46     protected $_db;
47     
48     /**
49      * 
50      * @var Tinebase_Backend_Sql
51      */
52     protected $_backend;
53     
54     /**
55      * the constructor
56      *
57      * don't use the constructor. use the singleton 
58      */
59     private function __construct() 
60     {
61     }
62     
63     /**
64      * don't clone. Use the singleton.
65      *
66      */
67     private function __clone() 
68     {
69     }
70
71     /**
72      * holds the instance of the singleton
73      *
74      * @var Tinebase_Application
75      */
76     private static $instance = NULL;
77     
78     /**
79      * Returns instance of Tinebase_Application
80      *
81      * @return Tinebase_Application
82      */
83     public static function getInstance() 
84     {
85         if (self::$instance === NULL) {
86             self::$instance = new Tinebase_Application;
87         }
88         
89         return self::$instance;
90     }
91     
92     /**
93      * returns one application identified by id
94      *
95      * @param Tinebase_Model_Application|string $_applicationId the id of the application
96      * @throws Tinebase_Exception_NotFound
97      * @return Tinebase_Model_Application the information about the application
98      */
99     public function getApplicationById($_applicationId)
100     {
101         $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($_applicationId);
102
103         $application = $this->getApplications()->getById($applicationId);
104         
105         if (!$application) {
106             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Application not found. Id: ' . $applicationId);
107             throw new Tinebase_Exception_NotFound("Application $applicationId not found.");
108         }
109         
110         return $application;
111     }
112
113     /**
114      * returns one application identified by application name
115      * - results are cached
116      *
117      * @param string $_applicationName the name of the application
118      * @return Tinebase_Model_Application the information about the application
119      * @throws Tinebase_Exception_InvalidArgument
120      * @throws Tinebase_Exception_NotFound
121      */
122     public function getApplicationByName($_applicationName)
123     {
124         if(empty($_applicationName) || ! is_string($_applicationName)) {
125             throw new Tinebase_Exception_InvalidArgument('$_applicationName can not be empty / has to be string.');
126         }
127
128         $applications = $this->getApplications();
129         if ($applications) {
130             $application = $applications->find('name', $_applicationName);
131         } else {
132             $application = false;
133         }
134         
135         if (!$application) {
136             throw new Tinebase_Exception_NotFound("Application $_applicationName not found.");
137         }
138         
139         return $application;
140     }
141     
142     /**
143      * get list of installed applications
144      *
145      * @param string $_sort optional the column name to sort by
146      * @param string $_dir optional the sort direction can be ASC or DESC only
147      * @param string $_filter optional search parameter
148      * @param int $_limit optional how many applications to return
149      * @param int $_start optional offset for applications
150      * @return Tinebase_Record_RecordSet of Tinebase_Model_Application
151      */
152     public function getApplications($_filter = NULL, $_sort = null, $_dir = 'ASC', $_start = NULL, $_limit = NULL)
153     {
154         $filter = null;
155         if ($_filter) {
156             $filter = new Tinebase_Model_ApplicationFilter(array(
157                 array('field' => 'name', 'operator' => 'contains', 'value' => $_filter),
158             ));
159         }
160         
161         $pagination = null;
162         if ($_sort) {
163             $pagination = new Tinebase_Model_Pagination(array(
164                 'sort'  => $_sort,
165                 'dir'   => $_dir,
166                 'start' => $_start,
167                 'limit' => $_limit
168             ));
169         }
170         
171         if ($filter === null && $pagination === null) {
172             try {
173                 $result = Tinebase_Cache_PerRequest::getInstance()->load(__CLASS__, __METHOD__, 'allApplications', Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
174                 
175                 return $result;
176             } catch (Tinebase_Exception_NotFound $tenf) {
177                 // do nothing
178             }
179         }
180         
181         $result = $this->_getBackend()->search($filter, $pagination);
182         
183         if ($filter === null && $pagination === null) {
184             // cache result in persistent shared cache too
185             // cache will be cleared, when an application will be added or updated
186             Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __METHOD__, 'allApplications', $result, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
187         }
188         
189         return $result;
190     }
191     
192     /**
193      * get enabled or disabled applications
194      *
195      * @param  string  $state  can be Tinebase_Application::ENABLED or Tinebase_Application::DISABLED
196      * @return Tinebase_Record_RecordSet list of applications
197      */
198     public function getApplicationsByState($state)
199     {
200         if (!in_array($state, array(Tinebase_Application::ENABLED, Tinebase_Application::DISABLED))) {
201             throw new Tinebase_Exception_InvalidArgument('$status can be only Tinebase_Application::ENABLED or Tinebase_Application::DISABLED');
202         }
203         
204         $result = $this->getApplications(null, /* sort = */ 'order')->filter('status', $state);
205         
206         return $result;
207     }
208     
209     /**
210      * get hash of installed applications
211      *
212      * @param string $_sort optional the column name to sort by
213      * @param string $_dir optional the sort direction can be ASC or DESC only
214      * @param string $_filter optional search parameter
215      * @param int $_limit optional how many applications to return
216      * @param int $_start optional offset for applications
217      * @return string
218      */
219     public function getApplicationsHash($_filter = NULL, $_sort = null, $_dir = 'ASC', $_start = NULL, $_limit = NULL)
220     {
221         $applications = $this->getApplications($_filter, $_sort, $_dir, $_start, $_limit);
222         
223         // create a hash of installed applications and their versions
224         $applications = array_combine(
225             Tinebase_Application::getInstance()->getApplications()->id,
226             Tinebase_Application::getInstance()->getApplications()->version
227         );
228         
229         ksort($applications);
230         
231         return Tinebase_Helper::arrayHash($applications, true);
232     }
233     
234     /**
235      * return the total number of applications installed
236      *
237      * @param $_filter
238      * 
239      * @return int
240      */
241     public function getTotalApplicationCount($_filter = NULL)
242     {
243         $select = $this->_getDb()->select()
244             ->from(SQL_TABLE_PREFIX . $this->_tableName, array('count' => 'COUNT(*)'));
245         
246         if($_filter !== NULL) {
247             $select->where($this->_getDb()->quoteIdentifier('name') . ' LIKE ?', '%' . $_filter . '%');
248         }
249         
250         $stmt = $this->_getDb()->query($select);
251         $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
252         
253         return $result[0];
254     }
255     
256     /**
257      * return if application is installed (and enabled)
258      *
259      * @param  Tinebase_Model_Application|string  $applicationId  the application name/id/object
260      * @param  boolean $checkEnabled (FALSE by default)
261      * 
262      * @return boolean
263      */
264     public function isInstalled($applicationId, $checkEnabled = FALSE)
265     {
266         try {
267             $app = $this->getApplicationById($applicationId);
268             return ($checkEnabled) ? ($app->status === self::ENABLED) : TRUE;
269         } catch (Tinebase_Exception_NotFound $tenf) {
270             return FALSE;
271         } catch (Zend_Db_Statement_Exception $tenf) {
272             // database tables might be not available yet
273             // @see 0011338: First Configuration fails after Installation
274             return FALSE;
275         }
276     }
277     
278     /**
279      * set application state
280      *
281      * @param   array   $_applicationIds application ids to set new state for
282      * @param   string  $state the new state
283      * @throws  Tinebase_Exception_InvalidArgument
284      */
285     public function setApplicationState($_applicationIds, $state)
286     {
287         if (!in_array($state, array(Tinebase_Application::ENABLED, Tinebase_Application::DISABLED))) {
288             throw new Tinebase_Exception_InvalidArgument('$_state can be only Tinebase_Application::DISABLED  or Tinebase_Application::ENABLED');
289         }
290         
291         if ($_applicationIds instanceof Tinebase_Model_Application ||
292             $_applicationIds instanceof Tinebase_Record_RecordSet
293         ) {
294             $applicationIds = (array)$_applicationIds->getId();
295         } else {
296             $applicationIds = (array)$_applicationIds;
297         }
298         
299         $data = array(
300             'status' => $state
301         );
302         
303         $affectedRows = $this->_getBackend()->updateMultiple($applicationIds, $data);
304         
305         if ($affectedRows === count($applicationIds)) {
306             if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
307                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Disabled/Enabled ' . $affectedRows . ' applications.');
308         } else {
309             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
310                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
311                 . ' Could not set state for all requested applications: ' . print_r($applicationIds, TRUE));
312         }
313         
314         $this->resetClassCache();
315     }
316     
317     /**
318      * add new appliaction 
319      *
320      * @param Tinebase_Model_Application $application the new application object
321      * @return Tinebase_Model_Application the new application with the applicationId set
322      */
323     public function addApplication(Tinebase_Model_Application $application)
324     {
325         $application = $this->_getBackend()->create($application);
326         
327         $this->resetClassCache();
328         
329         return $application;
330     }
331     
332     /**
333      * get all possible application rights
334      *
335      * @param   int application id
336      * @return  array   all application rights
337      */
338     public function getAllRights($_applicationId)
339     {
340         $application = $this->getApplicationById($_applicationId);
341         
342         // call getAllApplicationRights for application (if it has specific rights)
343         $appAclClassName = $application->name . '_Acl_Rights';
344         if (@class_exists($appAclClassName)) {
345             $appAclObj = call_user_func(array($appAclClassName, 'getInstance'));
346             $allRights = $appAclObj->getAllApplicationRights();
347         } else {
348             $allRights = Tinebase_Acl_Rights::getInstance()->getAllApplicationRights($application->name);
349         }
350         
351         return $allRights;
352     }
353     
354     /**
355      * get right description
356      *
357      * @param   int     application id
358      * @param   string  right
359      * @return  array   right description
360      */
361     public function getAllRightDescriptions($_applicationId)
362     {
363         $application = $this->getApplicationById($_applicationId);
364         
365         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
366             ' Getting right descriptions for ' . $application->name );
367         
368         // call getAllApplicationRights for application (if it has specific rights)
369         $appAclClassName = $application->name . '_Acl_Rights';
370         if (! @class_exists($appAclClassName)) {
371             $appAclClassName = 'Tinebase_Acl_Rights';
372             $function = 'getTranslatedBasicRightDescriptions';
373         } else {
374             $function = 'getTranslatedRightDescriptions';
375         }
376         
377         $descriptions = call_user_func(array($appAclClassName, $function));
378         
379         return $descriptions;
380     }
381     
382     /**
383      * get tables of application
384      *
385      * @param Tinebase_Model_Application $_applicationId
386      * @return array
387      */
388     public function getApplicationTables($_applicationId)
389     {
390         $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($_applicationId);
391         
392         $select = $this->_getDb()->select()
393             ->from(SQL_TABLE_PREFIX . 'application_tables', array('name'))
394             ->where($this->_getDb()->quoteIdentifier('application_id') . ' = ?', $applicationId);
395             
396         $stmt = $this->_getDb()->query($select);
397         $rows = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
398         
399         return $rows;
400     }
401
402     /**
403      * remove table from application_tables table
404      *
405      * @param Tinebase_Model_Application|string $_applicationId the applicationId
406      * @param string $_tableName the table name
407      */
408     public function removeApplicationTable($_applicationId, $_tableName)
409     {
410         $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($_applicationId);
411         
412         $where = array(
413             $this->_getDb()->quoteInto($this->_getDb()->quoteIdentifier('application_id') . '= ?', $applicationId),
414             $this->_getDb()->quoteInto($this->_getDb()->quoteIdentifier('name') . '= ?', $_tableName)
415         );
416         
417         $this->_getDb()->delete(SQL_TABLE_PREFIX . 'application_tables', $where);
418     }
419     
420     /**
421      * reset class cache
422      * 
423      * @param string $method
424      * @return Tinebase_Application
425      */
426     public function resetClassCache($method = null)
427     {
428         Tinebase_Cache_PerRequest::getInstance()->reset(__CLASS__, $method);
429         
430         return $this;
431     }
432     
433     /**
434      * remove application from applications table
435      *
436      * @param Tinebase_Model_Application|string $_applicationId the applicationId
437      */
438     public function deleteApplication($_applicationId)
439     {
440         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
441             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing app ' . $_applicationId . ' from applications table.');
442         
443         $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($_applicationId);
444         
445         $this->resetClassCache();
446         
447         $this->_getBackend()->delete($applicationId);
448     }
449     
450     /**
451      * add table to tine registry
452      *
453      * @param Tinebase_Model_Application
454      * @param string name of table
455      * @param int version of table
456      * @return int
457      */
458     public function addApplicationTable($_applicationId, $_name, $_version)
459     {
460         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
461             . ' Add application table: ' . $_name);
462
463         $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($_applicationId);
464         
465         $applicationData = array(
466             'application_id' => $applicationId,
467             'name'           => $_name,
468             'version'        => $_version
469         );
470         
471         $this->_getDb()->insert(SQL_TABLE_PREFIX . 'application_tables', $applicationData);
472     }
473     
474     /**
475      * update application
476      * 
477      * @param Tinebase_Model_Application $_application
478      * @return Tinebase_Model_Application
479      */
480     public function updateApplication(Tinebase_Model_Application $_application)
481     {
482         $result = $this->_getBackend()->update($_application);
483         
484         $this->resetClassCache();
485         
486         return $result;
487     }
488     
489     /**
490      * delete containers, configs and other data of an application
491      * ATTENTION this does NOT delete the application data itself! only auxiliary data
492      * 
493      * NOTE: if a table with foreign key constraints to applications is added, we need to make sure that the data is deleted here 
494      * 
495      * @param Tinebase_Model_Application $_application
496      */
497     public function removeApplicationAuxiliaryData(Tinebase_Model_Application $_application)
498     {
499         $dataToDelete = array(
500             'container'     => array('tablename' => ''),
501             'config'        => array('tablename' => ''),
502             'customfield'   => array('tablename' => ''),
503             'rights'        => array('tablename' => 'role_rights'),
504             'definitions'   => array('tablename' => 'importexport_definition'),
505             'filter'        => array('tablename' => 'filter'),
506             'modlog'        => array('tablename' => 'timemachine_modlog'),
507             'import'        => array('tablename' => 'import'),
508             'rootnode'      => array('tablename' => ''),
509         );
510         $countMessage = ' Deleted';
511         
512         $where = array(
513             $this->_getDb()->quoteInto($this->_getDb()->quoteIdentifier('application_id') . '= ?', $_application->getId())
514         );
515         
516         foreach ($dataToDelete as $dataType => $info) {
517             switch ($dataType) {
518                 case 'container':
519                     $count = Tinebase_Container::getInstance()->dropContainerByApplicationId($_application->getId());
520                     break;
521                 case 'config':
522                     $count = Tinebase_Config::getInstance()->deleteConfigByApplicationId($_application->getId());
523                     break;
524                 case 'customfield':
525                     $count = Tinebase_CustomField::getInstance()->deleteCustomFieldsForApplication($_application->getId());
526                     break;
527                 case 'rootnode':
528                     try {
529                         // note: TFS expects name here, not ID
530                         $count = Tinebase_FileSystem::getInstance()->rmdir($_application->name, true);
531                     } catch (Tinebase_Exception_NotFound $tenf) {
532                         // nothing to do
533                         Tinebase_Exception::log($tenf);
534                     } catch (Tinebase_Exception_Backend $teb) {
535                         // nothing to do
536                         Tinebase_Exception::log($teb);
537                     }
538                     break;
539                 default:
540                     if ((isset($info['tablename']) || array_key_exists('tablename', $info)) && ! empty($info['tablename'])) {
541                         try {
542                             $count = $this->_getDb()->delete(SQL_TABLE_PREFIX . $info['tablename'], $where);
543                         } catch (Zend_Db_Statement_Exception $zdse) {
544                             Tinebase_Exception::log($zdse);
545                             $count = 0;
546                         }
547                     } else {
548                         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' No tablename defined for ' . $dataType);
549                         $count = 0;
550                     }
551             }
552             $countMessage .= ' ' . $count . ' ' . $dataType . '(s) /';
553         }
554         
555         $countMessage .= ' for application ' . $_application->name;
556         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . $countMessage);
557     }
558     
559     /**
560      * 
561      * @return Tinebase_Backend_Sql
562      */
563     protected function _getBackend()
564     {
565         if (!isset($this->_backend)) {
566             $this->_backend = new Tinebase_Backend_Sql(array(
567                 'modelName' => 'Tinebase_Model_Application', 
568                 'tableName' => 'applications'
569             ), $this->_getDb());
570         }
571         
572         return $this->_backend;
573     }
574     
575     /**
576      * 
577      * @return Zend_Db_Adapter_Abstract
578      */
579     protected function _getDb()
580     {
581         if (!isset($this->_db)) {
582             $this->_db = Tinebase_Core::getDb();
583         }
584         
585         return $this->_db;
586     }
587
588     /**
589      * returns the Models of all installed applications
590      * uses Tinebase_Application::getApplicationsByState
591      * and Tinebase_Controller_Abstract::getModels
592      *
593      * @return array
594      */
595     public function getModelsOfAllApplications()
596     {
597         $models = array();
598
599         $apps = $this->getApplicationsByState(Tinebase_Application::ENABLED);
600
601         /** @var Tinebase_Model_Application $app */
602         foreach($apps as $app) {
603             $controllerClass = $app->name . '_Controller';
604             if (!class_exists(($controllerClass))) {
605                 try {
606                     $controllerInstance = Tinebase_Core::getApplicationInstance($app->name);
607                 } catch(Tinebase_Exception_NotFound $tenf) {
608                     continue;
609                 }
610             } else {
611                 $controllerInstance = $controllerClass::getInstance();
612             }
613
614             $appModels = $controllerInstance->getModels();
615             if (is_array($appModels)) {
616                 $models = array_merge($models, $appModels);
617             }
618         }
619
620         return $models;
621     }
622
623     /**
624      * extract model and app name from model name
625      *
626      * @param mixed $modelOrApplication
627      * @param null $model
628      * @return array
629      */
630     public static function extractAppAndModel($modelOrApplication, $model = null)
631     {
632         if (! $modelOrApplication instanceof Tinebase_Model_Application && $modelOrApplication instanceof Tinebase_Record_Abstract) {
633             $modelOrApplication = get_class($modelOrApplication);
634         }
635
636         // modified (some model names can have both . and _ in their names and we should treat them as JS model name
637         if (strpos($modelOrApplication, '_') && ! strpos($modelOrApplication, '.')) {
638             // got (complete) model name name as first param
639             list($appName, /*$i*/, $modelName) = explode('_', $modelOrApplication, 3);
640         } else if (strpos($modelOrApplication, '.')) {
641             // got (complete) model name name as first param (JS style)
642             list(/*$j*/, $appName, /*$i*/, $modelName) = explode('.', $modelOrApplication, 4);
643         } else {
644             $appName = $modelOrApplication;
645             $modelName = $model;
646         }
647
648         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
649             . ' Extracted appName: ' . $appName . ' modelName: ' . $modelName);
650
651         return array(
652             'appName'   => $appName,
653             'modelName' => $modelName
654         );
655     }
656 }