Completely uninstall obsolete extensions during upgrade
[civicrm-core.git] / CRM / Extension / Manager.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
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
6b83d5bd 33 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
34 */
35class CRM_Extension_Manager {
36 /**
fe482240 37 * The extension is fully installed and enabled.
6a488035
TO
38 */
39 const STATUS_INSTALLED = 'installed';
40
41 /**
fe482240 42 * The extension config has been applied to database but deactivated.
6a488035
TO
43 */
44 const STATUS_DISABLED = 'disabled';
45
46 /**
47 * The extension code is visible, but nothing has been applied to DB
48 */
49 const STATUS_UNINSTALLED = 'uninstalled';
50
51 /**
52 * The extension code is not locally accessible
53 */
54 const STATUS_UNKNOWN = 'unknown';
55
56 /**
25fcba46 57 * The extension is installed but the code is not accessible
6a488035
TO
58 */
59 const STATUS_INSTALLED_MISSING = 'installed-missing';
60
61 /**
25fcba46 62 * The extension was installed and is now disabled; the code is not accessible
6a488035
TO
63 */
64 const STATUS_DISABLED_MISSING = 'disabled-missing';
65
66 /**
67 * @var CRM_Extension_Container_Interface
68 *
69 * Note: Treat as private. This is only public to facilitate debugging.
70 */
71 public $fullContainer;
72
73 /**
041ecc95 74 * Default container.
75 *
76 * @var CRM_Extension_Container_Basic|false
6a488035
TO
77 *
78 * Note: Treat as private. This is only public to facilitate debugging.
79 */
80 public $defaultContainer;
81
82 /**
041ecc95 83 * Mapper.
84 *
6a488035
TO
85 * @var CRM_Extension_Mapper
86 *
87 * Note: Treat as private. This is only public to facilitate debugging.
88 */
89 public $mapper;
90
91 /**
041ecc95 92 * Type managers.
93 *
94 * @var array
95 *
96 * Format is (typeName => CRM_Extension_Manager_Interface)
6a488035
TO
97 *
98 * Note: Treat as private. This is only public to facilitate debugging.
99 */
100 public $typeManagers;
101
102 /**
041ecc95 103 * Statuses.
104 *
105 * @var array
106 *
107 * Format is (extensionKey => statusConstant)
6a488035
TO
108 *
109 * Note: Treat as private. This is only public to facilitate debugging.
110 */
111 public $statuses;
112
113 /**
041ecc95 114 * Class constructor.
115 *
6c8f6e67 116 * @param CRM_Extension_Container_Interface $fullContainer
6a488035 117 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
6c8f6e67 118 * @param CRM_Extension_Mapper $mapper
041ecc95 119 * @param array $typeManagers
6a488035 120 */
00be9182 121 public function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
6a488035
TO
122 $this->fullContainer = $fullContainer;
123 $this->defaultContainer = $defaultContainer;
124 $this->mapper = $mapper;
125 $this->typeManagers = $typeManagers;
126 }
127
128 /**
129 * Install or upgrade the code for an extension -- and perform any
130 * necessary database changes (eg replacing extension metadata).
131 *
132 * This only works if the extension is stored in the default container.
133 *
f41911fd
TO
134 * @param string $tmpCodeDir
135 * Path to a local directory containing a copy of the new (inert) code.
6a488035
TO
136 * @throws CRM_Extension_Exception
137 */
138 public function replace($tmpCodeDir) {
353ffa53 139 if (!$this->defaultContainer) {
6a488035
TO
140 throw new CRM_Extension_Exception("Default extension container is not configured");
141 }
142
143 $newInfo = CRM_Extension_Info::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
144 $oldStatus = $this->getStatus($newInfo->key);
145
146 // find $tgtPath, $oldInfo, $typeManager
147 switch ($oldStatus) {
148 case self::STATUS_UNINSTALLED:
149 case self::STATUS_INSTALLED:
150 case self::STATUS_DISABLED:
151 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
7b966967
SL
152 // throws Exception
153 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key);
6a488035 154 $tgtPath = $this->fullContainer->getPath($newInfo->key);
353ffa53 155 if (!CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
6a488035 156 // force installation in the default-container
615841de 157 $oldPath = $tgtPath;
6a488035 158 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
be2fb01f 159 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.', [
6a488035
TO
160 1 => $newInfo->key,
161 2 => $oldPath,
be2fb01f 162 ]));
6a488035
TO
163 }
164 break;
b3a4b879 165
6a488035
TO
166 case self::STATUS_INSTALLED_MISSING:
167 case self::STATUS_DISABLED_MISSING:
b3a4b879 168 // the extension does not exist in any container; we're free to put it anywhere
6a488035 169 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
7b966967
SL
170 // throws Exception
171 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key);
6a488035 172 break;
b3a4b879 173
6a488035 174 case self::STATUS_UNKNOWN:
b3a4b879 175 // the extension does not exist in any container; we're free to put it anywhere
6a488035
TO
176 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
177 $oldInfo = $typeManager = NULL;
178 break;
b3a4b879 179
6a488035 180 default:
615841de 181 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
182 }
183
184 // move the code!
185 switch ($oldStatus) {
186 case self::STATUS_UNINSTALLED:
187 case self::STATUS_UNKNOWN:
188 // There are no DB records to worry about, so we'll just put the files in place
189 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
190 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
191 }
192 break;
b3a4b879 193
6a488035
TO
194 case self::STATUS_INSTALLED:
195 case self::STATUS_INSTALLED_MISSING:
196 case self::STATUS_DISABLED:
197 case self::STATUS_DISABLED_MISSING:
198 // There are DB records; coordinate the file placement with the DB updates
199 $typeManager->onPreReplace($oldInfo, $newInfo);
200 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
201 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
202 }
203 $this->_updateExtensionEntry($newInfo);
204 $typeManager->onPostReplace($oldInfo, $newInfo);
205 break;
b3a4b879 206
6a488035 207 default:
615841de 208 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
209 }
210
211 $this->refresh();
a618b2b8 212 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
6a488035
TO
213 }
214
215 /**
216 * Add records of the extension to the database -- and enable it
217 *
df7a1988
CW
218 * @param string|array $keys
219 * One or more extension keys.
6a488035
TO
220 * @throws CRM_Extension_Exception
221 */
222 public function install($keys) {
df7a1988 223 $keys = (array) $keys;
6a488035
TO
224 $origStatuses = $this->getStatuses();
225
226 // TODO: to mitigate the risk of crashing during installation, scan
227 // keys/statuses/types before doing anything
228
25fcba46
CW
229 // Check compatibility
230 $incompatible = [];
6a488035 231 foreach ($keys as $key) {
25fcba46
CW
232 if ($this->isIncompatible($key)) {
233 $incompatible[] = $key;
234 }
235 }
236 if ($incompatible) {
237 throw new CRM_Extension_Exception('Cannot install incompatible extension: ' . implode(', ', $incompatible));
238 }
239
240 foreach ($keys as $key) {
241 /** @var CRM_Extension_Info $info */
242 /** @var CRM_Extension_Manager_Base $typeManager */
7b966967 243 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
244
245 switch ($origStatuses[$key]) {
246 case self::STATUS_INSTALLED:
247 // ok, nothing to do
248 break;
b3a4b879 249
6a488035
TO
250 case self::STATUS_DISABLED:
251 // re-enable it
252 $typeManager->onPreEnable($info);
253 $this->_setExtensionActive($info, 1);
254 $typeManager->onPostEnable($info);
34ba82e8
TO
255
256 // A full refresh would be preferrable but very slow. This at least allows
257 // later extensions to access classes from earlier extensions.
258 $this->statuses = NULL;
259 $this->mapper->refresh();
6a488035 260 break;
b3a4b879 261
6a488035
TO
262 case self::STATUS_UNINSTALLED:
263 // install anew
264 $typeManager->onPreInstall($info);
265 $this->_createExtensionEntry($info);
266 $typeManager->onPostInstall($info);
34ba82e8
TO
267
268 // A full refresh would be preferrable but very slow. This at least allows
269 // later extensions to access classes from earlier extensions.
270 $this->statuses = NULL;
271 $this->mapper->refresh();
6a488035 272 break;
b3a4b879 273
6a488035
TO
274 case self::STATUS_UNKNOWN:
275 default:
276 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
277 }
278 }
279
280 $this->statuses = NULL;
281 $this->mapper->refresh();
282 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
7ddd5e56
DK
283 $schema = new CRM_Logging_Schema();
284 $schema->fixSchemaDifferences();
3d0e24ec 285
286 foreach ($keys as $key) {
7b966967
SL
287 // throws Exception
288 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
3d0e24ec 289
290 switch ($origStatuses[$key]) {
291 case self::STATUS_INSTALLED:
292 // ok, nothing to do
293 break;
b3a4b879 294
3d0e24ec 295 case self::STATUS_DISABLED:
296 // re-enable it
297 break;
b3a4b879 298
3d0e24ec 299 case self::STATUS_UNINSTALLED:
300 // install anew
301 $typeManager->onPostPostInstall($info);
302 break;
b3a4b879 303
3d0e24ec 304 case self::STATUS_UNKNOWN:
305 default:
306 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
307 }
308 }
309
6a488035
TO
310 }
311
312 /**
313 * Add records of the extension to the database -- and enable it
314 *
f41911fd
TO
315 * @param array $keys
316 * List of extension keys.
6a488035
TO
317 * @throws CRM_Extension_Exception
318 */
319 public function enable($keys) {
320 $this->install($keys);
321 }
322
323 /**
25fcba46 324 * Disable extension without removing record from db.
6a488035 325 *
df7a1988
CW
326 * @param string|array $keys
327 * One or more extension keys.
6a488035
TO
328 * @throws CRM_Extension_Exception
329 */
330 public function disable($keys) {
df7a1988 331 $keys = (array) $keys;
6a488035
TO
332 $origStatuses = $this->getStatuses();
333
334 // TODO: to mitigate the risk of crashing during installation, scan
335 // keys/statuses/types before doing anything
336
f8a7cfff
TO
337 sort($keys);
338 $disableRequirements = $this->findDisableRequirements($keys);
7b966967
SL
339 // This munges order, but makes it comparable.
340 sort($disableRequirements);
f8a7cfff
TO
341 if ($keys !== $disableRequirements) {
342 throw new CRM_Extension_Exception_DependencyException("Cannot disable extension due dependencies. Consider disabling all these: " . implode(',', $disableRequirements));
343 }
344
6a488035
TO
345 foreach ($keys as $key) {
346 switch ($origStatuses[$key]) {
347 case self::STATUS_INSTALLED:
7b966967
SL
348 // throws Exception
349 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
350 $typeManager->onPreDisable($info);
351 $this->_setExtensionActive($info, 0);
352 $typeManager->onPostDisable($info);
353 break;
b3a4b879 354
6a488035 355 case self::STATUS_INSTALLED_MISSING:
7b966967
SL
356 // throws Exception
357 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
6a488035
TO
358 $typeManager->onPreDisable($info);
359 $this->_setExtensionActive($info, 0);
360 $typeManager->onPostDisable($info);
361 break;
b3a4b879 362
6a488035
TO
363 case self::STATUS_DISABLED:
364 case self::STATUS_DISABLED_MISSING:
365 case self::STATUS_UNINSTALLED:
366 // ok, nothing to do
367 break;
b3a4b879 368
6a488035
TO
369 case self::STATUS_UNKNOWN:
370 default:
371 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
372 }
373 }
374
375 $this->statuses = NULL;
376 $this->mapper->refresh();
377 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
378 }
379
380 /**
fe482240 381 * Remove all database references to an extension.
6a488035 382 *
df7a1988
CW
383 * @param string|array $keys
384 * One or more extension keys.
6a488035
TO
385 * @throws CRM_Extension_Exception
386 */
387 public function uninstall($keys) {
df7a1988 388 $keys = (array) $keys;
6a488035
TO
389 $origStatuses = $this->getStatuses();
390
391 // TODO: to mitigate the risk of crashing during installation, scan
392 // keys/statuses/types before doing anything
393
b3a4b879 394 foreach ($keys as $key) {
6a488035
TO
395 switch ($origStatuses[$key]) {
396 case self::STATUS_INSTALLED:
397 case self::STATUS_INSTALLED_MISSING:
398 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
b3a4b879 399
6a488035 400 case self::STATUS_DISABLED:
7b966967
SL
401 // throws Exception
402 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
403 $typeManager->onPreUninstall($info);
404 $this->_removeExtensionEntry($info);
405 $typeManager->onPostUninstall($info);
406 break;
b3a4b879 407
6a488035 408 case self::STATUS_DISABLED_MISSING:
7b966967
SL
409 // throws Exception
410 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
6a488035
TO
411 $typeManager->onPreUninstall($info);
412 $this->_removeExtensionEntry($info);
413 $typeManager->onPostUninstall($info);
414 break;
b3a4b879 415
6a488035
TO
416 case self::STATUS_UNINSTALLED:
417 // ok, nothing to do
418 break;
b3a4b879 419
6a488035
TO
420 case self::STATUS_UNKNOWN:
421 default:
422 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
423 }
424 }
425
426 $this->statuses = NULL;
427 $this->mapper->refresh();
428 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
429 }
430
431 /**
fe482240 432 * Determine the status of an extension.
6a488035 433 *
77b97be7
EM
434 * @param $key
435 *
a6c01b45 436 * @return string
25fcba46 437 * constant self::STATUS_*
6a488035
TO
438 */
439 public function getStatus($key) {
440 $statuses = $this->getStatuses();
441 if (array_key_exists($key, $statuses)) {
442 return $statuses[$key];
0db6c3e1
TO
443 }
444 else {
6a488035
TO
445 return self::STATUS_UNKNOWN;
446 }
447 }
448
25fcba46
CW
449 /**
450 * Check if a given extension is incompatible with this version of CiviCRM
451 *
452 * @param $key
453 * @return bool|array
454 */
455 public function isIncompatible($key) {
456 $info = CRM_Extension_System::getCompatibilityInfo();
457 return $info[$key] ?? FALSE;
458 }
459
6a488035 460 /**
fe482240 461 * Determine the status of all extensions.
6a488035 462 *
a6c01b45
CW
463 * @return array
464 * ($key => status_constant)
6a488035
TO
465 */
466 public function getStatuses() {
467 if (!is_array($this->statuses)) {
be2fb01f 468 $this->statuses = [];
6a488035
TO
469
470 foreach ($this->fullContainer->getKeys() as $key) {
471 $this->statuses[$key] = self::STATUS_UNINSTALLED;
472 }
473
474 $sql = '
475 SELECT full_name, is_active
476 FROM civicrm_extension
477 ';
478 $dao = CRM_Core_DAO::executeQuery($sql);
479 while ($dao->fetch()) {
480 try {
481 $path = $this->fullContainer->getPath($dao->full_name);
482 $codeExists = !empty($path) && is_dir($path);
0db6c3e1
TO
483 }
484 catch (CRM_Extension_Exception $e) {
6a488035
TO
485 $codeExists = FALSE;
486 }
487 if ($dao->is_active) {
488 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
0db6c3e1
TO
489 }
490 else {
6a488035
TO
491 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
492 }
493 }
494 }
495 return $this->statuses;
496 }
497
498 public function refresh() {
499 $this->statuses = NULL;
7b966967
SL
500 // and, indirectly, defaultContainer
501 $this->fullContainer->refresh();
6a488035
TO
502 $this->mapper->refresh();
503 }
504
505 // ----------------------
506
507 /**
508 * Find the $info and $typeManager for a $key
509 *
77b97be7
EM
510 * @param $key
511 *
6a488035 512 * @throws CRM_Extension_Exception
a6c01b45 513 * @return array
25fcba46 514 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
515 */
516 private function _getInfoTypeHandler($key) {
7b966967
SL
517 // throws Exception
518 $info = $this->mapper->keyToInfo($key);
6a488035 519 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 520 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
521 }
522 else {
6a488035
TO
523 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
524 }
525 }
526
527 /**
528 * Find the $info and $typeManager for a $key
529 *
2a6da8d7
EM
530 * @param $key
531 *
6a488035 532 * @throws CRM_Extension_Exception
a6c01b45 533 * @return array
25fcba46 534 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
535 */
536 private function _getMissingInfoTypeHandler($key) {
537 $info = $this->createInfoFromDB($key);
538 if ($info) {
539 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 540 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
541 }
542 else {
6a488035
TO
543 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
544 }
0db6c3e1
TO
545 }
546 else {
6a488035
TO
547 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
548 }
549 }
550
e0ef6999
EM
551 /**
552 * @param CRM_Extension_Info $info
553 *
554 * @return bool
555 */
6a488035
TO
556 private function _createExtensionEntry(CRM_Extension_Info $info) {
557 $dao = new CRM_Core_DAO_Extension();
558 $dao->label = $info->label;
559 $dao->name = $info->name;
560 $dao->full_name = $info->key;
561 $dao->type = $info->type;
562 $dao->file = $info->file;
563 $dao->is_active = 1;
564 return (bool) ($dao->insert());
565 }
566
e0ef6999
EM
567 /**
568 * @param CRM_Extension_Info $info
569 *
570 * @return bool
571 */
6a488035
TO
572 private function _updateExtensionEntry(CRM_Extension_Info $info) {
573 $dao = new CRM_Core_DAO_Extension();
574 $dao->full_name = $info->key;
575 if ($dao->find(TRUE)) {
576 $dao->label = $info->label;
577 $dao->name = $info->name;
578 $dao->full_name = $info->key;
579 $dao->type = $info->type;
580 $dao->file = $info->file;
581 $dao->is_active = 1;
582 return (bool) ($dao->update());
0db6c3e1
TO
583 }
584 else {
6a488035
TO
585 return $this->_createExtensionEntry($info);
586 }
587 }
588
e0ef6999
EM
589 /**
590 * @param CRM_Extension_Info $info
591 *
592 * @throws CRM_Extension_Exception
593 */
6a488035
TO
594 private function _removeExtensionEntry(CRM_Extension_Info $info) {
595 $dao = new CRM_Core_DAO_Extension();
596 $dao->full_name = $info->key;
597 if ($dao->find(TRUE)) {
598 if (CRM_Core_BAO_Extension::del($dao->id)) {
599 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
0db6c3e1
TO
600 }
601 else {
6a488035
TO
602 throw new CRM_Extension_Exception("Failed to remove extension entry");
603 }
604 } // else: post-condition already satisified
605 }
606
e0ef6999
EM
607 /**
608 * @param CRM_Extension_Info $info
609 * @param $isActive
610 */
6a488035 611 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
be2fb01f
CW
612 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', [
613 1 => [$isActive, 'Integer'],
614 2 => [$info->key, 'String'],
615 ]);
6a488035
TO
616 }
617
618 /**
619 * Auto-generate a place-holder for a missing extension using info from
620 * database.
621 *
2a6da8d7 622 * @param $key
6a488035
TO
623 * @return CRM_Extension_Info|NULL
624 */
625 public function createInfoFromDB($key) {
626 $dao = new CRM_Core_DAO_Extension();
627 $dao->full_name = $key;
628 if ($dao->find(TRUE)) {
629 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
630 return $info;
0db6c3e1
TO
631 }
632 else {
6a488035
TO
633 return NULL;
634 }
635 }
96025800 636
f8a7cfff
TO
637 /**
638 * Build a list of extensions to install, in an order that will satisfy dependencies.
639 *
640 * @param array $keys
641 * List of extensions to install.
642 * @return array
643 * List of extension keys, including dependencies, in order of installation.
644 */
645 public function findInstallRequirements($keys) {
646 $infos = $this->mapper->getAllInfos();
7b966967
SL
647 // array(string $key).
648 $todoKeys = array_unique($keys);
649 // array(string $key => 1);
650 $doneKeys = [];
f8a7cfff
TO
651 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
652
653 while (!empty($todoKeys)) {
654 $key = array_shift($todoKeys);
655 if (isset($doneKeys[$key])) {
656 continue;
657 }
658 $doneKeys[$key] = 1;
659
660 /** @var CRM_Extension_Info $info */
661 $info = @$infos[$key];
662
663 if ($this->getStatus($key) === self::STATUS_INSTALLED) {
be2fb01f 664 $sorter->add($key, []);
f8a7cfff
TO
665 }
666 elseif ($info && $info->requires) {
667 $sorter->add($key, $info->requires);
3ea86448 668 $todoKeys = array_merge($todoKeys, $info->requires);
f8a7cfff
TO
669 }
670 else {
be2fb01f 671 $sorter->add($key, []);
f8a7cfff
TO
672 }
673 }
674 return $sorter->sort();
675 }
676
677 /**
678 * Build a list of extensions to remove, in an order that will satisfy dependencies.
679 *
680 * @param array $keys
681 * List of extensions to install.
682 * @return array
683 * List of extension keys, including dependencies, in order of removal.
684 */
685 public function findDisableRequirements($keys) {
be2fb01f 686 $INSTALLED = [
f8a7cfff
TO
687 self::STATUS_INSTALLED,
688 self::STATUS_INSTALLED_MISSING,
be2fb01f 689 ];
f8a7cfff
TO
690 $installedInfos = $this->filterInfosByStatus($this->mapper->getAllInfos(), $INSTALLED);
691 $revMap = CRM_Extension_Info::buildReverseMap($installedInfos);
692 $todoKeys = array_unique($keys);
be2fb01f 693 $doneKeys = [];
f8a7cfff
TO
694 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
695
696 while (!empty($todoKeys)) {
697 $key = array_shift($todoKeys);
698 if (isset($doneKeys[$key])) {
699 continue;
700 }
701 $doneKeys[$key] = 1;
702
703 if (isset($revMap[$key])) {
704 $requiredBys = CRM_Utils_Array::collect('key',
705 $this->filterInfosByStatus($revMap[$key], $INSTALLED));
706 $sorter->add($key, $requiredBys);
707 $todoKeys = array_merge($todoKeys, $requiredBys);
708 }
709 else {
be2fb01f 710 $sorter->add($key, []);
f8a7cfff
TO
711 }
712 }
713 return $sorter->sort();
714 }
715
716 /**
717 * @param $infos
718 * @param $filterStatuses
719 * @return array
720 */
721 protected function filterInfosByStatus($infos, $filterStatuses) {
be2fb01f 722 $matches = [];
f8a7cfff
TO
723 foreach ($infos as $k => $v) {
724 if (in_array($this->getStatus($v->key), $filterStatuses)) {
725 $matches[$k] = $v;
726 }
727 }
728 return $matches;
729 }
730
6a488035 731}