Ian province abbreviation patch - issue 724
[civicrm-core.git] / CRM / Extension / Manager.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
e7112fa7 6 | Copyright CiviCRM LLC (c) 2004-2015 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * The extension manager handles installing, disabling enabling, and
30 * uninstalling extensions.
31 *
32 * @package CRM
e7112fa7 33 * @copyright CiviCRM LLC (c) 2004-2015
6a488035
TO
34 * $Id$
35 *
36 */
37class CRM_Extension_Manager {
38 /**
fe482240 39 * The extension is fully installed and enabled.
6a488035
TO
40 */
41 const STATUS_INSTALLED = 'installed';
42
43 /**
fe482240 44 * The extension config has been applied to database but deactivated.
6a488035
TO
45 */
46 const STATUS_DISABLED = 'disabled';
47
48 /**
49 * The extension code is visible, but nothing has been applied to DB
50 */
51 const STATUS_UNINSTALLED = 'uninstalled';
52
53 /**
54 * The extension code is not locally accessible
55 */
56 const STATUS_UNKNOWN = 'unknown';
57
58 /**
59 * The extension is fully installed and enabled
60 */
61 const STATUS_INSTALLED_MISSING = 'installed-missing';
62
63 /**
64 * The extension is fully installed and enabled
65 */
66 const STATUS_DISABLED_MISSING = 'disabled-missing';
67
68 /**
69 * @var CRM_Extension_Container_Interface
70 *
71 * Note: Treat as private. This is only public to facilitate debugging.
72 */
73 public $fullContainer;
74
75 /**
76 * @var CRM_Extension_Container_Basic|FALSE
77 *
78 * Note: Treat as private. This is only public to facilitate debugging.
79 */
80 public $defaultContainer;
81
82 /**
83 * @var CRM_Extension_Mapper
84 *
85 * Note: Treat as private. This is only public to facilitate debugging.
86 */
87 public $mapper;
88
89 /**
90 * @var array (typeName => CRM_Extension_Manager_Interface)
91 *
92 * Note: Treat as private. This is only public to facilitate debugging.
93 */
94 public $typeManagers;
95
96 /**
97 * @var array (extensionKey => statusConstant)
98 *
99 * Note: Treat as private. This is only public to facilitate debugging.
100 */
101 public $statuses;
102
103 /**
6c8f6e67 104 * @param CRM_Extension_Container_Interface $fullContainer
6a488035 105 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
6c8f6e67
EM
106 * @param CRM_Extension_Mapper $mapper
107 * @param $typeManagers
6a488035 108 */
00be9182 109 public function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
6a488035
TO
110 $this->fullContainer = $fullContainer;
111 $this->defaultContainer = $defaultContainer;
112 $this->mapper = $mapper;
113 $this->typeManagers = $typeManagers;
114 }
115
116 /**
117 * Install or upgrade the code for an extension -- and perform any
118 * necessary database changes (eg replacing extension metadata).
119 *
120 * This only works if the extension is stored in the default container.
121 *
f41911fd
TO
122 * @param string $tmpCodeDir
123 * Path to a local directory containing a copy of the new (inert) code.
6a488035
TO
124 * @return void
125 * @throws CRM_Extension_Exception
126 */
127 public function replace($tmpCodeDir) {
353ffa53 128 if (!$this->defaultContainer) {
6a488035
TO
129 throw new CRM_Extension_Exception("Default extension container is not configured");
130 }
131
132 $newInfo = CRM_Extension_Info::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
133 $oldStatus = $this->getStatus($newInfo->key);
134
135 // find $tgtPath, $oldInfo, $typeManager
136 switch ($oldStatus) {
137 case self::STATUS_UNINSTALLED:
138 case self::STATUS_INSTALLED:
139 case self::STATUS_DISABLED:
140 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
141 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key); // throws Exception
142 $tgtPath = $this->fullContainer->getPath($newInfo->key);
353ffa53 143 if (!CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
6a488035 144 // force installation in the default-container
615841de 145 $oldPath = $tgtPath;
6a488035
TO
146 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
147 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(
148 1 => $newInfo->key,
149 2 => $oldPath,
150 )));
151 }
152 break;
b3a4b879 153
6a488035
TO
154 case self::STATUS_INSTALLED_MISSING:
155 case self::STATUS_DISABLED_MISSING:
b3a4b879 156 // the extension does not exist in any container; we're free to put it anywhere
6a488035
TO
157 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
158 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key); // throws Exception
159 break;
b3a4b879 160
6a488035 161 case self::STATUS_UNKNOWN:
b3a4b879 162 // the extension does not exist in any container; we're free to put it anywhere
6a488035
TO
163 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
164 $oldInfo = $typeManager = NULL;
165 break;
b3a4b879 166
6a488035 167 default:
615841de 168 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
169 }
170
171 // move the code!
172 switch ($oldStatus) {
173 case self::STATUS_UNINSTALLED:
174 case self::STATUS_UNKNOWN:
175 // There are no DB records to worry about, so we'll just put the files in place
176 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
177 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
178 }
179 break;
b3a4b879 180
6a488035
TO
181 case self::STATUS_INSTALLED:
182 case self::STATUS_INSTALLED_MISSING:
183 case self::STATUS_DISABLED:
184 case self::STATUS_DISABLED_MISSING:
185 // There are DB records; coordinate the file placement with the DB updates
186 $typeManager->onPreReplace($oldInfo, $newInfo);
187 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
188 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
189 }
190 $this->_updateExtensionEntry($newInfo);
191 $typeManager->onPostReplace($oldInfo, $newInfo);
192 break;
b3a4b879 193
6a488035 194 default:
615841de 195 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
196 }
197
198 $this->refresh();
a618b2b8 199 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
6a488035
TO
200 }
201
202 /**
203 * Add records of the extension to the database -- and enable it
204 *
f41911fd
TO
205 * @param array $keys
206 * List of extension keys.
6a488035
TO
207 * @return void
208 * @throws CRM_Extension_Exception
209 */
210 public function install($keys) {
211 $origStatuses = $this->getStatuses();
212
213 // TODO: to mitigate the risk of crashing during installation, scan
214 // keys/statuses/types before doing anything
215
216 foreach ($keys as $key) {
217 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
218
219 switch ($origStatuses[$key]) {
220 case self::STATUS_INSTALLED:
221 // ok, nothing to do
222 break;
b3a4b879 223
6a488035
TO
224 case self::STATUS_DISABLED:
225 // re-enable it
226 $typeManager->onPreEnable($info);
227 $this->_setExtensionActive($info, 1);
228 $typeManager->onPostEnable($info);
229 break;
b3a4b879 230
6a488035
TO
231 case self::STATUS_UNINSTALLED:
232 // install anew
233 $typeManager->onPreInstall($info);
234 $this->_createExtensionEntry($info);
235 $typeManager->onPostInstall($info);
236 break;
b3a4b879 237
6a488035
TO
238 case self::STATUS_UNKNOWN:
239 default:
240 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
241 }
242 }
243
244 $this->statuses = NULL;
245 $this->mapper->refresh();
246 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
3d0e24ec 247
248 foreach ($keys as $key) {
249 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
250 //print_r(array('post post?', $info, 'k' => $key, 'os'=> $origStatuses[$key]));
251
252 switch ($origStatuses[$key]) {
253 case self::STATUS_INSTALLED:
254 // ok, nothing to do
255 break;
b3a4b879 256
3d0e24ec 257 case self::STATUS_DISABLED:
258 // re-enable it
259 break;
b3a4b879 260
3d0e24ec 261 case self::STATUS_UNINSTALLED:
262 // install anew
263 $typeManager->onPostPostInstall($info);
264 break;
b3a4b879 265
3d0e24ec 266 case self::STATUS_UNKNOWN:
267 default:
268 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
269 }
270 }
271
6a488035
TO
272 }
273
274 /**
275 * Add records of the extension to the database -- and enable it
276 *
f41911fd
TO
277 * @param array $keys
278 * List of extension keys.
6a488035
TO
279 * @return void
280 * @throws CRM_Extension_Exception
281 */
282 public function enable($keys) {
283 $this->install($keys);
284 }
285
286 /**
287 * Add records of the extension to the database -- and enable it
288 *
f41911fd
TO
289 * @param array $keys
290 * List of extension keys.
6a488035
TO
291 * @return void
292 * @throws CRM_Extension_Exception
293 */
294 public function disable($keys) {
295 $origStatuses = $this->getStatuses();
296
297 // TODO: to mitigate the risk of crashing during installation, scan
298 // keys/statuses/types before doing anything
299
300 foreach ($keys as $key) {
301 switch ($origStatuses[$key]) {
302 case self::STATUS_INSTALLED:
303 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
304 $typeManager->onPreDisable($info);
305 $this->_setExtensionActive($info, 0);
306 $typeManager->onPostDisable($info);
307 break;
b3a4b879 308
6a488035
TO
309 case self::STATUS_INSTALLED_MISSING:
310 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
311 $typeManager->onPreDisable($info);
312 $this->_setExtensionActive($info, 0);
313 $typeManager->onPostDisable($info);
314 break;
b3a4b879 315
6a488035
TO
316 case self::STATUS_DISABLED:
317 case self::STATUS_DISABLED_MISSING:
318 case self::STATUS_UNINSTALLED:
319 // ok, nothing to do
320 break;
b3a4b879 321
6a488035
TO
322 case self::STATUS_UNKNOWN:
323 default:
324 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
325 }
326 }
327
328 $this->statuses = NULL;
329 $this->mapper->refresh();
330 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
331 }
332
333 /**
fe482240 334 * Remove all database references to an extension.
6a488035
TO
335 *
336 * Add records of the extension to the database -- and enable it
337 *
f41911fd
TO
338 * @param array $keys
339 * List of extension keys.
6a488035
TO
340 * @return void
341 * @throws CRM_Extension_Exception
342 */
343 public function uninstall($keys) {
344 $origStatuses = $this->getStatuses();
345
346 // TODO: to mitigate the risk of crashing during installation, scan
347 // keys/statuses/types before doing anything
348
b3a4b879 349 foreach ($keys as $key) {
6a488035
TO
350 switch ($origStatuses[$key]) {
351 case self::STATUS_INSTALLED:
352 case self::STATUS_INSTALLED_MISSING:
353 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
b3a4b879 354
6a488035
TO
355 case self::STATUS_DISABLED:
356 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
357 $typeManager->onPreUninstall($info);
358 $this->_removeExtensionEntry($info);
359 $typeManager->onPostUninstall($info);
360 break;
b3a4b879 361
6a488035
TO
362 case self::STATUS_DISABLED_MISSING:
363 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
364 $typeManager->onPreUninstall($info);
365 $this->_removeExtensionEntry($info);
366 $typeManager->onPostUninstall($info);
367 break;
b3a4b879 368
6a488035
TO
369 case self::STATUS_UNINSTALLED:
370 // ok, nothing to do
371 break;
b3a4b879 372
6a488035
TO
373 case self::STATUS_UNKNOWN:
374 default:
375 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
376 }
377 }
378
379 $this->statuses = NULL;
380 $this->mapper->refresh();
381 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
382 }
383
384 /**
fe482240 385 * Determine the status of an extension.
6a488035 386 *
77b97be7
EM
387 * @param $key
388 *
a6c01b45
CW
389 * @return string
390 * constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
6a488035
TO
391 */
392 public function getStatus($key) {
393 $statuses = $this->getStatuses();
394 if (array_key_exists($key, $statuses)) {
395 return $statuses[$key];
0db6c3e1
TO
396 }
397 else {
6a488035
TO
398 return self::STATUS_UNKNOWN;
399 }
400 }
401
402 /**
fe482240 403 * Determine the status of all extensions.
6a488035 404 *
a6c01b45
CW
405 * @return array
406 * ($key => status_constant)
6a488035
TO
407 */
408 public function getStatuses() {
409 if (!is_array($this->statuses)) {
410 $this->statuses = array();
411
412 foreach ($this->fullContainer->getKeys() as $key) {
413 $this->statuses[$key] = self::STATUS_UNINSTALLED;
414 }
415
416 $sql = '
417 SELECT full_name, is_active
418 FROM civicrm_extension
419 ';
420 $dao = CRM_Core_DAO::executeQuery($sql);
421 while ($dao->fetch()) {
422 try {
423 $path = $this->fullContainer->getPath($dao->full_name);
424 $codeExists = !empty($path) && is_dir($path);
0db6c3e1
TO
425 }
426 catch (CRM_Extension_Exception $e) {
6a488035
TO
427 $codeExists = FALSE;
428 }
429 if ($dao->is_active) {
430 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
0db6c3e1
TO
431 }
432 else {
6a488035
TO
433 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
434 }
435 }
436 }
437 return $this->statuses;
438 }
439
440 public function refresh() {
441 $this->statuses = NULL;
442 $this->fullContainer->refresh(); // and, indirectly, defaultContainer
443 $this->mapper->refresh();
444 }
445
446 // ----------------------
447
448 /**
449 * Find the $info and $typeManager for a $key
450 *
77b97be7
EM
451 * @param $key
452 *
6a488035 453 * @throws CRM_Extension_Exception
a6c01b45
CW
454 * @return array
455 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
6a488035
TO
456 */
457 private function _getInfoTypeHandler($key) {
458 $info = $this->mapper->keyToInfo($key); // throws Exception
459 if (array_key_exists($info->type, $this->typeManagers)) {
460 return array($info, $this->typeManagers[$info->type]);
0db6c3e1
TO
461 }
462 else {
6a488035
TO
463 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
464 }
465 }
466
467 /**
468 * Find the $info and $typeManager for a $key
469 *
2a6da8d7
EM
470 * @param $key
471 *
6a488035 472 * @throws CRM_Extension_Exception
a6c01b45
CW
473 * @return array
474 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
6a488035
TO
475 */
476 private function _getMissingInfoTypeHandler($key) {
477 $info = $this->createInfoFromDB($key);
478 if ($info) {
479 if (array_key_exists($info->type, $this->typeManagers)) {
480 return array($info, $this->typeManagers[$info->type]);
0db6c3e1
TO
481 }
482 else {
6a488035
TO
483 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
484 }
0db6c3e1
TO
485 }
486 else {
6a488035
TO
487 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
488 }
489 }
490
e0ef6999
EM
491 /**
492 * @param CRM_Extension_Info $info
493 *
494 * @return bool
495 */
6a488035
TO
496 private function _createExtensionEntry(CRM_Extension_Info $info) {
497 $dao = new CRM_Core_DAO_Extension();
498 $dao->label = $info->label;
499 $dao->name = $info->name;
500 $dao->full_name = $info->key;
501 $dao->type = $info->type;
502 $dao->file = $info->file;
503 $dao->is_active = 1;
504 return (bool) ($dao->insert());
505 }
506
e0ef6999
EM
507 /**
508 * @param CRM_Extension_Info $info
509 *
510 * @return bool
511 */
6a488035
TO
512 private function _updateExtensionEntry(CRM_Extension_Info $info) {
513 $dao = new CRM_Core_DAO_Extension();
514 $dao->full_name = $info->key;
515 if ($dao->find(TRUE)) {
516 $dao->label = $info->label;
517 $dao->name = $info->name;
518 $dao->full_name = $info->key;
519 $dao->type = $info->type;
520 $dao->file = $info->file;
521 $dao->is_active = 1;
522 return (bool) ($dao->update());
0db6c3e1
TO
523 }
524 else {
6a488035
TO
525 return $this->_createExtensionEntry($info);
526 }
527 }
528
e0ef6999
EM
529 /**
530 * @param CRM_Extension_Info $info
531 *
532 * @throws CRM_Extension_Exception
533 */
6a488035
TO
534 private function _removeExtensionEntry(CRM_Extension_Info $info) {
535 $dao = new CRM_Core_DAO_Extension();
536 $dao->full_name = $info->key;
537 if ($dao->find(TRUE)) {
538 if (CRM_Core_BAO_Extension::del($dao->id)) {
539 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
0db6c3e1
TO
540 }
541 else {
6a488035
TO
542 throw new CRM_Extension_Exception("Failed to remove extension entry");
543 }
544 } // else: post-condition already satisified
545 }
546
e0ef6999
EM
547 /**
548 * @param CRM_Extension_Info $info
549 * @param $isActive
550 */
6a488035
TO
551 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
552 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
553 1 => array($isActive, 'Integer'),
554 2 => array($info->key, 'String'),
555 ));
556 }
557
558 /**
559 * Auto-generate a place-holder for a missing extension using info from
560 * database.
561 *
2a6da8d7 562 * @param $key
6a488035
TO
563 * @return CRM_Extension_Info|NULL
564 */
565 public function createInfoFromDB($key) {
566 $dao = new CRM_Core_DAO_Extension();
567 $dao->full_name = $key;
568 if ($dao->find(TRUE)) {
569 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
570 return $info;
0db6c3e1
TO
571 }
572 else {
6a488035
TO
573 return NULL;
574 }
575 }
96025800 576
6a488035 577}