diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index 12e595fb5cce..6ba3d50ccff6 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -1635,6 +1635,29 @@ public static function disable() { ); } + /** + * Alter redirect. + * + * This hook is called when the browser is being re-directed and allows the url + * to be altered. + * + * @param \Psr\Http\Message\UriInterface $url + * @param string $outputFormat + * The output format for the request. Generally this will be NULL or json. + * If json it will lead to json output being returned rather than a true redirect. + * @param array $context + * Optional additional information about context. + * + * @return null + * the return value is ignored + */ + public static function alterRedirect(&$url, &$outputFormat, $context) { + return self::singleton()->invoke(array('url', 'outputFormat', 'context'), $url, + $outputFormat, $context, + self::$_nullObject, self::$_nullObject, self::$_nullObject, + 'civicrm_alterRedirect' + ); + } /** * @param $varType * @param $var diff --git a/CRM/Utils/System.php b/CRM/Utils/System.php index a253b091c047..8ce4183320f6 100644 --- a/CRM/Utils/System.php +++ b/CRM/Utils/System.php @@ -433,8 +433,10 @@ public static function getClassName($object) { * * @param string $url * The URL to provide to the browser via the Location header. + * @param array $context + * Optional additional information for the hook. */ - public static function redirect($url = NULL) { + public static function redirect($url = NULL, $context = []) { if (!$url) { $url = self::url('civicrm/dashboard', 'reset=1'); } @@ -442,8 +444,14 @@ public static function redirect($url = NULL) { // this is kinda hackish but not sure how to do it right $url = str_replace('&', '&', $url); + $output = CRM_Utils_Array::value('snippet', $_GET); + + $parsedUrl = CRM_Utils_Url::parseUrl($url); + CRM_Utils_Hook::alterRedirect($parsedUrl, $context, $output); + $url = CRM_Utils_Url::unparseUrl($parsedUrl); + // If we are in a json context, respond appropriately - if (CRM_Utils_Array::value('snippet', $_GET) === 'json') { + if ($output === 'json') { CRM_Core_Page_AJAX::returnJsonResponse(array( 'status' => 'redirect', 'userContext' => $url, diff --git a/CRM/Utils/Url.php b/CRM/Utils/Url.php new file mode 100644 index 000000000000..00e28be4790b --- /dev/null +++ b/CRM/Utils/Url.php @@ -0,0 +1,55 @@ +__toString(); + } + +} diff --git a/composer.json b/composer.json index 5ac5d6427dad..0d4f738486c8 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,8 @@ "pear/Auth_SASL": "1.1.0", "pear/Net_SMTP": "1.6.*", "pear/Net_socket": "1.0.*", - "civicrm/civicrm-setup": "~0.2.0" + "civicrm/civicrm-setup": "~0.2.0", + "guzzlehttp/guzzle": "^5.3" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 7a81794d0534..76ea8c5c7eea 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "3aaaa7a6146043643e9fe2cbc8030cca", + "content-hash": "cdda1c006828ed2eac0bddcc7192fc37", "packages": [ { "name": "civicrm/civicrm-cxn-rpc", @@ -182,6 +182,160 @@ "homepage": "http://code.google.com/p/phpquery/", "time": "2013-03-21T12:39:33+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "5.3.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f9acb4761844317e626a32259205bec1f1bc60d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f9acb4761844317e626a32259205bec1f1bc60d2", + "reference": "f9acb4761844317e626a32259205bec1f1bc60d2", + "shasum": "" + }, + "require": { + "guzzlehttp/ringphp": "^1.1", + "php": ">=5.4.0", + "react/promise": "^2.2" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-01-15T07:18:01+00:00" + }, + { + "name": "guzzlehttp/ringphp", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/RingPHP.git", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "shasum": "" + }, + "require": { + "guzzlehttp/streams": "~3.0", + "php": ">=5.4.0", + "react/promise": "~2.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Guzzle will use specific adapters if cURL is present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Ring\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", + "time": "2015-05-20T03:37:09+00:00" + }, + { + "name": "guzzlehttp/streams", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/streams.git", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple abstraction over streams of data", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "stream" + ], + "time": "2014-10-12T19:18:40+00:00" + }, { "name": "marcj/topsort", "version": "dev-1.0-php53", @@ -890,6 +1044,52 @@ ], "time": "2012-12-21T11:40:51+00:00" }, + { + "name": "react/promise", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "62785ae604c8d69725d693eb370e1d67e94c4053" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/62785ae604c8d69725d693eb370e1d67e94c4053", + "reference": "62785ae604c8d69725d693eb370e1d67e94c4053", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "time": "2017-03-25T12:08:31+00:00" + }, { "name": "sabberworm/php-css-parser", "version": "6.0.1", diff --git a/tests/phpunit/CRM/Utils/SystemTest.php b/tests/phpunit/CRM/Utils/SystemTest.php index 2874bfe873da..f4223d4c9c4c 100644 --- a/tests/phpunit/CRM/Utils/SystemTest.php +++ b/tests/phpunit/CRM/Utils/SystemTest.php @@ -35,4 +35,73 @@ public function testEvalUrl() { $this->assertEquals('http://example.com/?cms=UnitTests', CRM_Utils_System::evalUrl('http://example.com/?cms={uf}')); } + /** + * Test the redirect hook. + * + * @param string $url + * @param array $parsedUrl + * + * @dataProvider getURLs + */ + public function testRedirectHook($url, $parsedUrl) { + $this->hookClass->setHook('civicrm_alterRedirect', array($this, 'hook_checkUrl')); + try { + CRM_Utils_System::redirect($url, [ + 'expected' => $parsedUrl, + 'original' => $url + ]); + } + catch (CRM_Core_Exception $e) { + $this->assertEquals(ts('hook called'), $e->getMessage()); + return; + } + $this->fail('Exception should have been thrown if hook was called'); + } + + /** + * Hook for alterRedirect. + * + * We do some checks here. + * + * @param array $urlQuery + * @param array $context + * + * @throws \CRM_Core_Exception + */ + public function hook_checkUrl($urlQuery, $context) { + $this->assertEquals($context['expected'], $urlQuery); + $this->assertEquals($context['original'], CRM_Utils_Url::unparseUrl($urlQuery)); + throw new CRM_Core_Exception(ts('hook called')); + } + + /** + * Get urls for testing. + * + * @return array + */ + public function getURLs() { + return [ + ['https://example.com?ab=cd', [ + 'scheme' => 'https', + 'host' => 'example.com', + 'query' => ['ab' => 'cd'], + ]], + ['http://myuser:mypass@foo.bar:123/whiz?a=b&c=d', [ + 'scheme' => 'http', + 'host' => 'foo.bar', + 'port' => 123, + 'user' => 'myuser', + 'pass' => 'mypass', + 'path' => '/whiz', + 'query' => [ + 'a' => 'b', + 'c' => 'd', + ], + ]], + ['/foo/bar', [ + 'path' => '/foo/bar' + ]], + ]; + } + } diff --git a/tests/phpunit/CRM/Utils/UrlTest.php b/tests/phpunit/CRM/Utils/UrlTest.php new file mode 100644 index 000000000000..0021bfd9b682 --- /dev/null +++ b/tests/phpunit/CRM/Utils/UrlTest.php @@ -0,0 +1,84 @@ +hookClass->setHook('civicrm_alterRedirect', array($this, 'hook_checkUrl')); + try { + CRM_Utils_System::redirect($url, [ + 'expected' => $parsedUrl, + 'original' => $url + ]); + } + catch (CRM_Core_Exception $e) { + $this->assertEquals(ts('hook called'), $e->getMessage()); + return; + } + $this->fail('Exception should have been thrown if hook was called'); + } + + /** + * Hook for alterRedirect. + * + * We do some checks here. + * + * @param UriInterface $urlQuery + * @param array $context + * + * @throws \CRM_Core_Exception + */ + public function hook_checkUrl($urlQuery, $context) { + $this->assertEquals(CRM_Utils_Array::value('scheme', $context['expected']), $urlQuery->getScheme()); + $this->assertEquals(CRM_Utils_Array::value('host', $context['expected']), $urlQuery->getHost()); + $this->assertEquals(CRM_Utils_Array::value('query', $context['expected']), $urlQuery->getQuery()); + $this->assertEquals($context['original'], CRM_Utils_Url::unparseUrl($urlQuery)); + + throw new CRM_Core_Exception(ts('hook called')); + } + + /** + * Get urls for testing. + * + * @return array + */ + public function getURLs() { + return [ + ['https://example.com?ab=cd', [ + 'scheme' => 'https', + 'host' => 'example.com', + 'query' => 'ab=cd', + ]], + ['http://myuser:mypass@foo.bar:123/whiz?a=b&c=d', [ + 'scheme' => 'http', + 'host' => 'foo.bar', + 'port' => 123, + 'user' => 'myuser', + 'pass' => 'mypass', + 'path' => '/whiz', + 'query' => 'a=b&c=d', + ]], + ['/foo/bar', [ + 'path' => '/foo/bar' + ]], + ]; + } + +}