3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
29 * This class glues together the various parts of the extension
33 * @copyright CiviCRM LLC (c) 2004-2016
35 class CRM_Extension_Browser
{
38 * An URL for public extensions repository.
40 * Note: This default is now handled through setting/*.php.
44 const DEFAULT_EXTENSIONS_REPOSITORY
= 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
47 * Relative path below remote repository URL for single extensions file.
49 const SINGLE_FILE_PATH
= '/single';
52 * The name of the single JSON extension cache file.
54 const CACHE_JSON_FILE
= 'extensions.json';
56 // timeout for when the connection or the server is slow
57 const CHECK_TIMEOUT
= 5;
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.
67 public function __construct($repoUrl, $indexPath, $cacheDir) {
68 $this->repoUrl
= $repoUrl;
69 $this->cacheDir
= $cacheDir;
70 $this->indexPath
= empty($indexPath) ? self
::SINGLE_FILE_PATH
: $indexPath;
71 if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writable(dirname($cacheDir))) {
72 CRM_Utils_File
::createDir($cacheDir, FALSE);
77 * Determine whether the system policy allows downloading new extensions.
79 * This is reflection of *policy* and *intent*; it does not indicate whether
80 * the browser will actually *work*. For that, see checkRequirements().
84 public function isEnabled() {
85 return (FALSE !== $this->getRepositoryUrl());
91 public function getRepositoryUrl() {
92 return $this->repoUrl
;
96 * Refresh the cache of remotely-available extensions.
98 public function refresh() {
99 $file = $this->getTsPath();
100 if (file_exists($file)) {
106 * Determine whether downloading is supported.
109 * List of error messages; empty if OK.
111 public function checkRequirements() {
112 if (!$this->isEnabled()) {
118 if (!$this->cacheDir ||
!is_dir($this->cacheDir
) ||
!is_writable($this->cacheDir
)) {
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}");
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/>',
125 1 => $this->cacheDir
,
136 * Get a list of all available extensions.
139 * ($key => CRM_Extension_Info)
141 public function getExtensions() {
142 if (!$this->isEnabled() ||
count($this->checkRequirements())) {
148 $remote = $this->_discoverRemote();
149 if (is_array($remote)) {
150 foreach ($remote as $dc => $e) {
159 * Get a description of a particular extension.
162 * Fully-qualified extension name.
164 * @return CRM_Extension_Info|NULL
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)) {
179 * @throws CRM_Extension_Exception_ParseException
181 private function _discoverRemote() {
182 $tsPath = $this->getTsPath();
185 if (file_exists($tsPath)) {
186 $timestamp = file_get_contents($tsPath);
189 // 3 minutes ago for now
190 $outdated = (int) $timestamp < (time() - 180) ?
TRUE : FALSE;
192 if (!$timestamp ||
$outdated) {
193 $remotes = json_decode($this->grabRemoteJson(), TRUE);
196 $remotes = json_decode($this->grabCachedJson(), TRUE);
199 $this->_remotesDiscovered
= array();
200 foreach ($remotes as $id => $xml) {
201 $ext = CRM_Extension_Info
::loadFromString($xml);
202 $this->_remotesDiscovered
[] = $ext;
205 if (file_exists(dirname($tsPath))) {
206 file_put_contents($tsPath, (string) time());
209 return $this->_remotesDiscovered
;
213 * Loads the extensions data from the cache file. If it is empty
214 * or doesn't exist, try fetching from remote instead.
218 private function grabCachedJson() {
219 $filename = $this->cacheDir
. DIRECTORY_SEPARATOR
. self
::CACHE_JSON_FILE
;
220 $json = file_get_contents($filename);
222 $json = $this->grabRemoteJson();
228 * Connects to public server and grabs the list of publicly available
232 * @throws \CRM_Extension_Exception
234 private function grabRemoteJson() {
236 ini_set('default_socket_timeout', self
::CHECK_TIMEOUT
);
237 set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
239 if (!ini_get('allow_url_fopen')) {
240 ini_set('allow_url_fopen', 1);
243 if (FALSE === $this->getRepositoryUrl()) {
244 // don't check if the user has configured civi not to check an external
245 // url for extensions. See CRM-10575.
249 $filename = $this->cacheDir
. DIRECTORY_SEPARATOR
. self
::CACHE_JSON_FILE
;
250 $url = $this->getRepositoryUrl() . $this->indexPath
;
251 $status = CRM_Utils_HttpClient
::singleton()->fetch($url, $filename);
253 ini_restore('allow_url_fopen');
254 ini_restore('default_socket_timeout');
256 restore_error_handler();
258 if ($status !== CRM_Utils_HttpClient
::STATUS_OK
) {
259 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');
262 // Don't call grabCachedJson here, that would risk infinite recursion
263 return file_get_contents($filename);
269 private function getTsPath() {
270 return $this->cacheDir
. DIRECTORY_SEPARATOR
. 'timestamp.txt';
274 * A dummy function required for suppressing download errors.
276 * @param $errorNumber
277 * @param $errorString
279 public static function downloadError($errorNumber, $errorString) {