Files
Fociz/includes/integrations/microsoft.php
T

260 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]);
}