Merge pull request #15863 from seamuslee001/status_bounce_extension
[civicrm-core.git] / CRM / Extension / Downloader.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 * This class handles downloads of remotely-provided extensions
14 *
15 * @package CRM
ca5cec67 16 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
17 */
18class CRM_Extension_Downloader {
19 /**
20 * @var CRM_Extension_Container_Basic the place where downloaded extensions are ultimately stored
21 */
22 public $container;
23
24 /**
25 * @var string local path to a temporary data directory
26 */
27 public $tmpDir;
28
29 /**
fd31fa4c 30 * @param CRM_Extension_Manager $manager
f41911fd
TO
31 * @param string $containerDir
32 * The place to store downloaded & extracted extensions.
6a488035
TO
33 * @param string $tmpDir
34 */
35 public function __construct(CRM_Extension_Manager $manager, $containerDir, $tmpDir) {
36 $this->manager = $manager;
37 $this->containerDir = $containerDir;
38 $this->tmpDir = $tmpDir;
39 }
40
41 /**
fe482240 42 * Determine whether downloading is supported.
6a488035 43 *
19ec0aa5
MWMC
44 * @param \CRM_EXtension_Info $extensionInfo Optional info for (updated) extension
45 *
a6c01b45
CW
46 * @return array
47 * list of error messages; empty if OK
6a488035 48 */
19ec0aa5 49 public function checkRequirements($extensionInfo = NULL) {
6a488035
TO
50 $errors = array();
51
22e263ad 52 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
6a488035
TO
53 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
54 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
55 $errors[] = array(
56 'title' => ts('Directory Unwritable'),
6a488035
TO
57 'message' => ts("Your extensions directory is not set or is not writable. Click <a href='%1'>here</a> to set the extensions directory.",
58 array(
6a488035
TO
59 1 => $url,
60 )
21dfd5f5 61 ),
6a488035
TO
62 );
63 }
64
65 if (!class_exists('ZipArchive')) {
66 $errors[] = array(
67 'title' => ts('ZIP Support Required'),
68 'message' => ts('You will not be able to install extensions at this time because your installation of PHP does not support ZIP archives. Please ask your system administrator to install the standard PHP-ZIP extension.'),
69 );
70 }
71
353ffa53 72 if (empty($errors) && !CRM_Utils_HttpClient::singleton()->isRedirectSupported()) {
eb066397 73 CRM_Core_Session::setStatus(ts('WARNING: The downloader may be unable to download files which require HTTP redirection. This may be a configuration issue with PHP\'s open_basedir or safe_mode.'));
df9f2d1a 74 Civi::log()->debug('WARNING: The downloader may be unable to download files which require HTTP redirection. This may be a configuration issue with PHP\'s open_basedir or safe_mode.');
eb066397
TO
75 }
76
19ec0aa5
MWMC
77 if ($extensionInfo) {
78 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
79 foreach ($requiredExtensions as $extension) {
64f6b376 80 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
19ec0aa5
MWMC
81 $errors[] = [
82 'title' => ts('Missing Requirement: %1', [1 => $extension]),
83 'message' => ts('You will not be able to install/upgrade %1 until you have installed the %2 extension.', [1 => $extensionInfo->key, 2 => $extension]),
84 ];
85 }
86 }
87 }
88
6a488035
TO
89 return $errors;
90 }
91
92 /**
fe482240 93 * Install or upgrade an extension from a remote URL.
6a488035 94 *
f41911fd
TO
95 * @param string $key
96 * The name of the extension being installed.
97 * @param string $downloadUrl
98 * URL of a .zip file.
a6c01b45
CW
99 * @return bool
100 * TRUE for success
6a488035
TO
101 * @throws CRM_Extension_Exception
102 */
103 public function download($key, $downloadUrl) {
104 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
105 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
106
107 if (!$downloadUrl) {
df9f2d1a 108 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
6a488035
TO
109 }
110
353ffa53 111 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
112 return FALSE;
113 }
114
115 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 116 if (!$extractedZipPath) {
6a488035
TO
117 return FALSE;
118 }
119
353ffa53 120 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
121 return FALSE;
122 }
123
124 $this->manager->replace($extractedZipPath);
125
126 return TRUE;
127 }
128
129 /**
130 * Download the remote zipfile.
131 *
f41911fd
TO
132 * @param string $remoteFile
133 * URL of a .zip file.
134 * @param string $localFile
135 * Path at which to store the .zip file.
ae5ffbb7 136 * @return bool
a6c01b45 137 * Whether the download was successful.
6a488035
TO
138 */
139 public function fetch($remoteFile, $localFile) {
3b6f287b 140 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
6a488035
TO
141 switch ($result) {
142 case CRM_Utils_HttpClient::STATUS_OK:
143 return TRUE;
b3a4b879 144
6a488035
TO
145 default:
146 return FALSE;
147 }
148 }
149
150 /**
fe482240 151 * Extract an extension from a zip file.
6a488035 152 *
f41911fd
TO
153 * @param string $key
154 * The name of the extension being installed; this usually matches the basedir in the .zip.
155 * @param string $zipFile
156 * The local path to a .zip file.
72b3a70c
CW
157 * @return string|FALSE
158 * zip file path
6a488035
TO
159 */
160 public function extractFiles($key, $zipFile) {
161 $config = CRM_Core_Config::singleton();
162
163 $zip = new ZipArchive();
164 $res = $zip->open($zipFile);
165 if ($res === TRUE) {
166 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
167 if ($zipSubDir === FALSE) {
168 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
169 return FALSE;
170 }
171 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
172 if (is_dir($extractedZipPath)) {
173 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
174 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
175 return FALSE;
176 }
177 }
178 if (!$zip->extractTo($this->tmpDir)) {
179 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
180 return FALSE;
181 }
182 $zip->close();
183 }
184 else {
185 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
186 return FALSE;
187 }
188
189 return $extractedZipPath;
190 }
191
192 /**
193 * Validate that $extractedZipPath contains valid for extension $key
194 *
da6b46f4
EM
195 * @param $key
196 * @param $extractedZipPath
197 *
6a488035 198 * @return bool
df9f2d1a 199 * @throws CRM_Core_Exception
6a488035 200 */
00be9182 201 public function validateFiles($key, $extractedZipPath) {
6a488035
TO
202 $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME;
203 if (!is_readable($filename)) {
204 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
205 return FALSE;
206 }
207
208 try {
209 $newInfo = CRM_Extension_Info::loadFromFile($filename);
0db6c3e1
TO
210 }
211 catch (Exception $e) {
6a488035
TO
212 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
213 return FALSE;
214 }
215
216 if ($newInfo->key != $key) {
df9f2d1a 217 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
6a488035
TO
218 }
219
220 return TRUE;
221 }
222
223}