N°3169 - Add feature to connect Gsuite mail box with OAuth

N°2504 - Add feature to connect Office mail box with OAuth2 for Microsoft Graph
N°5102 - Allow to send emails (eg. notifications) using GSuite SMTP and OAuth
This commit is contained in:
Eric Espie
2022-05-12 14:40:55 +02:00
parent c2607c4223
commit dd8a4a0082
27 changed files with 3254 additions and 30 deletions

View File

@@ -11,6 +11,9 @@
"ext-mysqli": "*",
"ext-soap": "*",
"combodo/tcpdf": "~6.4.4",
"laminas/laminas-mail": "^2.11",
"laminas/laminas-servicemanager": "^3.5",
"league/oauth2-google": "^3.0",
"nikic/php-parser": "~4.13.2",
"pear/archive_tar": "~1.4.14",
"pelago/emogrifier": "~3.1.0",
@@ -21,6 +24,7 @@
"symfony/framework-bundle": "~3.4.47",
"symfony/twig-bundle": "~3.4.47",
"symfony/yaml": "~3.4.47",
"thenetworg/oauth2-azure": "^2.0",
"twig/twig": "~1.42.5"
},
"require-dev": {

1177
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,6 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b::getLoader();

View File

@@ -149,7 +149,7 @@ class ClassLoader
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
* @psalm-var array<string, string>
*/
public function getClassMap()
{

View File

@@ -2,7 +2,7 @@
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
@@ -150,6 +150,7 @@ return array(
'Combodo\\iTop\\Application\\TwigBase\\Twig\\TwigHelper' => $baseDir . '/sources/application/TwigBase/Twig/TwigHelper.php',
'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php',
'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthWizardController' => $baseDir . '/sources/Controller/OAuth/OAuthWizardController.php',
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php',

View File

@@ -2,20 +2,20 @@
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
'023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
'32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
);

View File

@@ -2,7 +2,7 @@
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(

View File

@@ -2,7 +2,7 @@
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(

View File

@@ -25,20 +25,33 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b', 'loadClassLoader'));
$includePaths = require __DIR__ . '/include_paths.php';
$includePaths[] = get_include_path();
set_include_path(implode(PATH_SEPARATOR, $includePaths));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::getInitializer($loader));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::getInitializer($loader));
} else {
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->setClassMapAuthoritative(true);
$loader->register(true);
$includeFiles = \Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::$files;
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire0018331147de7601e7552f7da8e3bb8b($fileIdentifier, $file);
}
@@ -47,16 +60,11 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b
}
}
/**
* @param string $fileIdentifier
* @param string $file
* @return void
*/
function composerRequire0018331147de7601e7552f7da8e3bb8b($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

View File

@@ -8,16 +8,16 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
{
public static $files = array (
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
'023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php',
'32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php',
);
@@ -420,6 +420,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Combodo\\iTop\\Application\\TwigBase\\Twig\\TwigHelper' => __DIR__ . '/../..' . '/sources/application/TwigBase/Twig/TwigHelper.php',
'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php',
'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthWizardController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthWizardController.php',
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php',

View File

@@ -2,7 +2,7 @@
// include_paths.php @generated by Composer
$vendorDir = dirname(__DIR__);
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(

View File

@@ -0,0 +1,63 @@
<?php
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAbstract;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
require_once('../approot.inc.php');
require_once(APPROOT.'application/utils.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
$oPage = new JsonPage();
$oPage->SetOutputDataOnly(true);
$aResult = ['status' => 'success', 'data' => []];
try {
$operation = utils::ReadParam('operation', '');
switch ($operation) {
case 'get_authorization_url':
$sProvider = utils::ReadParam('provider', '', false, 'raw');
$sClientId = utils::ReadParam('client_id', '', false, 'raw');
$sClientSecret = utils::ReadParam('client_secret', '', false, 'raw');
$sScope = utils::ReadParam('scope', '', false, 'raw');
$sAdditional = utils::ReadParam('additional', '', false, 'raw');
$aAdditional = [];
parse_str($sAdditional, $aAdditional);
$sAuthorizationUrl = OAuthClientProviderFactory::getVendorProviderForAccessUrl($sProvider, $sClientId, $sClientSecret, $sScope, $aAdditional);
$aResult['data']['authorization_url'] = $sAuthorizationUrl;
break;
case 'get_display_authentication_results':
$sProvider = utils::ReadParam('provider', '', false, 'raw');
$sRedirectUrl = utils::ReadParam('redirect_url', '', false, 'raw');
$sClientId = utils::ReadParam('client_id', '', false, 'raw');
$sClientSecret = utils::ReadParam('client_secret', '', false, 'raw');
$sScope = utils::ReadParam('scope', '', false, 'raw');
$sAdditional = utils::ReadParam('additional', '', false, 'raw');
$sRedirectUrlQuery = parse_url($sRedirectUrl)['query'];
$aOAuthResultDisplayClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientResultDisplay', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]'));
$aAdditional = [];
parse_str($sAdditional, $aAdditional);
$sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProvider;
$sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri();
$aQuery = [];
parse_str($sRedirectUrlQuery, $aQuery);
$sCode = $aQuery['code'];
$oProvider = OAuthClientProviderFactory::getVendorProvider($sProvider, $sClientId, $sClientSecret, $sScope, $aAdditional);
$oAccessToken = OAuthClientProviderFactory::getAccessTokenFromCode($oProvider, $sCode);
foreach($aOAuthResultDisplayClasses as $sOAuthClass) {
$aResult['data'][] = $sOAuthClass::GetResultDisplayScript($sClientId, $sClientSecret, $sProvider, $oAccessToken);
}
}
}
catch(Exception $e){
$aResult['status'] = 'error';
IssueLog::Error($e->getMessage());
}
$oPage->SetData($aResult);
$oPage->output();

26
pages/oauth/landing.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContent;
require_once('../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
$oLayout = new PageContent();
$oLayout->AddCSSClass('ibo-oauth-wizard--side-pane');
$oPage = new WebPage(Dict::S('UI:Schema:Title'));
$sJS = <<<JS
window.addEventListener("message", function (event){
event.source.postMessage(window.location.href, event.origin);
window.close();
}, false);
JS;
$oPage->add_script($sJS);
$oPage->output();

159
pages/oauth/wizard.php Normal file
View File

@@ -0,0 +1,159 @@
<?php
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContent;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderGoogle;
require_once('../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
LoginWebPage::DoLogin(); // Check user rights and prompt if needed
$oLayout = new PageContent();
$oLayout->AddCSSClass('ibo-oauth-wizard');
$oPage = new iTopWebPage(Dict::S('UI:OAuth:Wizard:Page:Title'));
$oPage->SetContentLayout($oLayout);
$sReturnUri = utils::GetAbsoluteUrlAppRoot().'pages/oauth.landing.php';
$sAjaxUri = utils::GetAbsoluteUrlAppRoot().'pages/ajax.oauth.wizard.php';
$sJS = <<<JS
let windowObjectReference = null;
let previousUrl = null;
const openSignInWindow = (url, name) => {
// remove any existing event listeners
// window features
const strWindowFeatures =
'toolbar=no, menubar=no, width=600, height=700, top=100, left=100';
if (windowObjectReference === null || windowObjectReference.closed) {
/* if the pointer to the window object in memory does not exist
or if such pointer exists but the window was closed */
windowObjectReference = window.open(url, name, strWindowFeatures);
} else if (previousUrl !== url) {
/* if the resource to load is different,
then we load it in the already opened secondary window and then
we bring such window back on top/in front of its parent window. */
windowObjectReference = window.open(url, name, strWindowFeatures);
windowObjectReference.focus();
} else {
/* else the window reference must exist and the window
is not closed; therefore, we can bring it back on top of any other
window with the focus() method. There would be no need to re-create
the window or to reload the referenced resource. */
windowObjectReference.focus();
}
let oListener = window.setInterval(function(){
windowObjectReference.postMessage('anyone', '$sReturnUri');
}, 1000);
window.addEventListener("message", function (event){
clearInterval(oListener);
$.post(
'$sAjaxUri',
{
operation: 'get_display_authentication_results',
provider: $('[name="provider"]:checked').val(),
client_id: $('[name="client_id"]').val(),
client_secret: $('[name="client_secret"]').val(),
scope: $(this).find('[name="scope"]').val(),
additional: $(this).find('[name="additional"]').val(),
redirect_url: event.data,
},
function(oData){
if(oData.status == 'success')
{
oData.data.forEach(function(item, index){
new Function(item)();
});
}
$('.ibo-oauth-wizard--form--submit').trigger('leave_loading_state.button.itop');
}
);
}, false);
// add the listener for receiving a message from the popup
// assign the previous URL
previousUrl = url;
};
JS;
$oPage->add_script($sJS);
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'/js/pages/backoffice/oauth.wizard.js');
$oOauthInputsPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('UI:OAuth:Wizard:Form:Panel:Title'));
$oOauthInputsPanel->AddCSSClass('ibo-oauth-wizard');
$aOAuthClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientProvider', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]'));
$sFormJs = <<<JS
$('.ibo-oauth-wizard--form--submit').trigger('enter_loading_state.button.itop');
$.post(
'$sAjaxUri',
{
operation: 'get_authorization_url',
provider: $('[name="provider"]:checked').val(),
client_id: $(this).find('[name="client_id"]').val(),
client_secret: $(this).find('[name="client_secret"]').val(),
scope: $(this).find('[name="scope"]').val(),
additional: $(this).find('[name="additional"]').val()
},
function(oData){
if(oData.status == 'success')
{
openSignInWindow(oData.data.authorization_url, 'coucou')
}
else{
$('.ibo-oauth-wizard--form--submit').trigger('leave_loading_state.button.itop');
}
}
);
return false;
JS;
$oProviderSelect = new Html('<div class="ibo-oauth-wizard--form--container"><div id="select_layout">');
$oOauthInputsPanel->AddSubBlock($oProviderSelect);
$sIsChecked = ' checked ';
foreach($aOAuthClasses as $sOAuthClass){
$aColors = $sOAuthClass::GetVendorColors();
$oProviderSelect->AddHtml('<input type="radio" name="provider" '.$sIsChecked.' value="'.$sOAuthClass::GetVendorName().'" id="layout_'.$sOAuthClass::GetVendorName().'" data-color1="'.$aColors[0].'" data-color2="'.$aColors[1].'" data-color3="'.$aColors[2].'" data-color4="'.$aColors[3].'"><label for="layout_'.$sOAuthClass::GetVendorName().'"><img src="'.$sOAuthClass::GetVendorIcon().'" class="ibo-dashboard--properties--icon" data-role="ibo-dashboard--properties--icon"/></label>');
$sIsChecked = '';
}
$oForm = FormUIBlockFactory::MakeStandard();
$oForm->AddCSSClasses(['ibo-oauth-wizard--form', 'ibo-oauth-wizard--form-'.strtolower($sOAuthClass::GetVendorName())]);
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('url', ''));
foreach (['client_id' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientId:Label'),
'client_secret' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientSecret:Label'),
'scope' => Dict::S('UI:OAuth:Wizard:Form:Input:Scope:Label'),
'additional' => Dict::S('UI:OAuth:Wizard:Form:Input:Additional:Label')] as $sName => $sLabel){
$oForm->AddSubBlock(InputUIBlockFactory::MakeForInputWithLabel($sLabel, $sName, null, null, 'text'));
}
$oRedirectUriInput = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('UI:OAuth:Wizard:Form:Input:RedirectUri:Label'), 'redirect_uri', OAuthClientProviderGoogle::GetRedirectUri(), null, 'text');
$oRedirectUriInput->GetInput()->SetIsReadonly(true);
$oForm->AddSubBlock($oRedirectUriInput);
$oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:OAuth:Wizard:Form:Button:Submit:Label'),'submit', '', true)->AddCSSClass('ibo-oauth-wizard--form--submit'));
$oForm->SetOnSubmitJsCode($sFormJs);
$oOauthInputsPanel->AddSubBlock($oForm);
$oProviderSelect->AddHtml('</div>');
$oOauthInputsPanel->AddHtml('</div>');
$sOnReadyJs = "$('#select_layout').controlgroup(); $('.ibo-oauth-wizard--result--panel .ibo-panel--collapsible-toggler').click();";
$oPage->add_ready_script($sOnReadyJs);
$oOauthInputsPanel->AddHtml('<div class="ibo-oauth-wizard--illustration">'.file_get_contents(APPROOT.'images/illustrations/undraw_access_account.svg').'</div>');
$oPage->AddSubBlock($oOauthInputsPanel);
$aOAuthResultDisplayClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientResultDisplay', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]'));
foreach($aOAuthResultDisplayClasses as $sOAuthClass) {
$oPage->AddSubBlock($sOAuthClass::GetResultDisplayBlock());
}
$oPage->output();

