Skip to content

Commit

Permalink
afform_login_token - Re-import login-token feature as core extension
Browse files Browse the repository at this point in the history
Before+After
------------

* Up through 5.78-stable, `afform_core` included support for generating emails
  with form-links w/login-tokens. However, there was a long-standing
  request from multiple people to support form-links w/page-tokens.

* During 5.79-alpha (civicrm#30585), we introduced page-tokens. We discussed more
  at the sprint, and there was a distinct feeling that page-tokens were more
  desirable (more approprate to more users/less foot-gunny). We agreed that
  login-tokens were hypothetically better for some, but no one in that
  discussion was eager to use them. So we moved login-tokens to a contrib
  extension.

* But during 5.79-beta cycle, we got testing feedback from more people
  who were keen on login-tokens.

* This PR re-introduces login-token support as a core-extension for 5.79-beta,
  which means that:

    * The default/typical mode of operation is based on page-tokens.
    * Using login-tokens is a little bit of work, but not as much.
  • Loading branch information
totten committed Oct 30, 2024
1 parent be41751 commit e215470
Show file tree
Hide file tree
Showing 6 changed files with 1,090 additions and 0 deletions.
118 changes: 118 additions & 0 deletions ext/afform/login_token/Civi/AfformLoginToken/Tokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?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 |
+--------------------------------------------------------------------+
*/

namespace Civi\AfformLoginToken;

use Civi\Core\Service\AutoService;
use CRM_Afform_ExtensionUtil as E;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Every afform with the property `is_token=true` should have a corresponding
* set of tokens, `{afform.myFormLoginUrl}` and `{afform.myFormLoginLink}`.
*
* @service civi.afformLoginTokens.tokens
*/
class Tokens extends AutoService implements EventSubscriberInterface {

private static $placement = 'msg_token_login';

public static function getSubscribedEvents(): array {
if (!\CRM_Extension_System::singleton()->getMapper()->isActiveModule('authx')) {
return [];
}

return [
'civi.token.list' => 'listTokens',
'civi.token.eval' => 'evaluateTokens',
];
}

public function listTokens(\Civi\Token\Event\TokenRegisterEvent $e): void {
if (in_array('contactId', $e->getTokenProcessor()->getContextValues('schema')[0])) {
$tokenForms = static::getTokenForms();
foreach ($tokenForms as $formName => $afform) {
$e->entity('afformLogin')
->register("{$formName}Url", E::ts('%1 (URL, Login)', [1 => $afform['title'] ?? $afform['name']]))
->register("{$formName}Link", E::ts('%1 (Hyperlink, Login)', [1 => $afform['title'] ?? $afform['name']]));
}
}
}

/**
* Substitute any tokens of the form `{afformLogin.myFormUrl}` or `{afformLogin.myFormLink}` with actual values.
*/
public function evaluateTokens(\Civi\Token\Event\TokenValueEvent $e): void {
$activeTokens = $e->getTokenProcessor()->getMessageTokens();
if (empty($activeTokens['afformLogin'])) {
return;
}

$tokenForms = static::getTokenForms();
foreach ($tokenForms as $formName => $afform) {
foreach ($e->getRows() as $row) {
$url = self::createUrl($afform, $row->context['contactId']);
$row->format('text/plain')->tokens('afformLogin', "{$formName}Url", $url);
$row->format('text/html')->tokens('afformLogin', "{$formName}Link", sprintf('<a href="%s">%s</a>', htmlentities($url), htmlentities($afform['title'] ?? $afform['name'])));
}
}
}

/**
* Get a list of forms that have token support enabled.
*
* @return array
* $result[$formName] = ['name' => $formName, 'title' => $formTitle, 'server_route' => $route];
*/
public static function getTokenForms() {
$cache = &\Civi::$statics[__CLASS__]['tokenForms'];
if (!isset($cache)) {
$tokenForms = (array) \Civi\Api4\Afform::get(FALSE)
->addWhere('placement', 'CONTAINS', static::$placement)
->addWhere('server_route', 'IS NOT EMPTY')
->addSelect('name', 'title', 'server_route', 'is_public')
->execute()
->indexBy('name');
$cache = $tokenForms;
}
return $cache;
}

/**
* Generate an authenticated URL for viewing this form.
*
* @param array $afform
* @param int $contactId
*
* @return string
* @throws \Civi\Crypto\Exception\CryptoException
*/
public static function createUrl($afform, $contactId): string {
$expires = \CRM_Utils_Time::time() +
(\Civi::settings()->get('checksum_timeout') * 24 * 60 * 60);

/** @var \Civi\Crypto\CryptoJwt $jwt */
$jwt = \Civi::service('crypto.jwt');

$url = \Civi::url()
->setScheme($afform['is_public'] ? 'frontend' : 'backend')
->setPath($afform['server_route'])
->setPreferFormat('absolute');

$bearerToken = "Bearer " . $jwt->encode([
'exp' => $expires,
'sub' => "cid:" . $contactId,
'scope' => 'authx',
]);
return $url->addQuery(['_authx' => $bearerToken, '_authxSes' => 1]);
}

}
Loading

0 comments on commit e215470

Please sign in to comment.