3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
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-2015
35 class CRM_Extension_Manager
{
37 * The extension is fully installed and enabled.
39 const STATUS_INSTALLED
= 'installed';
42 * The extension config has been applied to database but deactivated.
44 const STATUS_DISABLED
= 'disabled';
47 * The extension code is visible, but nothing has been applied to DB
49 const STATUS_UNINSTALLED
= 'uninstalled';
52 * The extension code is not locally accessible
54 const STATUS_UNKNOWN
= 'unknown';
57 * The extension is fully installed and enabled
59 const STATUS_INSTALLED_MISSING
= 'installed-missing';
62 * The extension is fully installed and enabled
64 const STATUS_DISABLED_MISSING
= 'disabled-missing';
67 * @var CRM_Extension_Container_Interface
69 * Note: Treat as private. This is only public to facilitate debugging.
71 public $fullContainer;
74 * @var CRM_Extension_Container_Basic|FALSE
76 * Note: Treat as private. This is only public to facilitate debugging.
78 public $defaultContainer;
81 * @var CRM_Extension_Mapper
83 * Note: Treat as private. This is only public to facilitate debugging.
88 * @var array (typeName => CRM_Extension_Manager_Interface)
90 * Note: Treat as private. This is only public to facilitate debugging.
95 * @var array (extensionKey => statusConstant)
97 * Note: Treat as private. This is only public to facilitate debugging.
102 * @param CRM_Extension_Container_Interface $fullContainer
103 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
104 * @param CRM_Extension_Mapper $mapper
105 * @param $typeManagers
107 public function __construct(CRM_Extension_Container_Interface
$fullContainer, $defaultContainer, CRM_Extension_Mapper
$mapper, $typeManagers) {
108 $this->fullContainer
= $fullContainer;
109 $this->defaultContainer
= $defaultContainer;
110 $this->mapper
= $mapper;
111 $this->typeManagers
= $typeManagers;
115 * Install or upgrade the code for an extension -- and perform any
116 * necessary database changes (eg replacing extension metadata).
118 * This only works if the extension is stored in the default container.
120 * @param string $tmpCodeDir
121 * Path to a local directory containing a copy of the new (inert) code.
122 * @throws CRM_Extension_Exception
124 public function replace($tmpCodeDir) {
125 if (!$this->defaultContainer
) {
126 throw new CRM_Extension_Exception("Default extension container is not configured");
129 $newInfo = CRM_Extension_Info
::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR
. CRM_Extension_Info
::FILENAME
);
130 $oldStatus = $this->getStatus($newInfo->key
);
132 // find $tgtPath, $oldInfo, $typeManager
133 switch ($oldStatus) {
134 case self
::STATUS_UNINSTALLED
:
135 case self
::STATUS_INSTALLED
:
136 case self
::STATUS_DISABLED
:
137 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
138 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key
); // throws Exception
139 $tgtPath = $this->fullContainer
->getPath($newInfo->key
);
140 if (!CRM_Utils_File
::isChildPath($this->defaultContainer
->getBaseDir(), $tgtPath)) {
141 // force installation in the default-container
143 $tgtPath = $this->defaultContainer
->getBaseDir() . DIRECTORY_SEPARATOR
. $newInfo->key
;
144 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(
151 case self
::STATUS_INSTALLED_MISSING
:
152 case self
::STATUS_DISABLED_MISSING
:
153 // the extension does not exist in any container; we're free to put it anywhere
154 $tgtPath = $this->defaultContainer
->getBaseDir() . DIRECTORY_SEPARATOR
. $newInfo->key
;
155 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;
165 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
169 switch ($oldStatus) {
170 case self
::STATUS_UNINSTALLED
:
171 case self
::STATUS_UNKNOWN
:
172 // There are no DB records to worry about, so we'll just put the files in place
173 if (!CRM_Utils_File
::replaceDir($tmpCodeDir, $tgtPath)) {
174 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
178 case self
::STATUS_INSTALLED
:
179 case self
::STATUS_INSTALLED_MISSING
:
180 case self
::STATUS_DISABLED
:
181 case self
::STATUS_DISABLED_MISSING
:
182 // There are DB records; coordinate the file placement with the DB updates
183 $typeManager->onPreReplace($oldInfo, $newInfo);
184 if (!CRM_Utils_File
::replaceDir($tmpCodeDir, $tgtPath)) {
185 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
187 $this->_updateExtensionEntry($newInfo);
188 $typeManager->onPostReplace($oldInfo, $newInfo);
192 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
196 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
200 * Add records of the extension to the database -- and enable it
203 * List of extension keys.
204 * @throws CRM_Extension_Exception
206 public function install($keys) {
207 $origStatuses = $this->getStatuses();
209 // TODO: to mitigate the risk of crashing during installation, scan
210 // keys/statuses/types before doing anything
212 foreach ($keys as $key) {
213 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
215 switch ($origStatuses[$key]) {
216 case self
::STATUS_INSTALLED
:
220 case self
::STATUS_DISABLED
:
222 $typeManager->onPreEnable($info);
223 $this->_setExtensionActive($info, 1);
224 $typeManager->onPostEnable($info);
227 case self
::STATUS_UNINSTALLED
:
229 $typeManager->onPreInstall($info);
230 $this->_createExtensionEntry($info);
231 $typeManager->onPostInstall($info);
234 case self
::STATUS_UNKNOWN
:
236 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
240 $this->statuses
= NULL;
241 $this->mapper
->refresh();
242 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
243 $schema = new CRM_Logging_Schema();
244 $schema->fixSchemaDifferences();
246 foreach ($keys as $key) {
247 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
249 switch ($origStatuses[$key]) {
250 case self
::STATUS_INSTALLED
:
254 case self
::STATUS_DISABLED
:
258 case self
::STATUS_UNINSTALLED
:
260 $typeManager->onPostPostInstall($info);
263 case self
::STATUS_UNKNOWN
:
265 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
272 * Add records of the extension to the database -- and enable it
275 * List of extension keys.
276 * @throws CRM_Extension_Exception
278 public function enable($keys) {
279 $this->install($keys);
283 * Add records of the extension to the database -- and enable it
286 * List of extension keys.
287 * @throws CRM_Extension_Exception
289 public function disable($keys) {
290 $origStatuses = $this->getStatuses();
292 // TODO: to mitigate the risk of crashing during installation, scan
293 // keys/statuses/types before doing anything
295 foreach ($keys as $key) {
296 switch ($origStatuses[$key]) {
297 case self
::STATUS_INSTALLED
:
298 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
299 $typeManager->onPreDisable($info);
300 $this->_setExtensionActive($info, 0);
301 $typeManager->onPostDisable($info);
304 case self
::STATUS_INSTALLED_MISSING
:
305 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
306 $typeManager->onPreDisable($info);
307 $this->_setExtensionActive($info, 0);
308 $typeManager->onPostDisable($info);
311 case self
::STATUS_DISABLED
:
312 case self
::STATUS_DISABLED_MISSING
:
313 case self
::STATUS_UNINSTALLED
:
317 case self
::STATUS_UNKNOWN
:
319 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
323 $this->statuses
= NULL;
324 $this->mapper
->refresh();
325 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
329 * Remove all database references to an extension.
331 * Add records of the extension to the database -- and enable it
334 * List of extension keys.
335 * @throws CRM_Extension_Exception
337 public function uninstall($keys) {
338 $origStatuses = $this->getStatuses();
340 // TODO: to mitigate the risk of crashing during installation, scan
341 // keys/statuses/types before doing anything
343 foreach ($keys as $key) {
344 switch ($origStatuses[$key]) {
345 case self
::STATUS_INSTALLED
:
346 case self
::STATUS_INSTALLED_MISSING
:
347 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
349 case self
::STATUS_DISABLED
:
350 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
351 $typeManager->onPreUninstall($info);
352 $this->_removeExtensionEntry($info);
353 $typeManager->onPostUninstall($info);
356 case self
::STATUS_DISABLED_MISSING
:
357 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
358 $typeManager->onPreUninstall($info);
359 $this->_removeExtensionEntry($info);
360 $typeManager->onPostUninstall($info);
363 case self
::STATUS_UNINSTALLED
:
367 case self
::STATUS_UNKNOWN
:
369 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
373 $this->statuses
= NULL;
374 $this->mapper
->refresh();
375 CRM_Core_Invoke
::rebuildMenuAndCaches(TRUE);
379 * Determine the status of an extension.
384 * constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
386 public function getStatus($key) {
387 $statuses = $this->getStatuses();
388 if (array_key_exists($key, $statuses)) {
389 return $statuses[$key];
392 return self
::STATUS_UNKNOWN
;
397 * Determine the status of all extensions.
400 * ($key => status_constant)
402 public function getStatuses() {
403 if (!is_array($this->statuses
)) {
404 $this->statuses
= array();
406 foreach ($this->fullContainer
->getKeys() as $key) {
407 $this->statuses
[$key] = self
::STATUS_UNINSTALLED
;
411 SELECT full_name, is_active
412 FROM civicrm_extension
414 $dao = CRM_Core_DAO
::executeQuery($sql);
415 while ($dao->fetch()) {
417 $path = $this->fullContainer
->getPath($dao->full_name
);
418 $codeExists = !empty($path) && is_dir($path);
420 catch (CRM_Extension_Exception
$e) {
423 if ($dao->is_active
) {
424 $this->statuses
[$dao->full_name
] = $codeExists ? self
::STATUS_INSTALLED
: self
::STATUS_INSTALLED_MISSING
;
427 $this->statuses
[$dao->full_name
] = $codeExists ? self
::STATUS_DISABLED
: self
::STATUS_DISABLED_MISSING
;
431 return $this->statuses
;
434 public function refresh() {
435 $this->statuses
= NULL;
436 $this->fullContainer
->refresh(); // and, indirectly, defaultContainer
437 $this->mapper
->refresh();
440 // ----------------------
443 * Find the $info and $typeManager for a $key
447 * @throws CRM_Extension_Exception
449 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
451 private function _getInfoTypeHandler($key) {
452 $info = $this->mapper
->keyToInfo($key); // throws Exception
453 if (array_key_exists($info->type
, $this->typeManagers
)) {
454 return array($info, $this->typeManagers
[$info->type
]);
457 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type
);
462 * Find the $info and $typeManager for a $key
466 * @throws CRM_Extension_Exception
468 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
470 private function _getMissingInfoTypeHandler($key) {
471 $info = $this->createInfoFromDB($key);
473 if (array_key_exists($info->type
, $this->typeManagers
)) {
474 return array($info, $this->typeManagers
[$info->type
]);
477 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type
);
481 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
486 * @param CRM_Extension_Info $info
490 private function _createExtensionEntry(CRM_Extension_Info
$info) {
491 $dao = new CRM_Core_DAO_Extension();
492 $dao->label
= $info->label
;
493 $dao->name
= $info->name
;
494 $dao->full_name
= $info->key
;
495 $dao->type
= $info->type
;
496 $dao->file
= $info->file
;
498 return (bool) ($dao->insert());
502 * @param CRM_Extension_Info $info
506 private function _updateExtensionEntry(CRM_Extension_Info
$info) {
507 $dao = new CRM_Core_DAO_Extension();
508 $dao->full_name
= $info->key
;
509 if ($dao->find(TRUE)) {
510 $dao->label
= $info->label
;
511 $dao->name
= $info->name
;
512 $dao->full_name
= $info->key
;
513 $dao->type
= $info->type
;
514 $dao->file
= $info->file
;
516 return (bool) ($dao->update());
519 return $this->_createExtensionEntry($info);
524 * @param CRM_Extension_Info $info
526 * @throws CRM_Extension_Exception
528 private function _removeExtensionEntry(CRM_Extension_Info
$info) {
529 $dao = new CRM_Core_DAO_Extension();
530 $dao->full_name
= $info->key
;
531 if ($dao->find(TRUE)) {
532 if (CRM_Core_BAO_Extension
::del($dao->id
)) {
533 CRM_Core_Session
::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
536 throw new CRM_Extension_Exception("Failed to remove extension entry");
538 } // else: post-condition already satisified
542 * @param CRM_Extension_Info $info
545 private function _setExtensionActive(CRM_Extension_Info
$info, $isActive) {
546 CRM_Core_DAO
::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
547 1 => array($isActive, 'Integer'),
548 2 => array($info->key
, 'String'),
553 * Auto-generate a place-holder for a missing extension using info from
557 * @return CRM_Extension_Info|NULL
559 public function createInfoFromDB($key) {
560 $dao = new CRM_Core_DAO_Extension();
561 $dao->full_name
= $key;
562 if ($dao->find(TRUE)) {
563 $info = new CRM_Extension_Info($dao->full_name
, $dao->type
, $dao->name
, $dao->label
, $dao->file
);