Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Changelog_iseta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Changelog iseta — patchs spécifiques cœur Dolibarr

Check failure on line 1 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

patchs ==> patches, paths

Check failure on line 1 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

patchs ==> patches, paths

Modifications spécifiques iseta appliquées au cœur Dolibarr sur la branche `22.0_iseta`

Check failure on line 3 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

branche ==> branch, branches, branched

Check failure on line 3 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

branche ==> branch, branches, branched
(hors modules `htdocs/custom/`). Chaque changement est marqué en code par `BACKPORT PR #37329`
(ou une référence équivalente) pour être retrouvé lors des montées de version.

## 2026-07-16 — Backport OAuth Microsoft Exchange Online (PR Dolibarr #37329)

Backport de la fonctionnalité « Microsoft Exchange Online [SMTP/IMAP] OAuth2 » depuis la
branche 23.0 vers la 22.0.

Check failure on line 10 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

branche ==> branch, branches, branched

Check failure on line 10 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

branche ==> branch, branches, branched

- **Source** : https://github.com/Dolibarr/dolibarr/pull/37329 (mergée en 23.0)
- **Périmètre** : nouveau provider OAuth `MICROSOFT3` (scopes `outlook.office.com` :
`SMTP.Send` + `IMAP.AccessAsUser.All`, `offline_access`), pour l'envoi/lecture de mail
via OAuth2 sur Exchange Online.

### Fichiers ajoutés (copie conforme de la 23.0)

Check failure on line 17 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

copie ==> copy, copies, copied, copier

Check failure on line 17 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

copie ==> copy, copies, copied, copier
- `htdocs/includes/OAuth/OAuth2/Service/Microsoft3.php`
- `htdocs/core/modules/oauth/microsoft3_oauthcallback.php`

### Fichiers modifiés (hunks OAuth uniquement, jamais de copie complète)

Check failure on line 21 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

copie ==> copy, copies, copied, copier

Check failure on line 21 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

copie ==> copy, copies, copied, copier
- `htdocs/core/lib/oauth.lib.php` — enregistrement du provider `MICROSOFT3`
(`getAllOauth2Array` + `getSupportedOauth2Array`)
- `htdocs/admin/oauth.php` — affichage du champ Tenant pour `MICROSOFT3`
- `htdocs/core/modules/oauth/microsoft_oauthcallback.php` — gestion du paramètre `state`
- `htdocs/core/class/CMailFile.class.php` — `setAuthMode('XOAUTH2')` en tête du bloc XOAUTH2

Check failure on line 26 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

bloc ==> block

Check failure on line 26 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

bloc ==> block
+ `dol_syslog` quand l'objet token est invalide

Check failure on line 27 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

invalide ==> invalid

Check failure on line 27 in Changelog_iseta.md

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

invalide ==> invalid
- `htdocs/langs/en_US/oauth.lang`, `htdocs/langs/fr_FR/oauth.lang` — clé `OAuthErrorStateDiffers`

### Volontairement EXCLUS de la PR (bugfixes sans rapport avec OAuth)
- `htdocs/compta/facture/card.php` (guard `fetch_thirdparty`)
- `htdocs/website/class/websitepage.class.php` (`$this->errors[]`)
- suppressions cosmétiques de lignes vides dans `CMailFile.class.php`

### Intégration
- Branche : `NEW/oauth/PR37329/MicrosoftExchangeOAuth` → PR vers `22.0_iseta` (remote `atm`).
3 changes: 2 additions & 1 deletion htdocs/admin/oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,8 @@
print '</tr>';

