Fix php comments
[civicrm-core.git] / CRM / Extension / Manager.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 * The extension manager handles installing, disabling enabling, and
14 * uninstalling extensions.
15 *
20429eb9
RL
16 * You should obtain a singleton of this class via
17 *
c486a70f 18 * $manager = CRM_Extension_System::singleton()->getManager();
20429eb9 19 *
6a488035 20 * @package CRM
ca5cec67 21 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
22 */
23class CRM_Extension_Manager {
24 /**
fe482240 25 * The extension is fully installed and enabled.
6a488035
TO
26 */
27 const STATUS_INSTALLED = 'installed';
28
29 /**
fe482240 30 * The extension config has been applied to database but deactivated.
6a488035
TO
31 */
32 const STATUS_DISABLED = 'disabled';
33
34 /**
35 * The extension code is visible, but nothing has been applied to DB
36 */
37 const STATUS_UNINSTALLED = 'uninstalled';
38
39 /**
40 * The extension code is not locally accessible
41 */
42 const STATUS_UNKNOWN = 'unknown';
43
44 /**
25fcba46 45 * The extension is installed but the code is not accessible
6a488035
TO
46 */
47 const STATUS_INSTALLED_MISSING = 'installed-missing';
48
49 /**
25fcba46 50 * The extension was installed and is now disabled; the code is not accessible
6a488035
TO
51 */
52 const STATUS_DISABLED_MISSING = 'disabled-missing';
53
54 /**
55 * @var CRM_Extension_Container_Interface
56 *
57 * Note: Treat as private. This is only public to facilitate debugging.
58 */
59 public $fullContainer;
60
61 /**
041ecc95 62 * Default container.
63 *
64 * @var CRM_Extension_Container_Basic|false
6a488035
TO
65 *
66 * Note: Treat as private. This is only public to facilitate debugging.
67 */
68 public $defaultContainer;
69
70 /**
041ecc95 71 * Mapper.
72 *
6a488035
TO
73 * @var CRM_Extension_Mapper
74 *
75 * Note: Treat as private. This is only public to facilitate debugging.
76 */
77 public $mapper;
78
79 /**
041ecc95 80 * Type managers.
81 *
82 * @var array
83 *
84 * Format is (typeName => CRM_Extension_Manager_Interface)
6a488035
TO
85 *
86 * Note: Treat as private. This is only public to facilitate debugging.
87 */
88 public $typeManagers;
89
90 /**
041ecc95 91 * Statuses.
92 *
93 * @var array
94 *
95 * Format is (extensionKey => statusConstant)
6a488035
TO
96 *
97 * Note: Treat as private. This is only public to facilitate debugging.
98 */
99 public $statuses;
100
20429eb9
RL
101 /**
102 * Live process(es) per extension.
103 *
104 * @var array
105 *
106 * Format is: {
107 * extensionKey => [
108 * ['operation' => 'install|enable|uninstall|disable', 'phase' => 'queued|live|completed'
109 * ...
110 * ],
111 * ...
112 * }
113 *
114 * The inner array is a stack, so the most recent current operation is the
115 * last entry. As this manager handles multiple extensions at once, here's
116 * the flow for an install operation.
117 *
118 * $manager->install(['ext1', 'ext2']);
119 *
120 * 0. {}
121 * 1. { ext1: ['install'], ext2: ['install'] }
122 * 2. { ext1: ['install', 'installing'], ext2: ['install'] }
123 * 3. { ext1: ['install'], ext2: ['install', 'installing'] }
124 * 4. { ext1: ['install'], ext2: ['install'] }
125 * 5. {}
126 */
127 protected $processes = [];
128
6a488035 129 /**
041ecc95 130 * Class constructor.
131 *
6c8f6e67 132 * @param CRM_Extension_Container_Interface $fullContainer
2024d5b9 133 * @param CRM_Extension_Container_Basic|false $defaultContainer
6c8f6e67 134 * @param CRM_Extension_Mapper $mapper
041ecc95 135 * @param array $typeManagers
6a488035 136 */
00be9182 137 public function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
6a488035
TO
138 $this->fullContainer = $fullContainer;
139 $this->defaultContainer = $defaultContainer;
140 $this->mapper = $mapper;
141 $this->typeManagers = $typeManagers;
142 }
143
144 /**
145 * Install or upgrade the code for an extension -- and perform any
146 * necessary database changes (eg replacing extension metadata).
147 *
148 * This only works if the extension is stored in the default container.
149 *
f41911fd
TO
150 * @param string $tmpCodeDir
151 * Path to a local directory containing a copy of the new (inert) code.
6a488035
TO
152 * @throws CRM_Extension_Exception
153 */
154 public function replace($tmpCodeDir) {
353ffa53 155 if (!$this->defaultContainer) {
6a488035
TO
156 throw new CRM_Extension_Exception("Default extension container is not configured");
157 }
158
159 $newInfo = CRM_Extension_Info::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
160 $oldStatus = $this->getStatus($newInfo->key);
161
162 // find $tgtPath, $oldInfo, $typeManager
163 switch ($oldStatus) {
164 case self::STATUS_UNINSTALLED:
165 case self::STATUS_INSTALLED:
166 case self::STATUS_DISABLED:
167 // 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
168 // throws Exception
169 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key);
6a488035 170 $tgtPath = $this->fullContainer->getPath($newInfo->key);
353ffa53 171 if (!CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
6a488035 172 // force installation in the default-container
615841de 173 $oldPath = $tgtPath;
6a488035 174 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
be2fb01f 175 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
176 1 => $newInfo->key,
177 2 => $oldPath,
be2fb01f 178 ]));
6a488035
TO
179 }
180 break;
b3a4b879 181
6a488035
TO
182 case self::STATUS_INSTALLED_MISSING:
183 case self::STATUS_DISABLED_MISSING:
b3a4b879 184 // the extension does not exist in any container; we're free to put it anywhere
6a488035 185 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
7b966967
SL
186 // throws Exception
187 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key);
6a488035 188 break;
b3a4b879 189
6a488035 190 case self::STATUS_UNKNOWN:
b3a4b879 191 // the extension does not exist in any container; we're free to put it anywhere
6a488035
TO
192 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
193 $oldInfo = $typeManager = NULL;
194 break;
b3a4b879 195
6a488035 196 default:
615841de 197 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
198 }
199
200 // move the code!
201 switch ($oldStatus) {
202 case self::STATUS_UNINSTALLED:
203 case self::STATUS_UNKNOWN:
204 // There are no DB records to worry about, so we'll just put the files in place
205 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
206 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
207 }
208 break;
b3a4b879 209
6a488035
TO
210 case self::STATUS_INSTALLED:
211 case self::STATUS_INSTALLED_MISSING:
212 case self::STATUS_DISABLED:
213 case self::STATUS_DISABLED_MISSING:
214 // There are DB records; coordinate the file placement with the DB updates
215 $typeManager->onPreReplace($oldInfo, $newInfo);
216 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
217 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
218 }
219 $this->_updateExtensionEntry($newInfo);
220 $typeManager->onPostReplace($oldInfo, $newInfo);
221 break;
b3a4b879 222
6a488035 223 default:
615841de 224 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
225 }
226
227 $this->refresh();
a618b2b8 228 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
6a488035
TO
229 }
230
231 /**
232 * Add records of the extension to the database -- and enable it
233 *
df7a1988
CW
234 * @param string|array $keys
235 * One or more extension keys.
20429eb9 236 * @param string $mode install|enable
6a488035
TO
237 * @throws CRM_Extension_Exception
238 */
20429eb9 239 public function install($keys, $mode = 'install') {
df7a1988 240 $keys = (array) $keys;
6a488035
TO
241 $origStatuses = $this->getStatuses();
242
243 // TODO: to mitigate the risk of crashing during installation, scan
244 // keys/statuses/types before doing anything
245
25fcba46
CW
246 // Check compatibility
247 $incompatible = [];
6a488035 248 foreach ($keys as $key) {
25fcba46
CW
249 if ($this->isIncompatible($key)) {
250 $incompatible[] = $key;
251 }
252 }
253 if ($incompatible) {
254 throw new CRM_Extension_Exception('Cannot install incompatible extension: ' . implode(', ', $incompatible));
255 }
256
20429eb9
RL
257 // Keep state for these operations.
258 $this->addProcess($keys, $mode);
259
25fcba46
CW
260 foreach ($keys as $key) {
261 /** @var CRM_Extension_Info $info */
262 /** @var CRM_Extension_Manager_Base $typeManager */
7b966967 263 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
264
265 switch ($origStatuses[$key]) {
266 case self::STATUS_INSTALLED:
20429eb9
RL
267 // ok, nothing to do. As such the status of this process is no longer
268 // 'install' install was the intent, which might have resulted in
269 // changes but these changes will not be happening, so processes that
270 // are sensitive to installs (like the managed entities reconcile
271 // operation) should not assume that these changes have happened.
272 $this->popProcess([$key]);
6a488035 273 break;
b3a4b879 274
6a488035
TO
275 case self::STATUS_DISABLED:
276 // re-enable it
20429eb9 277 $this->addProcess([$key], 'enabling');
6a488035
TO
278 $typeManager->onPreEnable($info);
279 $this->_setExtensionActive($info, 1);
280 $typeManager->onPostEnable($info);
34ba82e8
TO
281
282 // A full refresh would be preferrable but very slow. This at least allows
283 // later extensions to access classes from earlier extensions.
284 $this->statuses = NULL;
285 $this->mapper->refresh();
20429eb9
RL
286
287 $this->popProcess([$key]);
6a488035 288 break;
b3a4b879 289
6a488035
TO
290 case self::STATUS_UNINSTALLED:
291 // install anew
20429eb9 292 $this->addProcess([$key], 'installing');
6a488035
TO
293 $typeManager->onPreInstall($info);
294 $this->_createExtensionEntry($info);
295 $typeManager->onPostInstall($info);
34ba82e8
TO
296
297 // A full refresh would be preferrable but very slow. This at least allows
298 // later extensions to access classes from earlier extensions.
299 $this->statuses = NULL;
300 $this->mapper->refresh();
20429eb9
RL
301
302 $this->popProcess([$key]);
6a488035 303 break;
b3a4b879 304
6a488035
TO
305 case self::STATUS_UNKNOWN:
306 default:
307 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
308 }
309 }
310
311 $this->statuses = NULL;
312 $this->mapper->refresh();
d5abe16a
SL
313 if (!CRM_Core_Config::isUpgradeMode()) {
314 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
3d0e24ec 315
d5abe16a
SL
316 $schema = new CRM_Logging_Schema();
317 $schema->fixSchemaDifferences();
318 }
3d0e24ec 319 foreach ($keys as $key) {
7b966967
SL
320 // throws Exception
321 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
3d0e24ec 322
323 switch ($origStatuses[$key]) {
324 case self::STATUS_INSTALLED:
325 // ok, nothing to do
326 break;
b3a4b879 327
3d0e24ec 328 case self::STATUS_DISABLED:
329 // re-enable it
330 break;
b3a4b879 331
3d0e24ec 332 case self::STATUS_UNINSTALLED:
333 // install anew
20429eb9 334 $this->addProcess([$key], 'installing');
3d0e24ec 335 $typeManager->onPostPostInstall($info);
20429eb9 336 $this->popProcess([$key]);
3d0e24ec 337 break;
b3a4b879 338
3d0e24ec 339 case self::STATUS_UNKNOWN:
340 default:
341 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
342 }
343 }
344
20429eb9
RL
345 // All processes for these keys
346 $this->popProcess($keys);
6a488035
TO
347 }
348
349 /**
350 * Add records of the extension to the database -- and enable it
351 *
f41911fd
TO
352 * @param array $keys
353 * List of extension keys.
6a488035
TO
354 * @throws CRM_Extension_Exception
355 */
356 public function enable($keys) {
20429eb9 357 $this->install($keys, 'enable');
6a488035
TO
358 }
359
360 /**
25fcba46 361 * Disable extension without removing record from db.
6a488035 362 *
df7a1988
CW
363 * @param string|array $keys
364 * One or more extension keys.
6a488035
TO
365 * @throws CRM_Extension_Exception
366 */
367 public function disable($keys) {
df7a1988 368 $keys = (array) $keys;
6a488035
TO
369 $origStatuses = $this->getStatuses();
370
371 // TODO: to mitigate the risk of crashing during installation, scan
372 // keys/statuses/types before doing anything
373
f8a7cfff
TO
374 sort($keys);
375 $disableRequirements = $this->findDisableRequirements($keys);
7b966967
SL
376 // This munges order, but makes it comparable.
377 sort($disableRequirements);
f8a7cfff 378 if ($keys !== $disableRequirements) {
41d113e9 379 throw new CRM_Extension_Exception_DependencyException("Cannot disable extension due to dependencies. Consider disabling all these: " . implode(',', $disableRequirements));
f8a7cfff
TO
380 }
381
20429eb9
RL
382 $this->addProcess($keys, 'disable');
383
6a488035 384 foreach ($keys as $key) {
adcd5156
SL
385 if (isset($origStatuses[$key])) {
386 switch ($origStatuses[$key]) {
387 case self::STATUS_INSTALLED:
388 $this->addProcess([$key], 'disabling');
389 // throws Exception
390 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
391 $typeManager->onPreDisable($info);
392 $this->_setExtensionActive($info, 0);
393 $typeManager->onPostDisable($info);
394 $this->popProcess([$key]);
395 break;
396
397 case self::STATUS_INSTALLED_MISSING:
398 // throws Exception
399 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
400 $typeManager->onPreDisable($info);
401 $this->_setExtensionActive($info, 0);
402 $typeManager->onPostDisable($info);
403 break;
404
405 case self::STATUS_DISABLED:
406 case self::STATUS_DISABLED_MISSING:
407 case self::STATUS_UNINSTALLED:
408 // ok, nothing to do
409 // Remove the 'disable' process as we're not doing that.
410 $this->popProcess([$key]);
411 break;
412
413 case self::STATUS_UNKNOWN:
414 default:
415 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
416 }
417 }
418 else {
419 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
6a488035
TO
420 }
421 }
422
423 $this->statuses = NULL;
424 $this->mapper->refresh();
425 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
20429eb9
RL
426
427 $this->popProcess($keys);
6a488035
TO
428 }
429
430 /**
fe482240 431 * Remove all database references to an extension.
6a488035 432 *
df7a1988
CW
433 * @param string|array $keys
434 * One or more extension keys.
6a488035
TO
435 * @throws CRM_Extension_Exception
436 */
437 public function uninstall($keys) {
df7a1988 438 $keys = (array) $keys;
6a488035
TO
439 $origStatuses = $this->getStatuses();
440
441 // TODO: to mitigate the risk of crashing during installation, scan
442 // keys/statuses/types before doing anything
443
20429eb9
RL
444 $this->addProcess($keys, 'uninstall');
445
b3a4b879 446 foreach ($keys as $key) {
6a488035
TO
447 switch ($origStatuses[$key]) {
448 case self::STATUS_INSTALLED:
449 case self::STATUS_INSTALLED_MISSING:
450 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
b3a4b879 451
6a488035 452 case self::STATUS_DISABLED:
20429eb9 453 $this->addProcess([$key], 'uninstalling');
7b966967
SL
454 // throws Exception
455 list ($info, $typeManager) = $this->_getInfoTypeHandler($key);
6a488035
TO
456 $typeManager->onPreUninstall($info);
457 $this->_removeExtensionEntry($info);
458 $typeManager->onPostUninstall($info);
459 break;
b3a4b879 460
6a488035 461 case self::STATUS_DISABLED_MISSING:
7b966967
SL
462 // throws Exception
463 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key);
6a488035
TO
464 $typeManager->onPreUninstall($info);
465 $this->_removeExtensionEntry($info);
466 $typeManager->onPostUninstall($info);
467 break;
b3a4b879 468
6a488035
TO
469 case self::STATUS_UNINSTALLED:
470 // ok, nothing to do
20429eb9
RL
471 // remove the 'uninstall' process since we're not doing that.
472 $this->popProcess([$key]);
6a488035 473 break;
b3a4b879 474
6a488035
TO
475 case self::STATUS_UNKNOWN:
476 default:
477 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
478 }
479 }
480
481 $this->statuses = NULL;
482 $this->mapper->refresh();
483 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
20429eb9 484 $this->popProcess($keys);
6a488035
TO
485 }
486
487 /**
fe482240 488 * Determine the status of an extension.
6a488035 489 *
77b97be7
EM
490 * @param $key
491 *
a6c01b45 492 * @return string
25fcba46 493 * constant self::STATUS_*
6a488035
TO
494 */
495 public function getStatus($key) {
496 $statuses = $this->getStatuses();
497 if (array_key_exists($key, $statuses)) {
498 return $statuses[$key];
0db6c3e1
TO
499 }
500 else {
6a488035
TO
501 return self::STATUS_UNKNOWN;
502 }
503 }
504
25fcba46
CW
505 /**
506 * Check if a given extension is incompatible with this version of CiviCRM
507 *
508 * @param $key
509 * @return bool|array
510 */
511 public function isIncompatible($key) {
512 $info = CRM_Extension_System::getCompatibilityInfo();
513 return $info[$key] ?? FALSE;
514 }
515
6a488035 516 /**
fe482240 517 * Determine the status of all extensions.
6a488035 518 *
a6c01b45
CW
519 * @return array
520 * ($key => status_constant)
6a488035
TO
521 */
522 public function getStatuses() {
523 if (!is_array($this->statuses)) {
6542d699
TO
524 $compat = CRM_Extension_System::getCompatibilityInfo();
525
be2fb01f 526 $this->statuses = [];
6a488035
TO
527
528 foreach ($this->fullContainer->getKeys() as $key) {
529 $this->statuses[$key] = self::STATUS_UNINSTALLED;
530 }
531
532 $sql = '
533 SELECT full_name, is_active
534 FROM civicrm_extension
535 ';
536 $dao = CRM_Core_DAO::executeQuery($sql);
537 while ($dao->fetch()) {
538 try {
539 $path = $this->fullContainer->getPath($dao->full_name);
540 $codeExists = !empty($path) && is_dir($path);
0db6c3e1
TO
541 }
542 catch (CRM_Extension_Exception $e) {
6a488035
TO
543 $codeExists = FALSE;
544 }
6542d699
TO
545 if (!empty($compat[$dao->full_name]['force-uninstall'])) {
546 $this->statuses[$dao->full_name] = self::STATUS_UNINSTALLED;
547 }
548 elseif ($dao->is_active) {
6a488035 549 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
0db6c3e1
TO
550 }
551 else {
6a488035
TO
552 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
553 }
554 }
555 }
556 return $this->statuses;
557 }
558
559 public function refresh() {
560 $this->statuses = NULL;
7b966967
SL
561 // and, indirectly, defaultContainer
562 $this->fullContainer->refresh();
6a488035
TO
563 $this->mapper->refresh();
564 }
565
20429eb9
RL
566 /**
567 * Return current processes for given extension.
568 *
569 * @param String $key extension key
570 *
571 * @return array
572 */
573 public function getActiveProcesses(string $key) :Array {
574 return $this->processes[$key] ?? [];
575 }
576
577 /**
578 * Determine if the extension specified is currently involved in an install
579 * or enable process. Just sugar code to make things more readable.
580 *
581 * @param String $key extension key
582 *
583 * @return bool
584 */
585 public function extensionIsBeingInstalledOrEnabled($key) :bool {
586 foreach ($this->getActiveProcesses($key) as $process) {
587 if (in_array($process, ['install', 'installing', 'enable', 'enabling'])) {
588 return TRUE;
589 }
590 }
591 return FALSE;
592 }
593
6a488035
TO
594 // ----------------------
595
596 /**
597 * Find the $info and $typeManager for a $key
598 *
77b97be7
EM
599 * @param $key
600 *
6a488035 601 * @throws CRM_Extension_Exception
a6c01b45 602 * @return array
25fcba46 603 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
604 */
605 private function _getInfoTypeHandler($key) {
7b966967
SL
606 // throws Exception
607 $info = $this->mapper->keyToInfo($key);
6a488035 608 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 609 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
610 }
611 else {
6a488035
TO
612 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
613 }
614 }
615
616 /**
617 * Find the $info and $typeManager for a $key
618 *
2a6da8d7
EM
619 * @param $key
620 *
6a488035 621 * @throws CRM_Extension_Exception
a6c01b45 622 * @return array
25fcba46 623 * [CRM_Extension_Info, CRM_Extension_Manager_Interface]
6a488035
TO
624 */
625 private function _getMissingInfoTypeHandler($key) {
626 $info = $this->createInfoFromDB($key);
627 if ($info) {
628 if (array_key_exists($info->type, $this->typeManagers)) {
be2fb01f 629 return [$info, $this->typeManagers[$info->type]];
0db6c3e1
TO
630 }
631 else {
6a488035
TO
632 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
633 }
0db6c3e1
TO
634 }
635 else {
6a488035
TO
636 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
637 }
638 }
639
e0ef6999
EM
640 /**
641 * @param CRM_Extension_Info $info
642 *
643 * @return bool
644 */
6a488035
TO
645 private function _createExtensionEntry(CRM_Extension_Info $info) {
646 $dao = new CRM_Core_DAO_Extension();
647 $dao->label = $info->label;
648 $dao->name = $info->name;
649 $dao->full_name = $info->key;
650 $dao->type = $info->type;
651 $dao->file = $info->file;
652 $dao->is_active = 1;
653 return (bool) ($dao->insert());
654 }
655
e0ef6999
EM
656 /**
657 * @param CRM_Extension_Info $info
658 *
659 * @return bool
660 */
6a488035
TO
661 private function _updateExtensionEntry(CRM_Extension_Info $info) {
662 $dao = new CRM_Core_DAO_Extension();
663 $dao->full_name = $info->key;
664 if ($dao->find(TRUE)) {
665 $dao->label = $info->label;
666 $dao->name = $info->name;
667 $dao->full_name = $info->key;
668 $dao->type = $info->type;
669 $dao->file = $info->file;
670 $dao->is_active = 1;
671 return (bool) ($dao->update());
0db6c3e1
TO
672 }
673 else {
6a488035
TO
674 return $this->_createExtensionEntry($info);
675 }
676 }
677
e0ef6999
EM
678 /**
679 * @param CRM_Extension_Info $info
680 *
681 * @throws CRM_Extension_Exception
682 */
6a488035
TO
683 private function _removeExtensionEntry(CRM_Extension_Info $info) {
684 $dao = new CRM_Core_DAO_Extension();
685 $dao->full_name = $info->key;
686 if ($dao->find(TRUE)) {
687 if (CRM_Core_BAO_Extension::del($dao->id)) {
688 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
0db6c3e1
TO
689 }
690 else {
6a488035
TO
691 throw new CRM_Extension_Exception("Failed to remove extension entry");
692 }
693 } // else: post-condition already satisified
694 }
695
e0ef6999
EM
696 /**
697 * @param CRM_Extension_Info $info
698 * @param $isActive
699 */
6a488035 700 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
be2fb01f
CW
701 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', [
702 1 => [$isActive, 'Integer'],
703 2 => [$info->key, 'String'],
704 ]);
6a488035
TO
705 }
706
707 /**
708 * Auto-generate a place-holder for a missing extension using info from
709 * database.
710 *
2a6da8d7 711 * @param $key
6a488035
TO
712 * @return CRM_Extension_Info|NULL
713 */
714 public function createInfoFromDB($key) {
715 $dao = new CRM_Core_DAO_Extension();
716 $dao->full_name = $key;
717 if ($dao->find(TRUE)) {
718 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
719 return $info;
0db6c3e1
TO
720 }
721 else {
6a488035
TO
722 return NULL;
723 }
724 }
96025800 725
f8a7cfff
TO
726 /**
727 * Build a list of extensions to install, in an order that will satisfy dependencies.
728 *
729 * @param array $keys
730 * List of extensions to install.
19ec0aa5
MWMC
731 * @param \CRM_Extension_Info $info
732 * An extension info object that we should use instead of our local versions (eg. when checking for upgradeability).
733 *
f8a7cfff
TO
734 * @return array
735 * List of extension keys, including dependencies, in order of installation.
19ec0aa5
MWMC
736 * @throws \CRM_Extension_Exception
737 * @throws \MJS\TopSort\CircularDependencyException
738 * @throws \MJS\TopSort\ElementNotFoundException
f8a7cfff 739 */
19ec0aa5
MWMC
740 public function findInstallRequirements($keys, $info = NULL) {
741 // Use our passed in info, or get the local versions
742 if ($info) {
743 $infos[$info->key] = $info;
744 }
745 else {
746 $infos = $this->mapper->getAllInfos();
747 }
7b966967
SL
748 // array(string $key).
749 $todoKeys = array_unique($keys);
750 // array(string $key => 1);
751 $doneKeys = [];
f8a7cfff
TO
752 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
753
754 while (!empty($todoKeys)) {
755 $key = array_shift($todoKeys);
756 if (isset($doneKeys[$key])) {
757 continue;
758 }
759 $doneKeys[$key] = 1;
760
761 /** @var CRM_Extension_Info $info */
762 $info = @$infos[$key];
763
19ec0aa5 764 if ($info && $info->requires) {
f8a7cfff 765 $sorter->add($key, $info->requires);
3ea86448 766 $todoKeys = array_merge($todoKeys, $info->requires);
f8a7cfff
TO
767 }
768 else {
be2fb01f 769 $sorter->add($key, []);
f8a7cfff
TO
770 }
771 }
772 return $sorter->sort();
773 }
774
775 /**
776 * Build a list of extensions to remove, in an order that will satisfy dependencies.
777 *
778 * @param array $keys
779 * List of extensions to install.
780 * @return array
781 * List of extension keys, including dependencies, in order of removal.
782 */
783 public function findDisableRequirements($keys) {
be2fb01f 784 $INSTALLED = [
f8a7cfff
TO
785 self::STATUS_INSTALLED,
786 self::STATUS_INSTALLED_MISSING,
be2fb01f 787 ];
f8a7cfff
TO
788 $installedInfos = $this->filterInfosByStatus($this->mapper->getAllInfos(), $INSTALLED);
789 $revMap = CRM_Extension_Info::buildReverseMap($installedInfos);
790 $todoKeys = array_unique($keys);
be2fb01f 791 $doneKeys = [];
f8a7cfff
TO
792 $sorter = new \MJS\TopSort\Implementations\FixedArraySort();
793
794 while (!empty($todoKeys)) {
795 $key = array_shift($todoKeys);
796 if (isset($doneKeys[$key])) {
797 continue;
798 }
799 $doneKeys[$key] = 1;
800
801 if (isset($revMap[$key])) {
802 $requiredBys = CRM_Utils_Array::collect('key',
803 $this->filterInfosByStatus($revMap[$key], $INSTALLED));
804 $sorter->add($key, $requiredBys);
805 $todoKeys = array_merge($todoKeys, $requiredBys);
806 }
807 else {
be2fb01f 808 $sorter->add($key, []);
f8a7cfff
TO
809 }
810 }
811 return $sorter->sort();
812 }
813
20429eb9
RL
814 /**
815 * Provides way to set processes property for phpunit tests - not for general use.
816 *
817 * @param $processes
818 */
819 public function setProcessesForTesting(array $processes) {
820 $this->processes = $processes;
821 }
822
f8a7cfff
TO
823 /**
824 * @param $infos
825 * @param $filterStatuses
826 * @return array
827 */
828 protected function filterInfosByStatus($infos, $filterStatuses) {
be2fb01f 829 $matches = [];
f8a7cfff
TO
830 foreach ($infos as $k => $v) {
831 if (in_array($this->getStatus($v->key), $filterStatuses)) {
832 $matches[$k] = $v;
833 }
834 }
835 return $matches;
836 }
837
20429eb9
RL
838 /**
839 * Add a process to the stacks for the extensions.
840 *
841 * @param array $keys extensionKey
842 * @param string $process one of: install|uninstall|enable|disable|installing|uninstalling|enabling|disabling
843 */
844 protected function addProcess(array $keys, string $process) {
845 foreach ($keys as $key) {
846 $this->processes[$key][] = $process;
847 }
848 }
849
850 /**
851 * Pop the top op from the stacks for the extensions.
852 *
853 * @param array $keys extensionKey
854 */
855 protected function popProcess(array $keys) {
856 foreach ($keys as $key) {
857 if (!empty($this->process[$key])) {
858 array_pop($this->process[$key]);
859 }
860 }
861 }
862
6a488035 863}