Participant import fix - broken uniqueName fields, mapping saving, event_id
[civicrm-core.git] / CRM / Extension / Browser.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 glues together the various parts of the extension
14 * system.
15 *
16 * @package CRM
ca5cec67 17 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
18 */
19class CRM_Extension_Browser {
20
21 /**
fe482240 22 * An URL for public extensions repository.
7595b57f
TO
23 *
24 * Note: This default is now handled through setting/*.php.
25 *
26 * @deprecated
6a488035 27 */
72b14f38 28 const DEFAULT_EXTENSIONS_REPOSITORY = 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
6a488035 29
61c87ccb
DK
30 /**
31 * Relative path below remote repository URL for single extensions file.
32 */
33 const SINGLE_FILE_PATH = '/single';
34
35 /**
36 * The name of the single JSON extension cache file.
37 */
38 const CACHE_JSON_FILE = 'extensions.json';
39
a0e1d4d3
CW
40 // timeout for when the connection or the server is slow
41 const CHECK_TIMEOUT = 5;
42
7865cc4e
MW
43 /**
44 * @var GuzzleHttp\Client
45 */
46 protected $guzzleClient;
47
48 /**
49 * @return \GuzzleHttp\Client
50 */
51 public function getGuzzleClient(): \GuzzleHttp\Client {
52 return $this->guzzleClient ?? new \GuzzleHttp\Client();
53 }
54
55 /**
56 * @param \GuzzleHttp\Client $guzzleClient
57 */
58 public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) {
59 $this->guzzleClient = $guzzleClient;
60 }
61
6a488035 62 /**
78612209
TO
63 * @param string $repoUrl
64 * URL of the remote repository.
65 * @param string $indexPath
66 * Relative path of the 'index' file within the repository.
67 * @param string $cacheDir
68 * Local path in which to cache files.
6a488035
TO
69 */
70 public function __construct($repoUrl, $indexPath, $cacheDir) {
71 $this->repoUrl = $repoUrl;
72 $this->cacheDir = $cacheDir;
61c87ccb 73 $this->indexPath = empty($indexPath) ? self::SINGLE_FILE_PATH : $indexPath;
78612209 74 if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writable(dirname($cacheDir))) {
6a488035
TO
75 CRM_Utils_File::createDir($cacheDir, FALSE);
76 }
77 }
78
79 /**
80 * Determine whether the system policy allows downloading new extensions.
81 *
82 * This is reflection of *policy* and *intent*; it does not indicate whether
83 * the browser will actually *work*. For that, see checkRequirements().
84 *
85 * @return bool
86 */
87 public function isEnabled() {
88 return (FALSE !== $this->getRepositoryUrl());
89 }
90
e0ef6999
EM
91 /**
92 * @return string
93 */
6a488035
TO
94 public function getRepositoryUrl() {
95 return $this->repoUrl;
96 }
97
78612209
TO
98 /**
99 * Refresh the cache of remotely-available extensions.
100 */
6a488035
TO
101 public function refresh() {
102 $file = $this->getTsPath();
103 if (file_exists($file)) {
104 unlink($file);
105 }
106 }
107
108 /**
fe482240 109 * Determine whether downloading is supported.
6a488035 110 *
78612209
TO
111 * @return array
112 * List of error messages; empty if OK.
6a488035
TO
113 */
114 public function checkRequirements() {
115 if (!$this->isEnabled()) {
affcc9d2 116 return [];
6a488035
TO
117 }
118
affcc9d2 119 $errors = [];
6a488035 120
78612209 121 if (!$this->cacheDir || !is_dir($this->cacheDir) || !is_writable($this->cacheDir)) {
6a488035
TO
122 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
123 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
124 $errors[] = array(
125 'title' => ts('Directory Unwritable'),
126 'message' => ts('Your extensions cache directory (%1) is not web server writable. Please go to the <a href="%2">path setting page</a> and correct it.<br/>',
127 array(
128 1 => $this->cacheDir,
129 2 => $url,
130 )
78612209 131 ),
6a488035
TO
132 );
133 }
134
135 return $errors;
136 }
137
138 /**
fe482240 139 * Get a list of all available extensions.
6a488035 140 *
64f4eebe 141 * @return CRM_Extension_Info[]
78612209 142 * ($key => CRM_Extension_Info)
6a488035
TO
143 */
144 public function getExtensions() {
145 if (!$this->isEnabled() || count($this->checkRequirements())) {
affcc9d2 146 return [];
6a488035
TO
147 }
148
affcc9d2 149 $exts = [];
6a488035
TO
150
151 $remote = $this->_discoverRemote();
152 if (is_array($remote)) {
153 foreach ($remote as $dc => $e) {
154 $exts[$e->key] = $e;
155 }
156 }
157
158 return $exts;
159 }
160
161 /**
fe482240 162 * Get a description of a particular extension.
6a488035 163 *
78612209
TO
164 * @param string $key
165 * Fully-qualified extension name.
fd31fa4c 166 *
6a488035
TO
167 * @return CRM_Extension_Info|NULL
168 */
169 public function getExtension($key) {
170 // TODO optimize performance -- we don't need to fetch/cache the entire repo
171 $exts = $this->getExtensions();
172 if (array_key_exists($key, $exts)) {
173 return $exts[$key];
78612209
TO
174 }
175 else {
6a488035
TO
176 return NULL;
177 }
178 }
179
e0ef6999 180 /**
64f4eebe 181 * @return CRM_Extension_Info[]
e0ef6999
EM
182 * @throws CRM_Extension_Exception_ParseException
183 */
6a488035 184 private function _discoverRemote() {
78612209 185 $tsPath = $this->getTsPath();
6a488035
TO
186 $timestamp = FALSE;
187
188 if (file_exists($tsPath)) {
189 $timestamp = file_get_contents($tsPath);
190 }
191
192 // 3 minutes ago for now
193 $outdated = (int) $timestamp < (time() - 180) ? TRUE : FALSE;
194
195 if (!$timestamp || $outdated) {
61c87ccb 196 $remotes = json_decode($this->grabRemoteJson(), TRUE);
6a488035
TO
197 }
198 else {
61c87ccb 199 $remotes = json_decode($this->grabCachedJson(), TRUE);
6a488035
TO
200 }
201
affcc9d2 202 $this->_remotesDiscovered = [];
6190f479 203 foreach ((array) $remotes as $id => $xml) {
61c87ccb
DK
204 $ext = CRM_Extension_Info::loadFromString($xml);
205 $this->_remotesDiscovered[] = $ext;
6a488035
TO
206 }
207
208 if (file_exists(dirname($tsPath))) {
209 file_put_contents($tsPath, (string) time());
210 }
211
212 return $this->_remotesDiscovered;
213 }
214
e0ef6999 215 /**
61c87ccb
DK
216 * Loads the extensions data from the cache file. If it is empty
217 * or doesn't exist, try fetching from remote instead.
218 *
219 * @return string
e0ef6999 220 */
61c87ccb 221 private function grabCachedJson() {
5bcef2c3
TO
222 $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE . '.' . md5($this->getRepositoryUrl());
223 $json = NULL;
224 if (file_exists($filename)) {
225 $json = file_get_contents($filename);
226 }
61c87ccb
DK
227 if (empty($json)) {
228 $json = $this->grabRemoteJson();
6a488035 229 }
61c87ccb 230 return $json;
6a488035
TO
231 }
232
233 /**
b44e3f84 234 * Connects to public server and grabs the list of publicly available
6a488035
TO
235 * extensions.
236 *
61c87ccb 237 * @return string
b769826b 238 * @throws \CRM_Extension_Exception
6a488035 239 */
61c87ccb 240 private function grabRemoteJson() {
6a488035
TO
241 set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
242
78612209 243 if (FALSE === $this->getRepositoryUrl()) {
6a488035
TO
244 // don't check if the user has configured civi not to check an external
245 // url for extensions. See CRM-10575.
c4ba4203 246 return '';
6a488035
TO
247 }
248
5bcef2c3 249 $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE . '.' . md5($this->getRepositoryUrl());
61c87ccb 250 $url = $this->getRepositoryUrl() . $this->indexPath;
6a488035 251
7865cc4e 252 $client = $this->getGuzzleClient();
c4ba4203 253 $response = $client->request('GET', $url, ['sink' => $filename, 'timeout' => \Civi::settings()->get('http_timeout')]);
6a488035
TO
254 restore_error_handler();
255
c4ba4203
MW
256 if ($response->getStatusCode() !== 200) {
257 throw new CRM_Extension_Exception(ts('The CiviCRM public extensions directory at %1 could not be contacted - please check your webserver can make external HTTP requests', [1 => $this->getRepositoryUrl()]), 'connection_error');
b769826b
CW
258 }
259
260 // Don't call grabCachedJson here, that would risk infinite recursion
261 return file_get_contents($filename);
6a488035
TO
262 }
263
e0ef6999
EM
264 /**
265 * @return string
266 */
6a488035 267 private function getTsPath() {
78612209 268 return $this->cacheDir . DIRECTORY_SEPARATOR . 'timestamp.txt';
6a488035
TO
269 }
270
271 /**
fe482240 272 * A dummy function required for suppressing download errors.
c2b5a0af
EM
273 *
274 * @param $errorNumber
275 * @param $errorString
6a488035
TO
276 */
277 public static function downloadError($errorNumber, $errorString) {
6a488035
TO
278 }
279
232624b1 280}