CRM-16757 - Cache CiviConnect apps and CRLs.
authorTim Otten <totten@civicrm.org>
Sat, 4 Jul 2015 01:17:03 +0000 (18:17 -0700)
committerTim Otten <totten@civicrm.org>
Tue, 14 Jul 2015 04:00:09 +0000 (21:00 -0700)
CRM/Cxn/BAO/Cxn.php
CRM/Cxn/CiviCxnHttp.php [new file with mode: 0644]
CRM/Utils/Http.php [new file with mode: 0644]
api/v3/CxnApp.php

index 83267fbe1f9e891f6b7f9ceace408e2adec7be1d..d34271ecf889670d5d2215b522b5c07f309a25ef 100644 (file)
@@ -27,6 +27,7 @@
  */
 
 use Civi\Cxn\Rpc\Constants;
+use Civi\Cxn\Rpc\DefaultCertificateValidator;
 
 /**
  *
@@ -145,6 +146,7 @@ class CRM_Cxn_BAO_Cxn extends CRM_Cxn_DAO_Cxn {
     $client = new \Civi\Cxn\Rpc\RegistrationClient($cxnStore, \CRM_Cxn_BAO_Cxn::getSiteCallbackUrl());
     $client->setLog(new \CRM_Utils_SystemLogger());
     $client->setCertValidator(self::createCertificateValidator());
+    $client->setHttp(CRM_Cxn_CiviCxnHttp::singleton());
     return $client;
   }
 
@@ -158,21 +160,33 @@ class CRM_Cxn_BAO_Cxn extends CRM_Cxn_DAO_Cxn {
     $apiServer = new \Civi\Cxn\Rpc\ApiServer($cxnStore);
     $apiServer->setLog(new CRM_Utils_SystemLogger());
     $apiServer->setCertValidator(self::createCertificateValidator());
+    $apiServer->setHttp(CRM_Cxn_CiviCxnHttp::singleton());
     $apiServer->setRouter(array('CRM_Cxn_ApiRouter', 'route'));
     return $apiServer;
   }
 
   /**
-   * @return \Civi\Cxn\Rpc\CertificateValidatorInterface
+   * @return DefaultCertificateValidator
    * @throws CRM_Core_Exception
    */
   public static function createCertificateValidator() {
     $caCert = self::getCACert();
     if ($caCert === NULL) {
-      return new \Civi\Cxn\Rpc\DefaultCertificateValidator(NULL, NULL, NULL);
+      return new DefaultCertificateValidator(
+        NULL,
+        NULL,
+        NULL,
+        NULL
+      );
     }
     else {
-      return new \Civi\Cxn\Rpc\DefaultCertificateValidator($caCert);
+      return new DefaultCertificateValidator(
+        $caCert,
+        DefaultCertificateValidator::AUTOLOAD,
+        DefaultCertificateValidator::AUTOLOAD,
+        CRM_Cxn_CiviCxnHttp::singleton()
+      );
     }
   }
+
 }
