Merge pull request #21854 from braders/feature/dev-core-2919-make-hidden-modal-button...
[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 ($extensionInfo) {
75 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
76 foreach ($requiredExtensions as $extension) {
77 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
78 $requiredExtensionInfo = CRM_Extension_System::singleton()->getBrowser()->getExtension($extension);
79 $requiredExtensionInfoName = empty($requiredExtensionInfo->name) ? $extension : $requiredExtensionInfo->name;
80 $errors[] = [
81 'title' => ts('Missing Requirement: %1', [1 => $extension]),
82 'message' => ts('You will not be able to install/upgrade %1 until you have installed the %2 extension.', [1 => $extensionInfo->name, 2 => $requiredExtensionInfoName]),
83 ];
84 }
85 }
86 }
87
88 return $errors;
89 }
90
91 /**
92 * Install or upgrade an extension from a remote URL.
93 *
94 * @param string $key
95 * The name of the extension being installed.
96 * @param string $downloadUrl
97 * URL of a .zip file.
98 * @return bool
99 * TRUE for success
100 * @throws CRM_Extension_Exception
101 */
102 public function download($key, $downloadUrl) {
103 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
104 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
105
106 if (!$downloadUrl) {
107 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
108 }
109
110 if (!$this->fetch($downloadUrl, $filename)) {
111 return FALSE;
112 }
113
114 $extractedZipPath = $this->extractFiles($key, $filename);
115 if (!$extractedZipPath) {
116 return FALSE;
117 }
118
119 if (!$this->validateFiles($key, $extractedZipPath)) {
120 return FALSE;
121 }
122
123 $this->manager->replace($extractedZipPath);
124
125 return TRUE;
126 }
127
128 /**
129 * Download the remote zipfile.
130 *
131 * @param string $remoteFile
132 * URL of a .zip file.
133 * @param string $localFile
134 * Path at which to store the .zip file.
135 * @return bool
136 * Whether the download was successful.
137 */
138 public function fetch($remoteFile, $localFile) {
139 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
140 switch ($result) {
141 case CRM_Utils_HttpClient::STATUS_OK:
142 return TRUE;
143
144 default:
145 return FALSE;
146 }
147 }
148
149 /**
150 * Extract an extension from a zip file.
151 *
152 * @param string $key
153 * The name of the extension being installed; this usually matches the basedir in the .zip.
154 * @param string $zipFile
155 * The local path to a .zip file.
156 * @return string|FALSE
157 * zip file path
158 */
159 public function extractFiles($key, $zipFile) {
160 $config = CRM_Core_Config::singleton();
161
162 $zip = new ZipArchive();
163 $res = $zip->open($zipFile);
164 if ($res === TRUE) {
165 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
166 if ($zipSubDir === FALSE) {
167 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
168 return FALSE;
169 }
170 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
171 if (is_dir($extractedZipPath)) {
172 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
173 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
174 return FALSE;
175 }
176 }
177 if (!$zip->extractTo($this->tmpDir)) {
178 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
179 return FALSE;
180 }
181 $zip->close();
182 }
183 else {
184 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
185 return FALSE;
186 }
187
188 return $extractedZipPath;
189 }
190
191 /**
192 * Validate that $extractedZipPath contains valid for extension $key
193 *
194 * @param $key
195 * @param $extractedZipPath
196 *
197 * @return bool
198 * @throws CRM_Core_Exception
199 */
200 public function validateFiles($key, $extractedZipPath) {
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);
209 }
210 catch (Exception $e) {
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) {
216 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
217 }
218
219 return TRUE;
220 }
221
222 }