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
Changes from all commits
3ad1d7e
064260d
4b4e711
a118654
c897967
523f52f
a203b12
513a76f
e701aa4
9d7a59c
488a5cd
bfbc088
8235147
ddc40d2
0c71d54
6df1971
8321788
568c1c1
664ad9c
799eca5
c083847
4c320fb
b753e68
e4a9bfe
81d771d
376a88d
cb67267
f404a98
a299419
eb71478
8129c03
f44e557
e6bcb69
659acfe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
|
||
|
@@ -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'))); | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', '') == '' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
@@ -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 …) */ | ||
|
@@ -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; | ||
|
@@ -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(); | ||
} | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why not depend on this one? Not possible/easy? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yii::app()->end; ?