261 lines
9.0 KiB
PHP
261 lines
9.0 KiB
PHP
<?php
|
|
/**
|
|
* Microsoft 365 / Graph API Integratie voor Fociz
|
|
* Beheert OAuth2 authenticatie, token refresh en kalendersynchronisatie.
|
|
*/
|
|
|
|
require_once __DIR__ . '/../http.php';
|
|
require_once __DIR__ . '/../../config/database.php';
|
|
|
|
const MS_OAUTH_AUTHORIZE_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/authorize';
|
|
const MS_OAUTH_TOKEN_URL = 'https://login.microsoftonline.com/%s/oauth2/v2.0/token';
|
|
const MS_GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
|
const MS_DEFAULT_SCOPES = 'offline_access openid profile email User.Read Calendars.Read';
|
|
|
|
/**
|
|
* Genereert de Microsoft OAuth2 login URL
|
|
*/
|
|
function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = 'common', string $state = ''): string {
|
|
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
|
|
$baseUrl = sprintf(MS_OAUTH_AUTHORIZE_URL, urlencode($tenant));
|
|
|
|
$params = [
|
|
'client_id' => trim($clientId),
|
|
'response_type' => 'code',
|
|
'redirect_uri' => trim($redirectUri),
|
|
'response_mode' => 'query',
|
|
'scope' => MS_DEFAULT_SCOPES,
|
|
'state' => $state,
|
|
'prompt' => 'select_account',
|
|
];
|
|
|
|
return $baseUrl . '?' . http_build_query($params);
|
|
}
|
|
|
|
/**
|
|
* Wisselt de ontvangen autorisatiecode om voor access- en refresh tokens
|
|
*/
|
|
function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = 'common'): array {
|
|
$tenant = !empty($tenantId) ? trim($tenantId) : 'common';
|
|
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($tenant));
|
|
|
|
$payload = [
|
|
'client_id' => trim($clientId),
|
|
'client_secret' => trim($clientSecret),
|
|
'code' => trim($code),
|
|
'redirect_uri' => trim($redirectUri),
|
|
'grant_type' => 'authorization_code',
|
|
'scope' => MS_DEFAULT_SCOPES,
|
|
];
|
|
|
|
$response = fociz_http_request($tokenUrl, [
|
|
'method' => 'POST',
|
|
'body' => $payload,
|
|
'timeout' => 15,
|
|
]);
|
|
|
|
if ($response['status'] !== 200 || empty($response['json']['access_token'])) {
|
|
$errorMsg = $response['json']['error_description'] ?? ($response['error'] ?? 'Onbekende fout bij token uitwisseling');
|
|
return [
|
|
'success' => false,
|
|
'error' => $errorMsg,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'access_token' => $response['json']['access_token'],
|
|
'refresh_token' => $response['json']['refresh_token'] ?? null,
|
|
'expires_in' => (int)($response['json']['expires_in'] ?? 3600),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Haalt het Microsoft gebruikersprofiel op via Graph API
|
|
*/
|
|
function microsoft_get_user_profile(string $accessToken): ?array {
|
|
$response = fociz_http_request(MS_GRAPH_BASE_URL . '/me', [
|
|
'method' => 'GET',
|
|
'headers' => [
|
|
'Authorization' => "Bearer {$accessToken}",
|
|
'Accept' => 'application/json',
|
|
],
|
|
'timeout' => 10,
|
|
]);
|
|
|
|
if ($response['status'] === 200 && is_array($response['json'])) {
|
|
return [
|
|
'displayName' => $response['json']['displayName'] ?? 'Microsoft 365 Gebruiker',
|
|
'mail' => $response['json']['mail'] ?? ($response['json']['userPrincipalName'] ?? ''),
|
|
'id' => $response['json']['id'] ?? '',
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Haalt een geldige access_token op voor de gebruiker; vernieuwt automatisch indien verlopen
|
|
*/
|
|
function microsoft_get_valid_token(int $userId): ?string {
|
|
$db = get_db_connection();
|
|
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
|
$stmt->execute([$userId]);
|
|
$integration = $stmt->fetch();
|
|
|
|
if (!$integration || empty($integration['access_token'])) {
|
|
return null;
|
|
}
|
|
|
|
$settings = json_decode($integration['settings'] ?? '{}', true) ?: [];
|
|
$expiresAt = (int)($settings['expires_at'] ?? 0);
|
|
|
|
// Als token nog minstens 4 minuten geldig is, direct retourneren
|
|
if ($expiresAt > (time() + 240)) {
|
|
return $integration['access_token'];
|
|
}
|
|
|
|
// Token is verlopen of verloopt bijna; refresh met refresh_token
|
|
$refreshToken = $integration['refresh_token'] ?? '';
|
|
$clientId = $settings['client_id'] ?? fociz_env('MICROSOFT_CLIENT_ID', '');
|
|
$clientSecret = $settings['client_secret'] ?? fociz_env('MICROSOFT_CLIENT_SECRET', '');
|
|
$tenantId = $settings['tenant_id'] ?? fociz_env('MICROSOFT_TENANT_ID', 'common');
|
|
|
|
if (empty($refreshToken) || empty($clientId) || empty($clientSecret)) {
|
|
return null;
|
|
}
|
|
|
|
$tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode(!empty($tenantId) ? $tenantId : 'common'));
|
|
$payload = [
|
|
'client_id' => $clientId,
|
|
'client_secret' => $clientSecret,
|
|
'grant_type' => 'refresh_token',
|
|
'refresh_token' => $refreshToken,
|
|
'scope' => MS_DEFAULT_SCOPES,
|
|
];
|
|
|
|
$response = fociz_http_request($tokenUrl, [
|
|
'method' => 'POST',
|
|
'body' => $payload,
|
|
'timeout' => 15,
|
|
]);
|
|
|
|
if ($response['status'] === 200 && !empty($response['json']['access_token'])) {
|
|
$newAccessToken = $response['json']['access_token'];
|
|
$newRefreshToken = $response['json']['refresh_token'] ?? $refreshToken;
|
|
$expiresIn = (int)($response['json']['expires_in'] ?? 3600);
|
|
$settings['expires_at'] = time() + $expiresIn;
|
|
|
|
$updateStmt = $db->prepare("
|
|
UPDATE integrations
|
|
SET access_token = ?, refresh_token = ?, settings = ?, last_sync_at = NOW()
|
|
WHERE id = ?
|
|
");
|
|
$updateStmt->execute([$newAccessToken, $newRefreshToken, json_encode($settings), $integration['id']]);
|
|
|
|
return $newAccessToken;
|
|
}
|
|
|
|
error_log("Microsoft token refresh mislukt voor user {$userId}: " . ($response['error'] ?? json_encode($response['json'])));
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Haalt agenda-afspraken van Microsoft Outlook op voor de opgegeven datum
|
|
*/
|
|
function microsoft_fetch_today_events(int $userId, ?string $date = null): array {
|
|
$targetDate = $date ?: date('Y-m-d');
|
|
$token = microsoft_get_valid_token($userId);
|
|
|
|
if (!$token) {
|
|
return [];
|
|
}
|
|
|
|
// Venster van 00:00:00 tot 23:59:59 lokale tijd
|
|
$startIso = "{$targetDate}T00:00:00";
|
|
$endIso = "{$targetDate}T23:59:59";
|
|
|
|
$url = MS_GRAPH_BASE_URL . '/me/calendarView?' . http_build_query([
|
|
'startDateTime' => $startIso,
|
|
'endDateTime' => $endIso,
|
|
'$select' => 'id,subject,bodyPreview,start,end,location,isAllDay,webLink,onlineMeeting,isCancelled',
|
|
'$orderby' => 'start/dateTime',
|
|
'$top' => '50',
|
|
]);
|
|
|
|
$response = fociz_http_request($url, [
|
|
'method' => 'GET',
|
|
'headers' => [
|
|
'Authorization' => "Bearer {$token}",
|
|
'Accept' => 'application/json',
|
|
'Prefer' => 'outlook.timezone="Europe/Amsterdam"',
|
|
],
|
|
'timeout' => 12,
|
|
]);
|
|
|
|
if ($response['status'] !== 200 || empty($response['json']['value'])) {
|
|
return [];
|
|
}
|
|
|
|
$rawEvents = $response['json']['value'];
|
|
$events = [];
|
|
|
|
foreach ($rawEvents as $e) {
|
|
// Annuleringen overslaan
|
|
if (!empty($e['isCancelled'])) {
|
|
continue;
|
|
}
|
|
|
|
$rawStart = $e['start']['dateTime'] ?? '';
|
|
$rawEnd = $e['end']['dateTime'] ?? '';
|
|
|
|
// Formatteer naar H:i (bijv. 09:30)
|
|
$startTime = '';
|
|
$endTime = '';
|
|
if (!empty($rawStart)) {
|
|
$startTime = date('H:i', strtotime($rawStart));
|
|
}
|
|
if (!empty($rawEnd)) {
|
|
$endTime = date('H:i', strtotime($rawEnd));
|
|
}
|
|
|
|
// Teams of meeting URL detecteren
|
|
$meetingUrl = $e['onlineMeeting']['joinUrl'] ?? ($e['webLink'] ?? 'https://outlook.office.com');
|
|
if (empty($meetingUrl) && !empty($e['bodyPreview'])) {
|
|
if (preg_match('#https://teams\.microsoft\.com/l/meetup-join/[^\s"\'<>]+#i', $e['bodyPreview'], $m)) {
|
|
$meetingUrl = $m[0];
|
|
}
|
|
}
|
|
|
|
$locationName = $e['location']['displayName'] ?? '';
|
|
if (empty($locationName) && !empty($e['onlineMeeting']['joinUrl'])) {
|
|
$locationName = 'Microsoft Teams';
|
|
}
|
|
|
|
$events[] = [
|
|
'id' => $e['id'] ?? uniqid('ms_'),
|
|
'subject' => !empty($e['subject']) ? $e['subject'] : '(Geen onderwerp)',
|
|
'start' => $startTime ?: '09:00',
|
|
'end' => $endTime ?: '10:00',
|
|
'start_raw' => $rawStart,
|
|
'end_raw' => $rawEnd,
|
|
'location' => $locationName ?: 'Geen locatie',
|
|
'meeting_url' => $meetingUrl,
|
|
'is_all_day' => !empty($e['isAllDay']),
|
|
'source' => 'OUTLOOK_LIVE',
|
|
];
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Ontkoppelt Microsoft integratie voor een gebruiker
|
|
*/
|
|
function microsoft_disconnect(int $userId): bool {
|
|
$db = get_db_connection();
|
|
$stmt = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
|
return $stmt->execute([$userId]);
|
|
}
|
|
|