--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.3 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ * Manage the download, validation, and rendering of community messages
+ */
+class CRM_Core_CommunityMessages {
+
+ /**
+ * Default time to wait before retrying
+ */
+ const DEFAULT_RETRY = 7200; // 2 hours
+
+ /**
+ * @var CRM_Utils_HttpClient
+ */
+ protected $client;
+
+ /**
+ * @var CRM_Utils_Cache_Interface
+ */
+ protected $cache;
+
+ /**
+ * @param CRM_Utils_Cache_Interface $cache
+ * @param CRM_Utils_HttpClient $client
+ */
+ public function __construct($cache, $client) {
+ $this->cache = $cache;
+ $this->client = $client;
+ }
+
+ /**
+ * Get the messages document
+ *
+ * @return NULL|array
+ */
+ public function getDocument() {
+ // FIXME register in settings
+ $url = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'communityMessagesUrl', NULL, TRUE);
+ if (empty($url)) {
+ return NULL;
+ }
+
+ $isChanged = FALSE;
+ $document = $this->cache->get('communityMessages');
+
+ if (empty($document) || !is_array($document)) {
+ $document = array(
+ 'messages' => array(),
+ 'expires' => 0, // ASAP
+ 'ttl' => self::DEFAULT_RETRY,
+ 'retry' => self::DEFAULT_RETRY,
+ );
+ $isChanged = TRUE;
+ }
+
+ if ($document['expires'] <= CRM_Utils_Time::getTimeRaw()) {
+ $newDocument = $this->fetchDocument($url);
+ if ($newDocument) {
+ $document = $newDocument;
+ $document['expires'] = CRM_Utils_Time::getTimeRaw() + $document['ttl'];
+ } else {
+ $document['expires'] = CRM_Utils_Time::getTimeRaw() + $document['retry'];
+ }
+ $isChanged = TRUE;
+ }
+
+ if ($isChanged) {
+ $this->cache->set('communityMessages', $document);
+ }
+
+ return $document;
+ }
+
+ /**
+ * Download document from URL and parse as JSON
+ *
+ * @param string $url
+ * @return NULL|array parsed JSON
+ */
+ public function fetchDocument($url) {
+ list($status, $json) = $this->client->get(self::evalUrl($url));
+ if ($status != CRM_Utils_HttpClient::STATUS_OK || empty($json)) {
+ return NULL;
+ }
+ $doc = json_decode($json, TRUE);
+ if (empty($doc) || json_last_error() != JSON_ERROR_NONE) {
+ return NULL;
+ }
+ return $doc;
+ }
+
+ /**
+ * Pick one message
+ *
+ * @param callable $permChecker
+ * @param array $components
+ * @return NULL|array
+ */
+ public function pick($permChecker, $components) {
+ throw new Exception('not implemented');
+ }
+
+ /**
+ * @param string $markup
+ * @return string
+ */
+ public static function evalMarkup($markup) {
+ throw new Exception('not implemented');
+ }
+
+ /**
+ * @param string $markup
+ * @return string
+ */
+ public static function evalUrl($url) {
+ return $url; // FIXME
+ }
+}
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.3 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+*/
+
+
+require_once 'CiviTest/CiviUnitTestCase.php';
+class CRM_Core_CommunityMessagesTest extends CiviUnitTestCase {
+
+ /**
+ * @var CRM_Utils_Cache_Interface
+ */
+ protected $cache;
+
+ /**
+ * @var array list of possible web responses
+ */
+ protected $webResponses;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->cache = new CRM_Utils_Cache_Arraycache(array());
+
+ $this->webResponses = array(
+ 'http-error' => array(
+ CRM_Utils_HttpClient::STATUS_DL_ERROR,
+ NULL
+ ),
+ 'bad-json' => array(
+ CRM_Utils_HttpClient::STATUS_OK,
+ '<html>this is not json!</html>'
+ ),
+ 'hello-world' => array(
+ CRM_Utils_HttpClient::STATUS_OK,
+ json_encode(array(
+ 'ttl' => 600,
+ 'retry' => 600,
+ 'messages' => array(
+ array(
+ 'markup' => '<h1>Hello world</h1>',
+ ),
+ ),
+ ))
+ ),
+ 'salut-a-tout' => array(
+ CRM_Utils_HttpClient::STATUS_OK,
+ json_encode(array(
+ 'ttl' => 600,
+ 'retry' => 600,
+ 'messages' => array(
+ array(
+ 'markup' => '<h1>Salut a tout</h1>',
+ ),
+ ),
+ ))
+ ),
+ );
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ CRM_Utils_Time::resetTime();
+ }
+
+ /**
+ * Download a document; after the set expiration period, download again.
+ */
+ public function testNewOK_CacheOK_UpdateOK() {
+ // first try, good response
+ CRM_Utils_Time::setTime('2013-03-01 10:00:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['hello-world'])
+ );
+ $doc1 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc1['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 10:10:00'), $doc1['expires']);
+
+ // second try, $doc1 hasn't expired yet, so still use it
+ CRM_Utils_Time::setTime('2013-03-01 10:09:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectNoHttpRequest()
+ );
+ $doc2 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc2['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 10:10:00'), $doc2['expires']);
+
+ // third try, $doc1 expired, update it
+ CRM_Utils_Time::setTime('2013-03-01 12:00:02'); // more than 2 hours later (DEFAULT_RETRY)
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['salut-a-tout'])
+ );
+ $doc3 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Salut a tout</h1>', $doc3['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 12:10:02'), $doc3['expires']);
+ }
+
+ /**
+ * First download attempt fails. Store the NACK and retry after
+ * the default time period (DEFAULT_RETRY).
+ */
+ public function testNewFailure_CacheOK_UpdateOK() {
+ // first try, bad response
+ CRM_Utils_Time::setTime('2013-03-01 10:00:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['http-error'])
+ );
+ $doc1 = $communityMessages->getDocument();
+ $this->assertEquals(array(), $doc1['messages']);
+ $this->assertTrue($doc1['expires'] > CRM_Utils_Time::getTimeRaw());
+
+ // second try, $doc1 hasn't expired yet, so still use it
+ CRM_Utils_Time::setTime('2013-03-01 10:09:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectNoHttpRequest()
+ );
+ $doc2 = $communityMessages->getDocument();
+ $this->assertEquals(array(), $doc2['messages']);
+ $this->assertEquals($doc1['expires'], $doc2['expires']);
+
+ // third try, $doc1 expired, try again, get a good response
+ CRM_Utils_Time::setTime('2013-03-01 12:00:02'); // more than 2 hours later (DEFAULT_RETRY)
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['hello-world'])
+ );
+ $doc3 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc3['messages'][0]['markup']);
+ $this->assertTrue($doc3['expires'] > CRM_Utils_Time::getTimeRaw());
+ }
+
+ /**
+ * First download of new doc is OK.
+ * The update fails.
+ * The failure cached.
+ * The failure eventually expires and new update succeeds.
+ */
+ public function testNewOK_UpdateFailure_CacheOK_UpdateOK() {
+ // first try, good response
+ CRM_Utils_Time::setTime('2013-03-01 10:00:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['hello-world'])
+ );
+ $doc1 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc1['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 10:10:00'), $doc1['expires']);
+
+ // second try, $doc1 has expired; bad response; keep old data
+ CRM_Utils_Time::setTime('2013-03-01 12:00:02'); // more than 2 hours later (DEFAULT_RETRY)
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['http-error'])
+ );
+ $doc2 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc2['messages'][0]['markup']);
+ $this->assertTrue($doc2['expires'] > CRM_Utils_Time::getTimeRaw());
+
+ // third try, $doc2 hasn't expired yet; no request; keep old data
+ CRM_Utils_Time::setTime('2013-03-01 12:09:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectNoHttpRequest()
+ );
+ $doc3 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc3['messages'][0]['markup']);
+ $this->assertEquals($doc2['expires'], $doc3['expires']);
+
+ // fourth try, $doc2 has expired yet; new request; replace data
+ CRM_Utils_Time::setTime('2013-03-01 12:10:02');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['salut-a-tout'])
+ );
+ $doc4 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Salut a tout</h1>', $doc4['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 12:20:02'), $doc4['expires']);
+ }
+
+ public function testNewOK_UpdateParseError() {
+ // first try, good response
+ CRM_Utils_Time::setTime('2013-03-01 10:00:00');
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['hello-world'])
+ );
+ $doc1 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc1['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 10:10:00'), $doc1['expires']);
+
+ // second try, $doc1 has expired; bad response; keep old data
+ CRM_Utils_Time::setTime('2013-03-01 12:00:02'); // more than 2 hours later (DEFAULT_RETRY)
+ $communityMessages = new CRM_Core_CommunityMessages(
+ $this->cache,
+ $this->expectOneHttpRequest($this->webResponses['bad-json'])
+ );
+ $doc2 = $communityMessages->getDocument();
+ $this->assertEquals('<h1>Hello world</h1>', $doc2['messages'][0]['markup']);
+ $this->assertEquals(strtotime('2013-03-01 12:10:02'), $doc2['expires']);
+ }
+
+ /**
+ * Generate a mock HTTP client with the expectation that it is never called.
+ *
+ * @return CRM_Utils_HttpClient|PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function expectNoHttpRequest() {
+ $client = $this->getMock('CRM_Utils_HttpClient');
+ $client->expects($this->never())
+ ->method('get');
+ return $client;
+ }
+
+ /**
+ * Generate a mock HTTP client with the expectation that it is called once.
+ *
+ * @return CRM_Utils_HttpClient|PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function expectOneHttpRequest($response) {
+ $client = $this->getMock('CRM_Utils_HttpClient');
+ $client->expects($this->once())
+ ->method('get')
+ ->will($this->returnValue($response));
+ return $client;
+ }
+}