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