Set version to 5.20.beta1
[civicrm-core.git] / CRM / Extension / Downloader.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * This class handles downloads of remotely-provided extensions
30 *
31 * @package CRM
6b83d5bd 32 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
33 */
34class CRM_Extension_Downloader {
35 /**
36 * @var CRM_Extension_Container_Basic the place where downloaded extensions are ultimately stored
37 */
38 public $container;
39
40 /**
41 * @var string local path to a temporary data directory
42 */
43 public $tmpDir;
44
45 /**
fd31fa4c 46 * @param CRM_Extension_Manager $manager
f41911fd
TO
47 * @param string $containerDir
48 * The place to store downloaded & extracted extensions.
6a488035
TO
49 * @param string $tmpDir
50 */
51 public function __construct(CRM_Extension_Manager $manager, $containerDir, $tmpDir) {
52 $this->manager = $manager;
53 $this->containerDir = $containerDir;
54 $this->tmpDir = $tmpDir;
55 }
56
57 /**
fe482240 58 * Determine whether downloading is supported.
6a488035 59 *
19ec0aa5
MWMC
60 * @param \CRM_EXtension_Info $extensionInfo Optional info for (updated) extension
61 *
a6c01b45
CW
62 * @return array
63 * list of error messages; empty if OK
6a488035 64 */
19ec0aa5 65 public function checkRequirements($extensionInfo = NULL) {
6a488035
TO
66 $errors = array();
67
22e263ad 68 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
6a488035
TO
69 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
70 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
71 $errors[] = array(
72 'title' => ts('Directory Unwritable'),
6a488035
TO
73 'message' => ts("Your extensions directory is not set or is not writable. Click <a href='%1'>here</a> to set the extensions directory.",
74 array(
6a488035
TO
75 1 => $url,
76 )
21dfd5f5 77 ),
6a488035
TO
78 );
79 }
80
81 if (!class_exists('ZipArchive')) {
82 $errors[] = array(
83 'title' => ts('ZIP Support Required'),
84 '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.'),
85 );
86 }
87
353ffa53 88 if (empty($errors) && !CRM_Utils_HttpClient::singleton()->isRedirectSupported()) {
eb066397
TO
89 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.'));
90 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.');
91 }
92
19ec0aa5
MWMC
93 if ($extensionInfo) {
94 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
95 foreach ($requiredExtensions as $extension) {
64f6b376 96 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
19ec0aa5
MWMC
97 $errors[] = [
98 'title' => ts('Missing Requirement: %1', [1 => $extension]),
99 'message' => ts('You will not be able to install/upgrade %1 until you have installed the %2 extension.', [1 => $extensionInfo->key, 2 => $extension]),
100 ];
101 }
102 }
103 }
104
6a488035
TO
105 return $errors;
106 }
107
108 /**
fe482240 109 * Install or upgrade an extension from a remote URL.
6a488035 110 *
f41911fd
TO
111 * @param string $key
112 * The name of the extension being installed.
113 * @param string $downloadUrl
114 * URL of a .zip file.
a6c01b45
CW
115 * @return bool
116 * TRUE for success
6a488035
TO
117 * @throws CRM_Extension_Exception
118 */
119 public function download($key, $downloadUrl) {
120 $filename = $this->tmpDir . DIRECTORY_SEPARATOR . $key . '.zip';
121 $destDir = $this->containerDir . DIRECTORY_SEPARATOR . $key;
122
123 if (!$downloadUrl) {
124 CRM_Core_Error::fatal('Cannot install this extension - downloadUrl is not set!');
125 }
126
353ffa53 127 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
128 return FALSE;
129 }
130
131 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 132 if (!$extractedZipPath) {
6a488035
TO
133 return FALSE;
134 }
135
353ffa53 136 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
137 return FALSE;
138 }
139
140 $this->manager->replace($extractedZipPath);
141
142 return TRUE;
143 }
144
145 /**
146 * Download the remote zipfile.
147 *
f41911fd
TO
148 * @param string $remoteFile
149 * URL of a .zip file.
150 * @param string $localFile
151 * Path at which to store the .zip file.
ae5ffbb7 152 * @return bool
a6c01b45 153 * Whether the download was successful.
6a488035
TO
154 */
155 public function fetch($remoteFile, $localFile) {
3b6f287b 156 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
6a488035
TO
157 switch ($result) {
158 case CRM_Utils_HttpClient::STATUS_OK:
159 return TRUE;
b3a4b879 160
6a488035
TO
161 default:
162 return FALSE;
163 }
164 }
165
166 /**
fe482240 167 * Extract an extension from a zip file.
6a488035 168 *
f41911fd
TO
169 * @param string $key
170 * The name of the extension being installed; this usually matches the basedir in the .zip.
171 * @param string $zipFile
172 * The local path to a .zip file.
72b3a70c
CW
173 * @return string|FALSE
174 * zip file path
6a488035
TO
175 */
176 public function extractFiles($key, $zipFile) {
177 $config = CRM_Core_Config::singleton();
178
179 $zip = new ZipArchive();
180 $res = $zip->open($zipFile);
181 if ($res === TRUE) {
182 $zipSubDir = CRM_Utils_Zip::guessBasedir($zip, $key);
183 if ($zipSubDir === FALSE) {
184 CRM_Core_Session::setStatus(ts('Unable to extract the extension: bad directory structure'), '', 'error');
185 return FALSE;
186 }
187 $extractedZipPath = $this->tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
188 if (is_dir($extractedZipPath)) {
189 if (!CRM_Utils_File::cleanDir($extractedZipPath, TRUE, FALSE)) {
190 CRM_Core_Session::setStatus(ts('Unable to extract the extension: %1 cannot be cleared', array(1 => $extractedZipPath)), ts('Installation Error'), 'error');
191 return FALSE;
192 }
193 }
194 if (!$zip->extractTo($this->tmpDir)) {
195 CRM_Core_Session::setStatus(ts('Unable to extract the extension to %1.', array(1 => $this->tmpDir)), ts('Installation Error'), 'error');
196 return FALSE;
197 }
198 $zip->close();
199 }
200 else {
201 CRM_Core_Session::setStatus(ts('Unable to extract the extension.'), '', 'error');
202 return FALSE;
203 }
204
205 return $extractedZipPath;
206 }
207
208 /**
209 * Validate that $extractedZipPath contains valid for extension $key
210 *
da6b46f4
EM
211 * @param $key
212 * @param $extractedZipPath
213 *
6a488035
TO
214 * @return bool
215 */
00be9182 216 public function validateFiles($key, $extractedZipPath) {
6a488035
TO
217 $filename = $extractedZipPath . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME;
218 if (!is_readable($filename)) {
219 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
220 return FALSE;
221 }
222
223 try {
224 $newInfo = CRM_Extension_Info::loadFromFile($filename);
0db6c3e1
TO
225 }
226 catch (Exception $e) {
6a488035
TO
227 CRM_Core_Session::setStatus(ts('Failed reading data from %1 during installation', array(1 => $filename)), ts('Installation Error'), 'error');
228 return FALSE;
229 }
230
231 if ($newInfo->key != $key) {
232 CRM_Core_Error::fatal('Cannot install - there are differences between extdir XML file and archive XML file!');
233 }
234
235 return TRUE;
236 }
237
238}