INFRA-132 - CRM/Export - phpcbf
[civicrm-core.git] / CRM / Extension / Manager.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
34 * $Id$
35 *
36 */
37 class 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_Interface $fullContainer
105 * @param CRM_Extension_Container_Basic|FALSE $defaultContainer
106 * @param CRM_Extension_Mapper $mapper
107 * @param $typeManagers
108 */
109 public function __construct(CRM_Extension_Container_Interface $fullContainer, $defaultContainer, CRM_Extension_Mapper $mapper, $typeManagers) {
110 $this->fullContainer = $fullContainer;
111 $this->defaultContainer = $defaultContainer;
112 $this->mapper = $mapper;
113 $this->typeManagers = $typeManagers;
114 }
115
116 /**
117 * Install or upgrade the code for an extension -- and perform any
118 * necessary database changes (eg replacing extension metadata).
119 *
120 * This only works if the extension is stored in the default container.
121 *
122 * @param string $tmpCodeDir
123 * Path to a local directory containing a copy of the new (inert) code.
124 * @return void
125 * @throws CRM_Extension_Exception
126 */
127 public function replace($tmpCodeDir) {
128 if (! $this->defaultContainer) {
129 throw new CRM_Extension_Exception("Default extension container is not configured");
130 }
131
132 $newInfo = CRM_Extension_Info::loadFromFile($tmpCodeDir . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
133 $oldStatus = $this->getStatus($newInfo->key);
134
135 // find $tgtPath, $oldInfo, $typeManager
136 switch ($oldStatus) {
137 case self::STATUS_UNINSTALLED:
138 case self::STATUS_INSTALLED:
139 case self::STATUS_DISABLED:
140 // There is an old copy of the extension. Try to install in the same place -- but it must go somewhere in the default-container
141 list ($oldInfo, $typeManager) = $this->_getInfoTypeHandler($newInfo->key); // throws Exception
142 $tgtPath = $this->fullContainer->getPath($newInfo->key);
143 if (! CRM_Utils_File::isChildPath($this->defaultContainer->getBaseDir(), $tgtPath)) {
144 // force installation in the default-container
145 $oldPath = $tgtPath;
146 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
147 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(
148 1 => $newInfo->key,
149 2 => $oldPath,
150 )));
151 }
152 break;
153 case self::STATUS_INSTALLED_MISSING:
154 case self::STATUS_DISABLED_MISSING:
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 list ($oldInfo, $typeManager) = $this->_getMissingInfoTypeHandler($newInfo->key); // throws Exception
158 break;
159 case self::STATUS_UNKNOWN:
160 // the extension does not exist in any container; we're free to put it anywhere
161 $tgtPath = $this->defaultContainer->getBaseDir() . DIRECTORY_SEPARATOR . $newInfo->key;
162 $oldInfo = $typeManager = NULL;
163 break;
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 case self::STATUS_INSTALLED:
178 case self::STATUS_INSTALLED_MISSING:
179 case self::STATUS_DISABLED:
180 case self::STATUS_DISABLED_MISSING:
181 // There are DB records; coordinate the file placement with the DB updates
182 $typeManager->onPreReplace($oldInfo, $newInfo);
183 if (!CRM_Utils_File::replaceDir($tmpCodeDir, $tgtPath)) {
184 throw new CRM_Extension_Exception("Failed to move $tmpCodeDir to $tgtPath");
185 }
186 $this->_updateExtensionEntry($newInfo);
187 $typeManager->onPostReplace($oldInfo, $newInfo);
188 break;
189 default:
190 throw new CRM_Extension_Exception("Cannot install or enable extension: {$newInfo->key}");
191 }
192
193 $this->refresh();
194 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
195 }
196
197 /**
198 * Add records of the extension to the database -- and enable it
199 *
200 * @param array $keys
201 * List of extension keys.
202 * @return void
203 * @throws CRM_Extension_Exception
204 */
205 public function install($keys) {
206 $origStatuses = $this->getStatuses();
207
208 // TODO: to mitigate the risk of crashing during installation, scan
209 // keys/statuses/types before doing anything
210
211 foreach ($keys as $key) {
212 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
213
214 switch ($origStatuses[$key]) {
215 case self::STATUS_INSTALLED:
216 // ok, nothing to do
217 break;
218 case self::STATUS_DISABLED:
219 // re-enable it
220 $typeManager->onPreEnable($info);
221 $this->_setExtensionActive($info, 1);
222 $typeManager->onPostEnable($info);
223 break;
224 case self::STATUS_UNINSTALLED:
225 // install anew
226 $typeManager->onPreInstall($info);
227 $this->_createExtensionEntry($info);
228 $typeManager->onPostInstall($info);
229 break;
230 case self::STATUS_UNKNOWN:
231 default:
232 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
233 }
234 }
235
236 $this->statuses = NULL;
237 $this->mapper->refresh();
238 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
239
240 foreach ($keys as $key) {
241 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
242 //print_r(array('post post?', $info, 'k' => $key, 'os'=> $origStatuses[$key]));
243
244 switch ($origStatuses[$key]) {
245 case self::STATUS_INSTALLED:
246 // ok, nothing to do
247 break;
248 case self::STATUS_DISABLED:
249 // re-enable it
250 break;
251 case self::STATUS_UNINSTALLED:
252 // install anew
253 $typeManager->onPostPostInstall($info);
254 break;
255 case self::STATUS_UNKNOWN:
256 default:
257 throw new CRM_Extension_Exception("Cannot install or enable extension: $key");
258 }
259 }
260
261 }
262
263 /**
264 * Add records of the extension to the database -- and enable it
265 *
266 * @param array $keys
267 * List of extension keys.
268 * @return void
269 * @throws CRM_Extension_Exception
270 */
271 public function enable($keys) {
272 $this->install($keys);
273 }
274
275 /**
276 * Add records of the extension to the database -- and enable it
277 *
278 * @param array $keys
279 * List of extension keys.
280 * @return void
281 * @throws CRM_Extension_Exception
282 */
283 public function disable($keys) {
284 $origStatuses = $this->getStatuses();
285
286 // TODO: to mitigate the risk of crashing during installation, scan
287 // keys/statuses/types before doing anything
288
289 foreach ($keys as $key) {
290 switch ($origStatuses[$key]) {
291 case self::STATUS_INSTALLED:
292 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
293 $typeManager->onPreDisable($info);
294 $this->_setExtensionActive($info, 0);
295 $typeManager->onPostDisable($info);
296 break;
297 case self::STATUS_INSTALLED_MISSING:
298 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
299 $typeManager->onPreDisable($info);
300 $this->_setExtensionActive($info, 0);
301 $typeManager->onPostDisable($info);
302 break;
303 case self::STATUS_DISABLED:
304 case self::STATUS_DISABLED_MISSING:
305 case self::STATUS_UNINSTALLED:
306 // ok, nothing to do
307 break;
308 case self::STATUS_UNKNOWN:
309 default:
310 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
311 }
312 }
313
314 $this->statuses = NULL;
315 $this->mapper->refresh();
316 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
317 }
318
319 /**
320 * Remove all database references to an extension
321 *
322 * Add records of the extension to the database -- and enable it
323 *
324 * @param array $keys
325 * List of extension keys.
326 * @return void
327 * @throws CRM_Extension_Exception
328 */
329 public function uninstall($keys) {
330 $origStatuses = $this->getStatuses();
331
332 // TODO: to mitigate the risk of crashing during installation, scan
333 // keys/statuses/types before doing anything
334
335 foreach ($keys as $key) {
336 switch ($origStatuses[$key]) {
337 case self::STATUS_INSTALLED:
338 case self::STATUS_INSTALLED_MISSING:
339 throw new CRM_Extension_Exception("Cannot uninstall extension; disable it first: $key");
340 break;
341 case self::STATUS_DISABLED:
342 list ($info, $typeManager) = $this->_getInfoTypeHandler($key); // throws Exception
343 $typeManager->onPreUninstall($info);
344 $this->_removeExtensionEntry($info);
345 $typeManager->onPostUninstall($info);
346 break;
347 case self::STATUS_DISABLED_MISSING:
348 list ($info, $typeManager) = $this->_getMissingInfoTypeHandler($key); // throws Exception
349 $typeManager->onPreUninstall($info);
350 $this->_removeExtensionEntry($info);
351 $typeManager->onPostUninstall($info);
352 break;
353 case self::STATUS_UNINSTALLED:
354 // ok, nothing to do
355 break;
356 case self::STATUS_UNKNOWN:
357 default:
358 throw new CRM_Extension_Exception("Cannot disable unknown extension: $key");
359 }
360 }
361
362 $this->statuses = NULL;
363 $this->mapper->refresh();
364 CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
365 }
366
367 /**
368 * Determine the status of an extension
369 *
370 * @param $key
371 *
372 * @return string constant (STATUS_INSTALLED, STATUS_DISABLED, STATUS_UNINSTALLED, STATUS_UNKNOWN)
373 */
374 public function getStatus($key) {
375 $statuses = $this->getStatuses();
376 if (array_key_exists($key, $statuses)) {
377 return $statuses[$key];
378 } else {
379 return self::STATUS_UNKNOWN;
380 }
381 }
382
383 /**
384 * Determine the status of all extensions
385 *
386 * @return array ($key => status_constant)
387 */
388 public function getStatuses() {
389 if (!is_array($this->statuses)) {
390 $this->statuses = array();
391
392 foreach ($this->fullContainer->getKeys() as $key) {
393 $this->statuses[$key] = self::STATUS_UNINSTALLED;
394 }
395
396 $sql = '
397 SELECT full_name, is_active
398 FROM civicrm_extension
399 ';
400 $dao = CRM_Core_DAO::executeQuery($sql);
401 while ($dao->fetch()) {
402 try {
403 $path = $this->fullContainer->getPath($dao->full_name);
404 $codeExists = !empty($path) && is_dir($path);
405 } catch (CRM_Extension_Exception $e) {
406 $codeExists = FALSE;
407 }
408 if ($dao->is_active) {
409 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_INSTALLED : self::STATUS_INSTALLED_MISSING;
410 } else {
411 $this->statuses[$dao->full_name] = $codeExists ? self::STATUS_DISABLED : self::STATUS_DISABLED_MISSING;
412 }
413 }
414 }
415 return $this->statuses;
416 }
417
418 public function refresh() {
419 $this->statuses = NULL;
420 $this->fullContainer->refresh(); // and, indirectly, defaultContainer
421 $this->mapper->refresh();
422 }
423
424 // ----------------------
425
426 /**
427 * Find the $info and $typeManager for a $key
428 *
429 * @param $key
430 *
431 * @throws CRM_Extension_Exception
432 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
433 */
434 private function _getInfoTypeHandler($key) {
435 $info = $this->mapper->keyToInfo($key); // throws Exception
436 if (array_key_exists($info->type, $this->typeManagers)) {
437 return array($info, $this->typeManagers[$info->type]);
438 } else {
439 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
440 }
441 }
442
443 /**
444 * Find the $info and $typeManager for a $key
445 *
446 * @param $key
447 *
448 * @throws CRM_Extension_Exception
449 * @return array (0 => CRM_Extension_Info, 1 => CRM_Extension_Manager_Interface)
450 */
451 private function _getMissingInfoTypeHandler($key) {
452 $info = $this->createInfoFromDB($key);
453 if ($info) {
454 if (array_key_exists($info->type, $this->typeManagers)) {
455 return array($info, $this->typeManagers[$info->type]);
456 } else {
457 throw new CRM_Extension_Exception("Unrecognized extension type: " . $info->type);
458 }
459 } else {
460 throw new CRM_Extension_Exception("Failed to reconstruct missing extension: " . $key);
461 }
462 }
463
464 /**
465 * @param CRM_Extension_Info $info
466 *
467 * @return bool
468 */
469 private function _createExtensionEntry(CRM_Extension_Info $info) {
470 $dao = new CRM_Core_DAO_Extension();
471 $dao->label = $info->label;
472 $dao->name = $info->name;
473 $dao->full_name = $info->key;
474 $dao->type = $info->type;
475 $dao->file = $info->file;
476 $dao->is_active = 1;
477 return (bool) ($dao->insert());
478 }
479
480 /**
481 * @param CRM_Extension_Info $info
482 *
483 * @return bool
484 */
485 private function _updateExtensionEntry(CRM_Extension_Info $info) {
486 $dao = new CRM_Core_DAO_Extension();
487 $dao->full_name = $info->key;
488 if ($dao->find(TRUE)) {
489 $dao->label = $info->label;
490 $dao->name = $info->name;
491 $dao->full_name = $info->key;
492 $dao->type = $info->type;
493 $dao->file = $info->file;
494 $dao->is_active = 1;
495 return (bool) ($dao->update());
496 } else {
497 return $this->_createExtensionEntry($info);
498 }
499 }
500
501 /**
502 * @param CRM_Extension_Info $info
503 *
504 * @throws CRM_Extension_Exception
505 */
506 private function _removeExtensionEntry(CRM_Extension_Info $info) {
507 $dao = new CRM_Core_DAO_Extension();
508 $dao->full_name = $info->key;
509 if ($dao->find(TRUE)) {
510 if (CRM_Core_BAO_Extension::del($dao->id)) {
511 CRM_Core_Session::setStatus(ts('Selected option value has been deleted.'), ts('Deleted'), 'success');
512 } else {
513 throw new CRM_Extension_Exception("Failed to remove extension entry");
514 }
515 } // else: post-condition already satisified
516 }
517
518 /**
519 * @param CRM_Extension_Info $info
520 * @param $isActive
521 */
522 private function _setExtensionActive(CRM_Extension_Info $info, $isActive) {
523 CRM_Core_DAO::executeQuery('UPDATE civicrm_extension SET is_active = %1 where full_name = %2', array(
524 1 => array($isActive, 'Integer'),
525 2 => array($info->key, 'String'),
526 ));
527 }
528
529 /**
530 * Auto-generate a place-holder for a missing extension using info from
531 * database.
532 *
533 * @param $key
534 * @return CRM_Extension_Info|NULL
535 */
536 public function createInfoFromDB($key) {
537 $dao = new CRM_Core_DAO_Extension();
538 $dao->full_name = $key;
539 if ($dao->find(TRUE)) {
540 $info = new CRM_Extension_Info($dao->full_name, $dao->type, $dao->name, $dao->label, $dao->file);
541 return $info;
542 } else {
543 return NULL;
544 }
545 }
546 }