diff --git a/composer.json b/composer.json index 10845c3e3..d9989fe1c 100755 --- a/composer.json +++ b/composer.json @@ -39,6 +39,8 @@ "illuminate/database": "~5.4.0", "intervention/image": "^2.3", "league/csv": "^8.2", + "league/oauth2-client": "^2.6", + "league/oauth2-google": "^3.0", "maximebf/debugbar": "^1.15", "monolog/monolog": "^1.22", "mpdf/mpdf": "^v8.0.7", @@ -55,6 +57,7 @@ "symfony/polyfill-php70": "^1.8", "symfony/translation": "^3.3", "symfony/var-dumper": "^3.3", + "thenetworg/oauth2-azure": "^2.0", "willdurand/geocoder": "^3.3" }, "require-dev": { diff --git a/core.php b/core.php index fbfae808a..e01ac0556 100755 --- a/core.php +++ b/core.php @@ -55,7 +55,7 @@ $rootdir = ROOTDIR; $baseurl = BASEURL; // Sicurezza della sessioni -ini_set('session.cookie_samesite', 'strict'); +//ini_set('session.cookie_samesite', 'strict'); ini_set('session.use_trans_sid', '0'); ini_set('session.use_only_cookies', '1'); diff --git a/modules/emails/src/Account.php b/modules/emails/src/Account.php index b6f8f6a21..7b003c83b 100755 --- a/modules/emails/src/Account.php +++ b/modules/emails/src/Account.php @@ -34,6 +34,9 @@ class Account extends Model protected $table = 'em_accounts'; + /** @var OAuth2 */ + protected $gestoreOAuth2; + public function testConnection() { // Impostazione di connected_at a NULL @@ -54,6 +57,17 @@ class Account extends Model return $result; } + public function getGestoreOAuth2() + { + if (isset($this->gestoreOAuth2)) { + return $this->gestoreOAuth2; + } + + $this->gestoreOAuth2 = new OAuth2($this); + + return $this->gestoreOAuth2; + } + /* Relazioni Eloquent */ public function templates() diff --git a/modules/emails/src/OAuth2.php b/modules/emails/src/OAuth2.php new file mode 100644 index 000000000..6c70a81bf --- /dev/null +++ b/modules/emails/src/OAuth2.php @@ -0,0 +1,190 @@ + [ + 'name' => 'Microsoft', + 'class' => Azure::class, + 'options' => [ + 'scope' => [ + 'offline_access', + 'https://graph.microsoft.com/SMTP.Send', + //'https://outlook.office.com/IMAP.AccessAsUser.All' + ], + ], + 'help' => 'https://docs.openstamanager.com/faq/configurazione-oauth2#microsoft', + ], + 'google' => [ + 'name' => 'Google', + 'class' => Google::class, + 'options' => [ + 'scope' => ['https://mail.google.com/'], + 'accessType' => 'offline', + ], + 'help' => 'https://docs.openstamanager.com/faq/configurazione-oauth2#google', + ], + ]; + protected $provider; + protected $account; + + public function __construct(Account $account) + { + $this->account = $account; + + $this->init(); + } + + /** + * Inizializza il ->inprovider per l'autenticazione OAuth2. + */ + public function init() + { + $redirect_uri = base_url().'/oauth2.php'; + + $class = $this->getProviderConfiguration()['class']; + + // Authorization + $this->provider = new $class([ + 'clientId' => $this->account->client_id, + 'clientSecret' => $this->account->client_secret, + 'redirectUri' => $redirect_uri, + ]); + + // Configurazioni specifiche per il provider di Microsoft Azure + if ($this->provider instanceof Azure) { + $this->provider->defaultEndPointVersion = Azure::ENDPOINT_VERSION_2_0; + $this->provider->tenant = 'consumers'; + } + } + + public function getProvider() + { + return $this->provider; + } + + public function getProviderConfiguration() + { + return self::$providers[$this->account->provider]; + } + + public function needsConfiguration() + { + $access_token = $this->getAccessToken(); + + return empty($access_token); + } + + /** + * Gestisce le operazioni di configurazione per l'autenticazione OAuth2. + * Restituisce l'URL di redirect per le operazioni di aggiornamento dei dati, lancia un eccezione in caso di errori e restituisce null in caso di completamento della configurazione. + * + * Nota: l'autenticazione OAuth2 richiede una serie di richieste su una singola pagina + * - Richiesta di autenticazione al server remoto (code, state vuoti) + * - Conferma di autenticazione alla pagina di redirect (code, state impostati) + * - Richiesta del token di accesso dalla pagina di redirect al server remoto + * + * @param string|null $code + * @param string|null $state + * + * @throws IdentityProviderException + * @throws InvalidArgumentException + * + * @return string|null + */ + public function configure($code, $state) + { + if (!$this->needsConfiguration()) { + return null; + } + + $provider = $this->getProvider(); + $options = $this->getProviderConfiguration()['options']; + if (empty($code)) { + // Fetch the authorization URL from the provider; this returns the + // urlAuthorize option and generates and applies any necessary parameters + // (e.g. state). + $authorizationUrl = $provider->getAuthorizationUrl($options); + + // Get the state generated for you and store it to the session. + $this->account->oauth2_state = $provider->getState(); + $this->account->save(); + + // Redirect the user to the authorization URL. + return $authorizationUrl; + } elseif (!empty($this->account->oauth2_state) && $this->account->oauth2_state !== $state) { + $this->account->oauth2_state = null; + $this->account->save(); + + throw new InvalidArgumentException(); + } else { + $this->account->oauth2_state = null; + $this->account->save(); + + // Try to get an access token using the authorization code grant. + $accessToken = $provider->getAccessToken('authorization_code', [ + 'code' => $code, + ]); + //dd($accessToken); + + $this->setAccessToken($accessToken); + } + + return null; + } + + /** + * Imposta l'access token per l'autenticazione OAuth2. + * + * @param AccessToken|null + */ + public function setAccessToken($value) + { + $this->account->access_token = serialize($value); + $this->account->save(); + } + + /** + * Restituisce l'access token per l'autenticazione OAuth2. + * + * @return AccessToken|null + */ + public function getAccessToken() + { + $access_token = unserialize($this->account->access_token); + + if (!empty($access_token) && $access_token->hasExpired()) { + // Tentativo di refresh del token di accessp + if (!empty($access_token->getRefreshToken())) { + $access_token = $this->getProvider()->getAccessToken('refresh_token', [ + 'refresh_token' => $access_token->getRefreshToken(), + ]); + } else { + $access_token = null; + } + + $this->setAccessToken($access_token); + } + + return $access_token; + } + + public function getRefreshToken() + { + $access_token = unserialize($this->account->access_token); + + if (!empty($access_token)) { + return $access_token->getRefreshToken(); + } + + return null; + } +} diff --git a/modules/smtp/actions.php b/modules/smtp/actions.php index 184f4cde4..1aa63b1a9 100755 --- a/modules/smtp/actions.php +++ b/modules/smtp/actions.php @@ -19,7 +19,7 @@ include_once __DIR__.'/../../core.php'; -switch (post('op')) { +switch (filter('op')) { case 'add': $dbo->insert('em_accounts', [ 'name' => post('name'), @@ -39,6 +39,8 @@ switch (post('op')) { $dbo->query('UPDATE em_accounts SET predefined = 0'); } + $abilita_oauth2 = post('abilita_oauth2'); + $dbo->update('em_accounts', [ 'name' => post('name'), 'note' => post('note'), @@ -53,6 +55,9 @@ switch (post('op')) { 'timeout' => post('timeout'), 'ssl_no_verify' => post('ssl_no_verify'), 'predefined' => $predefined, + 'provider' => $abilita_oauth2 ? post('provider') : null, + 'client_id' => $abilita_oauth2 ? post('client_id') : null, + 'client_secret' => $abilita_oauth2 ? post('client_secret') : null, ], ['id' => $id_record]); flash()->info(tr('Informazioni salvate correttamente!')); @@ -96,5 +101,11 @@ switch (post('op')) { flash()->info(tr('Account email eliminato!')); + break; + + case 'oauth2': + $redirect = base_path().'/oauth2.php?id_account='.$account->id; + redirect($redirect); + break; } diff --git a/modules/smtp/edit.php b/modules/smtp/edit.php index a1f970ccd..fb980e735 100755 --- a/modules/smtp/edit.php +++ b/modules/smtp/edit.php @@ -17,9 +17,11 @@ * along with this program. If not, see . */ +use Modules\Emails\OAuth2; + include_once __DIR__.'/../../core.php'; -?> +echo '
@@ -27,35 +29,35 @@ include_once __DIR__.'/../../core.php';
-

+

'.tr('Dati').'

- {[ "type": "text", "label": "", "name": "name", "value": "$name$", "required": 1 ]} + {[ "type": "text", "label": "'.tr('Nome account').'", "name": "name", "value": "$name$", "required": 1 ]}
- {[ "type": "checkbox", "label": "", "name": "pec", "value": "$pec$" ]} + {[ "type": "checkbox", "label": "'.tr('Indirizzo PEC').'", "name": "pec", "value": "$pec$" ]}
- {[ "type": "checkbox", "label": "", "name": "predefined", "value": "$predefined$", "help": "" ]} + {[ "type": "checkbox", "label": "'.tr('Indirizzo predefinito').'", "name": "predefined", "value": "$predefined$", "help": "'.tr('Account da utilizzare per l\'invio di tutte le email dal gestionale.').'" ]}
- {[ "type": "text", "label": "", "name": "from_name", "value": "$from_name$" ]} + {[ "type": "text", "label": "'.tr('Nome visualizzato').'", "name": "from_name", "value": "$from_name$" ]}
- {[ "type": "email", "label": "", "name": "from_address", "value": "$from_address$", "required": 1 ]} + {[ "type": "email", "label": "'.tr('Email mittente').'", "name": "from_address", "value": "$from_address$", "required": 1 ]}
- {[ "type": "checkbox", "label": "", "name": "ssl_no_verify", "value": "$ssl_no_verify$" ]} + {[ "type": "checkbox", "label": "'.tr('Non verificare il certificato SSL').'", "name": "ssl_no_verify", "value": "$ssl_no_verify$" ]}
@@ -63,44 +65,119 @@ include_once __DIR__.'/../../core.php';
- {[ "type": "text", "label": "", "name": "server", "required": 1, "value": "$server$" ]} + {[ "type": "text", "label": "'.tr('Server SMTP').'", "name": "server", "required": 1, "value": "$server$" ]}
- {[ "type": "number", "label": "", "name": "port", "required": 1, "class": "text-center", "decimals":"0", "max-value":"65535", "value": "$port$" ]} + {[ "type": "number", "label": "'.tr('Porta SMTP').'", "name": "port", "required": 1, "class": "text-center", "decimals":"0", "max-value":"65535", "value": "$port$" ]}
- {[ "type": "select", "label": "", "name": "encryption", "values": "list=\"\": \"\", \"tls\": \"TLS\", \"ssl\": \"SSL\"", "value": "$encryption$" ]} + {[ "type": "select", "label": "'.tr('Sicurezza SMTP').'", "name": "encryption", "values": "list=\"\": \"'.tr('Nessuna').'\", \"tls\": \"TLS\", \"ssl\": \"SSL\"", "value": "$encryption$" ]}
- {[ "type": "text", "label": "", "name": "username", "value": "$username$" ]} + {[ "type": "text", "label": "'.tr('Username SMTP').'", "name": "username", "value": "$username$" ]}
- {[ "type": "password", "label": "", "name": "password", "value": "$password$" ]} + {[ "type": "password", "label": "'.tr('Password SMTP').'", "name": "password", "value": "$password$" ]}
- {[ "type": "number", "label": "", "name": "timeout", "value": "$timeout$", "decimals": 1, "min-value": 100 ]} + {[ "type": "number", "label": "'.tr('Timeout coda di invio (millisecondi)').'", "name": "timeout", "value": "$timeout$", "decimals": 1, "min-value": 100 ]}
- {[ "type": "textarea", "label": "", "name": "note", "value": "$note$" ]} + {[ "type": "textarea", "label": "'.tr('Note').'", "name": "note", "value": "$note$" ]}
-
+
'; +// Elenco provider disponibili +$providers = OAuth2::$providers; +$elenco_provider = []; +foreach ($providers as $key => $provider) { + $elenco_provider[] = [ + 'id' => $key, + 'text' => $provider['name'], + 'help' => $provider['help'], + ]; +} + +echo ' + +
+
+

'.tr('OAuth2').'

+
+ +
+
+
+ + {[ "type": "select", "label": "'.tr('Provider account').'", "name": "provider", "value": "$provider$", "values": '.json_encode($elenco_provider).', "disabled": "'.intval(empty($account->provider)).'" ]} +
+ +
+ {[ "type": "checkbox", "label": "'.tr('Abilita OAuth2').'", "name": "abilita_oauth2", "value": "'.intval(!empty($account->provider)).'" ]} +
+ + + +
+ {[ "type": "text", "label": "'.tr('Client ID').'", "name": "client_id", "value": "$client_id$", "disabled": "'.intval(empty($account->provider)).'" ]} +
+ +
+ {[ "type": "text", "label": "'.tr('Client Secret').'", "name": "client_secret", "value": "$client_secret$", "disabled": "'.intval(empty($account->provider)).'" ]} +
+
+
+
- +var abilita_oauth2 = input("abilita_oauth2"); +var provider = input("provider"); +var client_id = input("client_id"); +var client_secret = input("client_secret"); +var guida = $("#guida-configurazione"); + +abilita_oauth2.change(function() { + const disable = !abilita_oauth2.get(); + provider.setDisabled(disable); + + client_id.setDisabled(disable); + client_secret.setDisabled(disable); +}); + +provider.change(function() { + const data = provider.getData(); + + if (data.id) { + guida.removeClass("hidden"); + guida.html(`'.tr('Istruzioni di configurazione').' `); + } else { + guida.addClass("hidden"); + } +}) + +$(document).ready(function() { + provider.trigger("change"); +}) +'; + // Collegamenti diretti // Template email collegati a questo account $elementi = $dbo->fetchArray('SELECT `id`, `name` FROM `em_templates` WHERE `id_account` = '.prepare($id_record)); @@ -134,4 +211,3 @@ if (!empty($elementi)) { '.tr('Elimina').' '; } -?> diff --git a/oauth2.php b/oauth2.php new file mode 100644 index 000000000..5418895be --- /dev/null +++ b/oauth2.php @@ -0,0 +1,52 @@ +first(); +} else { + $account = Account::find(get('id_account')); + + // Impostazione access token a null per reimpostare la configurazione + $account->access_token = null; + $account->save(); +} + +if (empty($account)) { + echo tr('Errore durante il completamento della configurazione: account non trovato'); + + return; +} + +// Inizializzazione +$oauth = new OAuth2($account); + +// Redirect all'URL di autorizzazione del servizio esterno +$redirect = $oauth->configure($code, $state); + +// Redirect automatico al record +if (empty($redirect)) { + $modulo_account_email = Module::pool('Account email'); + $redirect = base_path().'/editor.php?id_module='.$modulo_account_email->id.'&id_record='.$account->id; +} + +if (empty($_GET['error'])) { + redirect($redirect); + exit(); +} else { + echo $_GET['error'].'
'.$_GET['error_description'].' +

+'.tr('Riprova').''; +} diff --git a/src/Notifications/EmailNotification.php b/src/Notifications/EmailNotification.php index 3415607b2..0473c1274 100755 --- a/src/Notifications/EmailNotification.php +++ b/src/Notifications/EmailNotification.php @@ -23,6 +23,8 @@ use Modules\Emails\Account; use Modules\Emails\Mail; use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\OAuth; +use PHPMailer\PHPMailer\SMTP; use Prints; use Uploads; @@ -40,16 +42,16 @@ class EmailNotification extends PHPMailer implements NotificationInterface $this->CharSet = 'UTF-8'; // Configurazione di base - $config = Account::find($account); - if (empty($config)) { - $config = Account::where('predefined', true)->first(); + $account = $account instanceof Account ? $account : Account::find($account); + if (empty($account)) { + $account = Account::where('predefined', true)->first(); } // Preparazione email $this->IsHTML(true); - if (!empty($config['server'])) { - $this->IsSMTP(true); + if (!empty($account['server'])) { + $this->IsSMTP(); // Impostazioni di debug $this->SMTPDebug = \App::debug() ? 2 : 0; @@ -58,22 +60,40 @@ class EmailNotification extends PHPMailer implements NotificationInterface }; // Impostazioni dell'host - $this->Host = $config['server']; - $this->Port = $config['port']; + $this->Host = $account['server']; + $this->Port = $account['port']; // Impostazioni di autenticazione - if (!empty($config['username'])) { + if (!empty($account['username'])) { $this->SMTPAuth = true; - $this->Username = $config['username']; - $this->Password = $config['password']; + $this->Username = $account['username']; + + // Configurazione OAuth2 + if (!empty($account['access_token'])) { + $oauth2 = $account->getGestoreOAuth2(); + + $this->AuthType = 'XOAUTH2'; + $this->setOAuth( + new OAuth([ + 'provider' => $oauth2->getProvider(), + 'refreshToken' => $oauth2->getRefreshToken(), + 'clientId' => $account->client_id, + 'clientSecret' => $account->client_secret, + 'userName' => $account->username, + ]) + ); + } else { + $this->Password = $account['password']; + } } // Impostazioni di sicurezza - if (in_array(strtolower($config['encryption']), ['ssl', 'tls'])) { - $this->SMTPSecure = strtolower($config['encryption']); + if (in_array(strtolower($account['encryption']), ['ssl', 'tls'])) { + $this->SMTPSecure = strtolower($account['encryption']); } - if (!empty($config['ssl_no_verify'])) { + // Disabilitazione verifica host + if (!empty($account['ssl_no_verify'])) { $this->SMTPOptions = [ 'ssl' => [ 'verify_peer' => false, @@ -84,8 +104,8 @@ class EmailNotification extends PHPMailer implements NotificationInterface } } - $this->From = $config['from_address']; - $this->FromName = $config['from_name']; + $this->From = $account['from_address']; + $this->FromName = $account['from_name']; $this->WordWrap = 78; } diff --git a/update/2_4_24.sql b/update/2_4_24.sql index 4535da9a9..f6ea998f0 100644 --- a/update/2_4_24.sql +++ b/update/2_4_24.sql @@ -142,3 +142,10 @@ ALTER TABLE `dt_ddt` ADD `id_ddt_trasporto_interno` INT(11) NULL, ADD FOREIGN KE -- Aggiunto ragruppamento referenti per sede UPDATE `zz_plugins` SET `options` = ' { \"main_query\": [ { \"type\": \"table\", \"fields\": \"Nome, Indirizzo, Città, CAP, Provincia, Referente\", \"query\": \"SELECT an_sedi.id, an_sedi.nomesede AS Nome, an_sedi.indirizzo AS Indirizzo, an_sedi.citta AS Città, an_sedi.cap AS CAP, an_sedi.provincia AS Provincia, GROUP_CONCAT(an_referenti.nome SEPARATOR \\\", \\\") AS Referente FROM an_sedi LEFT OUTER JOIN an_referenti ON idsede = an_sedi.id WHERE 1=1 AND an_sedi.idanagrafica=|id_parent| GROUP BY an_sedi.id HAVING 2=2 ORDER BY an_sedi.id DESC\"} ]}' WHERE `zz_plugins`.`name` = 'Sedi'; UPDATE `zz_group_module` SET `clause` = 'in_interventi.id IN (SELECT idintervento FROM in_interventi_tecnici WHERE idintervento=in_interventi.id AND idtecnico=|id_anagrafica| UNION SELECT id_intervento FROM in_interventi_tecnici_assegnati WHERE id_intervento=in_interventi.id AND id_tecnico=|id_anagrafica|)' WHERE `zz_group_module`.`name` = 'Mostra interventi ai tecnici coinvolti'; + +-- Aggiunto supporto autenticazione OAuth 2 +ALTER TABLE `em_accounts` ADD `provider` varchar(255), + ADD `client_id` TEXT, + ADD `client_secret` TEXT, + ADD `oauth2_state` TEXT, + ADD `access_token` TEXT;