Merge pull request #1524 from totten/master-contactget-in
[civicrm-core.git] / CRM / Extension / Manager.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
232624b1 4 | CiviCRM version 4.4 |
6a488035
TO
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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-2013
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
141 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
142 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(
143 1 => $newInfo->key,
144 2 => $oldPath,
145 )));
146 }
147 break;
148 case self::STATUS_INSTALLED_MISSING:
149 case self::STATUS_DISABLED_MISSING:
150 // the extension does not exist in any container; we're free to put it anywhere
151 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
152 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key); // throws Exception
153 break;
154 case self::STATUS_UNKNOWN:
155 // the extension does not exist in any container; we're free to put it anywhere
156 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
157 $oldInfo = $typeManager = NULL;
158 break;
159 default:
160 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
161 }
162
163 // move the code!
164 switch ($oldStatus) {
165 case self::STATUS_UNINSTALLED:
166 case self::STATUS_UNKNOWN:
167 // There are no DB records to worry about, so we'll just put the files in place
168 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
169 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
170 }
171 break;
172 case self::STATUS_INSTALLED:
173 case self::STATUS_INSTALLED_MISSING:
174 case self::STATUS_DISABLED:
175 case self::STATUS_DISABLED_MISSING:
176 // There are DB records; coordinate the file placement with the DB updates
177 $typeManager->onPreReplace($oldInfo, $newInfo);
178 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
179 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
180 }
181 $this->_updateExtensionEntry($newInfo);
182 $typeManager->onPostReplace($oldInfo, $newInfo);
183 break;
184 default:
185 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
186 }
187
188 $this->refresh();
189 }
190
191 /**
192 * Add records of the extension to the database -- and enable it
193 *
194 * @param array $keys list of extension keys
195 * @return void
196 * @throws CRM_Extension_Exception
197 */
198 public function install($keys) {
199 $origStatuses = $this->getStatuses();
200
201 // TODO: to mitigate the risk of crashing during installation, scan
202 // keys/statuses/types before doing anything
203
204 foreach ($keys as $key) {
205 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
206
207 switch ($origStatuses[$key]) {
208 case self::STATUS_INSTALLED:
209 // ok, nothing to do
210 break;
211 case self::STATUS_DISABLED:
212 // re-enable it
213 $typeManager->onPreEnable($info);
214 $this->_setExtensionActive($info, 1);
215 $typeManager->onPostEnable($info);
216 break;
217 case self::STATUS_UNINSTALLED:
218 // install anew
219 $typeManager->onPreInstall($info);
220 $this->_createExtensionEntry($info);
221 $typeManager->onPostInstall($info);
222 break;
223 case self::STATUS_UNKNOWN:
224 default:
225 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
226 }
227 }
228
229 $this->statuses = NULL;
230 $this->mapper->refresh();
231 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
232 }
233
234 /**
235 * Add records of the extension to the database -- and enable it
236 *
237 * @param array $keys list of extension keys
238 * @return void
239 * @throws CRM_Extension_Exception
240 */
241 public function enable($keys) {
242 $this->install($keys);
243 }
244
245 /**
246 * Add records of the extension to the database -- and enable it
247 *
248 * @param array $keys list of extension keys
249 * @return void
250 * @throws CRM_Extension_Exception
251 */
252 public function disable($keys) {
253 $origStatuses = $this->getStatuses();
254
255 // TODO: to mitigate the risk of crashing during installation, scan
256 // keys/statuses/types before doing anything
257
258 foreach ($keys as $key) {
259 switch ($origStatuses[$key]) {
260 case self::STATUS_INSTALLED:
261 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
262 $typeManager->onPreDisable($info);
263 $this->_setExtensionActive($info, 0);
264 $typeManager->onPostDisable($info);
265 break;
266 case self::STATUS_INSTALLED_MISSING:
267 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
268 $typeManager->onPreDisable($info);
269 $this->_setExtensionActive($info, 0);
270 $typeManager->onPostDisable($info);
271 break;
272 case self::STATUS_DISABLED:
273 case self::STATUS_DISABLED_MISSING:
274 case self::STATUS_UNINSTALLED:
275 // ok, nothing to do
276 break;
277 case self::STATUS_UNKNOWN:
278 default:
279 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
280 }
281 }
282
283 $this->statuses = NULL;
284 $this->mapper->refresh();
285 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
286 }
287
288 /**
289 * Remove all database references to an extension
290 *
291 * Add records of the extension to the database -- and enable it
292 *
293 * @param array $keys list of extension keys
294 * @return void
295 * @throws CRM_Extension_Exception
296 */
297 public function uninstall($keys) {
298 $origStatuses = $this->getStatuses();
299
300 // TODO: to mitigate the risk of crashing during installation, scan
301 // keys/statuses/types before doing anything
302
303 foreach ($keys as $key) {
304 switch ($origStatuses[$key]) {
305 case self::STATUS_INSTALLED:
306 case self::STATUS_INSTALLED_MISSING:
307 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
308 break;
309 case self::STATUS_DISABLED:
310 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
311 $typeManager->onPreUninstall($info);
312 $this->_removeExtensionEntry($info);
313 $typeManager->onPostUninstall($info);
314 break;
315 case self::STATUS_DISABLED_MISSING:
316 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
317 $typeManager->onPreUninstall($info);
318 $this->_removeExtensionEntry($info);
319 $typeManager->onPostUninstall($info);
320 break;
321 case self::STATUS_UNINSTALLED:
322 // ok, nothing to do
323 break;
324 case self::STATUS_UNKNOWN:
325 default:
326 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
327 }
328 }
329
330 $this->statuses = NULL;
331 $this->mapper->refresh();
332 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
333 }
334
335 /**
336 * Determine the status of an extension
337 *
338 * @return string constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
339 */
340 public function getStatus($key) {
341 $statuses = $this->getStatuses();
342 if (array_key_exists($key, $statuses)) {
343 return $statuses[$key];
344 } else {
345 return self::STATUS_UNKNOWN;
346 }
347 }
348
349 /**
350 * Determine the status of all extensions
351 *
352 * @return array ($key => status_constant)
353 */
354 public function getStatuses() {
355 if (!is_array($this->statuses)) {
356 $this->statuses = array();
357
358 foreach ($this->fullContainer->getKeys() as $key) {
359 $this->statuses[$key] = self::STATUS_UNINSTALLED;
360 }
361
362 $sql = '
363 SELECT full_name, is_active
364 FROM civicrm_extension
365 ';
366 $dao = CRM_Core_DAO::executeQuery($sql);
367 while ($dao->fetch()) {
368 try {
369 $path = $this->fullContainer->getPath($dao->full_name);
370 $codeExists = !empty($path) && is_dir($path);
371 } catch (CRM_Extension_Exception $e) {
372 $codeExists = FALSE;
373 }
374 if ($dao->is_active) {
375 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
376 } else {
377 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
378 }
379 }
380 }
381 return $this->statuses;
382 }
383
384 public function refresh() {
385 $this->statuses = NULL;
386 $this->fullContainer->refresh(); // and, indirectly, defaultContainer
387 $this->mapper->refresh();
388 }
389
390 // ----------------------
391
392 /**
393 * Find the $info and $typeManager for a $key
394 *
395 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
396 * @throws CRM_Extension_Exception
397 */
398 private function _getInfoTypeHandler($key) {
399 $info = $this->mapper->keyToInfo($key); // throws Exception
400 if (array_key_exists($info->type, $this->typeManagers)) {
401 return array($info, $this->typeManagers[$info->type]);
402 } else {
403 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
404 }
405 }
406
407 /**
408 * Find the $info and $typeManager for a $key
409 *
410 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
411 * @throws CRM_Extension_Exception
412 */
413 private function _getMissingInfoTypeHandler($key) {
414 $info = $this->createInfoFromDB($key);
415 if ($info) {
416 if (array_key_exists($info->type, $this->typeManagers)) {
417 return array($info, $this->typeManagers[$info->type]);
418 } else {
419 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
420 }
421 } else {
422 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
423 }
424 }
425
426 private function _createExtensionEntry(CRM_Extension_Info $info) {
427 $dao = new CRM_Core_DAO_Extension();
428 $dao->label = $info->label;
429 $dao->name = $info->name;
430 $dao->full_name = $info->key;
431 $dao->type = $info->type;
432 $dao->file = $info->file;
433 $dao->is_active = 1;
434 return (bool) ($dao->insert());
435 }
436
437 private function _updateExtensionEntry(CRM_Extension_Info $info) {
438 $dao = new CRM_Core_DAO_Extension();
439 $dao->full_name = $info->key;
440 if ($dao->find(TRUE)) {
441 $dao->label = $info->label;
442 $dao->name = $info->name;
443 $dao->full_name = $info->key;
444 $dao->type = $info->type;
445 $dao->file = $info->file;
446 $dao->is_active = 1;
447 return (bool) ($dao->update());
448 } else {
449 return $this->_createExtensionEntry($info);
450 }
451 }
452
453 private function _removeExtensionEntry(CRM_Extension_Info $info) {
454 $dao = new CRM_Core_DAO_Extension();
455 $dao->full_name = $info->key;
456 if ($dao->find(TRUE)) {
457 if (CRM_Core_BAO_Extension::del($dao->id)) {
458 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
459 } else {
460 throw new CRM_Extension_Exception("Failed to remove extension entry");
461 }
462 } // else: post-condition already satisified
463 }
464
465 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
466 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
467 1 => array($isActive, 'Integer'),
468 2 => array($info->key, 'String'),
469 ));
470 }
471
472 /**
473 * Auto-generate a place-holder for a missing extension using info from
474 * database.
475 *
476 * @return CRM_Extension_Info|NULL
477 */
478 public function createInfoFromDB($key) {
479 $dao = new CRM_Core_DAO_Extension();
480 $dao->full_name = $key;
481 if ($dao->find(TRUE)) {
482 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
483 return $info;
484 } else {
485 return NULL;
486 }
487 }
488}