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