Commit | Line | Data |
---|---|---|
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 | */ |
18 | class 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 TO |
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.')); |
74 | CRM_Core_Error::debug_log_message('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.'); | |
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) { | |
2108fe32 | 108 | CRM_Core_Error::fatal(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 TO |
198 | * @return bool |
199 | */ | |
00be9182 | 200 | public function validateFiles($key, $extractedZipPath) { |
6a488035 TO |
201 | $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME; |
202 | if (!is_readable($filename)) { | |
203 | CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error'); | |
204 | return FALSE; | |
205 | } | |
206 | ||
207 | try { | |
208 | $newInfo = CRM_Extension_Info::loadFromFile($filename); | |
0db6c3e1 TO |
209 | } |
210 | catch (Exception $e) { | |
6a488035 TO |
211 | CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error'); |
212 | return FALSE; | |
213 | } | |
214 | ||
215 | if ($newInfo->key != $key) { | |
2108fe32 | 216 | CRM_Core_Error::fatal(ts('Cannot install - there are differences between extdir XML file and archive XML file!')); |
6a488035 TO |
217 | } |
218 | ||
219 | return TRUE; | |
220 | } | |
221 | ||
222 | } |