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

Feature #15664: Google OAuth plugin for emails #3054

Merged
merged 34 commits into from Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3ad1d7e
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 4, 2023
064260d
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 5, 2023
4b4e711
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 6, 2023
a118654
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 6, 2023
c897967
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 9, 2023
523f52f
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 9, 2023
a203b12
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jan 19, 2023
513a76f
Merge branch 'develop' into feature/15664-Google-OAuth-plugin
lapiudevgit Apr 4, 2023
e701aa4
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 5, 2023
9d7a59c
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 7, 2023
488a5cd
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 12, 2023
bfbc088
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 12, 2023
8235147
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 12, 2023
ddc40d2
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 14, 2023
0c71d54
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 14, 2023
6df1971
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 17, 2023
8321788
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 20, 2023
568c1c1
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 20, 2023
664ad9c
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 20, 2023
799eca5
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 21, 2023
c083847
Merge branch 'develop' into feature/15664-Google-OAuth-plugin-3
lapiudevgit Apr 21, 2023
4c320fb
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 21, 2023
b753e68
Feature #15664: OAuth support for LimeMailer
lapiudevgit Apr 22, 2023
e4a9bfe
Feature #15664: Google OAuth plugin for emails
lapiudevgit Apr 26, 2023
81d771d
Feature #15664: Google OAuth plugin for emails
lapiudevgit Apr 26, 2023
376a88d
Feature #15664: Google OAuth plugin for emails
lapiudevgit Apr 28, 2023
cb67267
Merge branch 'develop' into feature/15664-Google-OAuth-plugin-3
olleharstedt Jul 4, 2023
f404a98
Merge branch 'develop' into feature/15664-Google-OAuth-plugin-3
olleharstedt Jul 4, 2023
a299419
Feature #15664: Google OAuth plugin for emails
lapiudevgit Jul 7, 2023
eb71478
Merge branch 'develop' into feature/15664-Google-OAuth-plugin-3
lapiudevgit Sep 26, 2023
8129c03
Merge remote-tracking branch 'limesurvey/develop' into feature/15664-…
gabrieljenik Oct 5, 2023
f44e557
Feature #15664: Google OAuth plugin for emails
gabrieljenik Oct 5, 2023
e6bcb69
Feature #15664: Google OAuth plugin for emails
gabrieljenik Oct 5, 2023
659acfe
Feature #15664: Google OAuth plugin for emails
lapiudevgit Oct 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
171 changes: 171 additions & 0 deletions application/controllers/SmtpOAuthController.php
@@ -0,0 +1,171 @@
<?php

