copyright and version fixes
[civicrm-core.git] / CRM / Extension / Manager.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
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 +--------------------------------------------------------------------+
26*/
27
28/**
29 * The extension manager handles installing, disabling enabling, and
30 * uninstalling extensions.
31 *
32 * @package CRM
06b69b18 33 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
34 * $Id$
35 *
36 */
37class CRM_Extension_Manager {
38 /**
39 * The extension is fully installed and enabled
40 */
41 const STATUS_INSTALLED = 'installed';
42
43 /**
44 * The extension config has been applied to database but deactivated
45 */
46 const STATUS_DISABLED = 'disabled';
47
48 /**
49 * The extension code is visible, but nothing has been applied to DB
50 */
51 const STATUS_UNINSTALLED = 'uninstalled';
52
53 /**
54 * The extension code is not locally accessible
55 */
56 const STATUS_UNKNOWN = 'unknown';
57
58 /**
59 * The extension is fully installed and enabled
60 */
61 const STATUS_INSTALLED_MISSING = 'installed-missing';
62
63 /**
64 * The extension is fully installed and enabled
65 */
66 const STATUS_DISABLED_MISSING = 'disabled-missing';
67
68 /**
69 * @var CRM_Extension_Container_Interface
70 *
71 * Note: Treat as private. This is only public to facilitate debugging.
72 */
73 public $fullContainer;
74
75 /**
76 * @var CRM_Extension_Container_Basic|FALSE
77 *
78 * Note: Treat as private. This is only public to facilitate debugging.
79 */
80 public $defaultContainer;
81
82 /**
83 * @var CRM_Extension_Mapper
84 *
85 * Note: Treat as private. This is only public to facilitate debugging.
86 */
87 public $mapper;
88
89 /**
90 * @var array (typeName => CRM_Extension_Manager_Interface)
91 *
92 * Note: Treat as private. This is only public to facilitate debugging.
93 */
94 public $typeManagers;
95
96 /**
97 * @var array (extensionKey => statusConstant)
98 *
99 * Note: Treat as private. This is only public to facilitate debugging.
100 */
101 public $statuses;
102
103 /**
104 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
105 */
106 function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
107 $this->fullContainer = $fullContainer;
108 $this->defaultContainer = $defaultContainer;
109 $this->mapper = $mapper;
110 $this->typeManagers = $typeManagers;
111 }
112
113 /**
114 * Install or upgrade the code for an extension -- and perform any
115 * necessary database changes (eg replacing extension metadata).
116 *
117 * This only works if the extension is stored in the default container.
118 *
119 * @param string $tmpCodeDir path to a local directory containing a copy of the new (inert) code
120 * @return void
121 * @throws CRM_Extension_Exception
122 */
123 public function replace($tmpCodeDir) {
124 if (! $this->defaultContainer) {
125 throw new CRM_Extension_Exception("Default extension container is not configured");
126 }
127
128 $newInfo = CRM_Extension_Info::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
129 $oldStatus = $this->getStatus($newInfo->key);
130
131 // find $tgtPath, $oldInfo, $typeManager
132 switch ($oldStatus) {
133 case self::STATUS_UNINSTALLED:
134 case self::STATUS_INSTALLED:
135 case self::STATUS_DISABLED:
136 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
137 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key); // throws Exception
138 $tgtPath = $this->fullContainer->getPath($newInfo->key);
139 if (! CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
140 // force installation in the default-container
615841de 141 $oldPath = $tgtPath;
6a488035
TO
142 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
143 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(
144 1 => $newInfo->key,
145 2 => $oldPath,
146 )));
147 }
148 break;
149 case self::STATUS_INSTALLED_MISSING:
150 case self::STATUS_DISABLED_MISSING:
151 // the extension does not exist in any container; we're free to put it anywhere
152 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
153 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key); // throws Exception
154 break;
155 case self::STATUS_UNKNOWN:
156 // the extension does not exist in any container; we're free to put it anywhere
157 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
158 $oldInfo = $typeManager = NULL;
159 break;
160 default:
615841de 161 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
162 }
163
164 // move the code!
165 switch ($oldStatus) {
166 case self::STATUS_UNINSTALLED:
167 case self::STATUS_UNKNOWN:
168 // There are no DB records to worry about, so we'll just put the files in place
169 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
170 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
171 }
172 break;
173 case self::STATUS_INSTALLED:
174 case self::STATUS_INSTALLED_MISSING:
175 case self::STATUS_DISABLED:
176 case self::STATUS_DISABLED_MISSING:
177 // There are DB records; coordinate the file placement with the DB updates
178 $typeManager->onPreReplace($oldInfo, $newInfo);
179 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
180 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
181 }
182 $this->_updateExtensionEntry($newInfo);
183 $typeManager->onPostReplace($oldInfo, $newInfo);
184 break;
185 default:
615841de 186 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
6a488035
TO
187 }
188
189 $this->refresh();
190 }
191
192 /**
193 * Add records of the extension to the database -- and enable it
194 *
195 * @param array $keys list of extension keys
196 * @return void
197 * @throws CRM_Extension_Exception
198 */
199 public function install($keys) {
200 $origStatuses = $this->getStatuses();
201
202 // TODO: to mitigate the risk of crashing during installation, scan
203 // keys/statuses/types before doing anything
204
205 foreach ($keys as $key) {
206 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
207
208 switch ($origStatuses[$key]) {
209 case self::STATUS_INSTALLED:
210 // ok, nothing to do
211 break;
212 case self::STATUS_DISABLED:
213 // re-enable it
214 $typeManager->onPreEnable($info);
215 $this->_setExtensionActive($info, 1);
216 $typeManager->onPostEnable($info);
217 break;
218 case self::STATUS_UNINSTALLED:
219 // install anew
220 $typeManager->onPreInstall($info);
221 $this->_createExtensionEntry($info);
222 $typeManager->onPostInstall($info);
223 break;
224 case self::STATUS_UNKNOWN:
225 default:
226 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
227 }
228 }
229
230 $this->statuses = NULL;
231 $this->mapper->refresh();
232 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
3d0e24ec 233
234 foreach ($keys as $key) {
235 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
236 //print_r(array('post post?', $info, 'k' => $key, 'os'=> $origStatuses[$key]));
237
238 switch ($origStatuses[$key]) {
239 case self::STATUS_INSTALLED:
240 // ok, nothing to do
241 break;
242 case self::STATUS_DISABLED:
243 // re-enable it
244 break;
245 case self::STATUS_UNINSTALLED:
246 // install anew
247 $typeManager->onPostPostInstall($info);
248 break;
249 case self::STATUS_UNKNOWN:
250 default:
251 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
252 }
253 }
254
6a488035
TO
255 }
256
257 /**
258 * Add records of the extension to the database -- and enable it
259 *
260 * @param array $keys list of extension keys
261 * @return void
262 * @throws CRM_Extension_Exception
263 */
264 public function enable($keys) {
265 $this->install($keys);
266 }
267
268 /**
269 * Add records of the extension to the database -- and enable it
270 *
271 * @param array $keys list of extension keys
272 * @return void
273 * @throws CRM_Extension_Exception
274 */
275 public function disable($keys) {
276 $origStatuses = $this->getStatuses();
277
278 // TODO: to mitigate the risk of crashing during installation, scan
279 // keys/statuses/types before doing anything
280
281 foreach ($keys as $key) {
282 switch ($origStatuses[$key]) {
283 case self::STATUS_INSTALLED:
284 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
285 $typeManager->onPreDisable($info);
286 $this->_setExtensionActive($info, 0);
287 $typeManager->onPostDisable($info);
288 break;
289 case self::STATUS_INSTALLED_MISSING:
290 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
291 $typeManager->onPreDisable($info);
292 $this->_setExtensionActive($info, 0);
293 $typeManager->onPostDisable($info);
294 break;
295 case self::STATUS_DISABLED:
296 case self::STATUS_DISABLED_MISSING:
297 case self::STATUS_UNINSTALLED:
298 // ok, nothing to do
299 break;
300 case self::STATUS_UNKNOWN:
301 default:
302 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
303 }
304 }
305
306 $this->statuses = NULL;
307 $this->mapper->refresh();
308 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
309 }
310
311 /**
312 * Remove all database references to an extension
313 *
314 * Add records of the extension to the database -- and enable it
315 *
316 * @param array $keys list of extension keys
317 * @return void
318 * @throws CRM_Extension_Exception
319 */
320 public function uninstall($keys) {
321 $origStatuses = $this->getStatuses();
322
323 // TODO: to mitigate the risk of crashing during installation, scan
324 // keys/statuses/types before doing anything
325
326 foreach ($keys as $key) {
327 switch ($origStatuses[$key]) {
328 case self::STATUS_INSTALLED:
329 case self::STATUS_INSTALLED_MISSING:
330 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
331 break;
332 case self::STATUS_DISABLED:
333 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
334 $typeManager->onPreUninstall($info);
335 $this->_removeExtensionEntry($info);
336 $typeManager->onPostUninstall($info);
337 break;
338 case self::STATUS_DISABLED_MISSING:
339 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
340 $typeManager->onPreUninstall($info);
341 $this->_removeExtensionEntry($info);
342 $typeManager->onPostUninstall($info);
343 break;
344 case self::STATUS_UNINSTALLED:
345 // ok, nothing to do
346 break;
347 case self::STATUS_UNKNOWN:
348 default:
349 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
350 }
351 }
352
353 $this->statuses = NULL;
354 $this->mapper->refresh();
355 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
356 }
357
358 /**
359 * Determine the status of an extension
360 *
361 * @return string constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
362 */
363 public function getStatus($key) {
364 $statuses = $this->getStatuses();
365 if (array_key_exists($key, $statuses)) {
366 return $statuses[$key];
367 } else {
368 return self::STATUS_UNKNOWN;
369 }
370 }
371
372 /**
373 * Determine the status of all extensions
374 *
375 * @return array ($key => status_constant)
376 */
377 public function getStatuses() {
378 if (!is_array($this->statuses)) {
379 $this->statuses = array();
380
381 foreach ($this->fullContainer->getKeys() as $key) {
382 $this->statuses[$key] = self::STATUS_UNINSTALLED;
383 }
384
385 $sql = '
386 SELECT full_name, is_active
387 FROM civicrm_extension
388 ';
389 $dao = CRM_Core_DAO::executeQuery($sql);
390 while ($dao->fetch()) {
391 try {
392 $path = $this->fullContainer->getPath($dao->full_name);
393 $codeExists = !empty($path) && is_dir($path);
394 } catch (CRM_Extension_Exception $e) {
395 $codeExists = FALSE;
396 }
397 if ($dao->is_active) {
398 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
399 } else {
400 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
401 }
402 }
403 }
404 return $this->statuses;
405 }
406
407 public function refresh() {
408 $this->statuses = NULL;
409 $this->fullContainer->refresh(); // and, indirectly, defaultContainer
410 $this->mapper->refresh();
411 }
412
413 // ----------------------
414
415 /**
416 * Find the $info and $typeManager for a $key
417 *
418 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
419 * @throws CRM_Extension_Exception
420 */
421 private function _getInfoTypeHandler($key) {
422 $info = $this->mapper->keyToInfo($key); // throws Exception
423 if (array_key_exists($info->type, $this->typeManagers)) {
424 return array($info, $this->typeManagers[$info->type]);
425 } else {
426 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
427 }
428 }
429
430 /**
431 * Find the $info and $typeManager for a $key
432 *
433 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
434 * @throws CRM_Extension_Exception
435 */
436 private function _getMissingInfoTypeHandler($key) {
437 $info = $this->createInfoFromDB($key);
438 if ($info) {
439 if (array_key_exists($info->type, $this->typeManagers)) {
440 return array($info, $this->typeManagers[$info->type]);
441 } else {
442 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
443 }
444 } else {
445 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
446 }
447 }
448
449 private function _createExtensionEntry(CRM_Extension_Info $info) {
450 $dao = new CRM_Core_DAO_Extension();
451 $dao->label = $info->label;
452 $dao->name = $info->name;
453 $dao->full_name = $info->key;
454 $dao->type = $info->type;
455 $dao->file = $info->file;
456 $dao->is_active = 1;
457 return (bool) ($dao->insert());
458 }
459
460 private function _updateExtensionEntry(CRM_Extension_Info $info) {
461 $dao = new CRM_Core_DAO_Extension();
462 $dao->full_name = $info->key;
463 if ($dao->find(TRUE)) {
464 $dao->label = $info->label;
465 $dao->name = $info->name;
466 $dao->full_name = $info->key;
467 $dao->type = $info->type;
468 $dao->file = $info->file;
469 $dao->is_active = 1;
470 return (bool) ($dao->update());
471 } else {
472 return $this->_createExtensionEntry($info);
473 }
474 }
475
476 private function _removeExtensionEntry(CRM_Extension_Info $info) {
477 $dao = new CRM_Core_DAO_Extension();
478 $dao->full_name = $info->key;
479 if ($dao->find(TRUE)) {
480 if (CRM_Core_BAO_Extension::del($dao->id)) {
481 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
482 } else {
483 throw new CRM_Extension_Exception("Failed to remove extension entry");
484 }
485 } // else: post-condition already satisified
486 }
487
488 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
489 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
490 1 => array($isActive, 'Integer'),
491 2 => array($info->key, 'String'),
492 ));
493 }
494
495 /**
496 * Auto-generate a place-holder for a missing extension using info from
497 * database.
498 *
499 * @return CRM_Extension_Info|NULL
500 */
501 public function createInfoFromDB($key) {
502 $dao = new CRM_Core_DAO_Extension();
503 $dao->full_name = $key;
504 if ($dao->find(TRUE)) {
505 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
506 return $info;
507 } else {
508 return NULL;
509 }
510 }
511}