<?php
namespace MediaWiki\Extension\WSOAuth\AuthenticationProvider;
use WSOAuth\AuthenticationProvider\AuthProvider;
use MediaWiki\User\UserIdentity as MWUserIdentity;
use MediaWiki\User\UserNameUtils;
use MediaWiki\MediaWikiServices;
class XenForoOAuthProvider extends AuthProvider {
private $authUri;
private $tokenUri;
private $userInfoUri;
private $clientId;
private $clientSecret;
private $redirectUri;
private $scopes = 'user:read';
private $logFilePath = '/var/www/public_html/wiki/wsoauth.log';
private function writeToLog(string $message): void {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] {$message}" . PHP_EOL;
if (is_writable(dirname($this->logFilePath))) {
file_put_contents($this->logFilePath, $logMessage, FILE_APPEND);
} else {
error_log("WSOAuth Log Error: Directory or file not writable: " . $this->logFilePath);
}
}
public function __construct(
string $clientId,
string $clientSecret,
?string $authUri,
?string $redirectUri,
array $extensionData = []
) {
parent::__construct($clientId, $clientSecret, $authUri, $redirectUri, $extensionData);
$this->tokenUri = $extensionData['tokenUri'] ?? null;
$this->authUri = $authUri;
$this->userInfoUri = $extensionData['userInfoUri'] ?? 'https://teststar2.astromech.net/xenforo/api/me';
$this->scopes = $extensionData['scopes'] ?? 'user:read';
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->redirectUri = $redirectUri;
$this->writeToLog("XenForoOAuthProvider Initialized.");
$this->writeToLog("Auth URI: " . $this->authUri);
$this->writeToLog("Token URI: " . $this->tokenUri);
$this->writeToLog("User Info URI: " . $this->userInfoUri);
$this->writeToLog("Redirect URI: " . $this->redirectUri);
$this->writeToLog("Auth URI (constructor): " . ($this->authUri ?? 'not set'));
$this->writeToLog("Token URI (constructor): " . ($this->tokenUri ?? 'not set'));
if (!file_exists($this->logFilePath)) {
@touch($this->logFilePath);
}
if (!is_writable($this->logFilePath)) {
error_log("WSOAuth: Log file is not writable: " . $this->logFilePath);
}
}
private function postRequest($url, $data) {
$this->writeToLog("POST Request to: " . $url);
$this->writeToLog("POST Data: " . http_build_query($data));
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_USERAGENT, 'MediaWiki-WSOAuth-Client');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->writeToLog("POST Response Code: " . $httpCode);
if ($response === false) {
$this->writeToLog("cURL Error (POST): " . $error);
error_log("WSOAuth cURL Error (POST) to " . $url . ": " . $error);
return false;
}
if ($httpCode < 200 || $httpCode >= 300) {
$this->writeToLog("HTTP Error (POST): " . $httpCode . " Response: " . $response);
error_log("WSOAuth HTTP Error (POST) " . $httpCode . " from " . $url . ": " . $response);
return false;
}
return $response;
}
private function getRequest($url, $headers = []) {
$this->writeToLog("GET Request to: " . $url);
$this->writeToLog("GET Headers: " . implode(', ', $headers));
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_USERAGENT, 'MediaWiki-WSOAuth-Client');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->writeToLog("GET Response Code: " . $httpCode);
if ($response === false) {
$this->writeToLog("cURL Error (GET): " . $error);
error_log("WSOAuth cURL Error (GET) to " . $url . ": " . $error);
return false;
}
if ($httpCode < 200 || $httpCode >= 300) {
$this->writeToLog("HTTP Error (GET): " . $httpCode . " Response: " . $response);
error_log("WSOAuth HTTP Error (GET) " . $httpCode . " from " . $url . ": " . $response);
return false;
}
return $response;
}
public function getUser(string $key, string $secret, &$errorMessage): ?MWUserIdentity {
$code = $key;
$this->writeToLog("getUser called. Code received (via \$key): " . $code);
$this->writeToLog("getUser called. \$secret parameter value: " . $secret);
$this->writeToLog("Attempting to exchange code for token.");
$tokenRequestData = [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->redirectUri,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret
];
$rawTokenData = $this->postRequest($this->tokenUri, $tokenRequestData);
if ($rawTokenData === false) {
$errorMessage = 'Failed to communicate with the token endpoint.';
$this->writeToLog("Error: " . $errorMessage);
return null;
}
$tokenData = json_decode($rawTokenData, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($tokenData['access_token'])) {
$this->writeToLog("Failed to decode token response or access_token missing. Error: " . json_last_error_msg());
$this->writeToLog("Raw Token Response: " . $rawTokenData);
$errorMessage = 'Failed to retrieve a valid access token.';
if (isset($tokenData['error_description'])) {
$errorMessage .= ' Provider message: ' . $tokenData['error_description'];
} elseif (isset($tokenData['error'])) {
$errorMessage .= ' Provider error: ' . $tokenData['error'];
} elseif (isset($tokenData['message'])) {
$errorMessage .= ' Provider message: ' . $tokenData['message'];
}
return null;
}
$accessToken = $tokenData['access_token'];
$this->writeToLog("Access token obtained successfully.");
$userInfoResponse = $this->getRequest($this->userInfoUri, [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json'
]);
$this->writeToLog("Raw UserInfo Response: " . $userInfoResponse);
if ($userInfoResponse === false) {
$errorMessage = 'Failed to communicate with the user info endpoint.';
$this->writeToLog("Error: " . $errorMessage);
return null;
}
$userInfo = json_decode($userInfoResponse, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($userInfo['user']) || !isset($userInfo['user']['user_id']) || !isset($userInfo['user']['username'])) {
$this->writeToLog("User info decode error or missing required fields (user, user.user_id, user.username). Error: " . json_last_error_msg());
$this->writeToLog("Raw UserInfo Response: " . $userInfoResponse);
$errorMessage = 'Failed to retrieve valid user information structure (user_id, username).';
return null;
}
$xenforoUser = $userInfo['user'];
$userId = $xenforoUser['user_id'];
$username = $xenforoUser['username'];
$this->writeToLog("User info retrieved: ID=" . $userId . ", Username=" . $username);
$this->writeToLog("Original XenForo Username: " . $username);
$userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
$mwUsername = trim($username);
$mwUsername = str_replace(' ', '_', $mwUsername);
if (!$userNameUtils->isValid($mwUsername)) {
$this->writeToLog("Cleaned Username '" . $mwUsername . "' is invalid, using fallback.");
$mwUsername = 'XFUser_' . $userId;
if (!$userNameUtils->isValid($mwUsername)) {
$mwUsername = 'XFUser_' . uniqid();
$this->writeToLog("Using uniqid fallback: " . $mwUsername);
}
} else {
$mwUsername = $userNameUtils->getCanonical($mwUsername);
}
$this->writeToLog("Final MediaWiki Username: " . $mwUsername);
$userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
$mwUsername = trim($username);
$mwUsername = str_replace(' ', '_', $mwUsername);
if (!$userNameUtils->isValid($mwUsername)) {
$this->writeToLog("Original username '" . $username . "' ('" . $mwUsername . "') is invalid for MediaWiki. Generating fallback.");
$mwUsername = 'XFUser_' . $userId;.
if (!$userNameUtils->isValid($mwUsername)) {
$mwUsername = 'XFUser_' . uniqid();
$this->writeToLog("Primary fallback invalid, using uniqid fallback: " . $mwUsername);
}
} else {
$mwUsername = $userNameUtils->getCanonical($mwUsername);
}
$this->writeToLog("Using MediaWiki Username: " . $mwUsername);
return new MWUserIdentity( (string)$userId, $mwUsername );
}
public function login(?string &$key, ?string &$secret, ?string &$authUrl): bool {
$this->writeToLog("login called. Generating authorization URL.");
if (empty($this->authUri)) {
$this->writeToLog("Error: authUri is not set. Cannot generate authorization URL.");
return false;
}
$params = [
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'scope' => $this->scopes,
];
$authUrl = $this->authUri . '?' . http_build_query($params);
$this->writeToLog("Authorization URL generated: " . $authUrl);
$key = 'oauth_key_placeholder';
$secret = 'oauth_secret_placeholder';
$this->writeToLog("login output: Key=" . $key . ", Secret=" . $secret);
$this->writeToLog("Authorization code dynamically set: " . $key);
return true;
}
public function logout(MWUserIdentity &$user): void {
$this->writeToLog("logout called for user ID: " . $user->getId());
return;
}
public function saveExtraAttributes(int $id): void {
$this->writeToLog("saveExtraAttributes called for MediaWiki user ID: " . $id . ". (Not Implemented)");
}
}