/**
* @class SmtpOAuthController
*/
class SmtpOAuthController extends LSBaseController
{
/**
* It's important to have the accessRules set (security issue).
* Only logged in users should have access to actions. All other permissions
* should be checked in the action itself.
*
* @return array
*/
public function accessRules()
{
return [
[
'allow',
'actions' => ['receiveOAuthResponse'],
'users' => ['*'], //everybody
],
[
'allow',
'actions' => ['prepareRefreshTokenRequest', 'launchRefreshTokenRequest'],
'users' => ['@'], //only login users
],
['deny'], //always deny all actions not mentioned above
];
}


/**
* Displays the view with Get Token button
* @param string $plugin
* @return void
*/
public function actionPrepareRefreshTokenRequest($plugin)
{
if (!Permission::model()->hasGlobalPermission('settings', 'update')) {
Yii::app()->user->setFlash('error', gT("Access denied"));
$this->redirect(Yii::app()->createUrl("/admin"));
}

$pluginModel = Plugin::model()->findByAttributes(['name' => $plugin]);
if (empty($pluginModel) || !$pluginModel->active) {
Yii::app()->user->setFlash('error', gT("Invalid plugin"));
$this->redirect(Yii::app()->createUrl("/admin"));
}

// Dispatch the plugin event to get details needed for the view,
// like the size of the auth window.
$event = new PluginEvent('beforePrepareRedirectToAuthPage', $this);
Yii::app()->getPluginManager()->dispatchEvent($event, $plugin);
$data['width'] = $event->get('width');
$data['height'] = $event->get('height');
$data['providerName'] = $event->get('providerName', $plugin);
$data['topbar']['title'] = gT('Get OAuth 2.0 token for SMTP authentication');
$data['providerUrl'] = $this->createUrl('smtpOAuth/launchRefreshTokenRequest', ['plugin' => $plugin]);

$pluginEventContent = $event->getContent($plugin);
$description = null;
if ($pluginEventContent->hasContent()) {
$description = CHtml::tag(
'div',
[
'id' => $pluginEventContent->getCssId(),
'class' => $pluginEventContent->getCssClass()
],
$pluginEventContent->getContent()
);
}
$data['description'] = $description;

$data['redirectUrl'] = $this->createUrl(
'/admin/pluginmanager',
[
'sa' => 'configure',
'id' => $pluginModel->id
]
);

$this->aData = $data;

$this->render('redirectToAuth', $data);
}

public function actionLaunchRefreshTokenRequest($plugin)
{
if (!Permission::model()->hasGlobalPermission('settings', 'update')) {
Yii::app()->user->setFlash('error', gT("Access denied"));
$this->redirect(Yii::app()->createUrl("/admin"));
}

// Dispatch the plugin event to get the redirect URL
$event = new PluginEvent('beforeRedirectToAuthPage', $this);
Yii::app()->getPluginManager()->dispatchEvent($event, $plugin);
$authUrl = $event->get('authUrl');

$this->setOAuthState($plugin, $event->get('state'));

header('Location: ' . $authUrl);
exit;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yii::app()->end; ?

}

/**
* Receive the response from the OAuth provider
* @return void
*/
public function actionReceiveOAuthResponse()
{
// Make sure the request includes the required data
$code = Yii::app()->request->getParam('code');
$state = Yii::app()->request->getParam('state');
if (empty($code) || empty($state)) {
throw new CHttpException(400);
}

// Find the plugin class matching the given OAuth state
$plugin = $this->getPluginClassByOAuthState($state);

// If no plugin was found, the state is invalid
if (empty($plugin)) {
throw new CHttpException(400);
}

$event = new PluginEvent('afterReceiveOAuthResponse', $this);
$event->set('code', $code);
$event->set('state', $state);
Yii::app()->getPluginManager()->dispatchEvent($event, $plugin);

// Remove the state from the session
$this->clearOAuthState($plugin);

Yii::app()->user->setFlash('success', gT('The OAuth 2.0 token was successfully retrieved.'));

// Render the HTML that will be displayed in the popup window
// The HTML will close the window and cause the page to reload
$this->renderPartial('/smtpOAuth/responseReceived', []);
}

/**
* Find the plugin class matching the given OAuth state.
* @param string $state
* @return string|null
*/
protected function getPluginClassByOAuthState($state)
{
$pluginsWithOAuthState = Yii::app()->session['smtpOAuthStates'] ?? [];
return array_search($state, $pluginsWithOAuthState, true);
}

/**
* Set the OAuth state for the given plugin.
* @param string $plugin
* @param string $state
*/
protected function setOAuthState($plugin, $state)
{
$smtpOAuthStates = Yii::app()->session['smtpOAuthStates'] ?? [];
$smtpOAuthStates[$plugin] = $state;
Yii::app()->session['smtpOAuthStates'] = $smtpOAuthStates;
}

protected function clearOAuthState($plugin)
{
$smtpOAuthStates = Yii::app()->session['smtpOAuthStates'] ?? [];
unset($smtpOAuthStates[$plugin]);
Yii::app()->session['smtpOAuthStates'] = $smtpOAuthStates;
}
}
23 changes: 22 additions & 1 deletion application/controllers/admin/globalsettings.php
Expand Up @@ -143,6 +143,12 @@ private function displaySettings()
$data['sideMenuBehaviour'] = getGlobalSetting('sideMenuBehaviour');
$data['aListOfThemeObjects'] = AdminTheme::getAdminThemeList();

// List of available email plugins
$event = new PluginEvent('listEmailPlugins', $this);
olleharstedt marked this conversation as resolved.
Show resolved Hide resolved
Yii::app()->getPluginManager()->dispatchEvent($event);
$emailPlugins = $event->get('plugins');
$data['emailPlugins'] = $emailPlugins;
olleharstedt marked this conversation as resolved.
Show resolved Hide resolved

$this->renderWrappedTemplate('globalsettings', 'globalSettings_view', $data);
}

Expand Down Expand Up @@ -327,7 +333,8 @@ private function saveSettings()
$sAdmintheme = sanitize_paranoid_string(Yii::app()->getRequest()->getPost('admintheme'));
SettingGlobal::setSetting('admintheme', $sAdmintheme);

