-
-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CRM-21659 - Add hook to CRM_Utils_System::redirect #11519
Conversation
dd182cf
to
5633848
Compare
Good idea. What is the best way to define execution context? - Is it assumed the developer will use other hooks and their variables ($form, $id, etc) to control when this hook is executed? |
@eileenmcnaughton What are some use case for this new hook? Why do we need it? |
The specific use case is on the JIRA - ie to alter the parameters in a url that a redirect is going to after deduping. Obviously we do redirects in other places - e.g after contribution pages - depending on how the page is written it's possible to do the redirect with existing hooks or not |
@adam-devapp I was looking at calling from an existing function that has very little info - but it might make more sense to wrap it in a more context-aware function |
nb test fail is something happening on the server of late - unrelated |
Good idea, code looks excellent. Two things:
|
CRM/Utils/Hook.php
Outdated
* @return mixed | ||
*/ | ||
public static function alterRedirect($url) { | ||
return self::singleton()->invoke(1, $url, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the notation `invoke(array('url'), $url, ...) to fully support all styles of listeners (e.g. Drupal hooks and Symfony events).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I made this change
CRM/Utils/Hook.php
Outdated
* | ||
* @return mixed | ||
*/ | ||
public static function alterRedirect($url) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the parameter be alterable? To my reading, a listener who prefers to alter the URL would need to execute the redirect themselves (e.g. emitting the Location:
header and exit()
ing).
If you read the code in CRM_Utils_System::redirect()
, there's actually a bit nuance to how the redirect is executed. (Ex: An AJAX snippet works different from a standalone page. Ex: it's not exit()
; it's civiExit()
.) The hook consumer should be focused on the content of the URL -- not on the mechanism of delivering that content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to &$url
CRM/Utils/Hook.php
Outdated
* This hook is called when the browser is being re-directed and allows the url | ||
* to be altered. | ||
* | ||
* @param string $url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a hard blocker, but just to verbalize -- wouldn't it scale-up better (to support many different listeners) to pass the URL in array
or object
format (rather than string format)? i.e. it would avoid redundant parsing/encoding logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure what you mean here - the url is a string in the redirect function - are suggesting unpacking it in that function to pass out as an array & recompiling it afterwards?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace line 443 in CRM/Utils/System.php with a more sophisticated conversion of the url into an object (e.g. $parsed_url = parse_url($url)), and then pass that into alterRedirect. Then any alter functions don't have to reparse it, since I would assume the first order of any alter would be to do things like see if it's sending you offsite, or going to a particular path. Also, not clear of the value of returning something from the alterRedirect function since the url is going to be modified by reference, and the caller on 445 isn't paying attention to the return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adixon Yeah, parse_url()
should do a pretty good job. Might also add parse_str()
so that the query-parameters are an array:
$parsedUrl = parse_url($url);
if (isset($parsedUrl['query'])) {
parse_str($parsedUrl['query'], $parsedUrl['query']);
}
(Example inputs/outputs: https://gist.github.com/totten/82918724be1293ffed4e4f7f87d01706 )
It takes a bit more code to put it back into string form. Here's an example of re-encoding the URL.
Re: return
-- I think that's just pro-forma. Most hook-stubs have the return
statement, and the IDE really wants you want to document it, so you see a lot of snippets like this:
* @return null
* the return value is ignored
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eileenmcnaughton Yup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first I was skeptical of parsing and re-encoding all redirected URLs all of the time, but realistically any consumer of this hook is going to be doing just that in order to decide whether to alter the redirect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated to include the parse + unparse & to have some unit tests on that. I did add the $context variable as it helped with the tests & I felt it would be easier to add now than to decide to later
@adixon @adam-devapp - does it make sense to add a second parameter $context = array()n then we could add the context in the calling function if desired (although this could equally be done later when such a desire arose) |
5633848
to
ac1cf90
Compare
test this please |
Hi Eileen, yes I agree - an array can be useless, as the values can change and may not even exist. It's a pity you can't pass a request object so you can get a range of parameters and methods with predictable method outcomes - Similar to the request object in laravel i..e https://laravel.com/docs/5.5/requests i.e. $url = $request->setUrl($newUrl); |
test this please |
I like this in general. I like the idea of passing some context to I also think it would be helpful to pull the condition |
d7348a0
to
cd4d48c
Compare
I added snippet parsing too per #11519 (comment) |
87d83e7
to
c840ce7
Compare
@eileenmcnaughton I like that the parsing/unparsing have been split out into separate functions that mirror each other. And that there are tests. 👍 FWIW, @adam-devapp, I had a similar thought. It would be better to use a request object, but that is a bigger task (since we don't really have a request object anywhere right now). Tangential: PSR-7 defines a UriInterface which is rather on-point. I think this is mergeable (i.e. consistent with other code/standard). However, if time permitted, I'd make one of these two changes:
|
d76b02e
to
3abe476
Compare
The test-failure seems to involve the new Guzzle dependency. Wait a minu-- Guzzle! That's a bigger deal than (a) I would love to have a proper HTTP client. Guzzle seems to be the most popular... (b) In the past, I've given folks a hard time about using Guzzle in core, but now that PHP 5.3 is out of the picture, maybe my main argument is gone. There's still some concern that it's too popular (e.g. prone to conflict on custom builds)... but I'm tired of being the buzzkill on Guzzle, and the one concrete case (D8) might work anyway (since D8 is moving toward composer-based distribution, which means better dependency-matching). I'm inclined to toss this to a bigger list and see if anyone actually has a complaint about adding it... |
Side note - the reason for the Guzzle fail is that it works on the version of Guzzle that works with php 5.5+ (& which has the PSR interface) - when I adjusted to php 5.4 it got an earlier version of Guzzle. I think we should aim to resolve this after Wed next week against 5.5 |
3abe476
to
41c00d4
Compare
(I just switched back to 5.5 version - I expect it will fail differently now :-) |
FWIW, I have brought guzzle into some (one?) extension. From memory I can't recall whether this was me deliberately adding it or whether it was brought in as a sub-dependency by a third party dependency (e.g. GoCardless Pro API uses itt). Hmmm, can't see this ending nicely for a while! |
Thanks everyone. |
41c00d4
to
faf8c91
Compare
CRM/Utils/Hook.php
Outdated
* @return null | ||
* the return value is ignored | ||
*/ | ||
public static function alterRedirect(&$url, &$outputFormat, $context) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since $url
is now an object, the &
operator isn't necessary anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - I've done that. I'm a bit torn though now as technically I should re-order & pass $outputFormat first since it's still a reference - but it is also the 'barely worth including' param in the set - I only passed it since @agh1 seemed to see some possible use case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a rule that says that references should come before the other params? Does the order make any difference in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, there's no obligation to put references first. Rather, just put the most important/salient field first.
In the case of alter-style hooks, the most important/salient field is also often a reference, so it may often look like references come first. But that's happenstance (to my eye).
CRM/Utils/Url.php
Outdated
* | ||
* @return string | ||
*/ | ||
static public function unparseUrl(UriInterface $parsed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be public static
, as the parseUrl()
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks -changed
tests/phpunit/CRM/Utils/UrlTest.php
Outdated
@@ -0,0 +1,84 @@ | |||
<?php | |||
|
|||
use Psr\Http\Message\UriInterface; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test looks exactly the same as the one you've added to the CRM_Utils_System
class. Maybe here it would make more sense to test the parseUrl()
and unparseUrl()
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved it out of system - it covers parse & unparse implicitly
tests/phpunit/CRM/Utils/UrlTest.php
Outdated
* Class CRM_Utils_UrlTest | ||
* @group headless | ||
*/ | ||
class CRM_Utils_URLTest extends CiviUnitTestCase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the class being tested is named CRM_Utils_Url
shouldn't the test be CRM_Utils_UrlTest
instead of CRM_Utils_URLTest
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opps - it should have been removed when I moved it to URLTest after moving the functions - gone now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'm following. I was talking about the class names. The class being tested is named CRM_Utils_Url
, but the test class is CRM_Utils_URLTest
(while the file is UrlTest.php). Basically, you're using all uppercase letters for the URL word in the test class name and Url
for all the rest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah - good point!
faf8c91
to
8ae8064
Compare
tests/phpunit/CRM/Utils/UrlTest.php
Outdated
* | ||
* @dataProvider getURLs | ||
*/ | ||
public function testRedirectHook($url, $parsedUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it makes sense for this test to be here. The CRM_Utils_Url
class doesn't know anything about hooks or redirects. It only parses and unparses URLs. That's why I've mentioned here this I think we should use this test class to only test the CRM_Utils_Url
methods and do the redirect/hook tests in the CRM_Utils_SystemTest
class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm - well it DOES test the parse / unparse in that test. I can move it back but I think the test itself gives adequate cover of those
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK - I've moved it back into the System test & ditched the UrlTest - I think the parsing / unparsing is covered enough by the test but it does make more sense to be in the headline tested class
431dff1
to
50b621e
Compare
@totten I looked more closely & the actual interface being used here is from "psr/http-message" - version 1 is required by both guzzle & httplug with one using ~ & one ^ which is effectively the same thing at the moment. So, in order to get this merged I backed up to adding psr/http-message to composer.json & using that. I still think the discussion supported adding guzzle to our vendor for 5.x / april release but will move to a separate PR. I think this is ready to 'merge on pass' having had a few rounds of review & being more or less approved prior to adding the whole psr-7 thing |
I take it back - it IS using the guzzle bit - I'll put guzzle back. I think we can merge this without closing the httplug question |
50b621e
to
a56b2d1
Compare
a56b2d1
to
2705a97
Compare
@@ -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": "^6.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to do a quick sanity check to ensure that we wouldn't be throwing a wrinkle in D8 support. This looks OK -- one should be able to satisfy this constraint as well as all these variants from D8:
- Drupal 8.0.x:
"guzzlehttp/guzzle": "~6.1",
- Drupal 8.1.x:
"guzzlehttp/guzzle": "^6.2.1",
- ...
- Drupal 8.6.x:
"guzzlehttp/guzzle": "^6.2.1",
CRM/Utils/System.php
Outdated
$output = CRM_Utils_Array::value('snippet', $_GET); | ||
|
||
$parsedUrl = CRM_Utils_Url::parseUrl($url); | ||
CRM_Utils_Hook::alterRedirect($parsedUrl, $context, $output); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eileenmcnaughton , this doesn't seem match-up with the function signature:
public static function alterRedirect($url, &$outputFormat, $context) {
- Which order seems more correct?
- What's an example where the
outputFormat
matters or needs to be altered? - Could we treat the
outputFormat
as part of$context
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think output format could be part of context
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK @totten have fixed to be one array
* @dataProvider getURLs | ||
*/ | ||
public function testRedirectHook($url, $parsedUrl) { | ||
$this->hookClass->setHook('civicrm_alterRedirect', array($this, 'hook_checkUrl')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This confused me a little while reading -- in the vast majority of test classes, a function named hook_foo
is an implementation of hook_foo
. Here the names don't match up. Suggestion: https://gist.github.com/totten/b5570ae6062fc115c4fb5e300385543b
Other examples:
tests/phpunit/api/v3/ContributionPageTest.php: public function hook_civicrm_alterPaymentProcessorParams($paymentObj, &$rawParams, &$cookedParams) {
tests/phpunit/api/v3/MembershipTest.php: public function hook_civicrm_pre_update_create_membership($op, $objectName, $id, &$params) {
tests/phpunit/api/v3/SelectQueryTest.php: public function hook_civicrm_selectWhereClause($entity, &$clauses) {
tests/phpunit/Civi/Angular/ManagerTest.php: public function hook_civicrm_alterAngular($angular) {
tests/phpunit/Civi/Angular/ManagerTest.php: public function hook_civicrm_angularModules_fooBar(&$angularModules) {
tests/phpunit/Civi/CCase/SequenceListenerTest.php: public function hook_caseTypes(&$caseTypes) {
tests/phpunit/Civi/Core/Event/GenericHookEventTest.php: public function hook_civicrm_ghet(&$roString, &$rwString, &$roArray, &$rwArray, $plainObj, &$refObj) {
tests/phpunit/Civi/Test/ExampleHookTest.php: public function hook_civicrm_alterContent(&$content, $context, $tplName, &$object) {
tests/phpunit/CiviTest/CiviCaseTestCase.php: public function hook_caseTypes(&$caseTypes) {
tests/phpunit/CRM/ACL/ListTest.php: public function hook_civicrm_aclWhereClause($type, &$tables, &$whereTables, &$contactID, &$where) {
tests/phpunit/CRM/Case/BAO/CaseTypeForkTest.php: public function hook_caseTypes(&$caseTypes) {
tests/phpunit/CRM/Contribute/Form/Task/PDFLetterCommonTest.php: public function hook_tokens(&$tokens) {
tests/phpunit/CRM/Contribute/Form/Task/PDFLetterCommonTest.php: public function hook_aggregateTokenValues(&$values, $contactIDs, $job = NULL, $tokens = array(), $context = NULL) {
tests/phpunit/CRM/Core/FieldOptionsTest.php: public function hook_civicrm_fieldOptions($entity, $field, &$options, $params) {
tests/phpunit/CRM/Group/Page/AjaxTest.php: public function hook_civicrm_aclGroup($type, $contactID, $tableName, &$allGroups, &$currentGroups) {
tests/phpunit/CRM/Mailing/MailingSystemTest.php: public function hook_alterMailParams(&$params, $context = NULL) {
tests/phpunit/CRM/Utils/SystemTest.php: public function hook_civicrm_alterRedirect($urlQuery, $context) {
529485d
to
eb4c6f6
Compare
fixup CRM-21659 Add hook to CRM_Utils_System::redirect This renames the hook function to be stylistically more consistent. In other tests, the function name matches the hook name.
Kudos @eileenmcnaughton for patience with all rounds on this one. Final review: (CiviCRM Review Template WORD-1.1)
|
PSR-7 specifies that the `UriInterface` is immutable. There are methods like `withQuery(...)` which generate a *new instance*. For the hook to support altering the URL, you must be able to replace the `$url` with a newer instance.
While If you're OK with this change, then I'd say it's merge-on-pass. |
OK - that makes sense although I might need to re-read to fully take that complexity in - I'd only just embraced the fact you don't need to pass objects by ref :-) |
Overview
Adds a hook when the browser is being redirected. This allows extensions to override the destination of an HTTP redirect.
Before
After
hook_civicrm_alterRedirect(\Psr\Http\Message\UriInterface $url, array &$context)
UriInterface
from PSR-7 is included; the concrete implementation ofUri
is loaded from Guzzle v6.3+.Technical Details
Here is an example of changing the query string in the redirect URL:
Comments
Will do PR for documentation once approved