From: Tim Otten Date: Fri, 30 Oct 2020 08:39:58 +0000 (-0700) Subject: dev/core#2141 - Allow OAuth2 services to define mail setup routine X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=7be4bff1c08120f2c42b2c26912710de938cd1e0;p=civicrm-core.git dev/core#2141 - Allow OAuth2 services to define mail setup routine Overview -------- For certain types of mail accounts -- such as Google Mail and Microsoft Exchange Online -- the setup process may require interaction with a remote web-service. If you have OAuth2 enabled for one of these services, then this will create an option for "Add Mail Account". Before ------ There is no setup procedure. After ----- * Navigate to "Administer => CiviMail => Mail Accounts". * Below the table, there is a select2 box for "Add Mail Account". * If "Microsoft Exchange Online" is configured, then it will appear in the dropdown. Choose it. * It redirects to MS to get authorization from the user (OAuth2 Authorization Code). * The user comes back. * We initialize a new mail account (`MailSettings` / `civicrm_mail_settings`) * We accept the code and save the token (`OAuthSysToken`) with the account. * We redirect to the account configuration form. Technical Details ----------------- The new mail account will have some details, such as `server`, `protocol`, and `username` pre-filled. This uses a template -- see e.g. `providers/ms-exchange.dist.json` (`mailSettingsTemplate`). --- diff --git a/ext/oauth-client/CRM/OAuth/MailSetup.php b/ext/oauth-client/CRM/OAuth/MailSetup.php new file mode 100644 index 0000000000..7f144f1464 --- /dev/null +++ b/ext/oauth-client/CRM/OAuth/MailSetup.php @@ -0,0 +1,144 @@ +addWhere('is_active', '=', 1)->execute(); + $providers = Civi\Api4\OAuthProvider::get(0)->execute()->indexBy('name'); + + $setupActions = []; + foreach ($clients as $client) { + $provider = $providers[$client['provider']] ?? NULL; + if ($provider === NULL) { + continue; + } + // v api OptionValue.get option_group_id=mail_protocol + if (!empty($provider['mailSettingsTemplate'])) { + $setupActions['oauth_' . $client['id']] = [ + 'title' => sprintf('%s (ID #%s)', $provider['title'] ?? $provider['name'] ?? ts('OAuth2'), $client['id']), + 'callback' => ['CRM_OAuth_MailSetup', 'setup'], + 'oauth_client_id' => $client['id'], + ]; + } + } + + return $setupActions; + } + + /** + * When a user chooses to add one of our mail options, we kick off + * the authorization-code workflow. + * + * @param array $setupAction + * The chosen descriptor from mailSetupActions. + * @return array + * With keys: + * - url: string, the final URL to go to. + * @see CRM_Utils_Hook::mailSetupActions() + */ + public static function setup($setupAction) { + $authCode = Civi\Api4\OAuthClient::authorizationCode(0) + ->addWhere('id', '=', $setupAction['oauth_client_id']) + ->setStorage('OAuthSysToken') + ->setTag('MailSettings:setup') + ->execute() + ->single(); + + return [ + 'url' => $authCode['url'], + ]; + } + + /** + * When the user returns with a token, we add a new record to + * civicrm_mail_settings with defaults and redirect to the edit screen. + * + * @param array $token + * OAuthSysToken + * @param string $nextUrl + */ + public static function onReturn($token, &$nextUrl) { + if ($token['tag'] !== 'MailSettings:setup') { + return; + } + + $client = \Civi\Api4\OAuthClient::get(0)->addWhere('id', '=', $token['client_id'])->execute()->single(); + $provider = \Civi\Api4\OAuthProvider::get(0)->addWhere('name', '=', $client['provider'])->execute()->single(); + + $vars = ['token' => $token, 'client' => $client, 'provider' => $provider]; + $mailSettings = civicrm_api4('MailSettings', 'create', [ + 'values' => self::evalArrayTemplate($provider['mailSettingsTemplate'], $vars), + ])->single(); + + \Civi\Api4\OAuthSysToken::update(0) + ->addWhere('id', '=', $token['id']) + ->setValues(['tag' => 'MailSettings:' . $mailSettings['id']]) + ->execute(); + + CRM_Core_Session::setStatus( + ts('Here are the account defaults we detected for %1. Please check them carefully.', [ + 1 => $mailSettings['name'], + ]), + ts('Account created!'), + 'info' + ); + + $nextUrl = CRM_Utils_System::url('civicrm/admin/mailSettings', [ + 'action' => 'update', + 'id' => $mailSettings['id'], + 'reset' => 1, + ], TRUE, NULL, FALSE); + } + + /** + * @param array $template + * List of key-value expressions. + * Ex: ['name' => '{{person.first}} {{person.last}}'] + * Expressions begin with the dotted-name of a variable. + * Optionally, the value may be piped through other functions + * @param array $vars + * Array tree of data to interpolate. + * @return array + * The template array, with '{{...}}' expressions evaluated. + */ + public static function evalArrayTemplate($template, $vars) { + $filters = [ + 'getMailDomain' => function($v) { + $parts = explode('@', $v); + return $parts[1] ?? NULL; + }, + 'getMailUser' => function($v) { + $parts = explode('@', $v); + return $parts[0] ?? NULL; + }, + ]; + + $lookupVars = function($m) use ($vars, $filters) { + $parts = explode('|', $m[1]); + $value = (string) CRM_Utils_Array::pathGet($vars, explode('.', array_shift($parts))); + foreach ($parts as $part) { + if (isset($filters[$part])) { + $value = $filters[$part]($value); + } + else { + $value = NULL; + } + } + return $value; + }; + + $values = []; + foreach ($template as $key => $value) { + $values[$key] = is_string($value) + ? preg_replace_callback(';{{([a-zA-Z0-9_\.\|]+)}};', $lookupVars, $value) + : $value; + } + return $values; + } + +} diff --git a/ext/oauth-client/oauth_client.php b/ext/oauth-client/oauth_client.php index cc24a4a419..684187ab57 100644 --- a/ext/oauth-client/oauth_client.php +++ b/ext/oauth-client/oauth_client.php @@ -223,3 +223,19 @@ function oauth_client_civicrm_oauthProviders(&$providers) { $ingest($localDir . '/*.json'); } } + +/** + * Implements hook_civicrm_mailSetupActions(). + * + * @see CRM_Utils_Hook::mailSetupActions() + */ +function oauth_client_civicrm_mailSetupActions(&$setupActions) { + $setupActions = array_merge($setupActions, CRM_OAuth_MailSetup::buildSetupLinks()); +} + +/** + * Implements hook_civicrm_oauthReturn(). + */ +function oauth_client_civicrm_oauthReturn($token, &$nextUrl) { + CRM_OAuth_MailSetup::onReturn($token, $nextUrl); +} diff --git a/ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php b/ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php new file mode 100644 index 0000000000..06ee4b8c20 --- /dev/null +++ b/ext/oauth-client/tests/phpunit/CRM/OAuth/MailSetupTest.php @@ -0,0 +1,85 @@ +install('oauth-client')->apply(); + } + + public function setUp() { + parent::setUp(); + } + + public function tearDown() { + parent::tearDown(); + } + + public function testEvalArrayTemplate() { + $vars = array( + 'token' => [ + 'client_id' => 10, + 'resource_owner' => ['mail' => 'foo@bar.com'], + ], + 'client' => [ + 'id' => 1, + 'provider' => 'ms-exchange', + 'guid' => 'abcd-1234-efgh-5678', + 'secret' => '8765-hgfe-4321-dcba', + 'options' => NULL, + 'is_active' => TRUE, + 'created_date' => '2020-10-29 10:11:12', + 'modified_date' => '2020-10-29 10:11:12', + ], + 'provider' => [ + 'name' => 'foozball', + 'title' => 'Foozball Association', + 'options' => [ + 'urlAuthorize' => 'https://login.example.com/common/oauth2/v2.0/authorize', + 'urlAccessToken' => 'https://login.example.com/common/oauth2/v2.0/token', + 'urlResourceOwnerDetails' => 'https://resource.example.com/v9.0/me', + 'scopeSeparator' => ' ', + 'scopes' => [], + ], + 'mailSettingsTemplate' => [ + 'name' => '{{provider.title}}: {{token.resource_owner.mail}}', + 'domain' => '{{token.resource_owner.mail|getMailDomain}}', + 'localpart' => NULL, + 'return_path' => NULL, + 'protocol:name' => 'IMAP', + 'server' => 'imap.foozball.com', + 'username' => '{{token.resource_owner.mail}}', + 'password' => NULL, + 'is_ssl' => TRUE, + ], + 'class' => 'Civi\\OAuth\\CiviGenericProvider', + ], + ); + $expected = [ + 'name' => 'Foozball Association: foo@bar.com', + 'domain' => 'bar.com', + 'localpart' => NULL, + 'return_path' => NULL, + 'protocol:name' => 'IMAP', + 'server' => 'imap.foozball.com', + 'username' => 'foo@bar.com', + 'password' => '', + 'is_ssl' => TRUE, + ]; + $actual = \CRM_OAuth_MailSetup::evalArrayTemplate($vars['provider']['mailSettingsTemplate'], $vars); + $this->assertEquals($expected, $actual); + $this->assertTrue($actual['localpart'] === NULL); + } + +}