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