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