Skip to content
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

Migrate CiviMail "extern" scripts to conventional routes #17312

Merged
merged 13 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CRM/Admin/Form/Setting/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
class CRM_Admin_Form_Setting_Url extends CRM_Admin_Form_Setting {
protected $_settings = [
'disable_core_css' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
'defaultExternUrl' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
'userFrameworkResourceURL' => CRM_Core_BAO_Setting::URL_PREFERENCES_NAME,
'imageUploadURL' => CRM_Core_BAO_Setting::URL_PREFERENCES_NAME,
'customCSSURL' => CRM_Core_BAO_Setting::URL_PREFERENCES_NAME,
Expand Down
4 changes: 3 additions & 1 deletion CRM/Mailing/BAO/TrackableURL.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public static function getTrackerURL($url, $mailing_id, $queue_id) {
$urlCache[$mailing_id . $url] = $redirect;
}

$returnUrl = CRM_Utils_System::externUrl('extern/url', "u=$id&qid=$queue_id");
// This looks silly - calling the hook twice. This smells like an accident. Restoring old cache-based lookup.
// $returnUrl = CRM_Utils_System::externUrl('extern/url', "u=$id&qid=$queue_id");
$returnUrl = "{$urlCache[$mailing_id . $url]}&qid={$queue_id}";

if ($hrefExists) {
$returnUrl = "href='{$returnUrl}' rel='nofollow'";
Expand Down
58 changes: 58 additions & 0 deletions CRM/Mailing/Page/Open.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/

/**
* Indicate that a CiviMail message has been opened
*
* General Usage: civicrm/mailing/open?qid={event_queue_id}
*
* NOTE: The parameter name has changed slightly from 'extern/open.php?q={event_queue_id}`.
*/
class CRM_Mailing_Page_Open extends CRM_Core_Page {

/**
* Mark the mailing as opened
*
* @throws \CRM_Core_Exception
*/
public function run() {
$queue_id = CRM_Utils_Request::retrieveValue('qid', 'Positive', NULL, FALSE, 'GET');
if (!$queue_id) {
// Deprecated: "?q=" is problematic in Drupal integrations, but we'll accept if igiven
$queue_id = CRM_Utils_Request::retrieveValue('q', 'Positive', NULL, FALSE, 'GET');;
}
if (!$queue_id) {
echo "Missing input parameters\n";
exit();
}

CRM_Mailing_Event_BAO_Opened::open($queue_id);

$filename = Civi::paths()->getPath('[civicrm.root]/i/tracker.gif');

header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Description: File Transfer');
header('Content-type: image/gif');
header('Content-Length: ' . filesize($filename));
header('Content-Disposition: inline; filename=tracker.gif');

readfile($filename);

CRM_Utils_System::civiExit();
}

}
104 changes: 104 additions & 0 deletions CRM/Mailing/Page/Url.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/

/**
* Redirects a user to the full url from a mailing url.
*
* General Usage: civicrm/mailing/url?qid={event_queue_id}&u={url_id}
*
* Additional arguments may be handled by extractPassthroughParameters().
*/
class CRM_Mailing_Page_Url extends CRM_Core_Page {

/**
* Redirect the user to the specified url.
*
* @throws \CRM_Core_Exception
*/
public function run() {
$queue_id = CRM_Utils_Request::retrieveValue('qid', 'Integer');
$url_id = CRM_Utils_Request::retrieveValue('u', 'Integer', NULL, TRUE);
$url = CRM_Mailing_Event_BAO_TrackableURLOpen::track($queue_id, $url_id);
$query_string = $this->extractPassthroughParameters();

if (strlen($query_string) > 0) {
// Parse the url to preserve the fragment.
$pieces = parse_url($url);

if (isset($pieces['fragment'])) {
$url = str_replace('#' . $pieces['fragment'], '', $url);
}

// Handle additional query string params.
if ($query_string) {
if (stristr($url, '?')) {
$url .= '&' . $query_string;
}
else {
$url .= '?' . $query_string;
}
}

// slap the fragment onto the end per URL spec
if (isset($pieces['fragment'])) {
$url .= '#' . $pieces['fragment'];
}
}
CRM_Utils_System::redirect($url, [
'for' => 'civicrm/mailing/url',
'queue_id' => $queue_id,
'url_id' => $url_id,
]);
}

/**
* Determine if this request has any valid pass-through parameters.
*
* Under CRM-7103 (v3.3), all unrecognized query-parameters (besides qid/u) are passed
* through as part of the redirect. This mechanism is relevant to certain
* customizations (eg using `hook_alterMailParams` to append extra URL args)
* but does not matter for normal URLs.
*
* The functionality seems vaguely problematic (IMHO) - especially now that
* 'extern/url.php' is moving into the CMS/Civi router ('civicrm/mailing/url').
* But it's the current protocol.
*
* A better design might be to support `hook_alterRedirect` in the CiviMail
* click-through tracking. Then you don't have to take any untrusted inputs
* and you can fix URL mistakes in realtime.
*
* @return string
* @link https://issues.civicrm.org/jira/browse/CRM-7103
*/
protected function extractPassthroughParameters():string {
$config = CRM_Core_Config::singleton();

$query_param = $_GET;
unset($query_param['qid']);
unset($query_param['u']);
unset($query_param[$config->userFrameworkURLVar]);
if ($config->userFramework === 'WordPress') {
// Ugh
unset($query_param['page']);
unset($query_param['noheader']);
}
Copy link
Member Author

@totten totten May 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I first tested on a wpmaster site, extractPassthroughParameters() was still producing some noisy GET params (e.g. a link to https://www.google.com winds up as https://www.google.com?noheader=1). This extra conditional resolved that.

But it's also ugly. TBH I'd be happier to rip out extractPassthroughParameters() and make a small BC-break:

  • The notion in https://issues.civicrm.org/jira/browse/CRM-7103 / extractPassthroughParameters() is not well-defined across different routing systems.
  • This PR makes a better API available for the same use-case (hook_civicrm_alterRedirect).
  • I skimmed hook_civicrm_alterMailParams consumers in universe and couldn't spot any doing URL manipulation on CiviMail content. (The fuzion transactional ext was close, but it skips CiviMail.)
  • If one is actually relying on hook_civicrm_alterMailParams for URL manipulation, then you can buy some time for a transition -- set defaultExternUrl=standalone and you get the old behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@totten I'm pretty sure that this issue on Lab will have an impact here - because bleeding edge WordPress has changed the way in which the page query var is used and it now assumes that this is the the desired path and redirects accordingly. We're going to have to audit CiviCRM's usage of this query var in a WordPress context and strip out any usage of it on the front-end. This should lead to unset($query_param['page']); being unnecessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, well, ?page= won't be missed. :)

Since coordinating the timelines on these changes may be tricky, I guess the implication here is... if #17312 does keep extractPassthroughParameters(), then eventually we can remove the unset($query_param['page'). For the moment, from a WP-integration perspective, there's no harm in keeping the unset().


$query_string = http_build_query($query_param);
return $query_string;
}

}
10 changes: 10 additions & 0 deletions CRM/Mailing/xml/Menu/Mailing.xml
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,14 @@
<page_callback>CRM_Mailing_Page_AJAX::getContactMailings</page_callback>
<access_arguments>access CiviCRM</access_arguments>
</item>
<item>
<path>civicrm/mailing/url</path>
<page_callback>CRM_Mailing_Page_Url</page_callback>
<access_arguments>*always allow*</access_arguments>
</item>
<item>
<path>civicrm/mailing/open</path>
<page_callback>CRM_Mailing_Page_Open</page_callback>
<access_arguments>*always allow*</access_arguments>
</item>
</menu>
35 changes: 35 additions & 0 deletions CRM/Utils/System.php
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,41 @@ public static function externUrl($path = NULL, $query = NULL, $fragment = NULL,
return urldecode(CRM_Utils_Url::unparseUrl($event->url));
}

/**
* Perform any current conversions/migrations on the extern URL.
*
* @param \Civi\Core\Event\GenericHookEvent $e
* @see CRM_Utils_Hook::alterExternUrl
*/
public static function migrateExternUrl(\Civi\Core\Event\GenericHookEvent $e) {

/**
* $mkRouteUri is a small adapter to return generated URL as a "UriInterface".
* @param string $path
* @param string $query
* @return \Psr\Http\Message\UriInterface
*/
$mkRouteUri = function ($path, $query) use ($e) {
$urlTxt = CRM_Utils_System::url($path, $query, $e->absolute, $e->fragment, FALSE);
if ($e->isSSL || ($e->isSSL === NULL && \CRM_Utils_System::isSSL())) {
$urlTxt = str_replace('http://', 'https://', $urlTxt);
}
return CRM_Utils_Url::parseUrl($urlTxt);
};

switch (Civi::settings()->get('defaultExternUrl') . ':' . $e->path) {
case 'router:extern/open':
$e->url = $mkRouteUri('civicrm/mailing/open', preg_replace('/(^|&)q=/', '\1qid=', $e->query));
break;

case 'router:extern/url':
$e->url = $mkRouteUri('civicrm/mailing/url', $e->query);
break;

// Otherwise, keep the default.
}
}

/**
* @deprecated
* @see \CRM_Utils_System::currentPath
Expand Down
1 change: 1 addition & 0 deletions Civi/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ public function createEventDispatcher($container) {
$dispatcher->addListener('hook_civicrm_buildAsset', ['\CRM_Core_Resources', 'renderMenubarStylesheet']);
$dispatcher->addListener('hook_civicrm_coreResourceList', ['\CRM_Utils_System', 'appendCoreResources']);
$dispatcher->addListener('hook_civicrm_getAssetUrl', ['\CRM_Utils_System', 'alterAssetUrl']);
$dispatcher->addListener('hook_civicrm_alterExternUrl', ['\CRM_Utils_System', 'migrateExternUrl'], 1000);
$dispatcher->addListener('civi.dao.postInsert', ['\CRM_Core_BAO_RecurringEntity', 'triggerInsert']);
$dispatcher->addListener('civi.dao.postUpdate', ['\CRM_Core_BAO_RecurringEntity', 'triggerUpdate']);
$dispatcher->addListener('civi.dao.postDelete', ['\CRM_Core_BAO_RecurringEntity', 'triggerDelete']);
Expand Down
21 changes: 21 additions & 0 deletions settings/Core.setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,27 @@
'description' => NULL,
'help_text' => NULL,
],
'defaultExternUrl' => [
'group_name' => 'CiviCRM Preferences',
'group' => 'core',
'name' => 'defaultExternUrl',
'type' => 'String',
'quick_form_type' => 'Select',
'html_type' => 'Select',
'html_attributes' => [
'class' => 'crm-select2',
],
'default' => 'router',
'add' => '5.27',
'title' => ts('Extern URL Style'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transitional Extern URL style ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title tweaked:

Screen Shot 2020-05-14 at 2 34 08 AM

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just noticed your bit about gitlab. Here's another variation:

Screen Shot 2020-05-14 at 2 45 07 AM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@totten @eileenmcnaughton I'm confused -- why would a GitLab issue be necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christianwach the theory is that people would only use this setting if they need to because something weird is happening. Once there are no more something weirds then the setting & support for it can go. Probably in a few months we would remove from the form & later on from the code base

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton Sounds reasonable to me 👍

'is_domain' => 1,
'is_contact' => 0,
'help_text' => NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - the help text should ideally be in here - although I think that if the settings page is not using the generic tpl it may not work

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds cool :) - I didn't realize that dataflow was now working for some forms.

FWIW, when I grep settings/ for other examples of help_text, they all appear to be a couple sentences long. Since this is actually several paragraphs (which in turn requires markup), perhaps it's still OK in the .hlp file?

'options' => [
'standalone' => ts('Prefer standalone scripts'),
'router' => ts('Prefer normal router'),
],
],
'activity_assignee_notification' => [
'group_name' => 'CiviCRM Preferences',
'group' => 'core',
Expand Down
10 changes: 10 additions & 0 deletions templates/CRM/Admin/Form/Setting/Url.hlp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@
<p>{ts}You can modify the look and feel of CiviCRM by adding your own stylesheet. For small to medium sized modifications, use your css file to override some of the styles in civicrm.css. Or if you need to make drastic changes, you can choose to disable civicrm.css completely.{/ts}</p>
{/htxt}

{htxt id='id-defaultExternUrl'}
<p>{ts}CiviCRM generates callback URLs for external services.{/ts}</p>
<p>{ts}Some callback URLs are being migrated to a different style. During the transition, you may indicate a preferred style, such as:{/ts}</p>
<ul>
<li>{ts}"Standalone Scripts" - In the traditional style, each callback URL is a standalone PHP script. You may wish to use this style if you need to maximize performance or if you need continuity.{/ts}</li>
<li>{ts}"Router" - In the newer style, each callback URL is defined like a normal CiviCRM page. You may wish to use this style for greater consistency or portability.{/ts}</li>
</ul>
<p>{ts}This setting only affects the default URL produced by "civicrm-core". Extensions and add-ons may override specific URLs.{/ts}</p>
{/htxt}

{htxt id='id-url_vars'}
{ts}URL Variables{/ts}
<table>
Expand Down
8 changes: 8 additions & 0 deletions templates/CRM/Admin/Form/Setting/Url.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@
<p class="description font-red">{ts}{$verifySSL_description}{/ts}</p>
</td>
</tr>
<tr class="crm-url-form-block-defaultExternUrl">
<td class="label">
{$form.defaultExternUrl.label} {help id='id-defaultExternUrl'}
</td>
<td>
{$form.defaultExternUrl.html}
</td>
</tr>
</table>
<div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="bottom"}</div>
</div>
Loading