Fixed Code clean up for batch 15
[civicrm-core.git] / CRM / Extension / Browser.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
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 +--------------------------------------------------------------------+
26*/
27
28/**
29 * This class glues together the various parts of the extension
30 * system.
31 *
32 * @package CRM
06b69b18 33 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
34 * $Id$
35 *
36 */
37class CRM_Extension_Browser {
38
39 /**
40 * An URL for public extensions repository
41 */
72b14f38 42 const DEFAULT_EXTENSIONS_REPOSITORY = 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
6a488035
TO
43
44 /**
78612209
TO
45 * @param string $repoUrl
46 * URL of the remote repository.
47 * @param string $indexPath
48 * Relative path of the 'index' file within the repository.
49 * @param string $cacheDir
50 * Local path in which to cache files.
6a488035
TO
51 */
52 public function __construct($repoUrl, $indexPath, $cacheDir) {
53 $this->repoUrl = $repoUrl;
54 $this->cacheDir = $cacheDir;
55 $this->indexPath = $indexPath;
78612209 56 if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writable(dirname($cacheDir))) {
6a488035
TO
57 CRM_Utils_File::createDir($cacheDir, FALSE);
58 }
59 }
60
61 /**
62 * Determine whether the system policy allows downloading new extensions.
63 *
64 * This is reflection of *policy* and *intent*; it does not indicate whether
65 * the browser will actually *work*. For that, see checkRequirements().
66 *
67 * @return bool
68 */
69 public function isEnabled() {
70 return (FALSE !== $this->getRepositoryUrl());
71 }
72
e0ef6999
EM
73 /**
74 * @return string
75 */
6a488035
TO
76 public function getRepositoryUrl() {
77 return $this->repoUrl;
78 }
79
78612209
TO
80 /**
81 * Refresh the cache of remotely-available extensions.
82 */
6a488035
TO
83 public function refresh() {
84 $file = $this->getTsPath();
85 if (file_exists($file)) {
86 unlink($file);
87 }
88 }
89
90 /**
91 * Determine whether downloading is supported
92 *
78612209
TO
93 * @return array
94 * List of error messages; empty if OK.
6a488035
TO
95 */
96 public function checkRequirements() {
97 if (!$this->isEnabled()) {
98 return array();
99 }
100
101 $errors = array();
102
78612209 103 if (!$this->cacheDir || !is_dir($this->cacheDir) || !is_writable($this->cacheDir)) {
6a488035
TO
104 $civicrmDestination = urlencode(CRM_Utils_System::url('civicrm/admin/extensions', 'reset=1'));
105 $url = CRM_Utils_System::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
106 $errors[] = array(
107 'title' => ts('Directory Unwritable'),
108 '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/>',
109 array(
110 1 => $this->cacheDir,
111 2 => $url,
112 )
78612209 113 ),
6a488035
TO
114 );
115 }
116
117 return $errors;
118 }
119
120 /**
121 * Get a list of all available extensions
122 *
78612209
TO
123 * @return array
124 * ($key => CRM_Extension_Info)
6a488035
TO
125 */
126 public function getExtensions() {
127 if (!$this->isEnabled() || count($this->checkRequirements())) {
128 return array();
129 }
130
131 $exts = array();
132
133 $remote = $this->_discoverRemote();
134 if (is_array($remote)) {
135 foreach ($remote as $dc => $e) {
136 $exts[$e->key] = $e;
137 }
138 }
139
140 return $exts;
141 }
142
143 /**
144 * Get a description of a particular extension
145 *
78612209
TO
146 * @param string $key
147 * Fully-qualified extension name.
fd31fa4c 148 *
6a488035
TO
149 * @return CRM_Extension_Info|NULL
150 */
151 public function getExtension($key) {
152 // TODO optimize performance -- we don't need to fetch/cache the entire repo
153 $exts = $this->getExtensions();
154 if (array_key_exists($key, $exts)) {
155 return $exts[$key];
78612209
TO
156 }
157 else {
6a488035
TO
158 // throw new CRM_Extension_Exception("Unknown remote extension: $key");
159 return NULL;
160 }
161 }
162
e0ef6999
EM
163 /**
164 * @return array
165 * @throws CRM_Extension_Exception_ParseException
166 */
6a488035 167 private function _discoverRemote() {
78612209 168 $tsPath = $this->getTsPath();
6a488035
TO
169 $timestamp = FALSE;
170
171 if (file_exists($tsPath)) {
172 $timestamp = file_get_contents($tsPath);
173 }
174
175 // 3 minutes ago for now
176 $outdated = (int) $timestamp < (time() - 180) ? TRUE : FALSE;
177
178 if (!$timestamp || $outdated) {
179 $remotes = $this->grabRemoteKeyList();
180 $cached = FALSE;
181 }
182 else {
183 $remotes = $this->grabCachedKeyList();
184 $cached = TRUE;
185 }
186
187 $this->_remotesDiscovered = array();
188 foreach ($remotes as $id => $rext) {
189 $xml = $this->grabRemoteInfoFile($rext['key'], $cached);
190 if ($xml != FALSE) {
191 $ext = CRM_Extension_Info::loadFromString($xml);
192 $this->_remotesDiscovered[] = $ext;
193 }
194 }
195
196 if (file_exists(dirname($tsPath))) {
197 file_put_contents($tsPath, (string) time());
198 }
199
200 return $this->_remotesDiscovered;
201 }
202
e0ef6999
EM
203 /**
204 * @return array
205 */
6a488035 206 private function grabCachedKeyList() {
78612209 207 $result = array();
6a488035 208 $cachedPath = $this->cacheDir . DIRECTORY_SEPARATOR;
78612209 209 $files = scandir($cachedPath);
6a488035
TO
210 foreach ($files as $dc => $fname) {
211 if (substr($fname, -4) == '.xml') {
73505023 212 $result[] = array('key' => substr($fname, 0, -4));
6a488035
TO
213 }
214 }
215 return $result;
216 }
217
218 /**
219 * Connects to public server and grabs the list of publically available
220 * extensions.
221 *
6a488035 222 *
408b79bf 223 * @return array
a6c01b45 224 * list of extension names
6a488035
TO
225 */
226 private function grabRemoteKeyList() {
227
228 ini_set('default_socket_timeout', CRM_Utils_VersionCheck::CHECK_TIMEOUT);
229 set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
230
231 if (!ini_get('allow_url_fopen')) {
232 ini_set('allow_url_fopen', 1);
233 }
234
78612209 235 if (FALSE === $this->getRepositoryUrl()) {
6a488035
TO
236 // don't check if the user has configured civi not to check an external
237 // url for extensions. See CRM-10575.
238 CRM_Core_Session::setStatus(ts('Not checking remote URL for extensions since ext_repo_url is set to false.'), ts('Check Settings'), 'alert');
239 return array();
240 }
241
7aa1b99d 242 $exts = array();
72b14f38
TO
243 list ($status, $extdir) = CRM_Utils_HttpClient::singleton()->get($this->getRepositoryUrl() . $this->indexPath);
244 if ($extdir === FALSE || $status !== CRM_Utils_HttpClient::STATUS_OK) {
6a488035 245 CRM_Core_Session::setStatus(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>.<br />', array(1 => $this->getRepositoryUrl())), ts('Connection Error'), 'error');
78612209
TO
246 }
247 else {
7aa1b99d
DG
248 $lines = explode("\n", $extdir);
249
250 foreach ($lines as $ln) {
251 if (preg_match("@\<li\>(.*)\</li\>@i", $ln, $out)) {
252 // success
253 $extsRaw[] = $out;
254 $key = strip_tags($out[1]);
255 if (substr($key, -4) == '.xml') {
256 $exts[] = array('key' => substr($key, 0, -4));
257 }
6a488035
TO
258 }
259 }
260 }
261
70619090
DG
262 // CRM-13141 There may not be any compatible extensions available for the requested CiviCRM version + CMS. If so, $extdir is empty so just return a notification.
263 if (empty($exts)) {
264 $config = CRM_Core_Config::singleton();
78612209 265 CRM_Core_Session::setStatus(ts('There are currently no extensions on the CiviCRM public extension directory which are compatible with version %2 (<a href="%1">requested extensions from here</a>). If you want to install an extension which is not marked as compatible, you may be able to <a href="%3">download and install extensions manually</a> (depending on access to your web server).<br />', array(
353ffa53
TO
266 1 => $this->getRepositoryUrl(),
267 2 => $config->civiVersion,
268 3 => 'http://wiki.civicrm.org/confluence/display/CRMDOC/Extensions',
269 )), ts('No Extensions Available for this Version'), 'info');
70619090
DG
270 }
271
6a488035
TO
272 ini_restore('allow_url_fopen');
273 ini_restore('default_socket_timeout');
274
275 restore_error_handler();
276
277 return $exts;
278 }
279
280 /**
281 * Given the key, retrieves the info XML from a remote server
282 * and stores locally, returning the contents.
283 *
78612209
TO
284 * @param string $key
285 * Extension key.
286 * @param bool $cached
287 * Whether to use cached data.
6a488035 288 *
78612209
TO
289 * @return string
290 * Contents of info.xml, or null if info.xml cannot be retrieved or parsed.
6a488035
TO
291 */
292 private function grabRemoteInfoFile($key, $cached = FALSE) {
293 $filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.xml';
78612209 294 $url = $this->getRepositoryUrl() . '/' . $key . '.xml';
6a488035
TO
295
296 if (!$cached || !file_exists($filename)) {
72b14f38
TO
297 $fetchStatus = CRM_Utils_HttpClient::singleton()->fetch($url, $filename);
298 if ($fetchStatus != CRM_Utils_HttpClient::STATUS_OK) {
299 return NULL;
300 }
6a488035
TO
301 }
302
303 if (file_exists($filename)) {
304 $contents = file_get_contents($filename);
305
306 //parse just in case
307 $check = simplexml_load_string($contents);
308
309 if (!$check) {
310 foreach (libxml_get_errors() as $error) {
311 CRM_Core_Error::debug('xmlError', $error);
312 }
313 return;
314 }
315
316 return $contents;
317 }
318 }
319
e0ef6999
EM
320 /**
321 * @return string
322 */
6a488035 323 private function getTsPath() {
78612209 324 return $this->cacheDir . DIRECTORY_SEPARATOR . 'timestamp.txt';
6a488035
TO
325 }
326
327 /**
328 * A dummy function required for suppressing download errors
329 */
330 public static function downloadError($errorNumber, $errorString) {
6a488035
TO
331 }
332
232624b1 333}