3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
37 class CRM_Extension_Browser
{
40 * An URL for public extensions repository
42 const DEFAULT_EXTENSIONS_REPOSITORY
= 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
45 * @param string $repoUrl URL of the remote repository
46 * @param string $indexPath relative path of the 'index' file within the repository
47 * @param string $cacheDir local path in which to cache files
49 public function __construct($repoUrl, $indexPath, $cacheDir) {
50 $this->repoUrl
= $repoUrl;
51 $this->cacheDir
= $cacheDir;
52 $this->indexPath
= $indexPath;
53 if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writeable(dirname($cacheDir))) {
54 CRM_Utils_File
::createDir($cacheDir, FALSE);
59 * Determine whether the system policy allows downloading new extensions.
61 * This is reflection of *policy* and *intent*; it does not indicate whether
62 * the browser will actually *work*. For that, see checkRequirements().
66 public function isEnabled() {
67 return (FALSE !== $this->getRepositoryUrl());
73 public function getRepositoryUrl() {
74 return $this->repoUrl
;
77 public function refresh() {
78 $file = $this->getTsPath();
79 if (file_exists($file)) {
85 * Determine whether downloading is supported
87 * @return array list of error messages; empty if OK
89 public function checkRequirements() {
90 if (!$this->isEnabled()) {
96 if (!$this->cacheDir ||
!is_dir($this->cacheDir
) ||
!is_writeable($this->cacheDir
)) {
97 $civicrmDestination = urlencode(CRM_Utils_System
::url('civicrm/admin/extensions', 'reset=1'));
98 $url = CRM_Utils_System
::url('civicrm/admin/setting/path', "reset=1&civicrmDestination=${civicrmDestination}");
100 'title' => ts('Directory Unwritable'),
101 '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/>',
103 1 => $this->cacheDir
,
114 * Get a list of all available extensions
116 * @return array ($key => CRM_Extension_Info)
118 public function getExtensions() {
119 if (!$this->isEnabled() ||
count($this->checkRequirements())) {
125 $remote = $this->_discoverRemote();
126 if (is_array($remote)) {
127 foreach ($remote as $dc => $e) {
136 * Get a description of a particular extension
140 * @return CRM_Extension_Info|NULL
142 public function getExtension($key) {
143 // TODO optimize performance -- we don't need to fetch/cache the entire repo
144 $exts = $this->getExtensions();
145 if (array_key_exists($key, $exts)) {
148 // throw new CRM_Extension_Exception("Unknown remote extension: $key");
155 * @throws CRM_Extension_Exception_ParseException
157 private function _discoverRemote() {
158 $tsPath = $this->getTsPath();
161 if (file_exists($tsPath)) {
162 $timestamp = file_get_contents($tsPath);
165 // 3 minutes ago for now
166 $outdated = (int) $timestamp < (time() - 180) ?
TRUE : FALSE;
168 if (!$timestamp ||
$outdated) {
169 $remotes = $this->grabRemoteKeyList();
173 $remotes = $this->grabCachedKeyList();
177 $this->_remotesDiscovered
= array();
178 foreach ($remotes as $id => $rext) {
179 $xml = $this->grabRemoteInfoFile($rext['key'], $cached);
181 $ext = CRM_Extension_Info
::loadFromString($xml);
182 $this->_remotesDiscovered
[] = $ext;
186 if (file_exists(dirname($tsPath))) {
187 file_put_contents($tsPath, (string) time());
190 return $this->_remotesDiscovered
;
196 private function grabCachedKeyList() {
198 $cachedPath = $this->cacheDir
. DIRECTORY_SEPARATOR
;
199 $files = scandir($cachedPath);
200 foreach ($files as $dc => $fname) {
201 if (substr($fname, -4) == '.xml') {
202 $result[] = array('key' => substr($fname, 0, -4));
209 * Connects to public server and grabs the list of publically available
214 * @return Array list of extension names
216 private function grabRemoteKeyList() {
218 ini_set('default_socket_timeout', CRM_Utils_VersionCheck
::CHECK_TIMEOUT
);
219 set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
221 if (!ini_get('allow_url_fopen')) {
222 ini_set('allow_url_fopen', 1);
225 if(FALSE === $this->getRepositoryUrl()) {
226 // don't check if the user has configured civi not to check an external
227 // url for extensions. See CRM-10575.
228 CRM_Core_Session
::setStatus(ts('Not checking remote URL for extensions since ext_repo_url is set to false.'), ts('Check Settings'), 'alert');
233 list ($status, $extdir) = CRM_Utils_HttpClient
::singleton()->get($this->getRepositoryUrl() . $this->indexPath
);
234 if ($extdir === FALSE ||
$status !== CRM_Utils_HttpClient
::STATUS_OK
) {
235 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');
237 $lines = explode("\n", $extdir);
239 foreach ($lines as $ln) {
240 if (preg_match("@\<li\>(.*)\</li\>@i", $ln, $out)) {
243 $key = strip_tags($out[1]);
244 if (substr($key, -4) == '.xml') {
245 $exts[] = array('key' => substr($key, 0, -4));
251 // 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.
253 $config = CRM_Core_Config
::singleton();
254 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(1 => $this->getRepositoryUrl(), 2 => $config->civiVersion
, 3 => 'http://wiki.civicrm.org/confluence/display/CRMDOC/Extensions')), ts('No Extensions Available for this Version'), 'info');
257 ini_restore('allow_url_fopen');
258 ini_restore('default_socket_timeout');
260 restore_error_handler();
266 * Given the key, retrieves the info XML from a remote server
267 * and stores locally, returning the contents.
271 * @param string $key extension key
272 * @param boolean $cached whether to use cached data
274 * @return contents of info.xml, or null if info.xml cannot be retrieved or parsed
276 private function grabRemoteInfoFile($key, $cached = FALSE) {
277 $filename = $this->cacheDir
. DIRECTORY_SEPARATOR
. $key . '.xml';
278 $url = $this->getRepositoryUrl() . '/' . $key . '.xml';
280 if (!$cached ||
!file_exists($filename)) {
281 $fetchStatus = CRM_Utils_HttpClient
::singleton()->fetch($url, $filename);
282 if ($fetchStatus != CRM_Utils_HttpClient
::STATUS_OK
) {
287 if (file_exists($filename)) {
288 $contents = file_get_contents($filename);
291 $check = simplexml_load_string($contents);
294 foreach (libxml_get_errors() as $error) {
295 CRM_Core_Error
::debug('xmlError', $error);
307 private function getTsPath() {
308 return $this->cacheDir
. DIRECTORY_SEPARATOR
. 'timestamp.txt';
312 * A dummy function required for suppressing download errors
314 public static function downloadError($errorNumber, $errorString) {