Remove deprecated function.
[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 /**
57 * The extension is fully installed and enabled
58 */
59 const STATUS_INSTALLED_MISSING = 'installed-missing';
60
61 /**
62 * The extension is fully installed and enabled
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 *
f41911fd
TO
218 * @param array $keys
219 * List of extension keys.
6a488035
TO
220 * @throws CRM_Extension_Exception
221 */
222 public function install($keys) {
223 $origStatuses = $this->getStatuses();
224
225 // TODO: to mitigate the risk of crashing during installation, scan
226 // keys/statuses/types before doing anything
227
228 foreach ($keys as $key) {
7b966967
SL
229 // throws Exception
230 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
231
232 switch ($origStatuses[$key]) {
233 case self::STATUS_INSTALLED:
234 // ok, nothing to do
235 break;
b3a4b879 236
6a488035
TO
237 case self::STATUS_DISABLED:
238 // re-enable it
239 $typeManager->onPreEnable($info);
240 $this->_setExtensionActive($info, 1);
241 $typeManager->onPostEnable($info);
34ba82e8
TO
242
243 // A full refresh would be preferrable but very slow. This at least allows
244 // later extensions to access classes from earlier extensions.
245 $this->statuses = NULL;
246 $this->mapper->refresh();
6a488035 247 break;
b3a4b879 248
6a488035
TO
249 case self::STATUS_UNINSTALLED:
250 // install anew
251 $typeManager->onPreInstall($info);
252 $this->_createExtensionEntry($info);
253 $typeManager->onPostInstall($info);
34ba82e8
TO
254
255 // A full refresh would be preferrable but very slow. This at least allows
256 // later extensions to access classes from earlier extensions.
257 $this->statuses = NULL;
258 $this->mapper->refresh();
6a488035 259 break;
b3a4b879 260
6a488035
TO
261 case self::STATUS_UNKNOWN:
262 default:
263 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
264 }
265 }
266
267 $this->statuses = NULL;
268 $this->mapper->refresh();
269 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
7ddd5e56
DK
270 $schema = new CRM_Logging_Schema();
271 $schema->fixSchemaDifferences();
3d0e24ec 272
273 foreach ($keys as $key) {
7b966967
SL
274 // throws Exception
275 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
3d0e24ec 276
277 switch ($origStatuses[$key]) {
278 case self::STATUS_INSTALLED:
279 // ok, nothing to do
280 break;
b3a4b879 281
3d0e24ec 282 case self::STATUS_DISABLED:
283 // re-enable it
284 break;
b3a4b879 285
3d0e24ec 286 case self::STATUS_UNINSTALLED:
287 // install anew
288 $typeManager->onPostPostInstall($info);
289 break;
b3a4b879 290
3d0e24ec 291 case self::STATUS_UNKNOWN:
292 default:
293 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
294 }
295 }
296
6a488035
TO
297 }
298
299 /**
300 * Add records of the extension to the database -- and enable it
301 *
f41911fd
TO
302 * @param array $keys
303 * List of extension keys.
6a488035
TO
304 * @throws CRM_Extension_Exception
305 */
306 public function enable($keys) {
307 $this->install($keys);
308 }
309
310 /**
311 * Add records of the extension to the database -- and enable it
312 *
f41911fd
TO
313 * @param array $keys
314 * List of extension keys.
6a488035
TO
315 * @throws CRM_Extension_Exception
316 */
317 public function disable($keys) {
318 $origStatuses = $this->getStatuses();
319
320 // TODO: to mitigate the risk of crashing during installation, scan
321 // keys/statuses/types before doing anything
322
f8a7cfff
TO
323 sort($keys);
324 $disableRequirements = $this->findDisableRequirements($keys);
7b966967
SL
325 // This munges order, but makes it comparable.
326 sort($disableRequirements);
f8a7cfff
TO
327 if ($keys !== $disableRequirements) {
328 throw new CRM_Extension_Exception_DependencyException("Cannot disable extension due dependencies. Consider disabling all these: " . implode(',', $disableRequirements));
329 }
330
6a488035
TO
331 foreach ($keys as $key) {
332 switch ($origStatuses[$key]) {
333 case self::STATUS_INSTALLED:
7b966967
SL
334 // throws Exception
335 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
336 $typeManager->onPreDisable($info);
337 $this->_setExtensionActive($info, 0);
338 $typeManager->onPostDisable($info);
339 break;
b3a4b879 340
6a488035 341 case self::STATUS_INSTALLED_MISSING:
7b966967
SL
342 // throws Exception
343 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
6a488035
TO
344 $typeManager->onPreDisable($info);
345 $this->_setExtensionActive($info, 0);
346 $typeManager->onPostDisable($info);
347 break;
b3a4b879 348
6a488035
TO
349 case self::STATUS_DISABLED:
350 case self::STATUS_DISABLED_MISSING:
351 case self::STATUS_UNINSTALLED:
352 // ok, nothing to do
353 break;
b3a4b879 354
6a488035
TO
355 case self::STATUS_UNKNOWN:
356 default:
357 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
358 }
359 }
360
361 $this->statuses = NULL;
362 $this->mapper->refresh();
363 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
364 }
365
366 /**
fe482240 367 * Remove all database references to an extension.
6a488035
TO
368 *
369 * Add records of the extension to the database -- and enable it
370 *
f41911fd
TO
371 * @param array $keys
372 * List of extension keys.
6a488035
TO
373 * @throws CRM_Extension_Exception
374 */
375 public function uninstall($keys) {
376 $origStatuses = $this->getStatuses();
377
378 // TODO: to mitigate the risk of crashing during installation, scan
379 // keys/statuses/types before doing anything
380
b3a4b879 381 foreach ($keys as $key) {
6a488035
TO
382 switch ($origStatuses[$key]) {
383 case self::STATUS_INSTALLED:
384 case self::STATUS_INSTALLED_MISSING:
385 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
b3a4b879 386
6a488035 387 case self::STATUS_DISABLED:
7b966967
SL
388 // throws Exception
389 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
390 $typeManager->onPreUninstall($info);
391 $this->_removeExtensionEntry($info);
392 $typeManager->onPostUninstall($info);
393 break;
b3a4b879 394
6a488035 395 case self::STATUS_DISABLED_MISSING:
7b966967
SL
396 // throws Exception
397 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
6a488035
TO
398 $typeManager->onPreUninstall($info);
399 $this->_removeExtensionEntry($info);
400 $typeManager->onPostUninstall($info);
401 break;
b3a4b879 402
6a488035
TO
403 case self::STATUS_UNINSTALLED:
404 // ok, nothing to do
405 break;
b3a4b879 406
6a488035
TO
407 case self::STATUS_UNKNOWN:
408 default:
409 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
410 }
411 }
412
413 $this->statuses = NULL;
414 $this->mapper->refresh();
415 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
416 }
417
418 /**
fe482240 419 * Determine the status of an extension.
6a488035 420 *
77b97be7
EM
421 * @param $key
422 *
a6c01b45
CW
423 * @return string
424 * constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
6a488035
TO
425 */
426 public function getStatus($key) {
427 $statuses = $this->getStatuses();
428 if (array_key_exists($key, $statuses)) {
429 return $statuses[$key];
0db6c3e1
TO
430 }
431 else {
6a488035
TO
432 return self::STATUS_UNKNOWN;
433 }
434 }
435
436 /**
fe482240 437 * Determine the status of all extensions.
6a488035 438 *
a6c01b45
CW
439 * @return array
440 * ($key => status_constant)
6a488035
TO
441 */
442 public function getStatuses() {
443 if (!is_array($this->statuses)) {
be2fb01f 444 $this->statuses = [];
6a488035
TO
445
446 foreach ($this->fullContainer->getKeys() as $key) {
447 $this->statuses[$key] = self::STATUS_UNINSTALLED;
448 }
449
450 $sql = '
451 SELECT full_name, is_active
452 FROM civicrm_extension
453 ';
454 $dao = CRM_Core_DAO::executeQuery($sql);
455 while ($dao->fetch()) {
456 try {
457 $path = $this->fullContainer->getPath($dao->full_name);
458 $codeExists = !empty($path) && is_dir($path);
0db6c3e1
TO
459 }
460 catch (CRM_Extension_Exception $e) {
6a488035
TO
461 $codeExists = FALSE;
462 }
463 if ($dao->is_active) {
464 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
0db6c3e1
TO
465 }
466 else {
6a488035
TO
467 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
468 }
469 }
470 }
471 return $this->statuses;
472 }
473
474 public function refresh() {
475 $this->statuses = NULL;
7b966967
SL
476 // and, indirectly, defaultContainer
477 $this->fullContainer->refresh();
6a488035
TO
478 $this->mapper->refresh();
479 }
480
481 // ----------------------
482
483 /**
484 * Find the $info and $typeManager for a $key
485 *
77b97be7
EM
486 * @param $key
487 *
6a488035 488 * @throws CRM_Extension_Exception
a6c01b45
CW
489 * @return array
490 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
6a488035
TO
491 */
492 private function _getInfoTypeHandler($key) {
7b966967
SL
493 // throws Exception
494 $info = $this->mapper->keyToInfo($key);
6a488035 495 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 496 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
497 }
498 else {
6a488035
TO
499 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
500 }
501 }
502
503 /**
504 * Find the $info and $typeManager for a $key
505 *
2a6da8d7
EM
506 * @param $key
507 *
6a488035 508 * @throws CRM_Extension_Exception
a6c01b45
CW
509 * @return array
510 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
6a488035
TO
511 */
512 private function _getMissingInfoTypeHandler($key) {
513 $info = $this->createInfoFromDB($key);
514 if ($info) {
515 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 516 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
517 }
518 else {
6a488035
TO
519 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
520 }
0db6c3e1
TO
521 }
522 else {
6a488035
TO
523 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
524 }
525 }
526
e0ef6999
EM
527 /**
528 * @param CRM_Extension_Info $info
529 *
530 * @return bool
531 */
6a488035
TO
532 private function _createExtensionEntry(CRM_Extension_Info $info) {
533 $dao = new CRM_Core_DAO_Extension();
534 $dao->label = $info->label;
535 $dao->name = $info->name;
536 $dao->full_name = $info->key;
537 $dao->type = $info->type;
538 $dao->file = $info->file;
539 $dao->is_active = 1;
540 return (bool) ($dao->insert());
541 }
542
e0ef6999
EM
543 /**
544 * @param CRM_Extension_Info $info
545 *
546 * @return bool
547 */
6a488035
TO
548 private function _updateExtensionEntry(CRM_Extension_Info $info) {
549 $dao = new CRM_Core_DAO_Extension();
550 $dao->full_name = $info->key;
551 if ($dao->find(TRUE)) {
552 $dao->label = $info->label;
553 $dao->name = $info->name;
554 $dao->full_name = $info->key;
555 $dao->type = $info->type;
556 $dao->file = $info->file;
557 $dao->is_active = 1;
558 return (bool) ($dao->update());
0db6c3e1
TO
559 }
560 else {
6a488035
TO
561 return $this->_createExtensionEntry($info);
562 }
563 }
564
e0ef6999
EM
565 /**
566 * @param CRM_Extension_Info $info
567 *
568 * @throws CRM_Extension_Exception
569 */
6a488035
TO
570 private function _removeExtensionEntry(CRM_Extension_Info $info) {
571 $dao = new CRM_Core_DAO_Extension();
572 $dao->full_name = $info->key;
573 if ($dao->find(TRUE)) {
574 if (CRM_Core_BAO_Extension::del($dao->id)) {
575 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
0db6c3e1
TO
576 }
577 else {
6a488035
TO
578 throw new CRM_Extension_Exception("Failed to remove extension entry");
579 }
580 } // else: post-condition already satisified
581 }
582
e0ef6999
EM
583 /**
584 * @param CRM_Extension_Info $info
585 * @param $isActive
586 */
6a488035 587 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
be2fb01f
CW
588 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', [
589 1 => [$isActive, 'Integer'],
590 2 => [$info->key, 'String'],
591 ]);
6a488035
TO
592 }
593
594 /**
595 * Auto-generate a place-holder for a missing extension using info from
596 * database.
597 *
2a6da8d7 598 * @param $key
6a488035
TO
599 * @return CRM_Extension_Info|NULL
600 */
601 public function createInfoFromDB($key) {
602 $dao = new CRM_Core_DAO_Extension();
603 $dao->full_name = $key;
604 if ($dao->find(TRUE)) {
605 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
606 return $info;
0db6c3e1
TO
607 }
608 else {
6a488035
TO
609 return NULL;
610 }
611 }
96025800 612
f8a7cfff
TO
613 /**
614 * Build a list of extensions to install, in an order that will satisfy dependencies.
615 *
616 * @param array $keys
617 * List of extensions to install.
618 * @return array
619 * List of extension keys, including dependencies, in order of installation.
620 */
621 public function findInstallRequirements($keys) {
622 $infos = $this->mapper->getAllInfos();
7b966967
SL
623 // array(string $key).
624 $todoKeys = array_unique($keys);
625 // array(string $key => 1);
626 $doneKeys = [];
f8a7cfff
TO
627 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
628
629 while (!empty($todoKeys)) {
630 $key = array_shift($todoKeys);
631 if (isset($doneKeys[$key])) {
632 continue;
633 }
634 $doneKeys[$key] = 1;
635
636 /** @var CRM_Extension_Info $info */
637 $info = @$infos[$key];
638
639 if ($this->getStatus($key) === self::STATUS_INSTALLED) {
be2fb01f 640 $sorter->add($key, []);
f8a7cfff
TO
641 }
642 elseif ($info && $info->requires) {
643 $sorter->add($key, $info->requires);
3ea86448 644 $todoKeys = array_merge($todoKeys, $info->requires);
f8a7cfff
TO
645 }
646 else {
be2fb01f 647 $sorter->add($key, []);
f8a7cfff
TO
648 }
649 }
650 return $sorter->sort();
651 }
652
653 /**
654 * Build a list of extensions to remove, in an order that will satisfy dependencies.
655 *
656 * @param array $keys
657 * List of extensions to install.
658 * @return array
659 * List of extension keys, including dependencies, in order of removal.
660 */
661 public function findDisableRequirements($keys) {
be2fb01f 662 $INSTALLED = [
f8a7cfff
TO
663 self::STATUS_INSTALLED,
664 self::STATUS_INSTALLED_MISSING,
be2fb01f 665 ];
f8a7cfff
TO
666 $installedInfos = $this->filterInfosByStatus($this->mapper->getAllInfos(), $INSTALLED);
667 $revMap = CRM_Extension_Info::buildReverseMap($installedInfos);
668 $todoKeys = array_unique($keys);
be2fb01f 669 $doneKeys = [];
f8a7cfff
TO
670 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
671
672 while (!empty($todoKeys)) {
673 $key = array_shift($todoKeys);
674 if (isset($doneKeys[$key])) {
675 continue;
676 }
677 $doneKeys[$key] = 1;
678
679 if (isset($revMap[$key])) {
680 $requiredBys = CRM_Utils_Array::collect('key',
681 $this->filterInfosByStatus($revMap[$key], $INSTALLED));
682 $sorter->add($key, $requiredBys);
683 $todoKeys = array_merge($todoKeys, $requiredBys);
684 }
685 else {
be2fb01f 686 $sorter->add($key, []);
f8a7cfff
TO
687 }
688 }
689 return $sorter->sort();
690 }
691
692 /**
693 * @param $infos
694 * @param $filterStatuses
695 * @return array
696 */
697 protected function filterInfosByStatus($infos, $filterStatuses) {
be2fb01f 698 $matches = [];
f8a7cfff
TO
699 foreach ($infos as $k => $v) {
700 if (in_array($this->getStatus($v->key), $filterStatuses)) {
701 $matches[$k] = $v;
702 }
703 }
704 return $matches;
705 }
706
6a488035 707}