Merge pull request #22692 from demeritcowboy/composer
[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) {
affcc9d2 52 $errors = [];
6a488035 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
19ec0aa5
MWMC
74 if ($extensionInfo) {
75 $requiredExtensions = CRM_Extension_System::singleton()->getManager()->findInstallRequirements([$extensionInfo->key], $extensionInfo);
76 foreach ($requiredExtensions as $extension) {
64f6b376 77 if (CRM_Extension_System::singleton()->getManager()->getStatus($extension) !== CRM_Extension_Manager::STATUS_INSTALLED && $extension !== $extensionInfo->key) {
795b2761 78 $requiredExtensionInfo = CRM_Extension_System::singleton()->getBrowser()->getExtension($extension);
79 $requiredExtensionInfoName = empty($requiredExtensionInfo->name) ? $extension : $requiredExtensionInfo->name;
19ec0aa5
MWMC
80 $errors[] = [
81 'title' => ts('Missing Requirement: %1', [1 => $extension]),
795b2761 82 'message' => ts('You will not be able to install/upgrade %1 until you have installed the %2 extension.', [1 => $extensionInfo->name, 2 => $requiredExtensionInfoName]),
19ec0aa5
MWMC
83 ];
84 }
85 }
86 }
87
6a488035
TO
88 return $errors;
89 }
90
91 /**
fe482240 92 * Install or upgrade an extension from a remote URL.
6a488035 93 *
f41911fd
TO
94 * @param string $key
95 * The name of the extension being installed.
96 * @param string $downloadUrl
97 * URL of a .zip file.
a6c01b45
CW
98 * @return bool
99 * TRUE for success
6a488035
TO
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) {
df9f2d1a 107 throw new CRM_Extension_Exception(ts('Cannot install this extension - downloadUrl is not set!'));
6a488035
TO
108 }
109
353ffa53 110 if (!$this->fetch($downloadUrl, $filename)) {
6a488035
TO
111 return FALSE;
112 }
113
114 $extractedZipPath = $this->extractFiles($key, $filename);
353ffa53 115 if (!$extractedZipPath) {
6a488035
TO
116 return FALSE;
117 }
118
353ffa53 119 if (!$this->validateFiles($key, $extractedZipPath)) {
6a488035
TO
120 return FALSE;
121 }
122
123 $this->manager->replace($extractedZipPath);
124
125 return TRUE;
126 }
127
128 /**
129 * Download the remote zipfile.
130 *
f41911fd
TO
131 * @param string $remoteFile
132 * URL of a .zip file.
133 * @param string $localFile
134 * Path at which to store the .zip file.
ae5ffbb7 135 * @return bool
a6c01b45 136 * Whether the download was successful.
6a488035
TO
137 */
138 public function fetch($remoteFile, $localFile) {
3b6f287b 139 $result = CRM_Utils_HttpClient::singleton()->fetch($remoteFile, $localFile);
6a488035
TO
140 switch ($result) {
141 case CRM_Utils_HttpClient::STATUS_OK:
142 return TRUE;
b3a4b879 143
6a488035
TO
144 default:
145 return FALSE;
146 }
147 }
148
149 /**
fe482240 150 * Extract an extension from a zip file.
6a488035 151 *
f41911fd
TO
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.
72b3a70c
CW
156 * @return string|FALSE
157 * zip file path
6a488035
TO
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 *
da6b46f4
EM
194 * @param $key
195 * @param $extractedZipPath
196 *
6a488035 197 * @return bool
df9f2d1a 198 * @throws CRM_Core_Exception
6a488035 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) {
df9f2d1a 216 throw new CRM_Core_Exception(ts('Cannot install - there are differences between extdir XML file and archive XML file!'));
6a488035
TO
217 }
218
219 return TRUE;
220 }
221
222}