Minor comment fixes for CRM/Extensions directory.
[civicrm-core.git] / CRM / Extension / Manager.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * The extension manager handles installing, disabling enabling, and
30 * uninstalling extensions.
31 *
32 * @package CRM
33 * @copyright CiviCRM LLC (c) 2004-2015
34 */
35 class CRM_Extension_Manager {
36 /**
37 * The extension is fully installed and enabled.
38 */
39 const STATUS_INSTALLED = 'installed';
40
41 /**
42 * The extension config has been applied to database but deactivated.
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 /**
102 * @param CRM_Extension_Container_Interface $fullContainer
103 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
104 * @param CRM_Extension_Mapper $mapper
105 * @param $typeManagers
106 */
107 public function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
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 *
120 * @param string $tmpCodeDir
121 * Path to a local directory containing a copy of the new (inert) code.
122 * @throws CRM_Extension_Exception
123 */
124 public function replace($tmpCodeDir) {
125 if (!$this->defaultContainer) {
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);
140 if (!CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
141 // force installation in the default-container
142 $oldPath = $tgtPath;
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;
150
151 case self::STATUS_INSTALLED_MISSING:
152 case self::STATUS_DISABLED_MISSING:
153 // the extension does not exist in any container; we're free to put it anywhere
154 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
155 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key); // throws Exception
156 break;
157
158 case self::STATUS_UNKNOWN:
159 // the extension does not exist in any container; we're free to put it anywhere
160 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
161 $oldInfo = $typeManager = NULL;
162 break;
163
164 default:
165 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
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;
177
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;
190
191 default:
192 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
193 }
194
195 $this->refresh();
196 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
197 }
198
199 /**
200 * Add records of the extension to the database -- and enable it
201 *
202 * @param array $keys
203 * List of extension keys.
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;
219
220 case self::STATUS_DISABLED:
221 // re-enable it
222 $typeManager->onPreEnable($info);
223 $this->_setExtensionActive($info, 1);
224 $typeManager->onPostEnable($info);
225 break;
226
227 case self::STATUS_UNINSTALLED:
228 // install anew
229 $typeManager->onPreInstall($info);
230 $this->_createExtensionEntry($info);
231 $typeManager->onPostInstall($info);
232 break;
233
234 case self::STATUS_UNKNOWN:
235 default:
236 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
237 }
238 }
239
240 $this->statuses = NULL;
241 $this->mapper->refresh();
242 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
243 $schema = new CRM_Logging_Schema();
244 $schema->fixSchemaDifferences();
245
246 foreach ($keys as $key) {
247 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
248
249 switch ($origStatuses[$key]) {
250 case self::STATUS_INSTALLED:
251 // ok, nothing to do
252 break;
253
254 case self::STATUS_DISABLED:
255 // re-enable it
256 break;
257
258 case self::STATUS_UNINSTALLED:
259 // install anew
260 $typeManager->onPostPostInstall($info);
261 break;
262
263 case self::STATUS_UNKNOWN:
264 default:
265 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
266 }
267 }
268
269 }
270
271 /**
272 * Add records of the extension to the database -- and enable it
273 *
274 * @param array $keys
275 * List of extension keys.
276 * @throws CRM_Extension_Exception
277 */
278 public function enable($keys) {
279 $this->install($keys);
280 }
281
282 /**
283 * Add records of the extension to the database -- and enable it
284 *
285 * @param array $keys
286 * List of extension keys.
287 * @throws CRM_Extension_Exception
288 */
289 public function disable($keys) {
290 $origStatuses = $this->getStatuses();
291
292 // TODO: to mitigate the risk of crashing during installation, scan
293 // keys/statuses/types before doing anything
294
295 foreach ($keys as $key) {
296 switch ($origStatuses[$key]) {
297 case self::STATUS_INSTALLED:
298 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
299 $typeManager->onPreDisable($info);
300 $this->_setExtensionActive($info, 0);
301 $typeManager->onPostDisable($info);
302 break;
303
304 case self::STATUS_INSTALLED_MISSING:
305 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
306 $typeManager->onPreDisable($info);
307 $this->_setExtensionActive($info, 0);
308 $typeManager->onPostDisable($info);
309 break;
310
311 case self::STATUS_DISABLED:
312 case self::STATUS_DISABLED_MISSING:
313 case self::STATUS_UNINSTALLED:
314 // ok, nothing to do
315 break;
316
317 case self::STATUS_UNKNOWN:
318 default:
319 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
320 }
321 }
322
323 $this->statuses = NULL;
324 $this->mapper->refresh();
325 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
326 }
327
328 /**
329 * Remove all database references to an extension.
330 *
331 * Add records of the extension to the database -- and enable it
332 *
333 * @param array $keys
334 * List of extension keys.
335 * @throws CRM_Extension_Exception
336 */
337 public function uninstall($keys) {
338 $origStatuses = $this->getStatuses();
339
340 // TODO: to mitigate the risk of crashing during installation, scan
341 // keys/statuses/types before doing anything
342
343 foreach ($keys as $key) {
344 switch ($origStatuses[$key]) {
345 case self::STATUS_INSTALLED:
346 case self::STATUS_INSTALLED_MISSING:
347 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
348
349 case self::STATUS_DISABLED:
350 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
351 $typeManager->onPreUninstall($info);
352 $this->_removeExtensionEntry($info);
353 $typeManager->onPostUninstall($info);
354 break;
355
356 case self::STATUS_DISABLED_MISSING:
357 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
358 $typeManager->onPreUninstall($info);
359 $this->_removeExtensionEntry($info);
360 $typeManager->onPostUninstall($info);
361 break;
362
363 case self::STATUS_UNINSTALLED:
364 // ok, nothing to do
365 break;
366
367 case self::STATUS_UNKNOWN:
368 default:
369 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
370 }
371 }
372
373 $this->statuses = NULL;
374 $this->mapper->refresh();
375 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
376 }
377
378 /**
379 * Determine the status of an extension.
380 *
381 * @param $key
382 *
383 * @return string
384 * constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
385 */
386 public function getStatus($key) {
387 $statuses = $this->getStatuses();
388 if (array_key_exists($key, $statuses)) {
389 return $statuses[$key];
390 }
391 else {
392 return self::STATUS_UNKNOWN;
393 }
394 }
395
396 /**
397 * Determine the status of all extensions.
398 *
399 * @return array
400 * ($key => status_constant)
401 */
402 public function getStatuses() {
403 if (!is_array($this->statuses)) {
404 $this->statuses = array();
405
406 foreach ($this->fullContainer->getKeys() as $key) {
407 $this->statuses[$key] = self::STATUS_UNINSTALLED;
408 }
409
410 $sql = '
411 SELECT full_name, is_active
412 FROM civicrm_extension
413 ';
414 $dao = CRM_Core_DAO::executeQuery($sql);
415 while ($dao->fetch()) {
416 try {
417 $path = $this->fullContainer->getPath($dao->full_name);
418 $codeExists = !empty($path) && is_dir($path);
419 }
420 catch (CRM_Extension_Exception $e) {
421 $codeExists = FALSE;
422 }
423 if ($dao->is_active) {
424 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
425 }
426 else {
427 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
428 }
429 }
430 }
431 return $this->statuses;
432 }
433
434 public function refresh() {
435 $this->statuses = NULL;
436 $this->fullContainer->refresh(); // and, indirectly, defaultContainer
437 $this->mapper->refresh();
438 }
439
440 // ----------------------
441
442 /**
443 * Find the $info and $typeManager for a $key
444 *
445 * @param $key
446 *
447 * @throws CRM_Extension_Exception
448 * @return array
449 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
450 */
451 private function _getInfoTypeHandler($key) {
452 $info = $this->mapper->keyToInfo($key); // throws Exception
453 if (array_key_exists($info->type, $this->typeManagers)) {
454 return array($info, $this->typeManagers[$info->type]);
455 }
456 else {
457 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
458 }
459 }
460
461 /**
462 * Find the $info and $typeManager for a $key
463 *
464 * @param $key
465 *
466 * @throws CRM_Extension_Exception
467 * @return array
468 * (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
469 */
470 private function _getMissingInfoTypeHandler($key) {
471 $info = $this->createInfoFromDB($key);
472 if ($info) {
473 if (array_key_exists($info->type, $this->typeManagers)) {
474 return array($info, $this->typeManagers[$info->type]);
475 }
476 else {
477 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
478 }
479 }
480 else {
481 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
482 }
483 }
484
485 /**
486 * @param CRM_Extension_Info $info
487 *
488 * @return bool
489 */
490 private function _createExtensionEntry(CRM_Extension_Info $info) {
491 $dao = new CRM_Core_DAO_Extension();
492 $dao->label = $info->label;
493 $dao->name = $info->name;
494 $dao->full_name = $info->key;
495 $dao->type = $info->type;
496 $dao->file = $info->file;
497 $dao->is_active = 1;
498 return (bool) ($dao->insert());
499 }
500
501 /**
502 * @param CRM_Extension_Info $info
503 *
504 * @return bool
505 */
506 private function _updateExtensionEntry(CRM_Extension_Info $info) {
507 $dao = new CRM_Core_DAO_Extension();
508 $dao->full_name = $info->key;
509 if ($dao->find(TRUE)) {
510 $dao->label = $info->label;
511 $dao->name = $info->name;
512 $dao->full_name = $info->key;
513 $dao->type = $info->type;
514 $dao->file = $info->file;
515 $dao->is_active = 1;
516 return (bool) ($dao->update());
517 }
518 else {
519 return $this->_createExtensionEntry($info);
520 }
521 }
522
523 /**
524 * @param CRM_Extension_Info $info
525 *
526 * @throws CRM_Extension_Exception
527 */
528 private function _removeExtensionEntry(CRM_Extension_Info $info) {
529 $dao = new CRM_Core_DAO_Extension();
530 $dao->full_name = $info->key;
531 if ($dao->find(TRUE)) {
532 if (CRM_Core_BAO_Extension::del($dao->id)) {
533 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
534 }
535 else {
536 throw new CRM_Extension_Exception("Failed to remove extension entry");
537 }
538 } // else: post-condition already satisified
539 }
540
541 /**
542 * @param CRM_Extension_Info $info
543 * @param $isActive
544 */
545 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
546 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
547 1 => array($isActive, 'Integer'),
548 2 => array($info->key, 'String'),
549 ));
550 }
551
552 /**
553 * Auto-generate a place-holder for a missing extension using info from
554 * database.
555 *
556 * @param $key
557 * @return CRM_Extension_Info|NULL
558 */
559 public function createInfoFromDB($key) {
560 $dao = new CRM_Core_DAO_Extension();
561 $dao->full_name = $key;
562 if ($dao->find(TRUE)) {
563 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
564 return $info;
565 }
566 else {
567 return NULL;
568 }
569 }
570
571 }