From 9a10a6b5e7d4cce6a5f2237299db276f6bfb2bfe Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Sun, 7 May 2023 12:23:46 +0000 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8E=B7=E5=8F=96=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E7=9A=84API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension.json | 8 +- i18n/en.json | 7 ++ i18n/zh-hans.json | 7 ++ includes/ApiOIDCAvatar.php | 97 +++++++++++++++++++ ...{ApiOidcWebhook.php => ApiOIDCWebhook.php} | 14 ++- includes/IsekaiOIDCAuth.php | 20 ++-- includes/IsekaiOIDCAuthHooks.php | 44 ++++++++- 7 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 includes/ApiOIDCAvatar.php rename includes/{ApiOidcWebhook.php => ApiOIDCWebhook.php} (88%) diff --git a/extension.json b/extension.json index 3c0d43e..698ecbb 100644 --- a/extension.json +++ b/extension.json @@ -29,7 +29,8 @@ "UserLogoutComplete": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onLogout", "GetPreferences": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onGetPreferences", "SkinTemplateNavigation::Universal": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onSkinTemplateUniversalNavigation", - "ResourceLoaderGetConfigVars": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onResourceLoaderGetConfigVars" + "ResourceLoaderGetConfigVars": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onResourceLoaderGetConfigVars", + "Isekai::GetUserAvatar": "Isekai\\OIDC\\IsekaiOIDCAuthHooks::onGetUserAvatar" }, "ExtensionMessagesFiles": { "IsekaiOIDCAlias": "IsekaiOIDC.alias.php" @@ -50,7 +51,10 @@ } }, "APIModules": { - "oidcwebhook": "Isekai\\OIDC\\ApiOidcWebhook" + "oidcwebhook": "Isekai\\OIDC\\ApiOIDCWebhook" + }, + "APIPropModules": { + "oidcavatar": "Isekai\\OIDC\\ApiOIDCAvatar" }, "config": { "IsekaiOIDC": { diff --git a/i18n/en.json b/i18n/en.json index 1f3627d..6a81ddc 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -10,6 +10,13 @@ "apihelp-oidcwebhook-param-provider": "Provider", "apihelp-oidcwebhook-param-key": "Secret Key", + "apihelp-query+oidcavatar-summary": "Get user avatar from OpenID Connection Provider", + "apihelp-query+oidcavatar-param-username": "Username in local Wiki", + "apihelp-query+oidcavatar-param-userid": "User ID in local Wiki", + "apihelp-query+oidcavatar-param-size": "Avatar size", + "apihelp-query+oidcavatar-param-redirect": "Redirect to avatar URL", + "isekaioidc-api-example-isekaioidcavatar": "Get avatar of user 'Example' in size 128px", + "isekaioidc-api-provider-not-supported": "Provider Unsupported", "isekaioidc-api-key-invalid": "API Key invalid", "isekaioidc-api-post-body-invalid": "POST Body format invalid", diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json index 5d0ac6e..8c58bed 100644 --- a/i18n/zh-hans.json +++ b/i18n/zh-hans.json @@ -10,6 +10,13 @@ "apihelp-oidcwebhook-param-provider": "Provider", "apihelp-oidcwebhook-param-key": "Secret Key", + "apihelp-query+oidcavatar-summary": "从 OpenID Connection 用户中心获取用户头像", + "apihelp-query+oidcavatar-param-username": "本地Wiki用户名", + "apihelp-query+oidcavatar-param-userid": "本地Wiki用户ID", + "apihelp-query+oidcavatar-param-size": "头像大小", + "apihelp-query+oidcavatar-param-redirect": "重定向到头像URL", + "isekaioidc-api-example-isekaioidcavatar": "获取用户名为“Example”的用户的128px头像", + "isekaioidc-api-provider-not-supported": "不支持的Provider", "isekaioidc-api-key-invalid": "API Key错误", "isekaioidc-api-post-body-invalid": "POST Body格式错误", diff --git a/includes/ApiOIDCAvatar.php b/includes/ApiOIDCAvatar.php new file mode 100644 index 0000000..06aa3eb --- /dev/null +++ b/includes/ApiOIDCAvatar.php @@ -0,0 +1,97 @@ +getMainConfig(); + $wgIsekaiOIDC = $config->get('IsekaiOIDC'); + + //$this->requireOnlyOneParameter('username', 'userid'); + + $userid = $this->getParameter('userid'); + $username = $this->getParameter('username'); + + $userFactory = $services->getUserFactory(); + if ($userid) { + $user = $userFactory->newFromId($userid); + } else { + $user = $userFactory->newFromName($username); + } + + if (!$user) { + $this->dieWithError('isekaioidc-api-user-not-found', 'isekaioidc-api-user-not-found'); + } + + $size = $this->getParameter('size'); + + $avatarUrl = ''; + + $hookContainer = $services->getHookContainer(); + $hookContainer->run('Isekai::GetUserAvatar', [&$avatarUrl, $size, $user]); + + if (empty($avatarUrl)) { + if (isset($wgIsekaiOIDC['defaultAvatarUrl'])) { + $avatarUrl = $wgIsekaiOIDC['defaultAvatarUrl']; + } else { + $this->dieWithError('isekaioidc-api-user-avatar-not-found', 'isekaioidc-api-user-avatar-not-found', [ + 'userid' => $userid, + 'username' => $username, + ]); + } + } + + if ($this->getParameter('redirect')) { + header('HTTP/1.1 301 Moved Permanently'); + header('Location: ' . $avatarUrl); + header('Cache-Control: public, max-age=86400'); + exit(); + } else { + $this->getResult()->addValue('query', 'oidcavatar', $avatarUrl); + } + } + + public function getAllowedParams() { + return [ + 'username' => [ + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_TYPE => 'string', + ], + 'userid' => [ + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_TYPE => 'integer', + ], + 'size' => [ + ParamValidator::PARAM_DEFAULT => 128, + ParamValidator::PARAM_TYPE => 'integer', + ], + 'redirect' => [ + ParamValidator::PARAM_DEFAULT => false, + ParamValidator::PARAM_TYPE => 'boolean', + ] + ]; + } + + public function getExamplesMessages() { + return [ + 'action=query&prop=oidcavatar&username=Example&size=128' => 'isekaioidc-api-example-isekaioidcavatar', + ]; + } + + public function getCacheMode($params) { + return 'public'; + } +} diff --git a/includes/ApiOidcWebhook.php b/includes/ApiOIDCWebhook.php similarity index 88% rename from includes/ApiOidcWebhook.php rename to includes/ApiOIDCWebhook.php index c4aafe6..3363263 100644 --- a/includes/ApiOidcWebhook.php +++ b/includes/ApiOIDCWebhook.php @@ -6,13 +6,16 @@ use ApiBase; use MediaWiki\MediaWikiServices; use Wikimedia\ParamValidator\ParamValidator; -class ApiOidcWebhook extends ApiBase { +class ApiOIDCWebhook extends ApiBase { public function __construct( $main, $method ) { parent::__construct( $main->getMain(), $method ); } public function execute() { - global $wgIsekaiOIDC; + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig(); + $wgIsekaiOIDC = $config->get('IsekaiOIDC'); + $queryValues = $this->getRequest()->getQueryValues(); $provider = ''; if (isset($queryValues['provider'])) { @@ -36,6 +39,10 @@ class ApiOidcWebhook extends ApiBase { } private function keycloakCallback() { + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig(); + $wgIsekaiOIDC = $config->get('IsekaiOIDC'); + if (!$this->getRequest()->wasPosted()) { $this->addError('isekaioidc-api-post-body-invalid'); return; @@ -47,7 +54,6 @@ class ApiOidcWebhook extends ApiBase { $this->addError('isekaioidc-api-post-body-invalid'); return; } - global $wgIsekaiOIDC; $realm = $wgIsekaiOIDC['realm']; $apiMode = $wgIsekaiOIDC['apiMode'] ?? 'oauth'; @@ -76,7 +82,7 @@ class ApiOidcWebhook extends ApiBase { 'phone' => $userInfo->phone_number, ]; - $user = MediaWikiServices::getInstance()->getUserFactory()->newFromId($userId); + $user = $services->getUserFactory()->newFromId($userId); IsekaiOIDCAuth::updateUserInfo($user, $newProfile); } $this->getResult()->addValue(null, 'webhook', 1); diff --git a/includes/IsekaiOIDCAuth.php b/includes/IsekaiOIDCAuth.php index 6d82b99..17a3ad5 100644 --- a/includes/IsekaiOIDCAuth.php +++ b/includes/IsekaiOIDCAuth.php @@ -69,10 +69,13 @@ class IsekaiOIDCAuth extends AbstractPrimaryAuthenticationProvider { * @inheritDoc */ public function continuePrimaryAuthentication( array $reqs ) { - global $wgIsekaiOIDC; - $config = $wgIsekaiOIDC; + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig()->get('IsekaiOIDC'); + + $oidc = self::getOpenIDConnectClient(); + + $requestCtx = RequestContext::getMain(); - $oidc =self::getOpenIDConnectClient(); if ($oidc->authenticate()) { $accessToken = $oidc->getAccessToken(); $refreshToken = $oidc->getRefreshToken(); @@ -88,7 +91,7 @@ class IsekaiOIDCAuth extends AbstractPrimaryAuthenticationProvider { } $authManager = MediaWikiServices::getInstance()->getAuthManager(); - $request = RequestContext::getMain()->getRequest(); + $request = $requestCtx->getRequest(); $session = $request->getSession(); $session->clear('AuthManager::AutoCreateBlacklist'); // 防止缓存检测 @@ -228,14 +231,15 @@ class IsekaiOIDCAuth extends AbstractPrimaryAuthenticationProvider { } public static function getOpenIDConnectClient() { - global $wgIsekaiOIDC; - if (!is_array($wgIsekaiOIDC)) { + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig()->get('IsekaiOIDC'); + + if (!is_array($config)) { wfDebugLog( self::LOG_TAG, 'wgIsekaiOIDC not set' . PHP_EOL ); throw new Exception('wgIsekaiOIDC not set'); } - $config = $wgIsekaiOIDC; if (!isset($config['endpoint']) || !isset($config['clientID']) || !isset($config['clientSecret'])) { wfDebugLog( self::LOG_TAG, 'wgIsekaiOIDC not valid' . PHP_EOL ); @@ -251,7 +255,7 @@ class IsekaiOIDCAuth extends AbstractPrimaryAuthenticationProvider { $oidc->setRedirectURL( $redirectURL ); wfDebugLog( self::LOG_TAG, 'Redirect URL: ' . $redirectURL ); - if ( isset( $_REQUEST['forcelogin'] ) ) { + if ( isset( $_REQUEST['force'] ) || isset( $_REQUEST['forcelogin'] ) ) { $oidc->addAuthParam( [ 'prompt' => 'login' ] ); } if ( isset( $config['authparam'] ) && diff --git a/includes/IsekaiOIDCAuthHooks.php b/includes/IsekaiOIDCAuthHooks.php index 0b490c2..d556f84 100644 --- a/includes/IsekaiOIDCAuthHooks.php +++ b/includes/IsekaiOIDCAuthHooks.php @@ -44,7 +44,8 @@ class IsekaiOIDCAuthHooks { * @param OutputPage $out */ public static function onBeforePageDisplay($out) { - global $wgIsekaiOIDC; + $services = MediaWikiServices::getInstance(); + $wgIsekaiOIDC = $services->getMainConfig()->get('IsekaiOIDC'); if (isset($wgIsekaiOIDC['syncLogout']) && $wgIsekaiOIDC['syncLogout']) { $title = $out->getTitle(); @@ -97,7 +98,9 @@ class IsekaiOIDCAuthHooks { } public static function onGetPreferences(\User $user, &$preferences) { - global $wgIsekaiOIDC; + $services = MediaWikiServices::getInstance(); + $wgIsekaiOIDC = $services->getMainConfig()->get('IsekaiOIDC'); + $profileUrl = $wgIsekaiOIDC['endpoint'] . 'account/'; $referrer = $wgIsekaiOIDC['clientID']; $referrerUri = Title::newFromText('Special:Preferences')->getCanonicalURL(); @@ -159,10 +162,45 @@ class IsekaiOIDCAuthHooks { * @return bool */ public static function onResourceLoaderGetConfigVars(&$vars, $skin, $config) { - global $wgIsekaiOIDC; + $services = MediaWikiServices::getInstance(); + $wgIsekaiOIDC = $services->getMainConfig()->get('IsekaiOIDC'); + if (isset($wgIsekaiOIDC['avatarUrl'])) { $vars['wgAvatarTemplate'] = $wgIsekaiOIDC['avatarUrl']; } return true; } + + public static function onGetUserAvatar(&$avatarUrl, $size, $user) { + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig(); + $wgIsekaiOIDC = $config->get('IsekaiOIDC'); + + if (isset($wgIsekaiOIDC['avatarUrl'])) { + list($subject, $accessToken, $refreshToken) = IsekaiOIDCAuth::findOidcDataByUserId($user->getId()); + $avatarFinded = false; + if ($subject) { + $avatarUrl = str_replace(['{openid}', '{username}'], [urlencode($subject), urlencode($user->getName())], $wgIsekaiOIDC['avatarUrl']); + $avatarFinded = true; + } else { + if (isset($wgIsekaiOIDC['defaultAvatarUrl'])) { + $avatarUrl = $wgIsekaiOIDC['defaultAvatarUrl']; + $avatarFinded = true; + } + } + + if ($avatarFinded) { + if ($size <= 64) { + $avatarUrl = "$avatarUrl?size=md"; + } else if ($size <= 128) { + $avatarUrl = "$avatarUrl?size=lg"; + } else if ($size <= 256) { + $avatarUrl = "$avatarUrl?size=xl"; + } else { + $avatarUrl = "$avatarUrl?size=xxl"; + } + } + } + return true; + } }