// Tenant
if ($keybeforeprovider == 'MICROSOFT' || $keybeforeprovider == 'MICROSOFT2') {
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth (MICROSOFT3 tenant field)
if ($keybeforeprovider == 'MICROSOFT' || $keybeforeprovider == 'MICROSOFT2' || $keybeforeprovider == 'MICROSOFT3') {
print '<tr class="oddeven value">';
print '<td><label for="'.$key[2].'">'.$langs->trans("OAUTH_TENANT").'</label></td>';
print '<td><input type="text" size="100" id="OAUTH_'.$keybeforeprovider.($keyforprovider ? '-'.$keyforprovider : '').'_TENANT" name="OAUTH_'.$keybeforeprovider.($keyforprovider ? '-'.$keyforprovider : '').'_TENANT" value="'.getDolGlobalString('OAUTH_'.$keybeforeprovider.($keyforprovider ? '-'.$keyforprovider : '').'_TENANT').'">';
Expand Down
4 changes: 4 additions & 0 deletions htdocs/core/class/CMailFile.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,8 @@ public function sendfile()
$this->transport->setPassword(getDolGlobalString($keyforsmtppw));
}
if (getDolGlobalString($keyforsmtpauthtype) === "XOAUTH2") {
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth
$this->transport->setAuthMode('XOAUTH2');
require_once DOL_DOCUMENT_ROOT.'/core/lib/oauth.lib.php';

$supportedoauth2array = getSupportedOauth2Array();
Expand Down Expand Up @@ -1364,6 +1366,8 @@ public function sendfile()
$this->transport->setPassword($tokenobj->getAccessToken());
} else {
$this->errors[] = "Token not found";
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth
dol_syslog("CMailFile::sendfile: OAuth2 token object is not valid", LOG_ERR);
}
} catch (Exception $e) {
// Return an error if token not found
Expand Down
21 changes: 21 additions & 0 deletions htdocs/core/lib/oauth.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ function getAllOauth2Array()
'OAUTH_MICROSOFT2_ID',
'OAUTH_MICROSOFT2_SECRET',
),
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth
array(
'OAUTH_MICROSOFT3_NAME',
'OAUTH_MICROSOFT3_ID',
'OAUTH_MICROSOFT3_SECRET',
'OAUTH_MICROSOFT3_DESC',
),
array(
'OAUTH_NEST_NAME',
'OAUTH_NEST_ID',
Expand Down Expand Up @@ -341,6 +348,20 @@ function getSupportedOauth2Array()
'availablescopes' => 'openid,offline_access,profile,email,User.Read,https://outlook.office.com/.default',
'returnurl' => '/core/modules/oauth/microsoft2_oauthcallback.php'
);
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth
$supportedoauth2array['OAUTH_MICROSOFT3_NAME'] = array(
'callbackfile' => 'microsoft3',
'picto' => 'microsoft',
'urlforapp' => 'OAUTH_MICROSOFT3_DESC',
'name' => 'Microsoft Exchange Online [SMTP/IMAP]',
'urlforcredentials' => 'https://portal.azure.com/',
// CRITICAL: Use ONLY outlook.office.com scopes here, do NOT mix with Graph scopes (openid/profile/email).
// Mixing two resource namespaces in one token request causes AADSTS28000 error.
// offline_access is a neutral scope (no resource prefix) and is allowed alongside any resource.
// Azure permissions required: Microsoft Graph > Delegated > SMTP.Send and IMAP.AccessAsUser.All
'availablescopes' => 'offline_access,https://outlook.office.com/SMTP.Send,https://outlook.office.com/IMAP.AccessAsUser.All',
'returnurl' => '/core/modules/oauth/microsoft3_oauthcallback.php'
);

