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