3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
29 * The extension manager handles installing, disabling enabling, and
30 * uninstalling extensions.
33 * @copyright CiviCRM LLC (c) 2004-2014
37 class CRM_Extension_Manager
{
39 * The extension is fully installed and enabled
41 const STATUS_INSTALLED
= 'installed';
44 * The extension config has been applied to database but deactivated
46 const STATUS_DISABLED
= 'disabled';
49 * The extension code is visible, but nothing has been applied to DB
51 const STATUS_UNINSTALLED
= 'uninstalled';
54 * The extension code is not locally accessible
56 const STATUS_UNKNOWN
= 'unknown';
59 * The extension is fully installed and enabled
61 const STATUS_INSTALLED_MISSING
= 'installed-missing';
64 * The extension is fully installed and enabled
66 const STATUS_DISABLED_MISSING
= 'disabled-missing';
69 * @var CRM_Extension_Container_Interface
71 * Note: Treat as private. This is only public to facilitate debugging.
73 public $fullContainer;
76 * @var CRM_Extension_Container_Basic|FALSE
78 * Note: Treat as private. This is only public to facilitate debugging.
80 public $defaultContainer;
83 * @var CRM_Extension_Mapper
85 * Note: Treat as private. This is only public to facilitate debugging.
90 * @var array (typeName => CRM_Extension_Manager_Interface)
92 * Note: Treat as private. This is only public to facilitate debugging.
97 * @var array (extensionKey => statusConstant)
99 * Note: Treat as private. This is only public to facilitate debugging.
104 * @param CRM_Extension_Container_Interface $fullContainer
105 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
106 * @param CRM_Extension_Mapper $mapper
107 * @param $typeManagers
109 function __construct(CRM_Extension_Container_Interface
$fullContainer, $defaultContainer, CRM_Extension_Mapper
$mapper, $typeManagers) {
110 $this->fullContainer
= $fullContainer;
111 $this->defaultContainer
= $defaultContainer;
112 $this->mapper
= $mapper;
113 $this->typeManagers
= $typeManagers;
117 * Install or upgrade the code for an extension -- and perform any
118 * necessary database changes (eg replacing extension metadata).
120 * This only works if the extension is stored in the default container.
122 * @param string $tmpCodeDir path to a local directory containing a copy of the new (inert) code
124 * @throws CRM_Extension_Exception
126 public function replace($tmpCodeDir) {
127 if (! $this->defaultContainer
) {
128 throw new CRM_Extension_Exception("Default extension container is not configured");
131 $newInfo = CRM_Extension_Info
::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR
. CRM_Extension_Info
::FILENAME
);
132 $oldStatus = $this->getStatus($newInfo->key
);
134 // find $tgtPath, $oldInfo, $typeManager
135 switch ($oldStatus) {
136 case self
::STATUS_UNINSTALLED
:
137 case self
::STATUS_INSTALLED
:
138 case self
::STATUS_DISABLED
:
139 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
140 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key
); // throws Exception
141 $tgtPath = $this->fullContainer
->getPath($newInfo->key
);
142 if (! CRM_Utils_File
::isChildPath($this->defaultContainer
->getBaseDir(), $tgtPath)) {
143 // force installation in the default-container
145 $tgtPath = $this->defaultContainer
->getBaseDir() . DIRECTORY_SEPARATOR
. $newInfo->key
;
146 CRM_Core_Session
::setStatus(ts('A copy of the extension (%1) is in a system folder (%2). The system copy will be preserved, but the new copy will be used.', array(
152 case self
::STATUS_INSTALLED_MISSING
:
153 case self
::STATUS_DISABLED_MISSING
:
154 // the extension does not exist in any container; we're free to put it anywhere
155 $tgtPath = $this->defaultContainer
->getBaseDir() . DIRECTORY_SEPARATOR
. $newInfo->key
;
156 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key
); // throws Exception
158 case self
::STATUS_UNKNOWN
:
159 // the extension does not exist in any container; we're free to put it anywhere
160 $tgtPath = $this->defaultContainer
->getBaseDir() . DIRECTORY_SEPARATOR
. $newInfo->key
;
161 $oldInfo = $typeManager = NULL;
164 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
168 switch ($oldStatus) {
169 case self
::STATUS_UNINSTALLED
:
170 case self
::STATUS_UNKNOWN
:
171 // There are no DB records to worry about, so we'll just put the files in place
172 if (!CRM_Utils_File
::replaceDir($tmpCodeDir, $tgtPath)) {
173 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
176 case self
::STATUS_INSTALLED
:
177 case self
::STATUS_INSTALLED_MISSING
:
178 case self
::STATUS_DISABLED
:
179 case self
::STATUS_DISABLED_MISSING
:
180 // There are DB records; coordinate the file placement with the DB updates
181 $typeManager->onPreReplace($oldInfo, $newInfo);
182 if (!CRM_Utils_File
::replaceDir($tmpCodeDir, $tgtPath)) {
183 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
185 $this->_updateExtensionEntry($newInfo);
186 $typeManager->onPostReplace($oldInfo, $newInfo);
189 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
196 * Add records of the extension to the database -- and enable it
198 * @param array $keys list of extension keys
200 * @throws CRM_Extension_Exception
202 public function install($keys) {
203 $origStatuses = $this->getStatuses();
205 // TODO: to mitigate the risk of crashing during installation, scan
206 // keys/statuses/types before doing anything
208 foreach ($keys as $key) {
209 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
211 switch ($origStatuses[$key]) {
212 case self
::STATUS_INSTALLED
:
215 case self
::STATUS_DISABLED
:
217 $typeManager->onPreEnable($info);
218 $this->_setExtensionActive($info, 1);
219 $typeManager->onPostEnable($info);
221 case self
::STATUS_UNINSTALLED
:
223 $typeManager->onPreInstall($info);
224 $this->_createExtensionEntry($info);
225 $typeManager->onPostInstall($info);
227 case self
::STATUS_UNKNOWN
:
229 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
233 $this->statuses
= NULL;
234 $this->mapper
->refresh();
235 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
237 foreach ($keys as $key) {
238 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
239 //print_r(array('post post?', $info, 'k' => $key, 'os'=> $origStatuses[$key]));
241 switch ($origStatuses[$key]) {
242 case self
::STATUS_INSTALLED
:
245 case self
::STATUS_DISABLED
:
248 case self
::STATUS_UNINSTALLED
:
250 $typeManager->onPostPostInstall($info);
252 case self
::STATUS_UNKNOWN
:
254 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
261 * Add records of the extension to the database -- and enable it
263 * @param array $keys list of extension keys
265 * @throws CRM_Extension_Exception
267 public function enable($keys) {
268 $this->install($keys);
272 * Add records of the extension to the database -- and enable it
274 * @param array $keys list of extension keys
276 * @throws CRM_Extension_Exception
278 public function disable($keys) {
279 $origStatuses = $this->getStatuses();
281 // TODO: to mitigate the risk of crashing during installation, scan
282 // keys/statuses/types before doing anything
284 foreach ($keys as $key) {
285 switch ($origStatuses[$key]) {
286 case self
::STATUS_INSTALLED
:
287 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
288 $typeManager->onPreDisable($info);
289 $this->_setExtensionActive($info, 0);
290 $typeManager->onPostDisable($info);
292 case self
::STATUS_INSTALLED_MISSING
:
293 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
294 $typeManager->onPreDisable($info);
295 $this->_setExtensionActive($info, 0);
296 $typeManager->onPostDisable($info);
298 case self
::STATUS_DISABLED
:
299 case self
::STATUS_DISABLED_MISSING
:
300 case self
::STATUS_UNINSTALLED
:
303 case self
::STATUS_UNKNOWN
:
305 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
309 $this->statuses
= NULL;
310 $this->mapper
->refresh();
311 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
315 * Remove all database references to an extension
317 * Add records of the extension to the database -- and enable it
319 * @param array $keys list of extension keys
321 * @throws CRM_Extension_Exception
323 public function uninstall($keys) {
324 $origStatuses = $this->getStatuses();
326 // TODO: to mitigate the risk of crashing during installation, scan
327 // keys/statuses/types before doing anything
329 foreach ($keys as $key) {
330 switch ($origStatuses[$key]) {
331 case self
::STATUS_INSTALLED
:
332 case self
::STATUS_INSTALLED_MISSING
:
333 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
335 case self
::STATUS_DISABLED
:
336 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
337 $typeManager->onPreUninstall($info);
338 $this->_removeExtensionEntry($info);
339 $typeManager->onPostUninstall($info);
341 case self
::STATUS_DISABLED_MISSING
:
342 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
343 $typeManager->onPreUninstall($info);
344 $this->_removeExtensionEntry($info);
345 $typeManager->onPostUninstall($info);
347 case self
::STATUS_UNINSTALLED
:
350 case self
::STATUS_UNKNOWN
:
352 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
356 $this->statuses
= NULL;
357 $this->mapper
->refresh();
358 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
362 * Determine the status of an extension
366 * @return string constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
368 public function getStatus($key) {
369 $statuses = $this->getStatuses();
370 if (array_key_exists($key, $statuses)) {
371 return $statuses[$key];
373 return self
::STATUS_UNKNOWN
;
378 * Determine the status of all extensions
380 * @return array ($key => status_constant)
382 public function getStatuses() {
383 if (!is_array($this->statuses
)) {
384 $this->statuses
= array();
386 foreach ($this->fullContainer
->getKeys() as $key) {
387 $this->statuses
[$key] = self
::STATUS_UNINSTALLED
;
391 SELECT full_name, is_active
392 FROM civicrm_extension
394 $dao = CRM_Core_DAO
::executeQuery($sql);
395 while ($dao->fetch()) {
397 $path = $this->fullContainer
->getPath($dao->full_name
);
398 $codeExists = !empty($path) && is_dir($path);
399 } catch (CRM_Extension_Exception
$e) {
402 if ($dao->is_active
) {
403 $this->statuses
[$dao->full_name
] = $codeExists ? self
::STATUS_INSTALLED
: self
::STATUS_INSTALLED_MISSING
;
405 $this->statuses
[$dao->full_name
] = $codeExists ? self
::STATUS_DISABLED
: self
::STATUS_DISABLED_MISSING
;
409 return $this->statuses
;
412 public function refresh() {
413 $this->statuses
= NULL;
414 $this->fullContainer
->refresh(); // and, indirectly, defaultContainer
415 $this->mapper
->refresh();
418 // ----------------------
421 * Find the $info and $typeManager for a $key
425 * @throws CRM_Extension_Exception
426 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
428 private function _getInfoTypeHandler($key) {
429 $info = $this->mapper
->keyToInfo($key); // throws Exception
430 if (array_key_exists($info->type
, $this->typeManagers
)) {
431 return array($info, $this->typeManagers
[$info->type
]);
433 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type
);
438 * Find the $info and $typeManager for a $key
442 * @throws CRM_Extension_Exception
443 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
445 private function _getMissingInfoTypeHandler($key) {
446 $info = $this->createInfoFromDB($key);
448 if (array_key_exists($info->type
, $this->typeManagers
)) {
449 return array($info, $this->typeManagers
[$info->type
]);
451 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type
);
454 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
459 * @param CRM_Extension_Info $info
463 private function _createExtensionEntry(CRM_Extension_Info
$info) {
464 $dao = new CRM_Core_DAO_Extension();
465 $dao->label
= $info->label
;
466 $dao->name
= $info->name
;
467 $dao->full_name
= $info->key
;
468 $dao->type
= $info->type
;
469 $dao->file
= $info->file
;
471 return (bool) ($dao->insert());
475 * @param CRM_Extension_Info $info
479 private function _updateExtensionEntry(CRM_Extension_Info
$info) {
480 $dao = new CRM_Core_DAO_Extension();
481 $dao->full_name
= $info->key
;
482 if ($dao->find(TRUE)) {
483 $dao->label
= $info->label
;
484 $dao->name
= $info->name
;
485 $dao->full_name
= $info->key
;
486 $dao->type
= $info->type
;
487 $dao->file
= $info->file
;
489 return (bool) ($dao->update());
491 return $this->_createExtensionEntry($info);
496 * @param CRM_Extension_Info $info
498 * @throws CRM_Extension_Exception
500 private function _removeExtensionEntry(CRM_Extension_Info
$info) {
501 $dao = new CRM_Core_DAO_Extension();
502 $dao->full_name
= $info->key
;
503 if ($dao->find(TRUE)) {
504 if (CRM_Core_BAO_Extension
::del($dao->id
)) {
505 CRM_Core_Session
::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
507 throw new CRM_Extension_Exception("Failed to remove extension entry");
509 } // else: post-condition already satisified
513 * @param CRM_Extension_Info $info
516 private function _setExtensionActive(CRM_Extension_Info
$info, $isActive) {
517 CRM_Core_DAO
::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
518 1 => array($isActive, 'Integer'),
519 2 => array($info->key
, 'String'),
524 * Auto-generate a place-holder for a missing extension using info from
528 * @return CRM_Extension_Info|NULL
530 public function createInfoFromDB($key) {
531 $dao = new CRM_Core_DAO_Extension();
532 $dao->full_name
= $key;
533 if ($dao->find(TRUE)) {
534 $info = new CRM_Extension_Info($dao->full_name
, $dao->type
, $dao->name
, $dao->label
, $dao->file
);