// Add a generic Oauth token handler. Tested with Mastodon.
$supportedoauth2array['OAUTH_GENERIC_NAME'] = array(
Expand Down
210 changes: 210 additions & 0 deletions htdocs/core/modules/oauth/microsoft3_oauthcallback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php
/* Copyright (C) 2022 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2015-2024 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Vidal Nicolas <nicolas.vidal@atm-consulting.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth callback (new file copied from core 23.0)

/**
* \file htdocs/core/modules/oauth/microsoft3_oauthcallback.php
* \ingroup oauth
* \brief Page to get oauth callback for Microsoft Exchange Online (SMTP/IMAP)
*
* Uses Exchange Online OAuth2 scopes for SMTP/IMAP:
* - offline_access
* - https://outlook.office.com/SMTP.Send
* - https://outlook.office.com/IMAP.AccessAsUser.All
*/

// Load Dolibarr environment
require '../../../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/includes/OAuth/bootstrap.php';
/**
* @var Conf $conf
* @var DoliDB $db
* @var Translate $langs
* @var User $user
*
* @var string $dolibarr_main_url_root
*/

use OAuth\Common\Storage\DoliStorage;
use OAuth\Common\Consumer\Credentials;

// Define $urlwithroot
$urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
$urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file


$action = GETPOST('action', 'aZ09');
$backtourl = GETPOST('backtourl', 'alpha');
$keyforprovider = GETPOST('keyforprovider', 'aZ09');
if (empty($keyforprovider) && !empty($_SESSION["oauthkeyforproviderbeforeoauthjump"]) && (GETPOST('code') || $action == 'delete')) {
$keyforprovider = $_SESSION["oauthkeyforproviderbeforeoauthjump"];
}
$genericstring = 'MICROSOFT3';


/**
* Create a new instance of the URI class with the current URI, stripping the query string
*/
$uriFactory = new \OAuth\Common\Http\Uri\UriFactory();


$currentUri = $uriFactory->createFromAbsolute($urlwithroot.'/core/modules/oauth/microsoft3_oauthcallback.php');


/**
* Load the credential for the service
*/

/** @var \OAuth\ServiceFactory $serviceFactory An OAuth service factory. */
$serviceFactory = new \OAuth\ServiceFactory();
$httpClient = new \OAuth\Common\Http\Client\CurlClient();

$serviceFactory->setHttpClient($httpClient);

// Setup the credentials for the requests
$keyforparamid = 'OAUTH_'.$genericstring.($keyforprovider ? '-'.$keyforprovider : '').'_ID';
$keyforparamsecret = 'OAUTH_'.$genericstring.($keyforprovider ? '-'.$keyforprovider : '').'_SECRET';
$keyforparamtenant = 'OAUTH_'.$genericstring.($keyforprovider ? '-'.$keyforprovider : '').'_TENANT';

// Dolibarr storage
$storage = new DoliStorage($db, $conf, $keyforprovider, getDolGlobalString($keyforparamtenant));

$credentials = new Credentials(
getDolGlobalString($keyforparamid),
getDolGlobalString($keyforparamsecret),
$currentUri->getAbsoluteUri()
);

$state = GETPOST('state');

$requestedpermissionsarray = array();
if ($state) {
$requestedpermissionsarray = explode(',', $state); // Example: 'user'. 'state' parameter is standard to retrieve some parameters back
}
if ($action != 'delete' && empty($requestedpermissionsarray)) {
print 'Error, parameter state is not defined';
exit;
}

try {
$nameofservice = ucfirst(strtolower($genericstring));
$apiService = $serviceFactory->createService($nameofservice, $credentials, $storage, $requestedpermissionsarray);
'@phan-var-force OAuth\OAuth2\Service\AbstractService|OAuth\OAuth1\Service\AbstractService $apiService'; // createService is only ServiceInterface
} catch (Exception $e) {
print $e->getMessage();
exit;
}

if (empty($apiService)) {
print 'Error, failed to create serviceFactory';
exit;
}

$langs->load("oauth");

if (!getDolGlobalString($keyforparamid)) {
accessforbidden('Setup of service is not complete. Customer ID is missing');
}
if (!getDolGlobalString($keyforparamsecret)) {
accessforbidden('Setup of service is not complete. Secret key is missing');
}
if (!getDolGlobalString($keyforparamtenant)) {
accessforbidden('Setup of service is not complete. Tenant/Annuary ID key is missing');
}

/*
* Actions
*/

if ($action == 'delete' && (!empty($user->admin) || $user->id == GETPOSTINT('userid'))) {
$storage->userid = GETPOSTINT('userid');
$storage->clearToken($genericstring);

setEventMessages($langs->trans('TokenDeleted'), null, 'mesgs');

if (empty($backtourl)) {
$backtourl = DOL_URL_ROOT.'/';
}

header('Location: '.$backtourl);
exit();
}


if (GETPOST('code') || GETPOST('error')) { // We are coming from oauth provider page
// We should have

dol_syslog(basename(__FILE__)." We are coming from the oauth provider page code=".dol_trunc(GETPOST('code'), 5)." error=".GETPOST('error'));

// We must validate that the $state is the same than the one into $_SESSION['oauthstateanticsrf'], return error if not.
if (isset($_SESSION['oauthstateanticsrf']) && $state != $_SESSION['oauthstateanticsrf']) {
print $langs->trans("OAuthErrorStateDiffers", dol_escape_htmltag($state));
unset($_SESSION['oauthstateanticsrf']);
exit;
}

// This was a callback request from service, get the token
try {
if (GETPOST('error')) {
setEventMessages(GETPOST('error').' '.GETPOST('error_description'), null, 'errors');
} else {
$token = $apiService->requestAccessToken(GETPOST('code'));
setEventMessages($langs->trans('NewTokenStored'), null, 'mesgs'); // Stored into object managed by class DoliStorage so into table oauth_token
}

$backtourl = $_SESSION["backtourlsavedbeforeoauthjump"];
unset($_SESSION["backtourlsavedbeforeoauthjump"]);

header('Location: '.$backtourl);
exit();
} catch (Exception $e) {
print $e->getMessage();
}
} else {
// If we enter this page without 'code' parameter, we arrive here. This is the case when we want to get the redirect
// to the OAuth provider login page.
$_SESSION["backtourlsavedbeforeoauthjump"] = $backtourl;
$_SESSION["oauthkeyforproviderbeforeoauthjump"] = $keyforprovider;
$_SESSION['oauthstateanticsrf'] = $state;

// This may create record into oauth_state before the header redirect.
// Creation of record with state in this tables depend on the Provider used (see its constructor).
$params = array('prompt' => 'consent');
if ($state) {
$params['state'] = $state;
}
$url = $apiService->getAuthorizationUri($params);

// Show url to get authorization
dol_syslog("Redirect to url=".$url);

// we go on oauth provider authorization page
header('Location: '.$url);
exit();
}

/*
* View
*/

// No view at all, just actions

$db->close();
7 changes: 6 additions & 1 deletion htdocs/core/modules/oauth/microsoft_oauthcallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,13 @@

// This may create record into oauth_state before the header redirect.
// Creation of record with state in this tables depend on the Provider used (see its constructor).
// BACKPORT PR #37329 (Dolibarr 23.0 -> 22.0_iseta): Microsoft Exchange Online OAuth (state param handling)
$params = array();
if ($state) {
$url = $apiService->getAuthorizationUri(array('state' => $state));
$params['state'] = $state;
}
if (!empty($params)) {
$url = $apiService->getAuthorizationUri($params);
} else {
$url = $apiService->getAuthorizationUri(); // Parameter state will be randomly generated
}
Expand Down
Loading
Loading