Merge pull request #7950 from JKingsnorth/CRM-18196
[civicrm-core.git] / CRM / Extension / Browser.php
index 6dabce65de6c7d47cf0a7eda58ee1c681d337df4..3a58366433b32bde57dcbd3ff8518900e30f80d7 100644 (file)
@@ -3,7 +3,7 @@
  +--------------------------------------------------------------------+
  | CiviCRM version 4.7                                                |
  +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2015                                |
+ | Copyright CiviCRM LLC (c) 2004-2016                                |
  +--------------------------------------------------------------------+
  | This file is a part of CiviCRM.                                    |
  |                                                                    |
  * system.
  *
  * @package CRM
- * @copyright CiviCRM LLC (c) 2004-2015
- * $Id$
- *
+ * @copyright CiviCRM LLC (c) 2004-2016
  */
 class CRM_Extension_Browser {
 
   /**
    * An URL for public extensions repository.
+   *
+   * Note: This default is now handled through setting/*.php.
+   *
+   * @deprecated
    */
   const DEFAULT_EXTENSIONS_REPOSITORY = 'https://civicrm.org/extdir/ver={ver}|cms={uf}';
 
+  /**
+   * Relative path below remote repository URL for single extensions file.
+   */
+  const SINGLE_FILE_PATH = '/single';
+
+  /**
+   * The name of the single JSON extension cache file.
+   */
+  const CACHE_JSON_FILE = 'extensions.json';
+
+  // timeout for when the connection or the server is slow
+  const CHECK_TIMEOUT = 5;
+
   /**
    * @param string $repoUrl
    *   URL of the remote repository.
@@ -52,7 +67,7 @@ class CRM_Extension_Browser {
   public function __construct($repoUrl, $indexPath, $cacheDir) {
     $this->repoUrl = $repoUrl;
     $this->cacheDir = $cacheDir;
-    $this->indexPath = $indexPath;
+    $this->indexPath = empty($indexPath) ? self::SINGLE_FILE_PATH : $indexPath;
     if ($cacheDir && !file_exists($cacheDir) && is_dir(dirname($cacheDir)) && is_writable(dirname($cacheDir))) {
       CRM_Utils_File::createDir($cacheDir, FALSE);
     }
@@ -155,7 +170,6 @@ class CRM_Extension_Browser {
       return $exts[$key];
     }
     else {
-      // throw new CRM_Extension_Exception("Unknown remote extension: $key");
       return NULL;
     }
   }
@@ -176,21 +190,16 @@ class CRM_Extension_Browser {
     $outdated = (int) $timestamp < (time() - 180) ? TRUE : FALSE;
 
     if (!$timestamp || $outdated) {
-      $remotes = $this->grabRemoteKeyList();
-      $cached = FALSE;
+      $remotes = json_decode($this->grabRemoteJson(), TRUE);
     }
     else {
-      $remotes = $this->grabCachedKeyList();
-      $cached = TRUE;
+      $remotes = json_decode($this->grabCachedJson(), TRUE);
     }
 
     $this->_remotesDiscovered = array();
-    foreach ($remotes as $id => $rext) {
-      $xml = $this->grabRemoteInfoFile($rext['key'], $cached);
-      if ($xml != FALSE) {
-        $ext = CRM_Extension_Info::loadFromString($xml);
-        $this->_remotesDiscovered[] = $ext;
-      }
+    foreach ($remotes as $id => $xml) {
+      $ext = CRM_Extension_Info::loadFromString($xml);
+      $this->_remotesDiscovered[] = $ext;
     }
 
     if (file_exists(dirname($tsPath))) {
@@ -201,31 +210,30 @@ class CRM_Extension_Browser {
   }
 
   /**
-   * @return array
+   * Loads the extensions data from the cache file. If it is empty
+   * or doesn't exist, try fetching from remote instead.
+   *
+   * @return string
    */
-  private function grabCachedKeyList() {
-    $result = array();
-    $cachedPath = $this->cacheDir . DIRECTORY_SEPARATOR;
-    $files = scandir($cachedPath);
-    foreach ($files as $dc => $fname) {
-      if (substr($fname, -4) == '.xml') {
-        $result[] = array('key' => substr($fname, 0, -4));
-      }
+  private function grabCachedJson() {
+    $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE;
+    $json = file_get_contents($filename);
+    if (empty($json)) {
+      $json = $this->grabRemoteJson();
     }
-    return $result;
+    return $json;
   }
 
   /**
    * Connects to public server and grabs the list of publicly available
    * extensions.
    *
-   *
-   * @return array
-   *   list of extension names
+   * @return string
+   * @throws \CRM_Extension_Exception
    */
-  private function grabRemoteKeyList() {
+  private function grabRemoteJson() {
 
-    ini_set('default_socket_timeout', CRM_Utils_VersionCheck::CHECK_TIMEOUT);
+    ini_set('default_socket_timeout', self::CHECK_TIMEOUT);
     set_error_handler(array('CRM_Extension_Browser', 'downloadError'));
 
     if (!ini_get('allow_url_fopen')) {
@@ -235,86 +243,24 @@ class CRM_Extension_Browser {
     if (FALSE === $this->getRepositoryUrl()) {
       // don't check if the user has configured civi not to check an external
       // url for extensions. See CRM-10575.
-      CRM_Core_Session::setStatus(ts('Not checking remote URL for extensions since ext_repo_url is set to false.'), ts('Check Settings'), 'alert');
       return array();
     }
 
-    $exts = array();
-    list ($status, $extdir) = CRM_Utils_HttpClient::singleton()->get($this->getRepositoryUrl() . $this->indexPath);
-    if ($extdir === FALSE || $status !== CRM_Utils_HttpClient::STATUS_OK) {
-      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');
-    }
-    else {
-      $lines = explode("\n", $extdir);
-
-      foreach ($lines as $ln) {
-        if (preg_match("@\<li\>(.*)\</li\>@i", $ln, $out)) {
-          // success
-          $extsRaw[] = $out;
-          $key = strip_tags($out[1]);
-          if (substr($key, -4) == '.xml') {
-            $exts[] = array('key' => substr($key, 0, -4));
-          }
-        }
-      }
-    }
-
-    // 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.
-    if (empty($exts)) {
-      $config = CRM_Core_Config::singleton();
-      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 => CRM_Utils_System::version(),
-        3 => 'http://wiki.civicrm.org/confluence/display/CRMDOC/Extensions',
-      )), ts('No Extensions Available for this Version'), 'info');
-    }
+    $filename = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_JSON_FILE;
+    $url = $this->getRepositoryUrl() . $this->indexPath;
+    $status = CRM_Utils_HttpClient::singleton()->fetch($url, $filename);
 
     ini_restore('allow_url_fopen');
     ini_restore('default_socket_timeout');
 
     restore_error_handler();
 
-    return $exts;
-  }
-
-  /**
-   * Given the key, retrieves the info XML from a remote server
-   * and stores locally, returning the contents.
-   *
-   * @param string $key
-   *   Extension key.
-   * @param bool $cached
-   *   Whether to use cached data.
-   *
-   * @return string
-   *   Contents of info.xml, or null if info.xml cannot be retrieved or parsed.
-   */
-  private function grabRemoteInfoFile($key, $cached = FALSE) {
-    $filename = $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.xml';
-    $url = $this->getRepositoryUrl() . '/' . $key . '.xml';
-
-    if (!$cached || !file_exists($filename)) {
-      $fetchStatus = CRM_Utils_HttpClient::singleton()->fetch($url, $filename);
-      if ($fetchStatus != CRM_Utils_HttpClient::STATUS_OK) {
-        return NULL;
-      }
+    if ($status !== CRM_Utils_HttpClient::STATUS_OK) {
+      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');
     }
 
-    if (file_exists($filename)) {
-      $contents = file_get_contents($filename);
-
-      //parse just in case
-      $check = simplexml_load_string($contents);
-
-      if (!$check) {
-        foreach (libxml_get_errors() as $error) {
-          CRM_Core_Error::debug('xmlError', $error);
-        }
-        return;
-      }
-
-      return $contents;
-    }
+    // Don't call grabCachedJson here, that would risk infinite recursion
+    return file_get_contents($filename);
   }
 
   /**