View File

@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Controller\OAuth;
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAbstract;
use Dict;
use utils;
class OAuthWizardController extends Controller
{
public function WizardOperation()
{
$aParams = [];
$aParams['sReturnUri'] = OAuthClientProviderAbstract::GetRedirectUri();
$aParams['sAjaxUri'] = utils::GetAbsoluteUrlAppRoot().'pages/oauth/ajax.wizard.php';
//$this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().'/js/pages/backoffice/oauth.wizard.js');
$aOAuthClasses = [
'Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAzure',
'Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderGoogle',
];
foreach ($aOAuthClasses as $sOAuthClass) {
$aParams['aProviders'][] = [
'name' => $sOAuthClass::GetVendorName(),
'icon' => $sOAuthClass::GetVendorIcon(),
'colors' => $sOAuthClass::GetVendorColors(),
];
}
$aParams['aInputs'] = [
'client_id' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientId:Label'), 'read_only' => false, 'value' => ''],
'client_secret' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientSecret:Label'), 'read_only' => false, 'value' => ''],
'scope' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:Scope:Label'), 'read_only' => false, 'value' => ''],
'additional' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:Additional:Label'), 'read_only' => false, 'value' => ''],
'redirect_uri' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:RedirectUri:Label'), 'read_only' => true, 'value' => OAuthClientProviderAbstract::GetRedirectUri()],
];
$this->DisplayPage($aParams);
}
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
interface IOAuthClientProvider{
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use League\OAuth2\Client\Token\AccessToken;
interface IOAuthClientResultDisplay{
public static function GetResultDisplayBlock();
public static function GetResultDisplayScript($sClientId, $sClientSecret, $sVendor, AccessToken $oAccessToken);
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Token\AccessToken;
use utils;
abstract class OAuthClientProviderAbstract implements IOAuthClientProvider
{
/** @var string */
static protected $sVendorName = '';
/** @var array */
static protected $sVendorColors = ['', '', '', ''];
/** @var string */
static protected $sVendorIcon = '';
static protected $sRedirectUri = '';
static protected $sRequiredSMTPScope = '';
static protected $sRequiredIMAPScope = '';
static protected $sRequiredPOPScope = '';
/** @var \League\OAuth2\Client\Provider\GenericProvider */
protected $oVendorProvider;
/** @var \League\OAuth2\Client\Token\AccessToken */
protected $oAccessToken;
protected $sScope;
/**
* @return \League\OAuth2\Client\Provider\GenericProvider
*/
public function GetVendorProvider()
{
return $this->oVendorProvider;
}
/**
* @param \League\OAuth2\Client\Provider\GenericProvider $oVendorProvider
*/
public function SetVendorProvider(GenericProvider $oVendorProvider)
{
$this->oVendorProvider = $oVendorProvider;
}
/**
* @return \League\OAuth2\Client\Token\AccessToken
*/
public function GetAccessToken(): AccessToken
{
return $this->oAccessToken;
}
/**
* @param \League\OAuth2\Client\Token\AccessToken $oAccessToken
*/
public function SetAccessToken(AccessToken $oAccessToken)
{
$this->oAccessToken = $oAccessToken;
}
/**
* @return string
*/
public static function GetVendorIcon(): string
{
return static::$sVendorIcon;
}
/**
* @return string
*/
public static function GetVendorName(): string
{
return static::$sVendorName;
}
public static function getConfFromAccessToken($oAccessToken, $sClientId, $sClientSecret): string
{
$sAccessToken = $oAccessToken->getToken();
$sRefreshToken = $oAccessToken->getRefreshToken();
$sVendor = static::GetVendorName();
return <<<EOF
'email_transport' => 'SMTP_OAuth',
'email_transport_smtp.oauth.provider' => '$sVendor',
'email_transport_smtp.oauth.client_id' => '$sClientId',
'email_transport_smtp.oauth.client_secret' => '$sClientSecret',
'email_transport_smtp.oauth.access_token' => '$sAccessToken',
'email_transport_smtp.oauth.refresh_token' => '$sRefreshToken',
EOF;
}
/**
* @return array
*/
public static function GetVendorColors(): array
{
return static::$sVendorColors;
}
/**
* @return void
* @throws \Exception
*/
public static function InitizalizeRedirectUri()
{
static::$sRedirectUri = utils::GetAbsoluteUrlAppRoot().'pages/oauth/landing.php';
}
/**
* @return string
*/
public static function GetRedirectUri(): string
{
if (static::$sRedirectUri === '') {
static::InitizalizeRedirectUri();
}
return static::$sRedirectUri;
}
/**
* @return string
*/
public static function GetRequiredSMTPScope(): string
{
return static::$sRequiredSMTPScope;
}
/**
* @return string
*/
public static function GetRequiredIMAPScope(): string
{
return static::$sRequiredIMAPScope;
}
/**
* @return string
*/
public static function GetRequiredPOPScope(): string
{
return static::$sRequiredPOPScope;
}
/**
* @return mixed
*/
public function GetScope()
{
return $this->sScope;
}
/**
* @param mixed $sScope
*/
public function SetScope($sScope)
{
$this->sScope = $sScope;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use League\OAuth2\Client\Token\AccessToken;
use TheNetworg\OAuth2\Client\Provider\Azure;
class OAuthClientProviderAzure extends OAuthClientProviderAbstract
{
/** @var string */
static protected $sVendorName = 'Azure';
/** @var array */
static protected $sVendorColors = ['#0766b7', '#0d396b', '#2893df', '#3ccbf4'];
/** @var string */
static protected $sVendorIcon = '../images/icons/icons8-azure.svg';
static protected $sRequiredSMTPScope = 'https://outlook.office.com/SMTP.Send offline_access';
static protected $sRequiredIMAPScope = 'https://outlook.office.com/IMAP.AccessAsUser.All offline_access';
static protected $sRequiredPOPScope = 'https://outlook.office.com/POP.AccessAsUser.All offline access';
/** @var \League\OAuth2\Client\Provider\GenericProvider */
protected $oVendorProvider;
/** @var \League\OAuth2\Client\Token\AccessToken */
protected $oAccessToken;
public function __construct($aVendorProvider, array $collaborators = [], array $aAccessTokenParams = [])
{
$this->oVendorProvider = new Azure(array_merge([
'prompt' => 'consent',
'scope' => 'offline_access',
'defaultEndPointVersion' => Azure::ENDPOINT_VERSION_2_0,
],
$aVendorProvider), $collaborators);
if (!empty($aAccessTokenParams)) {
$this->oAccessToken = new AccessToken([
"access_token" => $aAccessTokenParams["access_token"],
"expires_in" => -1,
"refresh_token" => $aAccessTokenParams["refresh_token"],
"token_type" => "Bearer",
]);
}
if (isset($aVendorProvider['scope'])) {
$this->SetScope($aVendorProvider['scope']);
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use GuzzleHttp\Client;
use League\OAuth2\Client\Token\AccessTokenInterface;
use MetaModel;
class OAuthClientProviderFactory {
public static function getProviderForSMTP()
{
$sProviderVendor = MetaModel::GetConfig()->Get('email_transport_smtp.oauth.provider'); // email_transport_smtp.oauth.provider
$sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor;
$aProviderVendorParams = [
'clientId' => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.client_id'), // email_transport_smtp.oauth.client_id
'clientSecret' => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.client_secret'),// email_transport_smtp.oauth.client_secret
'redirectUri' => $sProviderClass::GetRedirectUri(),
'scope' => $sProviderClass::GetRequiredSMTPScope()
];
$aAccessTokenParams = [
"access_token" => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.access_token'), // email_transport_smtp.oauth.access_token
"refresh_token" => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.refresh_token'), // email_transport_smtp.oauth.refresh_token
'scope' => $sProviderClass::GetRequiredSMTPScope()
];
$aCollaborators = [
'httpClient' => new Client(['verify' => false]),
];
return new $sProviderClass($aProviderVendorParams, $aCollaborators, $aAccessTokenParams);
}
public static function getVendorProvider($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional){
$sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri();
$sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor;
$aCollaborators = [
'httpClient' => new Client(['verify' => false]),
];
return new $sProviderClass(array_merge(['clientId' => $sClientId, 'clientSecret' => $sClientSecret, 'redirectUri' => $sRedirectUrl, 'scope' => $sScope], $aAdditional), $aCollaborators);
}
public static function getVendorProviderForAccessUrl($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional){
$oProvider = static::getVendorProvider($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional);
return $oProvider->GetVendorProvider()->getAuthorizationUrl([
'scope' => [
$sScope
],
]);
}
/**
* @param \Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAbstract $oProvider
* @param $sCode
*
* @return AccessTokenInterface
* @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
*/
public static function getAccessTokenFromCode($oProvider, $sCode)
{
return $oProvider->GetVendorProvider()->getAccessToken('authorization_code', ['code' => $sCode, 'scope' => $oProvider->GetScope()]);
}
public static function getConfFromRedirectUrl($sProviderVendor, $sClientId, $sClientSecret, $sRedirectUrlQuery)
{
$sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri();
$sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor;
$aQuery = [];
parse_str($sRedirectUrlQuery, $aQuery);
$sCode = $aQuery['code'];
$oProvider = new $sProviderClass(['clientId' => $sClientId, 'clientSecret' => $sClientSecret, 'redirectUri' => $sRedirectUrl]);
return $sProviderClass::getConfFromAccessToken($oProvider->GetVendorProvider()->getAccessToken('authorization_code', ['code' => $sCode]), $sClientId, $sClientSecret);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Token\AccessToken;
class OAuthClientProviderGoogle extends OAuthClientProviderAbstract
{
/** @var string */
static protected $sVendorName = 'Google';
/** @var array */
static protected $sVendorColors = ['#DB4437', '#F4B400', '#0F9D58', '#4285F4'];
/** @var string */
static protected $sVendorIcon = '../images/icons/icons8-google.svg';
/** @var \League\OAuth2\Client\Provider\GenericProvider */
protected $oVendorProvider;
/** @var \League\OAuth2\Client\Token\AccessToken */
protected $oAccessToken;
static protected $sRequiredSMTPScope = 'https://mail.google.com/';
static protected $sRequiredIMAPScope = 'https://mail.google.com/';
static protected $sRequiredPOPScope = 'https://mail.google.com/';
public function __construct($aVendorProvider, array $collaborators = [], array $aAccessTokenParams = [])
{
$this->oVendorProvider = new Google(array_merge(['prompt' => 'consent', 'accessType' => 'offline'], $aVendorProvider), $collaborators);
if (!empty($aAccessTokenParams)) {
$this->oAccessToken = new AccessToken([
"access_token" => $aAccessTokenParams["access_token"],
"expires_in" => -1,
"refresh_token" => $aAccessTokenParams["refresh_token"],
"token_type" => "Bearer",
]);
}
if (isset($aVendorProvider['scope'])) {
$this->SetScope($aVendorProvider['scope']);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Combodo\iTop\Core\Authentication\Client\OAuth;
use Combodo\iTop\Application\UI\Base\Component\Panel\Panel;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Dict;
use League\OAuth2\Client\Token\AccessToken;
class OAuthClientResultDisplayConf implements IOAuthClientResultDisplay
{
public static function GetResultDisplayBlock()
{
$oConfResultPanel = new Panel(Dict::S('UI:OAuth:Wizard:ResultConf:Panel:Title'), [],Panel::DEFAULT_COLOR_SCHEME, 'ibo-oauth-wizard--conf--panel');
$oConfResultPanel->AddCSSClass('ibo-oauth-wizard--result--panel');
$oConfResultPanel->SetIsCollapsible(true);
$oConfResultPanel->AddHtml('<p>'.Dict::S('UI:OAuthEmailSynchro:Wizard:ResultConf:Panel:Description').'</p>');
$oConfResultPanel->AddHtml('<pre><code id="ibo-oauth-wizard--conf--result"></code></pre>');
return $oConfResultPanel;
}
public static function GetResultDisplayScript($sClientId, $sClientSecret, $sVendor, AccessToken $oAccessToken)
{
$sAccessToken = $oAccessToken->getToken();
$sRefreshToken = $oAccessToken->getRefreshToken();
$sConf = <<<EOF
'email_transport' => 'SMTP_OAuth',
'email_transport_smtp.oauth.provider' => '$sVendor',
'email_transport_smtp.oauth.client_id' => '$sClientId',
'email_transport_smtp.oauth.client_secret' => '$sClientSecret',
'email_transport_smtp.oauth.access_token' => '$sAccessToken',
'email_transport_smtp.oauth.refresh_token' => '$sRefreshToken',
EOF;
$sConf = json_encode($sConf);
return <<<JS
$('#ibo-oauth-wizard--conf--panel .ibo-panel--collapsible-toggler').click();
$('#ibo-oauth-wizard--conf--result').text($sConf);
JS;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAbstract;
use IssueLog;
use Laminas\Mail\Protocol\Exception\RuntimeException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
class Oauth extends Login
{
/**
* LOGIN username
*
* @var OAuthClientProviderAbstract
*/
protected static $oProvider;
const LOG_CHANNEL = 'OAuth';
/**
* Constructor.
*
* @param string $host (Default: 127.0.0.1)
* @param null $port (Default: null)
* @param null $config Auth-specific parameters
*/
public function __construct($host = '127.0.0.1', $port = null, $config = null)
{
$origConfig = $config;
if (is_array($host)) {
// Merge config array with principal array, if provided
if (is_array($config)) {
$config = array_replace_recursive($host, $config);
} else {
$config = $host;
}
}
if (is_array($config)) {
if (isset($config['username'])) {
$this->setUsername($config['username']);
}
}
// Call parent with original arguments
parent::__construct($host, $port, $origConfig);
}
/**
* @param OAuthClientProviderAbstract $oProvider
*
* @return void
*/
public static function setProvider(OAuthClientProviderAbstract $oProvider): void
{
self::$oProvider = $oProvider;
}
/**
* Perform LOGIN authentication with supplied credentials
*
*/
public function auth()
{
try {
if (empty(self::$oProvider->GetAccessToken())) {
throw new IdentityProviderException('Not prior authentication to OAuth', 255, []);
} elseif (self::$oProvider->GetAccessToken()->hasExpired()) {
self::$oProvider->SetAccessToken(self::$oProvider->GetVendorProvider()->getAccessToken('refresh_token', [
'refresh_token' => self::$oProvider->GetAccessToken()->getRefreshToken(),
'scope' => self::$oProvider->GetScope(),
]));
}
}
catch (IdentityProviderException $e) {
IssueLog::Error('Failed to get oAuth credentials for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL);
return false;
}
$sAccessToken = self::$oProvider->GetAccessToken()->getToken();
if (empty($sAccessToken)) {
IssueLog::Error('No OAuth token for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL);
return false;
}
$this->_send('AUTH XOAUTH2 '.base64_encode("user=$this->username\1auth=Bearer $sAccessToken\1\1"));
IssueLog::Debug("SMTP Oauth sending AUTH XOAUTH2 user=$this->username auth=Bearer $sAccessToken", static::LOG_CHANNEL);
try {
while (true) {
$sResponse = $this->_receive(60);
IssueLog::Debug("SMTP Oauth receiving $sResponse", static::LOG_CHANNEL);
if ($sResponse === '+') {
// Send empty client response.
$this->_send('');
} else {
if (preg_match('/Unauthorized/i', $sResponse) ||
preg_match('/Rejected/i', $sResponse) ||
preg_match('/^(535|432|454|534|500|530|538)/', $sResponse)) {
IssueLog::Error('Unable to authenticate for outgoing mails for provider '.self::$oProvider::GetVendorName()." Error: $sResponse", static::LOG_CHANNEL);
return false;
}
if (preg_match("/OK /i", $sResponse) ||
preg_match('/Accepted/i', $sResponse) ||
preg_match('/^235/i', $sResponse)) {
$this->auth = true;
return true;
}
}
}
} catch (RuntimeException $e) {
IssueLog::Error('Timeout connection for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL);
}
return false;
}
}

View File

@@ -0,0 +1,6 @@
<?php
class EmailFactory
{
}

View File

@@ -0,0 +1,643 @@
<?php
// Copyright (C) 2010-2021 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Send an email (abstraction for synchronous/asynchronous modes)
*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@include APPROOT."/core/oauth.php";
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Message;
use Laminas\Mail\Transport\File;
use Laminas\Mail\Transport\FileOptions;
use Laminas\Mail\Transport\Smtp;
use Laminas\Mail\Transport\SmtpOptions;
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
use Pelago\Emogrifier\CssInliner;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
define ('EMAIL_SEND_OK', 0);
define ('EMAIL_SEND_PENDING', 1);
define ('EMAIL_SEND_ERROR', 2);
class EMail
{
// Serialization formats
const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object.
// Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string
const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed)
protected static $m_oConfig = null;
protected $m_aData; // For storing data to serialize
public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE)
{
if (is_null(self::$m_oConfig))
{
self::$m_oConfig = new Config($sConfigFile);
}
}
protected $m_oMessage;
public function __construct()
{
$this->m_aData = array();
$this->m_oMessage = new Message();
$this->m_oMessage->setEncoding('UTF-8');
$this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label'));
}
/**
* Custom serialization method
* No longer use the brute force "serialize" method since
* 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field)
* 2) The size tends to be quite big (sometimes ten times the size of the email)
*/
public function SerializeV2()
{
return serialize($this->m_aData);
}
/**
* Custom de-serialization method
*
* @param string $sSerializedMessage The serialized representation of the message
*
* @return \EMail
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException
*/
public static function UnSerializeV2($sSerializedMessage)
{
$aData = unserialize($sSerializedMessage);
$oMessage = new Email();
if (array_key_exists('body', $aData))
{
$oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']);
}
if (array_key_exists('message_id', $aData))
{
$oMessage->SetMessageId($aData['message_id']);
}
if (array_key_exists('bcc', $aData))
{
$oMessage->SetRecipientBCC($aData['bcc']);
}
if (array_key_exists('cc', $aData))
{
$oMessage->SetRecipientCC($aData['cc']);
}
if (array_key_exists('from', $aData))
{
$oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']);
}
if (array_key_exists('reply_to', $aData))
{
$oMessage->SetRecipientReplyTo($aData['reply_to']['address'], $aData['reply_to']['label']);
}
if (array_key_exists('to', $aData))
{
$oMessage->SetRecipientTO($aData['to']);
}
if (array_key_exists('subject', $aData))
{
$oMessage->SetSubject($aData['subject']);
}
if (array_key_exists('headers', $aData))
{
foreach($aData['headers'] as $sKey => $sValue)
{
$oMessage->AddToHeader($sKey, $sValue);
}
}
if (array_key_exists('parts', $aData))
{
foreach($aData['parts'] as $aPart)
{
$oMessage->AddPart($aPart['text'], $aPart['mimeType']);
}
}
if (array_key_exists('attachments', $aData))
{
foreach($aData['attachments'] as $aAttachment)
{
$oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']);
}
}
return $oMessage;
}
protected function SendAsynchronous(&$aIssues, $oLog = null)
{
try
{
AsyncSendEmail::AddToQueue($this, $oLog);
}
catch(Exception $e)
{
$aIssues = array($e->GetMessage());
return EMAIL_SEND_ERROR;
}
$aIssues = array();
return EMAIL_SEND_PENDING;
}
/**
* @throws \Exception
*/
protected function SendSynchronous(&$aIssues, $oLog = null)
{
$this->LoadConfig();
$sTransport = self::$m_oConfig->Get('email_transport');
switch ($sTransport)
{
case 'SMTP':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$sPassword = self::$m_oConfig->Get('email_transport_smtp.password');
$oTransport = new Smtp();
$aOptions= [
'host' => $sHost,
'port' => $sPort,
'connection_class' => 'login',
'connection_config' => [
'ssl' => $sEncryption,
],
];
if (strlen($sUserName) > 0)
{
$aOptions['connection_config']['username'] = $sUserName;
$aOptions['connection_config']['password'] = $sPassword;
}
$oOptions = new SmtpOptions($aOptions);
$oTransport->setOptions($oOptions);
break;
case 'SMTP_OAuth':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$oTransport = new Smtp();
$aOptions= [
'host' => $sHost,
'port' => $sPort,
'connection_class' => 'Laminas\Mail\Protocol\Smtp\Auth\Oauth',
'connection_config' => [
'ssl' => $sEncryption,
],
];
if (strlen($sUserName) > 0)
{
$aOptions['connection_config']['username'] = $sUserName;
}
$oOptions = new SmtpOptions($aOptions);
$oTransport->setOptions($oOptions);
\Laminas\Mail\Protocol\Smtp\Auth\Oauth::setProvider(OAuthClientProviderFactory::getProviderForSMTP());
break;
case 'Null':
$oTransport = new Smtp();
break;
case 'LogFile':
$oTransport = new File();
$aOptions = new FileOptions([
'path' => APPROOT.'log/mail.log',
]);
$oTransport->setOptions($aOptions);
break;
case 'PHPMail':
default:
$oTransport = new Smtp();
}
$oKPI = new ExecutionKPI();
try
{
$oTransport->send($this->m_oMessage);
$aIssues = array();
$oKPI->ComputeStats('Email Sent', 'Succeded');
return EMAIL_SEND_OK;
}
catch(Laminas\Mail\Transport\Exception\RuntimeException $e){
IssueLog::Warning('Email sending failed: Some recipients were invalid');
$aIssues = array('Some recipients were invalid.');
$oKPI->ComputeStats('Email Sent', 'Error received');
return EMAIL_SEND_ERROR;
}
catch (Exception $e)
{
$oKPI->ComputeStats('Email Sent', 'Error received');
throw $e;
}
}
/**
* Reprocess the body of the message (if it is an HTML message)
* to replace the URL of images based on attachments by a link
* to an embedded image (i.e. cid:....) and returns images to be attached as an array
*
* @param string $sBody Email body to process/alter
*
* @return array Array of Part that needs to be added as inline attachment later to render as embed
* @throws \ArchivedObjectException
* @throws \CoreException
*/
protected function EmbedInlineImages(string &$sBody)
{
$oDOMDoc = new DOMDocument();
$oDOMDoc->preserveWhitespace = true;
@$oDOMDoc->loadHTML('<?xml encoding="UTF-8"?>'.$sBody); // For loading HTML chunks where the character set is not specified
$oXPath = new DOMXPath($oDOMDoc);
$sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']';
$oImagesList = $oXPath->query($sXPath);
$oImagesContent = new \Laminas\Mime\Message();
$aImagesParts = [];
if ($oImagesList->length != 0)
{
foreach($oImagesList as $oImg)
{
$iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID);
$oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */);
if ($oAttachment)
{
$sImageSecret = $oImg->getAttribute('data-img-secret');
$sAttachmentSecret = $oAttachment->Get('secret');
if ($sImageSecret !== $sAttachmentSecret)
{
// @see N°1921
// If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret
continue;
}
$oDoc = $oAttachment->Get('contents');
$sCid = uniqid('', true);
$oNewAttachment = new Part($oDoc->GetData());
$oNewAttachment->id = $sCid;
$oNewAttachment->type = $oDoc->GetMimeType();
$oNewAttachment->filename = $oDoc->GetFileName();
$oNewAttachment->disposition = Mime::DISPOSITION_INLINE;
$oNewAttachment->encoding = Mime::ENCODING_BASE64;
$oImagesContent->addPart($oNewAttachment);
$oImg->setAttribute('src', 'cid:'.$sCid);
$aImagesParts[] = $oNewAttachment;
}
}
}
$sBody = $oDOMDoc->saveHTML();
return $aImagesParts;
}
public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null)
{
//select a default sender if none is provided.
if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){
$this->SetRecipientFrom($this->m_aData['to']);
}
if ($bForceSynchronous)
{
return $this->SendSynchronous($aIssues, $oLog);
}
else
{
$bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous');
if ($bConfigASYNC)
{
return $this->SendAsynchronous($aIssues, $oLog);
}
else
{
return $this->SendSynchronous($aIssues, $oLog);
}
}
}
public function AddToHeader($sKey, $sValue)
{
if (!array_key_exists('headers', $this->m_aData))
{
$this->m_aData['headers'] = array();
}
$this->m_aData['headers'][$sKey] = $sValue;
if (strlen($sValue) > 0)
{
$oHeaders = $this->m_oMessage->getHeaders();
switch(strtolower($sKey))
{
case 'return-path':
$this->m_oMessage->setReturnPath($sValue);
break;
default:
$oHeaders->addHeaderLine($sKey, $sValue);
}
}
}
public function SetMessageId($sId)
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
$this->m_oMessage->getHeaders()->addHeaderLine('Message-ID', $sId);
}
public function SetReferences($sReferences)
{
$this->AddToHeader('References', $sReferences);
}
/**
* Set the "In-Reply-To" header to allow emails to group as a conversation in modern mail clients (GMail, Outlook 2016+, ...)
*
* @link https://en.wikipedia.org/wiki/Email#Header_fields
*
* @param string $sMessageId
*
* @since 3.0.1 N°4849
*/
public function SetInReplyTo(string $sMessageId)
{
$this->AddToHeader('In-Reply-To', $sMessageId);
}
/**
* Set current Email body and process inline images.
*
* @param $sBody
* @param string $sMimeType
* @param $sCustomStyles
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException
*/
public function SetBody($sBody, string $sMimeType = Mime::TYPE_HTML, $sCustomStyles = null)
{
$oBody = new Laminas\Mime\Message();
$aAdditionalParts = [];
if (($sMimeType === Mime::TYPE_HTML) && ($sCustomStyles !== null)) {
$oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument();
HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone();
$sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present
}
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
// We don't want these modifications in m_aData['body'], otherwise it'll ruin asynchronous mail as they go through this method twice
if ($sMimeType === Mime::TYPE_HTML){
$aAdditionalParts = $this->EmbedInlineImages($sBody);
}
// Add body content to as a new part
$oNewPart = new Part($sBody);
$oNewPart->encoding = Mime::ENCODING_8BIT;
$oNewPart->type = $sMimeType;
$oBody->addPart($oNewPart);
// Add additional images as new body parts
foreach ($aAdditionalParts as $oAdditionalPart) {
$oBody->addPart($oAdditionalPart);
}
if($oBody->isMultiPart()){
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
}
/**
* Add a new part to the existing body
* @param $sText
* @param string $sMimeType
*
* @return void
*/
public function AddPart($sText, string $sMimeType = Mime::TYPE_HTML)
{
if (!array_key_exists('parts', $this->m_aData))
{
$this->m_aData['parts'] = array();
}
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
$oNewPart = new Part($sText);
$oNewPart->encoding = Mime::ENCODING_8BIT;
$oNewPart->type = $sMimeType;
$this->m_oMessage->getBody()->addPart($oNewPart);
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
$oBody = $this->m_oMessage->getBody();
if(!$oBody->isMultiPart()){
$multipart_content = new Part($oBody->generateMessage());
$multipart_content->setType($oBody->getParts()[0]->getType());
$multipart_content->setBoundary($oBody->getMime()->boundary());
$oBody = new Laminas\Mime\Message();
$oBody->addPart($multipart_content);
}
if (!array_key_exists('attachments', $this->m_aData))
{
$this->m_aData['attachments'] = array();
}
$this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType);
$oNewAttachment = new Part($data);
$oNewAttachment->type = $sMimeType;
$oNewAttachment->filename = $sFileName;
$oNewAttachment->disposition = Mime::DISPOSITION_ATTACHMENT;
$oNewAttachment->encoding = Mime::ENCODING_BASE64;
$oBody->addPart($oNewAttachment);
if($oBody->isMultiPart()){
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
}
public function SetSubject($sSubject)
{
$this->m_aData['subject'] = $sSubject;
$this->m_oMessage->setSubject($sSubject);
}
public function GetSubject()
{
return $this->m_oMessage->getSubject();
}
/**
* Helper to transform and sanitize addresses
* - get rid of empty addresses
*/
protected function AddressStringToArray($sAddressCSVList)
{
$aAddresses = array();
foreach(explode(',', $sAddressCSVList) as $sAddress)
{
$sAddress = trim($sAddress);
if (strlen($sAddress) > 0)
{
$aAddresses[] = $sAddress;
}
}
return $aAddresses;
}
public function SetRecipientTO($sAddress)
{
$this->m_aData['to'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setTo($aAddresses);
}
}
public function GetRecipientTO($bAsString = false)
{
$aRes = $this->m_oMessage->getTo();
if ($aRes === null || $aRes->count() === 0)
{
// There is no "To" header field
$aRes = array();
}
if ($bAsString)
{
$aStrings = array();
foreach ($aRes as $oEmail)
{
$sName = $oEmail->getName();
$sEmail = $oEmail->getEmail();
if (is_null($sName))
{
$aStrings[] = $sEmail;
}
else
{
$sName = str_replace(array('<', '>'), '', $sName);
$aStrings[] = "$sName <$sEmail>";
}
}
return implode(', ', $aStrings);
}
else
{
return $aRes;
}
}
public function SetRecipientCC($sAddress)
{
$this->m_aData['cc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setCc($aAddresses);
}
}
public function SetRecipientBCC($sAddress)
{
$this->m_aData['bcc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setBcc($aAddresses);
}
}
public function SetRecipientFrom($sAddress, $sLabel = '')
{
$this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '')
{
$this->m_oMessage->setFrom(array($sAddress => $sLabel));
}
else if (!empty($sAddress))
{
$this->m_oMessage->setFrom($sAddress);
}
}
public function SetRecipientReplyTo($sAddress, $sLabel = '')
{
$this->m_aData['reply_to'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '')
{
$this->m_oMessage->setReplyTo(array($sAddress => $sLabel));
}
else if (!empty($sAddress))
{
$this->m_oMessage->setReplyTo($sAddress);
}
}
}

View File

@@ -0,0 +1,550 @@
<?php
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Send an email (abstraction for synchronous/asynchronous modes)
*
* @copyright Copyright (C) 2010-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Pelago\Emogrifier\CssInliner;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
Swift_Preferences::getInstance()->setCharset('UTF-8');
define ('EMAIL_SEND_OK', 0);
define ('EMAIL_SEND_PENDING', 1);
define ('EMAIL_SEND_ERROR', 2);
class EmailSwiftMailer
{
// Serialization formats
const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object.
// Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string
const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed)
protected static $m_oConfig = null;
protected $m_aData; // For storing data to serialize
public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE)
{
if (is_null(self::$m_oConfig))
{
self::$m_oConfig = new Config($sConfigFile);
}
}
protected $m_oMessage;
public function __construct()
{
$this->m_aData = array();
$this->m_oMessage = new Swift_Message();
$this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label'));
}
/**
* Custom serialization method
* No longer use the brute force "serialize" method since
* 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field)
* 2) The size tends to be quite big (sometimes ten times the size of the email)
*/
public function SerializeV2()
{
return serialize($this->m_aData);
}
/**
* Custom de-serialization method
* @param string $sSerializedMessage The serialized representation of the message
*/
static public function UnSerializeV2($sSerializedMessage)
{
$aData = unserialize($sSerializedMessage);
$oMessage = new Email();
if (array_key_exists('body', $aData))
{
$oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']);
}
if (array_key_exists('message_id', $aData))
{
$oMessage->SetMessageId($aData['message_id']);
}
if (array_key_exists('bcc', $aData))
{
$oMessage->SetRecipientBCC($aData['bcc']);
}
if (array_key_exists('cc', $aData))
{
$oMessage->SetRecipientCC($aData['cc']);
}
if (array_key_exists('from', $aData))
{
$oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']);
}
if (array_key_exists('reply_to', $aData))
{
$oMessage->SetRecipientReplyTo($aData['reply_to']);
}
if (array_key_exists('to', $aData))
{
$oMessage->SetRecipientTO($aData['to']);
}
if (array_key_exists('subject', $aData))
{
$oMessage->SetSubject($aData['subject']);
}
if (array_key_exists('headers', $aData))
{
foreach($aData['headers'] as $sKey => $sValue)
{
$oMessage->AddToHeader($sKey, $sValue);
}
}
if (array_key_exists('parts', $aData))
{
foreach($aData['parts'] as $aPart)
{
$oMessage->AddPart($aPart['text'], $aPart['mimeType']);
}
}
if (array_key_exists('attachments', $aData))
{
foreach($aData['attachments'] as $aAttachment)
{
$oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']);
}
}
return $oMessage;
}
protected function SendAsynchronous(&$aIssues, $oLog = null)
{
try
{
AsyncSendEmail::AddToQueue($this, $oLog);
}
catch(Exception $e)
{
$aIssues = array($e->GetMessage());
return EMAIL_SEND_ERROR;
}
$aIssues = array();
return EMAIL_SEND_PENDING;
}
protected function SendSynchronous(&$aIssues, $oLog = null)
{
// If the body of the message is in HTML, embed all images based on attachments
$this->EmbedInlineImages();
$this->LoadConfig();
$sTransport = self::$m_oConfig->Get('email_transport');
switch ($sTransport)
{
case 'SMTP':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$sPassword = self::$m_oConfig->Get('email_transport_smtp.password');
$oTransport = new Swift_SmtpTransport($sHost, $sPort, $sEncryption);
if (strlen($sUserName) > 0)
{
$oTransport->setUsername($sUserName);
$oTransport->setPassword($sPassword);
}
break;
case 'Null':
$oTransport = new Swift_NullTransport();
break;
case 'LogFile':
$oTransport = new Swift_LogFileTransport();
$oTransport->setLogFile(APPROOT.'log/mail.log');
break;
case 'PHPMail':
default:
$oTransport = new Swift_SendmailTransport();
}
$oMailer = new Swift_Mailer($oTransport);
$aFailedRecipients = array();
$this->m_oMessage->setMaxLineLength(0);
$oKPI = new ExecutionKPI();
try
{
$iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients);
if ($iSent === 0)
{
// Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!!
IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients));
$aIssues = array('Some recipients were invalid.');
$oKPI->ComputeStats('Email Sent', 'Error received');
return EMAIL_SEND_ERROR;
}
else
{
$aIssues = array();
$oKPI->ComputeStats('Email Sent', 'Succeded');
return EMAIL_SEND_OK;
}
}
catch (Exception $e)
{
$oKPI->ComputeStats('Email Sent', 'Error received');
throw $e;
}
}
/**
* Reprocess the body of the message (if it is an HTML message)
* to replace the URL of images based on attachments by a link
* to an embedded image (i.e. cid:....)
*/
protected function EmbedInlineImages()
{
if ($this->m_aData['body']['mimeType'] == 'text/html')
{
$oDOMDoc = new DOMDocument();
$oDOMDoc->preserveWhitespace = true;
@$oDOMDoc->loadHTML('<?xml encoding="UTF-8"?>'.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified
$oXPath = new DOMXPath($oDOMDoc);
$sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']';
$oImagesList = $oXPath->query($sXPath);
if ($oImagesList->length != 0)
{
foreach($oImagesList as $oImg)
{
$iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID);
$oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */);
if ($oAttachment)
{
$sImageSecret = $oImg->getAttribute('data-img-secret');
$sAttachmentSecret = $oAttachment->Get('secret');
if ($sImageSecret !== $sAttachmentSecret)
{
// @see N°1921
// If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret
continue;
}
$oDoc = $oAttachment->Get('contents');
$oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType());
$sCid = $this->m_oMessage->embed($oSwiftImage);
$oImg->setAttribute('src', $sCid);
}
}
}
$sHtmlBody = $oDOMDoc->saveHTML();
$this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8');
}
}
public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null)
{
//select a default sender if none is provided.
if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){
$this->SetRecipientFrom($this->m_aData['to']);
}
if ($bForceSynchronous)
{
return $this->SendSynchronous($aIssues, $oLog);
}
else
{
$bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous');
if ($bConfigASYNC)
{
return $this->SendAsynchronous($aIssues, $oLog);
}
else
{
return $this->SendSynchronous($aIssues, $oLog);
}
}
}
public function AddToHeader($sKey, $sValue)
{
if (!array_key_exists('headers', $this->m_aData))
{
$this->m_aData['headers'] = array();
}
$this->m_aData['headers'][$sKey] = $sValue;
if (strlen($sValue) > 0)
{
$oHeaders = $this->m_oMessage->getHeaders();
switch(strtolower($sKey))
{
case 'return-path':
$this->m_oMessage->setReturnPath($sValue);
break;
default:
$oHeaders->addTextHeader($sKey, $sValue);
}
}
}
public function SetMessageId($sId)
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
$oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID');
$oMsgId->SetId($sId);
}
public function SetReferences($sReferences)
{
$this->AddToHeader('References', $sReferences);
}
public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null)
{
if (($sMimeType === 'text/html') && ($sCustomStyles !== null))
{
$oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument();
HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone();
$sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present
}
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
$this->m_oMessage->setBody($sBody, $sMimeType);
}
public function AddPart($sText, $sMimeType = 'text/html')
{
if (!array_key_exists('parts', $this->m_aData))
{
$this->m_aData['parts'] = array();
}
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
$this->m_oMessage->addPart($sText, $sMimeType);
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
if (!array_key_exists('attachments', $this->m_aData))
{
$this->m_aData['attachments'] = array();
}
$this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType);
$this->m_oMessage->attach(new Swift_Attachment($data, $sFileName, $sMimeType));
}
public function SetSubject($sSubject)
{
$this->m_aData['subject'] = $sSubject;
$this->m_oMessage->setSubject($sSubject);
}
public function GetSubject()
{
return $this->m_oMessage->getSubject();
}
/**
* Helper to transform and sanitize addresses
* - get rid of empty addresses
*/
protected function AddressStringToArray($sAddressCSVList)
{
$aAddresses = array();
foreach(explode(',', $sAddressCSVList) as $sAddress)
{
$sAddress = trim($sAddress);
if (strlen($sAddress) > 0)
{
$aAddresses[] = $sAddress;
}
}
return $aAddresses;
}
public function SetRecipientTO($sAddress)
{
$this->m_aData['to'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setTo($aAddresses);
}
}
public function GetRecipientTO($bAsString = false)
{
$aRes = $this->m_oMessage->getTo();
if ($aRes === null)
{
// There is no "To" header field
$aRes = array();
}
if ($bAsString)
{
$aStrings = array();
foreach ($aRes as $sEmail => $sName)
{
if (is_null($sName))
{
$aStrings[] = $sEmail;
}
else
{
$sName = str_replace(array('<', '>'), '', $sName);
$aStrings[] = "$sName <$sEmail>";
}
}
return implode(', ', $aStrings);
}
else
{
return $aRes;
}
}
public function SetRecipientCC($sAddress)
{
$this->m_aData['cc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setCc($aAddresses);
}
}
public function SetRecipientBCC($sAddress)
{
$this->m_aData['bcc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setBcc($aAddresses);
}
}
public function SetRecipientFrom($sAddress, $sLabel = '')
{
$this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '')
{
$this->m_oMessage->setFrom(array($sAddress => $sLabel));
}
else if (!empty($sAddress))
{
$this->m_oMessage->setFrom($sAddress);
}
}
public function SetRecipientReplyTo($sAddress)
{
$this->m_aData['reply_to'] = $sAddress;
if (!empty($sAddress))
{
$this->m_oMessage->setReplyTo($sAddress);
}
}
}
/////////////////////////////////////////////////////////////////////////////////////
/**
* Extension to SwiftMailer: "debug" transport that pretends messages have been sent,
* but just log them to a file.
*
* @package Swift
* @author Denis Flaven
*/
class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport
{
protected $sLogFile;
/**
* @inheritDoc
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$hFile = @fopen($this->sLogFile, 'a');
if ($hFile) {
$sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n";
$sTxt .= $message->toString()."\n";
@fwrite($hFile, $sTxt);
@fclose($hFile);
}
return parent::send($message, $failedRecipients);
}
public function setLogFile($sFilename)
{
$this->sLogFile = $sFilename;
}
}
/**
* Pretends messages have been sent, but just log them to a file.
*
* @package Swift
* @author Denis Flaven
*/
class Swift_LogFileTransport extends Swift_Transport_LogFileTransport
{
/**
* Create a new LogFileTransport.
*/
public function __construct(Swift_Events_EventDispatcher $eventDispatcher)
{
parent::__construct($eventDispatcher);
call_user_func_array(
array($this, 'Swift_Transport_LogFileTransport::__construct'),
Swift_DependencyContainer::getInstance()
->createDependenciesFor('transport.null')
);
}
/**
* Create a new LogFileTransport instance.
*
* @return Swift_LogFileTransport
*/
public static function newInstance()
{
return new self();
}
}

View File

@@ -0,0 +1,48 @@
{# @copyright Copyright (C) 2010-2022 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div class="ibo-oauth-wizard">
<h1>{{ 'UI:OAuth:Wizard:Page:Title'|dict_s }}</h1>
<div class="ibo-oauth-wizard">
<div class="ibo-oauth-wizard--form--container">
<div id="select_layout">
{% set sIsChecked = 'checked' %}
{% for aSelect in aProviders %}
<input type="radio" name="provider" {{ sIsChecked }} value="{{ aSelect.name }}" id="layout_{{ aSelect.name }}" data-color1="{{ aSelect.colors.0 }}" data-color2="{{ aSelect.colors.1 }}" data-color3="{{ aSelect.colors.2 }}" data-color4="{{ aSelect.colors.3 }}">
<label for="layout_{{ aSelect.name }}">
<img src="{{ aSelect.icon }}" alt="{{ aSelect.name }}" class="ibo-dashboard--properties--icon" data-role="ibo-dashboard--properties--icon"/>
</label>
{% set sIsChecked = '' %}
{% endfor %}
</div>
</div>
<fieldset>
<legend>{{ 'UI:OAuth:Wizard:Form:Panel:Title'|dict_s }}</legend>
<form class="ibo-oauth-wizard--form">
<input type="hidden" name="url" value="">
{% for sName, aInput in aInputs %}
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><label for="wizard_input_{{ sName }}">{{ aInput.label }}</label></div>
<div class="field_data">
<input
type="{{ aInput.type }}"
id="wizard_input_{{ sName }}"
name="{{ sName }}"
{% if aInput.real_only %} readonly {% endif %}
value="{{ aInput.value }}"
>
</div>
</div>
</div>
{% endfor %}
</form>
</fieldset>
</div>
</div>