Merge pull request #15759 from tunbola/active-option-values-for-custom-group
[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 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 /**
30 * @param CRM_Extension_Manager $manager
31 * @param string $containerDir
32 * The place to store downloaded & extracted extensions.
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 /**
42 * Determine whether downloading is supported.
43 *
44 * @param \CRM_EXtension_Info $extensionInfo Optional info for (updated) extension
45 *
46 * @return array
47 * list of error messages; empty if OK
48 */
49 public function checkRequirements($extensionInfo = NULL) {
50 $errors = array();
51
52 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
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'),
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(
59 1 => $url,
60 )
61 ),
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
72 if (empty($errors) && !CRM_Utils_HttpClient::singleton()->isRedirectSupported()) {
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 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.');
75 }
76
77 if ($extensionInfo) {
78 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
79 foreach ($requiredExtensions as $extension) {
80 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
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
89 return $errors;
90 }
91
92 /**
93 * Install or upgrade an extension from a remote URL.
94 *
95 * @param string $key
96 * The name of the extension being installed.
97 * @param string $downloadUrl
98 * URL of a .zip file.
99 * @return bool
100 * TRUE for success
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) {
108 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
109 }
110
111 if (!$this->fetch($downloadUrl, $filename)) {
112 return FALSE;
113 }
114
115 $extractedZipPath = $this->extractFiles($key, $filename);
116 if (!$extractedZipPath) {
117 return FALSE;
118 }
119
120 if (!$this->validateFiles($key, $extractedZipPath)) {
121 return FALSE;
122 }
123
124 $this->manager->replace($extractedZipPath);
125
126 return TRUE;
127 }
128
129 /**
130 * Download the remote zipfile.
131 *
132 * @param string $remoteFile
133 * URL of a .zip file.
134 * @param string $localFile
135 * Path at which to store the .zip file.
136 * @return bool
137 * Whether the download was successful.
138 */
139 public function fetch($remoteFile, $localFile) {
140 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
141 switch ($result) {
142 case CRM_Utils_HttpClient::STATUS_OK:
143 return TRUE;
144
145 default:
146 return FALSE;
147 }
148 }
149
150 /**
151 * Extract an extension from a zip file.
152 *
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.
157 * @return string|FALSE
158 * zip file path
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 *
195 * @param $key
196 * @param $extractedZipPath
197 *
198 * @return bool
199 * @throws CRM_Core_Exception
200 */
201 public function validateFiles($key, $extractedZipPath) {
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);
210 }
211 catch (Exception $e) {
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) {
217 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
218 }
219
220 return TRUE;
221 }
222
223 }