Merge pull request #17253 from mattwire/utf8convertblocksize
[civicrm-core.git] / CRM / Extension / Downloader.php
CommitLineData
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 */
18class CRM_Extension_Downloader {
19 /**
51dda21e
SL
20 * @var CRM_Extension_Container_Basic
21 * The place where downloaded extensions are ultimately stored
6a488035
TO
22 */
23 public $container;
24
25 /**
51dda21e
SL
26 * @var string
27 * Local path to a temporary data directory
6a488035
TO
28 */
29 public $tmpDir;
30
31 /**
fd31fa4c 32 * @param CRM_Extension_Manager $manager
f41911fd
TO
33 * @param string $containerDir
34 * The place to store downloaded & extracted extensions.
6a488035
TO
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 /**
fe482240 44 * Determine whether downloading is supported.
6a488035 45 *
19ec0aa5
MWMC
46 * @param \CRM_EXtension_Info $extensionInfo Optional info for (updated) extension
47 *
a6c01b45
CW
48 * @return array
49 * list of error messages; empty if OK
6a488035 50 */
19ec0aa5 51 public function checkRequirements($extensionInfo = NULL) {
6a488035
TO
52 $errors = array();
53
22e263ad 54 if (!$this->containerDir || !is_dir($this->containerDir) || !is_writable($this->containerDir)) {
6a488035
TO
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'),
6a488035
TO
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(
6a488035
TO
61 1 => $url,
62 )
21dfd5f5 63 ),
6a488035
TO
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
353ffa53 74 if (empty($errors) && !CRM_Utils_HttpClient::singleton()->isRedirectSupported()) {
eb066397 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.'));
df9f2d1a 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.');
eb066397
TO
77 }
78
19ec0aa5
MWMC
79 if ($extensionInfo) {
80 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
81 foreach ($requiredExtensions as $extension) {
64f6b376 82 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
19ec0aa5
MWMC
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
6a488035
TO
91 return $errors;
92 }
93
94 /**
fe482240 95 * Install or upgrade an extension from a remote URL.
6a488035 96 *
f41911fd
TO
97 * @param string $key
98 * The name of the extension being installed.
99 * @param string $downloadUrl
100 * URL of a .zip file.
a6c01b45
CW
101 * @return bool
102 * TRUE for success
6a488035
TO
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) {
df9f2d1a 110 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
6a488035
TO
111 }
112
353ffa53 113 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
114 return FALSE;
115 }
116
117 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 118 if (!$extractedZipPath) {
6a488035
TO
119 return FALSE;
120 }
121
353ffa53 122 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
123 return FALSE;
124 }
125
126 $this->manager->replace($extractedZipPath);
127
128 return TRUE;
129 }
130
131 /**
132 * Download the remote zipfile.
133 *
f41911fd
TO
134 * @param string $remoteFile
135 * URL of a .zip file.
136 * @param string $localFile
137 * Path at which to store the .zip file.
ae5ffbb7 138 * @return bool
a6c01b45 139 * Whether the download was successful.
6a488035
TO
140 */
141 public function fetch($remoteFile, $localFile) {
3b6f287b 142 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
6a488035
TO
143 switch ($result) {
144 case CRM_Utils_HttpClient::STATUS_OK:
145 return TRUE;
b3a4b879 146
6a488035
TO
147 default:
148 return FALSE;
149 }
150 }
151
152 /**
fe482240 153 * Extract an extension from a zip file.
6a488035 154 *
f41911fd
TO
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.
72b3a70c
CW
159 * @return string|FALSE
160 * zip file path
6a488035
TO
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 *
da6b46f4
EM
197 * @param $key
198 * @param $extractedZipPath
199 *
6a488035 200 * @return bool
df9f2d1a 201 * @throws CRM_Core_Exception
6a488035 202 */
00be9182 203 public function validateFiles($key, $extractedZipPath) {
6a488035
TO
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);
0db6c3e1
TO
212 }
213 catch (Exception $e) {
6a488035
TO
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) {
df9f2d1a 219 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
6a488035
TO
220 }
221
222 return TRUE;
223 }
224
225}