diff --git a/CRM/Cxn/CiviCxnHttp.php b/CRM/Cxn/CiviCxnHttp.php
new file mode 100644 (file)
index 0000000..2495758
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * Class CRM_Cxn_CiviCxnHttp
+ *
+ * This extends the PhpHttp client used by CiviConnect and adds:
+ *  - Force-cache support for GET requests
+ *  - Compliance with SSL policy
+ */
+class CRM_Cxn_CiviCxnHttp extends \Civi\Cxn\Rpc\Http\PhpHttp {
+
+  protected static $singleton = NULL;
+
+  /**
+   * @var CRM_Utils_Cache_Interface|null
+   */
+  protected $cache;
+
+  /**
+   * @param bool $fresh
+   * @return CRM_Cxn_CiviCxnHttp
+   */
+  public static function singleton($fresh = FALSE) {
+    if (self::$singleton === NULL || $fresh) {
+      $config = CRM_Core_Config::singleton();
+
+      if ($config->debug) {
+        $cache = new CRM_Utils_Cache_Arraycache(array());
+      }
+      else {
+        $cache = new CRM_Utils_Cache_SqlGroup(array(
+          'group' => 'CiviCxnHttp',
+          'prefetch' => FALSE,
+        ));
+      }
+
+      self::$singleton = new CRM_Cxn_CiviCxnHttp($cache);
+    }
+    return self::$singleton;
+  }
+
+  /**
+   * @param CRM_Utils_Cache_Interface|NULL $cache
+   *   The cache data store.
+   */
+  public function __construct($cache) {
+    $this->cache = $cache;
+  }
+
+  /**
+   * @param string $verb
+   * @param string $url
+   * @param string $blob
+   * @param array $headers
+   *   Array of headers (e.g. "Content-type" => "text/plain").
+   * @return array
+   *   array($headers, $blob, $code)
+   */
+  public function send($verb, $url, $blob, $headers = array()) {
+    $lowVerb = strtolower($verb);
+
+    if ($lowVerb === 'get' && $this->cache) {
+      $cachePath = 'get/' . md5($url);
+      $cacheLine = $this->cache->get($cachePath);
+      if ($cacheLine && $cacheLine['expires'] > CRM_Utils_Time::getTimeRaw()) {
+        return $cacheLine['data'];
+      }
+    }
+
+    $result = parent::send($verb, $url, $blob, $headers);
+
+    if ($lowVerb === 'get' && $this->cache) {
+      $expires = CRM_Utils_Http::parseExpiration($result[0]);
+      if ($expires !== NULL) {
+        $cachePath = 'get/' . md5($url);
+        $cacheLine = array(
+          'url' => $url,
+          'expires' => $expires,
+          'data' => $result,
+        );
+        $this->cache->set($cachePath, $cacheLine);
+      }
+    }
+
+    return $result;
+  }
+
+  protected function createStreamOpts($verb, $url, $blob, $headers) {
+    $result = parent::createStreamOpts($verb, $url, $blob, $headers);
+
+    $caConfig = CA_Config_Stream::probe(array(
+      'verify_peer' => (bool) CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL', NULL, TRUE),
+    ));
+    if ($caConfig->isEnableSSL()) {
+      $result['ssl'] = $caConfig->toStreamOptions();
+    }
+    if (!$caConfig->isEnableSSL() && preg_match('/^https:/', $url)) {
+      CRM_Core_Error::fatal('Cannot fetch document - system does not support SSL');
+    }
+
+    return $result;
+  }
+
+}
diff --git a/CRM/Utils/Http.php b/CRM/Utils/Http.php
new file mode 100644 (file)
index 0000000..688ef8c
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+class CRM_Utils_Http {
+
+  /**
+   * Parse the expiration time from a series of HTTP headers.
+   *
+   * @param array $headers
+   * @return int|NULL
+   *   Expiration tme as seconds since epoch, or NULL if not cacheable.
+   */
+  public static function parseExpiration($headers) {
+    $headers = CRM_Utils_Array::rekey($headers, function ($k, $v) {
+      return strtolower($k);
+    });
+
+    if (!empty($headers['cache-control'])) {
+      $cc = self::parseCacheControl($headers['cache-control']);
+      if ($cc['max-age'] && is_numeric($cc['max-age'])) {
+        return CRM_Utils_Time::getTimeRaw() + $cc['max-age'];
+      }
+    }
+
+    return NULL;
+  }
+
+  /**
+   * @param string $value
+   *   Ex: "max-age=86400, public".
+   * @return array
+   *   Ex: Array("max-age"=>86400, "public"=>1).
+   */
+  public static function parseCacheControl($value) {
+    $result = array();
+
+    $parts = preg_split('/, */', $value);
+    foreach ($parts as $part) {
+      if (strpos($part, '=') !== FALSE) {
+        list ($key, $value) = explode('=', $part, 2);
+        $result[$key] = $value;
+      }
+      else {
+        $result[$part] = TRUE;
+      }
+    }
+
+    return $result;
+  }
+
+}
index 5f7afd6a413084fe66db7424e84e2df7895b9342..2b811e408c15920a59e8adf06ec6f7f1bef67d41 100644 (file)
@@ -94,13 +94,11 @@ function _civicrm_api3_cxn_app_get_spec(&$spec) {
  * @throws \Civi\Cxn\Rpc\Exception\InvalidMessageException
  */
 function civicrm_api3_cxn_app_get($params) {
-  // FIXME: We should cache, but CRM_Utils_Cache and CRM_Core_BAO_Cache don't seem to support TTL...
-
   // You should not change CIVICRM_CXN_APPS_URL in production; this is for local development.
   $url = defined('CIVICRM_CXN_APPS_URL') ? CIVICRM_CXN_APPS_URL : \Civi\Cxn\Rpc\Constants::OFFICIAL_APPMETAS_URL;
 
-  list ($status, $blob) = CRM_Utils_HttpClient::singleton()->get($url);
-  if (CRM_Utils_HttpClient::STATUS_OK != $status) {
+  list ($headers, $blob, $code) = CRM_Cxn_CiviCxnHttp::singleton()->send('GET', $url, '');
+  if ($code != 200) {
     throw new API_Exception("Failed to download application list.");
   }