Accept trxn_id to Payment.get
[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)) {
6542d699
TO
468 $compat = CRM_Extension_System::getCompatibilityInfo();
469
be2fb01f 470 $this->statuses = [];
6a488035
TO
471
472 foreach ($this->fullContainer->getKeys() as $key) {
473 $this->statuses[$key] = self::STATUS_UNINSTALLED;
474 }
475
476 $sql = '
477 SELECT full_name, is_active
478 FROM civicrm_extension
479 ';
480 $dao = CRM_Core_DAO::executeQuery($sql);
481 while ($dao->fetch()) {
482 try {
483 $path = $this->fullContainer->getPath($dao->full_name);
484 $codeExists = !empty($path) && is_dir($path);
0db6c3e1
TO
485 }
486 catch (CRM_Extension_Exception $e) {
6a488035
TO
487 $codeExists = FALSE;
488 }
6542d699
TO
489 if (!empty($compat[$dao->full_name]['force-uninstall'])) {
490 $this->statuses[$dao->full_name] = self::STATUS_UNINSTALLED;
491 }
492 elseif ($dao->is_active) {
6a488035 493 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
0db6c3e1
TO
494 }
495 else {
6a488035
TO
496 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
497 }
498 }
499 }
500 return $this->statuses;
501 }
502
503 public function refresh() {
504 $this->statuses = NULL;
7b966967
SL
505 // and, indirectly, defaultContainer
506 $this->fullContainer->refresh();
6a488035
TO
507 $this->mapper->refresh();
508 }
509
510 // ----------------------
511
512 /**
513 * Find the $info and $typeManager for a $key
514 *
77b97be7
EM
515 * @param $key
516 *
6a488035 517 * @throws CRM_Extension_Exception
a6c01b45 518 * @return array
25fcba46 519 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
520 */
521 private function _getInfoTypeHandler($key) {
7b966967
SL
522 // throws Exception
523 $info = $this->mapper->keyToInfo($key);
6a488035 524 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 525 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
526 }
527 else {
6a488035
TO
528 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
529 }
530 }
531
532 /**
533 * Find the $info and $typeManager for a $key
534 *
2a6da8d7
EM
535 * @param $key
536 *
6a488035 537 * @throws CRM_Extension_Exception
a6c01b45 538 * @return array
25fcba46 539 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
540 */
541 private function _getMissingInfoTypeHandler($key) {
542 $info = $this->createInfoFromDB($key);
543 if ($info) {
544 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 545 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
546 }
547 else {
6a488035
TO
548 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
549 }
0db6c3e1
TO
550 }
551 else {
6a488035
TO
552 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
553 }
554 }
555
e0ef6999
EM
556 /**
557 * @param CRM_Extension_Info $info
558 *
559 * @return bool
560 */
6a488035
TO
561 private function _createExtensionEntry(CRM_Extension_Info $info) {
562 $dao = new CRM_Core_DAO_Extension();
563 $dao->label = $info->label;
564 $dao->name = $info->name;
565 $dao->full_name = $info->key;
566 $dao->type = $info->type;
567 $dao->file = $info->file;
568 $dao->is_active = 1;
569 return (bool) ($dao->insert());
570 }
571
e0ef6999
EM
572 /**
573 * @param CRM_Extension_Info $info
574 *
575 * @return bool
576 */
6a488035
TO
577 private function _updateExtensionEntry(CRM_Extension_Info $info) {
578 $dao = new CRM_Core_DAO_Extension();
579 $dao->full_name = $info->key;
580 if ($dao->find(TRUE)) {
581 $dao->label = $info->label;
582 $dao->name = $info->name;
583 $dao->full_name = $info->key;
584 $dao->type = $info->type;
585 $dao->file = $info->file;
586 $dao->is_active = 1;
587 return (bool) ($dao->update());
0db6c3e1
TO
588 }
589 else {
6a488035
TO
590 return $this->_createExtensionEntry($info);
591 }
592 }
593
e0ef6999
EM
594 /**
595 * @param CRM_Extension_Info $info
596 *
597 * @throws CRM_Extension_Exception
598 */
6a488035
TO
599 private function _removeExtensionEntry(CRM_Extension_Info $info) {
600 $dao = new CRM_Core_DAO_Extension();
601 $dao->full_name = $info->key;
602 if ($dao->find(TRUE)) {
603 if (CRM_Core_BAO_Extension::del($dao->id)) {
604 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
0db6c3e1
TO
605 }
606 else {
6a488035
TO
607 throw new CRM_Extension_Exception("Failed to remove extension entry");
608 }
609 } // else: post-condition already satisified
610 }
611
e0ef6999
EM
612 /**
613 * @param CRM_Extension_Info $info
614 * @param $isActive
615 */
6a488035 616 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
be2fb01f
CW
617 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', [
618 1 => [$isActive, 'Integer'],
619 2 => [$info->key, 'String'],
620 ]);
6a488035
TO
621 }
622
623 /**
624 * Auto-generate a place-holder for a missing extension using info from
625 * database.
626 *
2a6da8d7 627 * @param $key
6a488035
TO
628 * @return CRM_Extension_Info|NULL
629 */
630 public function createInfoFromDB($key) {
631 $dao = new CRM_Core_DAO_Extension();
632 $dao->full_name = $key;
633 if ($dao->find(TRUE)) {
634 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
635 return $info;
0db6c3e1
TO
636 }
637 else {
6a488035
TO
638 return NULL;
639 }
640 }
96025800 641
f8a7cfff
TO
642 /**
643 * Build a list of extensions to install, in an order that will satisfy dependencies.
644 *
645 * @param array $keys
646 * List of extensions to install.
647 * @return array
648 * List of extension keys, including dependencies, in order of installation.
649 */
650 public function findInstallRequirements($keys) {
651 $infos = $this->mapper->getAllInfos();
7b966967
SL
652 // array(string $key).
653 $todoKeys = array_unique($keys);
654 // array(string $key => 1);
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 /** @var CRM_Extension_Info $info */
666 $info = @$infos[$key];
667
668 if ($this->getStatus($key) === self::STATUS_INSTALLED) {
be2fb01f 669 $sorter->add($key, []);
f8a7cfff
TO
670 }
671 elseif ($info && $info->requires) {
672 $sorter->add($key, $info->requires);
3ea86448 673 $todoKeys = array_merge($todoKeys, $info->requires);
f8a7cfff
TO
674 }
675 else {
be2fb01f 676 $sorter->add($key, []);
f8a7cfff
TO
677 }
678 }
679 return $sorter->sort();
680 }
681
682 /**
683 * Build a list of extensions to remove, in an order that will satisfy dependencies.
684 *
685 * @param array $keys
686 * List of extensions to install.
687 * @return array
688 * List of extension keys, including dependencies, in order of removal.
689 */
690 public function findDisableRequirements($keys) {
be2fb01f 691 $INSTALLED = [
f8a7cfff
TO
692 self::STATUS_INSTALLED,
693 self::STATUS_INSTALLED_MISSING,
be2fb01f 694 ];
f8a7cfff
TO
695 $installedInfos = $this->filterInfosByStatus($this->mapper->getAllInfos(), $INSTALLED);
696 $revMap = CRM_Extension_Info::buildReverseMap($installedInfos);
697 $todoKeys = array_unique($keys);
be2fb01f 698 $doneKeys = [];
f8a7cfff
TO
699 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
700
701 while (!empty($todoKeys)) {
702 $key = array_shift($todoKeys);
703 if (isset($doneKeys[$key])) {
704 continue;
705 }
706 $doneKeys[$key] = 1;
707
708 if (isset($revMap[$key])) {
709 $requiredBys = CRM_Utils_Array::collect('key',
710 $this->filterInfosByStatus($revMap[$key], $INSTALLED));
711 $sorter->add($key, $requiredBys);
712 $todoKeys = array_merge($todoKeys, $requiredBys);
713 }
714 else {
be2fb01f 715 $sorter->add($key, []);
f8a7cfff
TO
716 }
717 }
718 return $sorter->sort();
719 }
720
721 /**
722 * @param $infos
723 * @param $filterStatuses
724 * @return array
725 */
726 protected function filterInfosByStatus($infos, $filterStatuses) {
be2fb01f 727 $matches = [];
f8a7cfff
TO
728 foreach ($infos as $k => $v) {
729 if (in_array($this->getStatus($v->key), $filterStatuses)) {
730 $matches[$k] = $v;
731 }
732 }
733 return $matches;
734 }
735
6a488035 736}