Skip to content

Commit

Permalink
Feature #15664: Google OAuth plugin for emails (#3054)
Browse files Browse the repository at this point in the history
Co-authored-by: lapiudevgit <devgit@lapiu.biz>
Co-authored-by: Olle Haerstedt <olle.haerstedt@limesurvey.org>
  • Loading branch information
3 people committed Oct 6, 2023
1 parent 251e460 commit a27c12a
Show file tree
Hide file tree
Showing 372 changed files with 48,633 additions and 86 deletions.
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;
}

/**
* 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);
Yii::app()->getPluginManager()->dispatchEvent($event);
$emailPlugins = $event->get('plugins');
$data['emailPlugins'] = $emailPlugins;

$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.
if ($emailMethod == LimeMailer::MethodPlugin && $oldEmailPlugin != $emailPlugin) {
$event = new PluginEvent('afterSelectEmailPlugin', $this);
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);
$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.
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

0 comments on commit a27c12a

Please sign in to comment.