SettingGlobal::setSetting('emailmethod', strip_tags(Yii::app()->getRequest()->getPost('emailmethod', '')));
$emailMethod = strip_tags(Yii::app()->getRequest()->getPost('emailmethod', ''));
SettingGlobal::setSetting('emailmethod', $emailMethod);
SettingGlobal::setSetting('emailsmtphost', strip_tags((string) returnGlobal('emailsmtphost')));
if (returnGlobal('emailsmtppassword') != 'somepassword') {
SettingGlobal::setSetting('emailsmtppassword', LSActiveRecord::encryptSingle(returnGlobal('emailsmtppassword')));
Expand All @@ -346,6 +353,20 @@ private function saveSettings()
SettingGlobal::setSetting('emailsmtpuser', strip_tags((string) returnGlobal('emailsmtpuser')));
SettingGlobal::setSetting('filterxsshtml', strip_tags(Yii::app()->getRequest()->getPost('filterxsshtml', '')));
SettingGlobal::setSetting('disablescriptwithxss', strip_tags(Yii::app()->getRequest()->getPost('disablescriptwithxss', '')));

$oldEmailPlugin = Yii::app()->getConfig('emailplugin');
$emailPlugin = strip_tags(Yii::app()->getRequest()->getPost('emailplugin', ''));
SettingGlobal::setSetting('emailplugin', $emailPlugin);
// If the email plugin has changed, dispatch an event to allow the new plugin to do any necessary setup.
olleharstedt marked this conversation as resolved.
Show resolved Hide resolved
if ($emailMethod == LimeMailer::MethodPlugin && $oldEmailPlugin != $emailPlugin) {
$event = new PluginEvent('afterSelectEmailPlugin', $this);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this new event needed, or can we use afterControllerAction? Oh wait, there's only beforeControllerAction... Should we add afterControllerAction instead? It's way more general than afterSelectEmailPlugin.

Yii::app()->getPluginManager()->dispatchEvent($event, $emailPlugin);
$emailPluginWarning = $event->get('warning');
if (!empty($emailPluginWarning)) {
$warning .= $emailPluginWarning . '<br/>';
}
}

// make sure emails are valid before saving them
if (
Yii::app()->request->getPost('siteadminbounce', '') == ''
Expand Down
45 changes: 41 additions & 4 deletions application/core/LimeMailer.php
@@ -1,9 +1,12 @@
<?php

use PHPMailer\PHPMailer\PHPMailer;

/**
* WIP
* A SubClass of phpMailer adapted for LimeSurvey
*/
class LimeMailer extends \PHPMailer\PHPMailer\PHPMailer
class LimeMailer extends PHPMailer
{
/**
* Singleton
Expand All @@ -21,6 +24,20 @@ class LimeMailer extends \PHPMailer\PHPMailer\PHPMailer
/* Complete reset : all except survey part , remind : you always can get a new one */
const ResetComplete = 2;

/**
* Email methods
*/
/* PHP mail() */
const MethodMail = 'mail';
/* Sendmail */
const MethodSendmail = 'sendmail';
/* Qmail */
const MethodQmail = 'qmail';
/* SMTP */
const MethodSmtp = 'smtp';
/* Plugin */
const MethodPlugin = 'plugin';

/* @var null|integer $surveyId Current survey id */
public $surveyId;
/* @var null|string $mailLanguage Current language for the mail (=language is used for language of mailer (error etc …) */
Expand Down Expand Up @@ -154,10 +171,10 @@ public function __construct($exceptions = false)
$this->SMTPAutoTLS = false;

switch ($emailmethod) {
case "qmail":
case self::MethodQmail:
$this->IsQmail();
break;
case "smtp":
case self::MethodSmtp:
$this->IsSMTP();
if ($emailsmtpdebug > 0) {
$this->SMTPDebug = $emailsmtpdebug;
Expand All @@ -179,9 +196,15 @@ public function __construct($exceptions = false)
$this->SMTPAuth = true;
}
break;
case "sendmail":
case self::MethodSendmail:
$this->IsSendmail();
break;
case self::MethodPlugin:
$emailPlugin = Yii::app()->getConfig('emailplugin');
$event = new PluginEvent('MailerConstruct', $this);
olleharstedt marked this conversation as resolved.
Show resolved Hide resolved
$event->set('mailer', $this);
Yii::app()->getPluginManager()->dispatchEvent($event, $emailPlugin);
break;
default:
$this->IsMail();
}
Expand Down Expand Up @@ -604,6 +627,20 @@ public function Send()
$this->setError(gT('Email was not sent because demo-mode is activated.'));
return false;
}

// If the email method is set to "Plugin", we need to dispatch an event to that specific plugin
// so it can perform it's logic without depending on the more generic "beforeEmail" event.
Copy link
Collaborator

Choose a reason for hiding this comment

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

without depending on the more generic "beforeEmail" event.

Why not depend on this one? Not possible/easy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should read some back and forth to remember, but I recall one of the issue was the priority among beforeEmail events.

if (Yii::app()->getConfig('emailmethod') == self::MethodPlugin) {
$emailPlugin = Yii::app()->getConfig('emailplugin');
$event = new PluginEvent('beforeEmailDispatch', $this);
$event->set('mailer', $this);
Yii::app()->getPluginManager()->dispatchEvent($event, $emailPlugin);
if (!$event->get('send', true)) {
$this->ErrorInfo = $event->get('error');
return $event->get('error') == null;
}
}

return parent::Send();
}

Expand Down