From 270eb211b545236ffc531d60c9f0f473d9e3aaf2 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Fri, 16 Jun 2023 06:35:28 +0000 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 14 + extension.json | 108 ++++ i18n/zh-hans.json | 52 ++ includes/Api/ApiChatComplete.php | 105 +++ includes/Api/ApiChatCompleteBot.php | 100 +++ includes/Api/ChatComplete/ApiCheckAccess.php | 115 ++++ includes/Api/ChatComplete/ApiCreateToken.php | 80 +++ .../Api/ChatCompleteBot/ApiReportUsage.php | 379 +++++++++++ includes/Api/ChatCompleteBot/ApiUserInfo.php | 131 ++++ includes/ChatCompleteUtils.php | 43 ++ includes/Hooks.php | 19 + modules/ext.isekai.chatcomplete.launcher.js | 61 ++ vendor/autoload.php | 25 + vendor/composer/ClassLoader.php | 585 +++++++++++++++++ vendor/composer/InstalledVersions.php | 359 ++++++++++ vendor/composer/LICENSE | 21 + vendor/composer/autoload_classmap.php | 10 + vendor/composer/autoload_namespaces.php | 9 + vendor/composer/autoload_psr4.php | 10 + vendor/composer/autoload_real.php | 38 ++ vendor/composer/autoload_static.php | 36 ++ vendor/composer/installed.json | 66 ++ vendor/composer/installed.php | 32 + vendor/composer/platform_check.php | 26 + vendor/firebase/php-jwt/LICENSE | 30 + vendor/firebase/php-jwt/README.md | 289 +++++++++ vendor/firebase/php-jwt/composer.json | 36 ++ .../php-jwt/src/BeforeValidException.php | 7 + .../firebase/php-jwt/src/ExpiredException.php | 7 + vendor/firebase/php-jwt/src/JWK.php | 172 +++++ vendor/firebase/php-jwt/src/JWT.php | 611 ++++++++++++++++++ vendor/firebase/php-jwt/src/Key.php | 59 ++ .../php-jwt/src/SignatureInvalidException.php | 7 + 33 files changed, 3642 insertions(+) create mode 100644 composer.json create mode 100644 extension.json create mode 100644 i18n/zh-hans.json create mode 100644 includes/Api/ApiChatComplete.php create mode 100644 includes/Api/ApiChatCompleteBot.php create mode 100644 includes/Api/ChatComplete/ApiCheckAccess.php create mode 100644 includes/Api/ChatComplete/ApiCreateToken.php create mode 100644 includes/Api/ChatCompleteBot/ApiReportUsage.php create mode 100644 includes/Api/ChatCompleteBot/ApiUserInfo.php create mode 100644 includes/ChatCompleteUtils.php create mode 100644 includes/Hooks.php create mode 100644 modules/ext.isekai.chatcomplete.launcher.js create mode 100644 vendor/autoload.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/InstalledVersions.php create mode 100644 vendor/composer/LICENSE create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/autoload_static.php create mode 100644 vendor/composer/installed.json create mode 100644 vendor/composer/installed.php create mode 100644 vendor/composer/platform_check.php create mode 100644 vendor/firebase/php-jwt/LICENSE create mode 100644 vendor/firebase/php-jwt/README.md create mode 100644 vendor/firebase/php-jwt/composer.json create mode 100644 vendor/firebase/php-jwt/src/BeforeValidException.php create mode 100644 vendor/firebase/php-jwt/src/ExpiredException.php create mode 100644 vendor/firebase/php-jwt/src/JWK.php create mode 100644 vendor/firebase/php-jwt/src/JWT.php create mode 100644 vendor/firebase/php-jwt/src/Key.php create mode 100644 vendor/firebase/php-jwt/src/SignatureInvalidException.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7880b3d --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "name": "hyperzlib/isekai-chat-complete", + "type": "library", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Lex Lim", + "email": "hyperzlib@outlook.com" + } + ], + "require": { + "firebase/php-jwt": "^5.2.0" + } +} diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..c83b70d --- /dev/null +++ b/extension.json @@ -0,0 +1,108 @@ +{ + "name": "Isekai Chat Complete", + "namemsg": "isekai-chatcomplete", + "author": "Hyperzlib", + "version": "1.0.0", + "url": "https://github.com/Isekai-Project/mediawiki-extension-IsekaiChatComplete", + "descriptionmsg": "isekai-chatcomplete-desc", + "license-name": "GPL-2.0-or-later", + "type": "api", + "requires": { + "MediaWiki": ">= 1.39.0" + }, + "MessagesDirs": { + "IsekaiChatComplete": [ + "i18n" + ] + }, + "GroupPermissions": { + "user": { + "ccembeddingpage": true, + "chatcomplete": true + }, + "sysop": { + "unlimitedchatcomplete": true, + "chatcomplete": true, + "chatcompletebot": true + } + }, + "GrantPermissions": { + "chatcomplete": { + "ccembeddingpage": true, + "chatcomplete": true + }, + "chatcompletebot": { + "chatcompletebot": true, + "unlimitedchatcomplete": true + } + }, + "AvailableRights": [ + "chatcomplete", + "ccembeddingpage", + "unlimitedchatcomplete", + "chatcompletebot" + ], + "GrantPermissionGroups": { + "chatcomplete": "chatcomplete", + "chatcompletebot": "chatcompletebot" + }, + "AutoloadNamespaces": { + "Isekai\\ChatComplete\\": "includes" + }, + "Hooks": { + "BeforePageDisplay": [ + "Isekai\\ChatComplete\\Hooks::onLoad" + ], + "ResourceLoaderGetConfigVars": [ + "Isekai\\ChatComplete\\Hooks::onResourceLoaderGetConfigVars" + ] + }, + "APIModules": { + "chatcomplete": "Isekai\\ChatComplete\\Api\\ApiChatComplete", + "chatcompletebot": "Isekai\\ChatComplete\\Api\\ApiChatCompleteBot" + }, + "ResourceModules": { + "ext.isekai.chatcomplete.launcher": { + "scripts": [ + "ext.isekai.chatcomplete.launcher.js" + ], + "styles": [], + "dependencies": [ + "ext.isekai.baseWidgets", + "oojs-ui.styles.icons-content" + ], + "targets": [ + "desktop", + "mobile" + ], + "messages": [ + "isekai-chatcomplete-menubutton" + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "modules", + "remoteExtPath": "IsekaiChatComplete/modules" + }, + "config": { + "IsekaiChatCompleteTokenId": { + "value": "" + }, + "IsekaiChatCompleteToken": { + "value": "" + }, + "IsekaiChatCompleteUserPoints": { + "value": null + }, + "IsekaiChatCompleteUserAvatar": { + "value": { + "type": "gravatar" + } + }, + "IsekaiChatCompleteFrontendUrl": { + "value": "/chatcomplete/{title}?token={token}" + } + }, + "load_composer_autoloader": true, + "manifest_version": 2 +} \ No newline at end of file diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json new file mode 100644 index 0000000..f0c6a8f --- /dev/null +++ b/i18n/zh-hans.json @@ -0,0 +1,52 @@ +{ + "isekai-chatcomplete": "异世界百科 智能聊天助理", + "isekai-chatcomplete-desc": "支持询问智能聊天助理页面相关的内容。", + + "isekai-chatcomplete-menubutton": "启动智能助理", + + "action-chatcomplete": "使用 Chat Complete 功能", + "right-chatcomplete": "使用 Chat Complete 功能", + + "action-unlimitedchatcomplete": "无限次数使用 Chat Complete", + "right-unlimitedchatcomplete": "无限次数使用 Chat Complete", + + "action-ccembeddingpage": "创建页面的向量索引", + "right-ccembeddingpage": "创建页面的向量索引", + + "grant-group-chatcomplete": "使用 Chat Complete", + "grant-chatcomplete": "使用 Chat Complete", + + "action-chatcompletebot": "访问 Chat Complete 管理API", + "right-chatcompletebot": "访问 Chat Complete 管理API", + + "grant-group-chatcompletebot": "Chat Complete 管理API", + "grant-chatcompletebot": "Chat Complete 管理API", + + "apihelp-chatcomplete-summary": "Chat Complete 相关 API", + "apihelp-chatcomplete-param-method": "操作", + + "apihelp-chatcomplete+checkaccess-summary": "检测是否拥有权限,并获取使用开销", + "apihelp-chatcomplete+checkaccess-param-ccaction": "使用的操作", + "apihelp-chatcomplete+checkaccess-param-tokens": "提问 Token 数", + "apihelp-chatcomplete+checkaccess-param-extractlines": "从文档中提取的信息行数上限", + + "apihelp-chatcomplete+createtoken-summary": "创建访问 Chat Complete 所需的 Token", + + "apihelp-chatcompletebot-summary": "Chat Complete 管理API", + "apihelp-chatcompletebot-description": "为 Chat Complete 独立后端提供管理API", + + "apihelp-chatcompletebot-param-method": "操作", + + "apihelp-chatcompletebot+reportusage-summary": "报告用户使用 Chat Complete 的情况。需要 chatcompletebot 权限。", + "apihelp-chatcompletebot+reportusage-param-userid": "用户", + "apihelp-chatcompletebot+reportusage-param-useraction": "使用的操作", + "apihelp-chatcompletebot+reportusage-param-tokens": "使用的Token数", + "apihelp-chatcompletebot+reportusage-param-extractlines": "从文档中提取的信息行数", + "apihelp-chatcompletebot+reportusage-param-step": "操作的阶段", + "apihelp-chatcompletebot+reportusage-param-error": "报告错误信息", + "apihelp-chatcompletebot+reportusage-param-transactionid": "当step为end或cancel时需要事务ID", + + "apierror-isekai-chatcomplete-nopermission": "没有使用智能聊天助理的权限", + "apierror-isekai-chatcomplete-user-not-exists": "用户不存在", + "apierror-isekai-chatcomplete-noenoughpoints": "积分不足" +} \ No newline at end of file diff --git a/includes/Api/ApiChatComplete.php b/includes/Api/ApiChatComplete.php new file mode 100644 index 0000000..bd3e964 --- /dev/null +++ b/includes/Api/ApiChatComplete.php @@ -0,0 +1,105 @@ + 'ccembeddingpage', + 'chatcomplete' => 'chatcomplete', + ]; + + public static $methodModules = [ + 'checkaccess' => [ + 'class' => ApiCheckAccess::class, + ], + 'createtoken' => [ + 'class' => ApiCreateToken::class, + ], + ]; + + private $mModuleMgr; + private $mParams; + /** + * @param ApiMain $main + * @param string $action + */ + public function __construct( ApiMain $main, $action ) + { + parent::__construct($main, $action); + + $this->mModuleMgr = new ApiModuleManager( + $this, + MediaWikiServices::getInstance()->getObjectFactory() + ); + + // Allow custom modules to be added in LocalSettings.php + $config = $this->getConfig(); + $this->mModuleMgr->addModules( self::$methodModules, 'method' ); + } + + /** + * Overrides to return this instance's module manager. + * @return ApiModuleManager + */ + public function getModuleManager() { + return $this->mModuleMgr; + } + + public function execute() { + $this->mParams = $this->extractRequestParams(); + + $modules = []; + $this->instantiateModules( $modules, 'method' ); + $this->getResult()->addValue(null, 'modules', array_keys($modules)); + + foreach ( $modules as $module ) { + $module->execute(); + } + } + + /** + * Create instances of all modules requested by the client + * @param array &$modules To append instantiated modules to + * @param string $param Parameter name to read modules from + */ + private function instantiateModules( &$modules, $param ) { + $wasPosted = $this->getRequest()->wasPosted(); + if ( isset( $this->mParams[$param] ) ) { + foreach ( $this->mParams[$param] as $moduleName ) { + $instance = $this->mModuleMgr->getModule( $moduleName, $param ); + if ( $instance === null ) { + ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); + } + if ( !$wasPosted && $instance->mustBePosted() ) { + $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] ); + } + // Ignore duplicates. TODO 2.0: die()? + if ( !array_key_exists( $moduleName, $modules ) ) { + $modules[$moduleName] = $instance; + } + } + } + } + + public function getAllowedParams( $flags = 0 ) { + $result = [ + 'method' => [ + ParamValidator::PARAM_ISMULTI => true, + ParamValidator::PARAM_TYPE => 'submodule', + ], + ]; + + return $result; + } + + public function isInternal() { + return true; + } +} \ No newline at end of file diff --git a/includes/Api/ApiChatCompleteBot.php b/includes/Api/ApiChatCompleteBot.php new file mode 100644 index 0000000..105a313 --- /dev/null +++ b/includes/Api/ApiChatCompleteBot.php @@ -0,0 +1,100 @@ + [ + 'class' => ApiReportUsage::class, + ], + 'userinfo' => [ + 'class' => ApiUserInfo::class, + ], + ]; + + private $mModuleMgr; + private $mParams; + /** + * @param ApiMain $main + * @param string $action + */ + public function __construct( ApiMain $main, $action ) + { + parent::__construct($main, $action); + + $this->mModuleMgr = new ApiModuleManager( + $this, + MediaWikiServices::getInstance()->getObjectFactory() + ); + + // Allow custom modules to be added in LocalSettings.php + $config = $this->getConfig(); + $this->mModuleMgr->addModules( self::$methodModules, 'method' ); + } + + /** + * Overrides to return this instance's module manager. + * @return ApiModuleManager + */ + public function getModuleManager() { + return $this->mModuleMgr; + } + + public function execute() { + $this->mParams = $this->extractRequestParams(); + + $modules = []; + $this->instantiateModules( $modules, 'method' ); + $this->getResult()->addValue(null, 'modules', array_keys($modules)); + + foreach ( $modules as $module ) { + $module->execute(); + } + } + + /** + * Create instances of all modules requested by the client + * @param array &$modules To append instantiated modules to + * @param string $param Parameter name to read modules from + */ + private function instantiateModules( &$modules, $param ) { + $wasPosted = $this->getRequest()->wasPosted(); + if ( isset( $this->mParams[$param] ) ) { + foreach ( $this->mParams[$param] as $moduleName ) { + $instance = $this->mModuleMgr->getModule( $moduleName, $param ); + if ( $instance === null ) { + ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); + } + if ( !$wasPosted && $instance->mustBePosted() ) { + $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] ); + } + // Ignore duplicates. TODO 2.0: die()? + if ( !array_key_exists( $moduleName, $modules ) ) { + $modules[$moduleName] = $instance; + } + } + } + } + + public function getAllowedParams( $flags = 0 ) { + $result = [ + 'method' => [ + ParamValidator::PARAM_ISMULTI => true, + ParamValidator::PARAM_TYPE => 'submodule', + ], + ]; + + return $result; + } + + public function isInternal() { + return true; + } +} \ No newline at end of file diff --git a/includes/Api/ChatComplete/ApiCheckAccess.php b/includes/Api/ChatComplete/ApiCheckAccess.php new file mode 100644 index 0000000..63cbaa3 --- /dev/null +++ b/includes/Api/ChatComplete/ApiCheckAccess.php @@ -0,0 +1,115 @@ +getMain(), $method); + + $this->mParent = $main; + $this->services = MediaWikiServices::getInstance(); + } + + public function execute() { + $userAction = strtolower($this->getParameter('ccaction')); + $tokens = $this->getParameter('tokens'); + $extractLimit = $this->getParameter('extractlines'); + + $user = $this->getUser(); + + if (!$user->isRegistered()) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + return false; + } + + $result = $this->getResult(); + + $permissionMap = [ + 'embeddingpage' => 'ccembeddingpage', + 'chatcomplete' => 'chatcomplete', + ]; + + $permissionManager = $this->services->getPermissionManager(); + + $permissionKey = $permissionMap[$userAction] ?? null; + if ($permissionKey && !$user->isAllowed($permissionKey)) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + $result->addValue(['chatcomplete', $this->getModuleName()], 'available', false); + return false; + } + + $noCost = false; + if ($userAction === 'chatcomplete' && $user->isAllowed('unlimitedchatcomplete')) { + $noCost = true; + } + + $isAvailable = true; + + $pointCostData = ChatCompleteUtils::getPointCost($userAction, $tokens, $extractLimit); + if ($pointCostData && !$noCost) { + list($pointType, $pointCost) = $pointCostData; + + $result->addValue(['chatcomplete', $this->getModuleName()], 'pointtype', $pointType); + $result->addValue(['chatcomplete', $this->getModuleName()], 'pointcost', $pointCost); + + // Check if user have enough points + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUser($user, $pointType); + + if (!$pointService->hasEnoughPoints($pointCost)) { // User doesn't have enough points + $this->addError('apierror-isekai-chatcomplete-notenoughpoints', 'notenoughpoints'); + $isAvailable = false; + } + } else { + $result->addValue(['chatcomplete', $this->getModuleName()], 'pointtype', null); + $result->addValue(['chatcomplete', $this->getModuleName()], 'pointcost', 0); + } + + $result->addValue(['chatcomplete', $this->getModuleName()], 'available', $isAvailable); + + return true; + } + + public function getAllowedParams($flags = 0) { + return [ + 'ccaction' => [ + ParamValidator::PARAM_TYPE => [ + 'embeddingpage', + 'chatcomplete', + ], + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => true, + ], + 'tokens' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => 100, + ParamValidator::PARAM_REQUIRED => false, + ], + 'extractlines' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => 5, + ParamValidator::PARAM_REQUIRED => false, + ], + ]; + } + + public function getCacheMode($params) { + return 'private'; + } + + public function getParent() { + return $this->mParent; + } +} \ No newline at end of file diff --git a/includes/Api/ChatComplete/ApiCreateToken.php b/includes/Api/ChatComplete/ApiCreateToken.php new file mode 100644 index 0000000..dc3660a --- /dev/null +++ b/includes/Api/ChatComplete/ApiCreateToken.php @@ -0,0 +1,80 @@ +getMain(), $method); + + $this->mParent = $main; + $this->services = MediaWikiServices::getInstance(); + $this->config = $this->getConfig(); + } + + public function execute() { + $user = $this->getUser(); + + if (!$user->isRegistered()) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + return false; + } + + $result = $this->getResult(); + + $permissionMap = ApiChatComplete::$permissionMap; + + if (!$user->isAllowedAny(...array_values($permissionMap))) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + return false; + } + + // Create JWT + $tokenId = $this->config->get('IsekaiChatCompleteTokenId'); + $token = $this->config->get('IsekaiChatCompleteToken'); + + $currentTime = time(); + $expireTime = $currentTime + 86400; // 1 day + + $payload = [ + 'iss' => 'mwchatcomplete', + 'sub' => $user->getId(), + 'userName' => $user->getName(), + 'iat' => $currentTime, + 'exp' => $expireTime, + ]; + + $jwt = JWT::encode($payload, $token, 'HS256', $tokenId); + + $result->addValue(['chatcomplete', $this->getModuleName()], 'token', $jwt); + + return true; + } + + public function getCacheMode($params) { + return 'private'; + } + + public function getParent() { + return $this->mParent; + } + + public function needsToken() { + return 'csrf'; + } +} \ No newline at end of file diff --git a/includes/Api/ChatCompleteBot/ApiReportUsage.php b/includes/Api/ChatCompleteBot/ApiReportUsage.php new file mode 100644 index 0000000..56fd2a6 --- /dev/null +++ b/includes/Api/ChatCompleteBot/ApiReportUsage.php @@ -0,0 +1,379 @@ +getMain(), $method); + + $this->mParent = $main; + $this->services = MediaWikiServices::getInstance(); + $this->cache = $this->services->getMainWANObjectCache(); + } + + public function requireParameter(array $paramKeys) { + $missing = []; + foreach ($paramKeys as $paramKey) { + if ($this->getParameter($paramKey) === null) { + $missing[] = $paramKey; + } + } + + if (count($missing) > 0) { + $this->dieWithError( [ + 'apierror-missingparam', + Message::listParam( array_map( + function ( $p ) { + return '' . $this->encodeParamName( $p ) . ''; + }, + $missing + ) ), + count( $missing ), + ], 'missingparam' ); + } + } + + public function execute() { + // Check permission + $this->checkUserRightsAny('chatcompletebot'); + + $step = $this->getParameter('step'); + + $result = $this->getResult(); + if ($step) { + switch ($step) { + case 'check': + $this->requireParameter(['userid', 'useraction']); + + $userAction = $this->getParameter('useraction'); + $tokens = $this->getParameter('tokens'); + $extractLines = $this->getParameter('extractlines'); + + $pointCostData = ChatCompleteUtils::getPointCost($userAction, $tokens, $extractLines); + list($pointType, $pointCost) = $pointCostData; + + $result = $this->getResult(); + + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointtype', $pointType); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointcost', $pointCost); + case 'start': + $this->requireParameter(['userid', 'useraction']); + + $userId = $this->getParameter('userid'); + $userAction = $this->getParameter('useraction'); + $tokens = $this->getParameter('tokens'); + $extractLines = $this->getParameter('extractlines'); + + $user = $this->services->getUserFactory()->newFromId($userId); + + if (!$user->isRegistered()) { + $this->addError('apierror-isekai-chatcomplete-user-not-exists', 'user-not-exists'); + return false; + } + + $permissionManager = $this->services->getPermissionManager(); + $permissionMap = ApiChatComplete::$permissionMap; + + $permissionKey = $permissionMap[$userAction] ?? null; + if ($permissionKey && !$user->isAllowedAny($permissionKey)) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission', [ + 'permission' => $permissionKey, + 'user' => $user->getName(), + 'user_permissions' => $permissionManager->getUserPermissions($this->getUser()), + ]); + return false; + } + + $noCost = false; + if ($user->isAllowedAny('unlimitedchatcomplete')) { + $noCost = true; + } + + $transactionid = bin2hex(random_bytes(8)); + + $data = [ + 'userid' => $userId, + 'useraction' => $userAction, + 'tokens' => $tokens, + 'step' => $step, + ]; + + $pointCostData = ChatCompleteUtils::getPointCost($userAction, $tokens, $extractLines); + if ($pointCostData && !$noCost) { + list($pointType, $pointCost) = $pointCostData; + + $data['pointtype'] = $pointType; + + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUserId($userId, $pointType); + + if (!$pointService->hasEnoughPoints($pointCost)) { // User doesn't have enough points + $this->addError('apierror-isekai-chatcomplete-noenoughpoints', 'noenoughpoints'); + return false; + } + + $pointTransactionId = $pointService->startConsumePointsTransaction($pointCost); + $data['pointtransaction'] = $pointTransactionId; + } + + $this->cache->set( + $this->cache->makeKey('chatcomplete', 'reportusage', 'transaction', $transactionid), + $data, + self::TRANSACTION_TIMEOUT + ); + + $result->addValue(['chatcompletebot', $this->getModuleName()], 'success', 1); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'transactionid', $transactionid); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointtype', $pointType); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointcost', $pointCost); + break; + case 'end': + $transactionId = $this->getParameter('transactionid'); + $transactionData = $this->cache->get( + $this->cache->makeKey('chatcomplete', 'reportusage', 'transaction', $transactionId) + ); + + $isSuccess = true; + + if ($transactionData) { + $pointTransactionId = $transactionData['pointtransaction'] ?? null; + $pointType = $transactionData['pointtype'] ?? null; + if ($pointTransactionId && $pointType) { + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUserId($transactionData['userid'], $pointType); + + $ret = $pointService->commitConsumePointsTransaction($pointTransactionId); + if (!$ret) { + $this->addWarning('apiwarn-isekai-chatcomplete-pointtransactionfailed', 'pointtransactionfailed'); + $isSuccess = false; + } + } + + $realTokens = $this->getParameter('tokens') ?? $transactionData['tokens']; + + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + $dbw->insert( + 'chatcomplete_usage', + [ + 'user_id' => $transactionData['userid'], + 'action' => $transactionData['useraction'], + 'tokens' => $transactionData['tokens'], + 'real_tokens' => $realTokens, + 'timestamp' => $dbw->timestamp(), + 'success' => 1, + ], + __METHOD__ + ); + + $this->cache->delete( + $this->cache->makeKey('chatcomplete', 'reportusage', 'transaction', $transactionId) + ); + } else { + $this->addWarning('apierror-isekai-chatcomplete-invalidtransaction', 'invalidtransaction'); + $isSuccess = false; + } + + $result->addValue(['chatcompletebot', $this->getModuleName()], 'success', $isSuccess ? 1 : 0); + break; + case 'cancel': + $transactionId = $this->getParameter('transactionid'); + $transactionData = $this->cache->get( + $this->cache->makeKey('chatcomplete', 'reportusage', 'transaction', $transactionId) + ); + + $isSuccess = true; + + if ($transactionData) { + $pointTransactionId = $transactionData['pointtransaction'] ?? null; + $pointType = $transactionData['pointtype'] ?? null; + if ($pointTransactionId && $pointType) { + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUserId($transactionData['userid'], $pointType); + + $ret = $pointService->rollbackConsumePointsTransaction($pointTransactionId); + if (!$ret) { + $this->addWarning('apiwarn-isekai-chatcomplete-pointtransactionfailed', 'pointtransactionfailed'); + $isSuccess = false; + } + } + + $reportedError = $this->getParameter('error'); + + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + $dbw->insert( + 'chatcomplete_usage', + [ + 'user_id' => $transactionData['userid'], + 'action' => $transactionData['useraction'], + 'tokens' => $transactionData['tokens'], + 'timestamp' => $dbw->timestamp(), + 'success' => 0, + 'error' => $reportedError, + ], + __METHOD__ + ); + + $this->cache->delete( + $this->cache->makeKey('chatcomplete', 'reportusage', 'transaction', $transactionId) + ); + } else { + $this->addWarning('apierror-isekai-chatcomplete-invalidtransaction', 'invalidtransaction'); + $isSuccess = false; + } + + $result->addValue(['chatcompletebot', $this->getModuleName()], 'success', $isSuccess ? 1 : 0); + break; + } + } else { + $this->requireParameter(['userid', 'useraction']); + + $userId = $this->getParameter('userid'); + $userAction = strtolower($this->getParameter('useraction')); + $realTokens = $tokens = $this->getParameter('tokens'); + $extractLines = $this->getParameter('extractlines'); + + $user = $this->services->getUserFactory()->newFromId($userId); + + if (!$user->isRegistered()) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + return false; + } + + $permissionMap = ApiChatComplete::$permissionMap; + + $permissionKey = $permissionMap[$userAction] ?? null; + if ($permissionKey && !$user->isAllowed($permissionKey)) { + $this->addError('apierror-isekai-chatcomplete-nopermission', 'nopermission'); + return false; + } + + $noCost = false; + if ($userAction === 'chatcomplete' && $user->isAllowed('unlimitedchatcomplete')) { + $noCost = true; + } + + $pointCostData = ChatCompleteUtils::getPointCost($userAction, $tokens, $extractLines); + + $isSuccess = true; + + if ($pointCostData && !$noCost) { + list($pointType, $pointCost) = $pointCostData; + + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUserId($userId, $pointType); + + if (!$pointService->hasEnoughPoints($pointCost)) { // User doesn't have enough points + $this->addError('apierror-isekai-chatcomplete-notenoughpoints', 'notenoughpoints'); + return false; + } + + $ret = $pointService->consumePoints($pointCost, true); + if (!$ret) { + $this->addWarning('apiwarn-isekai-chatcomplete-pointtransactionfailed', 'pointtransactionfailed'); + $isSuccess = false; + } + } + + $reportedError = $this->getParameter('error'); + + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + $dbw->insert( + 'chatcomplete_usage', + [ + 'user_id' => $userId, + 'action' => $userAction, + 'tokens' => $tokens, + 'real_tokens' => $realTokens, + 'timestamp' => $dbw->timestamp(), + 'success' => $reportedError ? 0 : 1, + 'error' => $reportedError, + ], + __METHOD__ + ); + + $result->addValue(['chatcompletebot', $this->getModuleName()], 'success', $isSuccess ? 1 : 0); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointtype', $pointType); + $result->addValue(['chatcompletebot', $this->getModuleName()], 'pointcost', $pointCost); + } + + return true; + } + + public function getAllowedParams($flags = 0) { + return [ + 'userid' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => false, + ], + 'useraction' => [ + ParamValidator::PARAM_TYPE => [ + 'embeddingpage', + 'chatcomplete', + ], + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => false, + ], + 'tokens' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => 100, + ParamValidator::PARAM_REQUIRED => false, + ], + 'extractlines' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => 5, + ParamValidator::PARAM_REQUIRED => false, + ], + 'error' => [ + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => false, + ], + 'step' => [ + ParamValidator::PARAM_TYPE => [ + 'check', + 'start', + 'end', + 'cancel', + ], + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => false, + ], + 'transactionid' => [ + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => false, + ], + ]; + } + + public function getCacheMode($params) { + return 'private'; + } + + public function getParent() { + return $this->mParent; + } +} \ No newline at end of file diff --git a/includes/Api/ChatCompleteBot/ApiUserInfo.php b/includes/Api/ChatCompleteBot/ApiUserInfo.php new file mode 100644 index 0000000..33f482e --- /dev/null +++ b/includes/Api/ChatCompleteBot/ApiUserInfo.php @@ -0,0 +1,131 @@ +getMain(), $method); + + $this->mParent = $main; + $this->services = MediaWikiServices::getInstance(); + $this->config = $this->services->getMainConfig(); + } + + public function requireParameter(array $paramKeys) { + $missing = []; + foreach ($paramKeys as $paramKey) { + if ($this->getParameter($paramKey) === null) { + $missing[] = $paramKey; + } + } + + if (count($missing) > 0) { + $this->dieWithError( [ + 'apierror-missingparam', + Message::listParam( array_map( + function ( $p ) { + return '' . $this->encodeParamName( $p ) . ''; + }, + $missing + ) ), + count( $missing ), + ], 'missingparam' ); + } + } + + public function execute() { + // Check permission + $this->checkUserRightsAny('chatcompletebot'); + + $userFactory = $this->services->getUserFactory(); + $userOptionsLookup = $this->services->getUserOptionsLookup(); + + $userId = $this->getParameter('userid'); + + $user = $userFactory->newFromId($userId); + + if (!$user->isRegistered()) { + $this->dieWithError('apierror-chatcomplete-user-not-found', 'user-not-found'); + } + + $userInfo = [ + 'userid' => $user->getId(), + 'username' => $user->getName(), + 'realname' => $user->getRealName(), + // 'nickname' => $userOptionsLookup->getOption($user, 'nickname'), + 'email' => $user->getEmail(), + ]; + + // Get user points from IsekaiUserPoints + $pointConfig = $this->config->get('IsekaiChatCompleteUserPoints'); + if (is_array($pointConfig) && isset($pointConfig[self::DEFAULT_CC_ACTION])) { + $pointType = $pointConfig[self::DEFAULT_CC_ACTION]['pointType']; + + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $pointFactory = $this->services->getService('IsekaiUserPoints'); + $pointService = $pointFactory->newFromUserId($userId, $pointType); + + if ($pointService) { + $userInfo['points'] = $pointService->points; + } + } + + // Get user avatar + $avatarTpl = $this->config->get('IsekaiChatCompleteUserAvatar'); + $avatarUrl = null; + if (is_array($avatarTpl) && isset($avatarTpl['type'])) { + if ($avatarTpl['type'] === 'gravatar') { + $gravatarUrl = $avatarTpl['mirror'] ?? 'https://secure.gravatar.com/avatar/'; + $gravatarUrl .= md5(strtolower(trim($user->getEmail()))) . '?s=256'; + $avatarUrl = $gravatarUrl; + } elseif ($avatarTpl['type'] === 'local') { + $avatarUrl = $avatarTpl['url'] ?? ''; + $avatarUrl = str_replace(['{username}', '{userid}'], [$user->getName(), $user->getId()], $avatarUrl); + } + } + if ($avatarUrl) { + if (strpos($avatarUrl, 'http://') === false && strpos($avatarUrl, 'https://') === false) { + $avatarUrl = $this->config->get(MainConfigNames::Server) . $avatarUrl; + } + $userInfo['avatar'] = $avatarUrl; + } + + $result = $this->getResult(); + $result->addValue(['chatcompletebot'], $this->getModuleName(), $userInfo); + } + + public function getAllowedParams($flags = 0) { + return [ + 'userid' => [ + ParamValidator::PARAM_TYPE => 'integer', + ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_REQUIRED => true, + ], + ]; + } + + public function getCacheMode($params) { + return 'private'; + } + + public function getParent() { + return $this->mParent; + } +} \ No newline at end of file diff --git a/includes/ChatCompleteUtils.php b/includes/ChatCompleteUtils.php new file mode 100644 index 0000000..2ff9e8a --- /dev/null +++ b/includes/ChatCompleteUtils.php @@ -0,0 +1,43 @@ +getMainConfig()->get('IsekaiChatCompleteUserPoints'); + if (!is_array($pointConfig) && !isset($pointConfig[$action])) { + return false; + } + + $pointConfig = $pointConfig[$action]; + if (!isset($pointConfig['pointType'])) { + return false; + } + + $cost = 0; + $pointType = $pointConfig['pointType']; + + // Fixed cost + if (isset($pointConfig['fixedCost'])) { + $cost += $pointConfig['fixedCost']; + } + + // Per token cost + if (isset($pointConfig['perTokenCost'])) { + if (isset($pointConfig['fixedTokens'])) { + $tokens = max(0, $tokens - $pointConfig['fixedTokens']); + } + $cost += $pointConfig['perTokenCost'] * $tokens; + } + + // Per extract cost + if (isset($pointConfig['perExtractCost'])) { + $cost += $pointConfig['perExtractCost'] * $extractLimit; + } + + $cost = (int) ceil($cost); + + return [$pointType, $cost]; + } +} \ No newline at end of file diff --git a/includes/Hooks.php b/includes/Hooks.php new file mode 100644 index 0000000..2ddd6ba --- /dev/null +++ b/includes/Hooks.php @@ -0,0 +1,19 @@ +getUser(); + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); + if ($user->isRegistered() && $permissionManager->userHasRight($user, 'chatcomplete')) { + $outputPage->addModules(["ext.isekai.chatcomplete.launcher"]); + } + } + + public static function onResourceLoaderGetConfigVars(array &$vars, string $skin, Config $config){ + $vars['wgIsekaiChatCompleteFrontendUrl'] = $config->get('IsekaiChatCompleteFrontendUrl'); + } +} \ No newline at end of file diff --git a/modules/ext.isekai.chatcomplete.launcher.js b/modules/ext.isekai.chatcomplete.launcher.js new file mode 100644 index 0000000..6c79649 --- /dev/null +++ b/modules/ext.isekai.chatcomplete.launcher.js @@ -0,0 +1,61 @@ +var authToken = ''; +var tokenRefreshTime = 0; + +function createToken() { + var api = new mw.Api(); + return api.postWithToken('csrf', { + action: 'chatcomplete', + method: 'createtoken' + }); +} + +function openChatCompletePage(token) { + var urlTemplate = mw.config.get('wgIsekaiChatCompleteFrontendUrl'); + var title = mw.config.get('wgTitle'); + var url = urlTemplate.replace(/\{title\}/g, encodeURIComponent(title)).replace(/\{token\}/g, token); + window.open(url, '_blank'); +} + +function launchChatComplete() { + var currentTime = new Date().getTime(); + if (currentTime - tokenRefreshTime > 3600000) { + mw.notify(mw.msg('isekai-chatcomplete-loading'), { + id: "loading-chatcomplete-notify", + autoHide: false + }); + createToken().done(function(data) { + if (data.chatcomplete && data.chatcomplete.createtoken) { + authToken = data.chatcomplete.createtoken.token; + tokenRefreshTime = new Date().getTime(); + openChatCompletePage(authToken); + } + }); + } else { + openChatCompletePage(authToken); + } +} + +$(function() { + if (mw.config.get('wgIsArticle')) { + var menuIcon = new OO.ui.IconWidget({ icon: 'robot' }); + isekai.fab.addButton({ + id: 'chatcomplete-launcher', + label: mw.msg('isekai-chatcomplete-menubutton'), + icon: menuIcon.$element[0], + priority: 90, + onClick: function() { + launchChatComplete(); + } + }); + var bottomMenuIcon = new OO.ui.IconWidget({ icon: 'robot' }); + bottomNavBtn = isekai.bottomNav.addButton({ + id: 'chatcomplete-launcher', + label: mw.msg('isekai-chatcomplete-menubutton'), + icon: bottomMenuIcon.$element[0], + priority: 90, + onClick: function() { + launchChatComplete(); + } + }); + } +}); \ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..1b0cfad --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/firebase/php-jwt/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..49b70ad --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..1101f23 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Firebase\\JWT\\' => 13, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Firebase\\JWT\\' => + array ( + 0 => __DIR__ . '/..' . '/firebase/php-jwt/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit4a641ef89ab4ebc68d910ae89f3831d8::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit4a641ef89ab4ebc68d910ae89f3831d8::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit4a641ef89ab4ebc68d910ae89f3831d8::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..b7e1d3f --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,66 @@ +{ + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v5.5.1", + "version_normalized": "5.5.1.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "time": "2021-11-08T20:18:51+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v5.5.1" + }, + "install-path": "../firebase/php-jwt" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..2fe7c0b --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,32 @@ + array( + 'name' => 'hyperzlib/isekai-chat-complete', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => NULL, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'firebase/php-jwt' => array( + 'pretty_version' => 'v5.5.1', + 'version' => '5.5.1.0', + 'reference' => '83b609028194aa042ea33b5af2d41a7427de80e6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../firebase/php-jwt', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'hyperzlib/isekai-chat-complete' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => NULL, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..7621d4f --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 50300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 5.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/firebase/php-jwt/LICENSE b/vendor/firebase/php-jwt/LICENSE new file mode 100644 index 0000000..11c0146 --- /dev/null +++ b/vendor/firebase/php-jwt/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011, Neuman Vong + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/firebase/php-jwt/README.md b/vendor/firebase/php-jwt/README.md new file mode 100644 index 0000000..1d392cd --- /dev/null +++ b/vendor/firebase/php-jwt/README.md @@ -0,0 +1,289 @@ +[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) +[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) +[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) + +PHP-JWT +======= +A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). + +Installation +------------ + +Use composer to manage your dependencies and download PHP-JWT: + +```bash +composer require firebase/php-jwt +``` + +Optionally, install the `paragonie/sodium_compat` package from composer if your +php is < 7.2 or does not have libsodium installed: + +```bash +composer require paragonie/sodium_compat +``` + +Example +------- +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +$key = "example_key"; +$payload = array( + "iss" => "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +/** + * IMPORTANT: + * You must specify supported algorithms for your application. See + * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + * for a list of spec-compliant algorithms. + */ +$jwt = JWT::encode($payload, $key, 'HS256'); +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); + +print_r($decoded); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; + +/** + * You can add a leeway to account for when there is a clock skew times between + * the signing and verifying servers. It is recommended that this leeway should + * not be bigger than a few minutes. + * + * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef + */ +JWT::$leeway = 60; // $leeway in seconds +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); +``` +Example with RS256 (openssl) +---------------------------- +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +$privateKey = << "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; +echo "Decode:\n" . print_r($decoded_array, true) . "\n"; +``` + +Example with a passphrase +------------------------- + +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +// Your passphrase +$passphrase = '[YOUR_PASSPHRASE]'; + +// Your private key file with passphrase +// Can be generated with "ssh-keygen -t rsa -m pem" +$privateKeyFile = '/path/to/key-with-passphrase.pem'; + +// Create a private key of type "resource" +$privateKey = openssl_pkey_get_private( + file_get_contents($privateKeyFile), + $passphrase +); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +// Get public key from the private key, or pull from from a file. +$publicKey = openssl_pkey_get_details($privateKey)['key']; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +``` + +Example with EdDSA (libsodium and Ed25519 signature) +---------------------------- +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +// Public and private keys are expected to be Base64 encoded. The last +// non-empty line is used so that keys can be generated with +// sodium_crypto_sign_keypair(). The secret keys generated by other tools may +// need to be adjusted to match the input expected by libsodium. + +$keyPair = sodium_crypto_sign_keypair(); + +$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); + +$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'EdDSA'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +```` + +Using JWKs +---------- + +```php +use Firebase\JWT\JWK; +use Firebase\JWT\JWT; + +// Set of keys. The "keys" key is required. For example, the JSON response to +// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk +$jwks = ['keys' => []]; + +// JWK::parseKeySet($jwks) returns an associative array of **kid** to private +// key. Pass this as the second parameter to JWT::decode. +// NOTE: The deprecated $supportedAlgorithm must be supplied when parsing from JWK. +JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +``` + +Changelog +--------- + +#### 5.0.0 / 2017-06-26 +- Support RS384 and RS512. + See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! +- Add an example for RS256 openssl. + See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! +- Detect invalid Base64 encoding in signature. + See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! +- Update `JWT::verify` to handle OpenSSL errors. + See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! +- Add `array` type hinting to `decode` method + See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! +- Add all JSON error types. + See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! +- Bugfix 'kid' not in given key list. + See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! +- Miscellaneous cleanup, documentation and test fixes. + See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), + [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and + [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), + [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! + +#### 4.0.0 / 2016-07-17 +- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! +- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! +- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! +- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! + +#### 3.0.0 / 2015-07-22 +- Minimum PHP version updated from `5.2.0` to `5.3.0`. +- Add `\Firebase\JWT` namespace. See +[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to +[@Dashron](https://github.com/Dashron)! +- Require a non-empty key to decode and verify a JWT. See +[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to +[@sjones608](https://github.com/sjones608)! +- Cleaner documentation blocks in the code. See +[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to +[@johanderuijter](https://github.com/johanderuijter)! + +#### 2.2.0 / 2015-06-22 +- Add support for adding custom, optional JWT headers to `JWT::encode()`. See +[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to +[@mcocaro](https://github.com/mcocaro)! + +#### 2.1.0 / 2015-05-20 +- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew +between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! +- Add support for passing an object implementing the `ArrayAccess` interface for +`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! + +#### 2.0.0 / 2015-04-01 +- **Note**: It is strongly recommended that you update to > v2.0.0 to address + known security vulnerabilities in prior versions when both symmetric and + asymmetric keys are used together. +- Update signature for `JWT::decode(...)` to require an array of supported + algorithms to use when verifying token signatures. + + +Tests +----- +Run the tests using phpunit: + +```bash +$ pear install PHPUnit +$ phpunit --configuration phpunit.xml.dist +PHPUnit 3.7.10 by Sebastian Bergmann. +..... +Time: 0 seconds, Memory: 2.50Mb +OK (5 tests, 5 assertions) +``` + +New Lines in private keys +----- + +If your private key contains `\n` characters, be sure to wrap it in double quotes `""` +and not single quotes `''` in order to properly interpret the escaped characters. + +License +------- +[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). diff --git a/vendor/firebase/php-jwt/composer.json b/vendor/firebase/php-jwt/composer.json new file mode 100644 index 0000000..6146e2d --- /dev/null +++ b/vendor/firebase/php-jwt/composer.json @@ -0,0 +1,36 @@ +{ + "name": "firebase/php-jwt", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "php", + "jwt" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "license": "BSD-3-Clause", + "require": { + "php": ">=5.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + } +} diff --git a/vendor/firebase/php-jwt/src/BeforeValidException.php b/vendor/firebase/php-jwt/src/BeforeValidException.php new file mode 100644 index 0000000..c147852 --- /dev/null +++ b/vendor/firebase/php-jwt/src/BeforeValidException.php @@ -0,0 +1,7 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWK +{ + /** + * Parse a set of JWK keys + * + * @param array $jwks The JSON Web Key Set as an associative array + * + * @return array An associative array that represents the set of keys + * + * @throws InvalidArgumentException Provided JWK Set is empty + * @throws UnexpectedValueException Provided JWK Set was invalid + * @throws DomainException OpenSSL failure + * + * @uses parseKey + */ + public static function parseKeySet(array $jwks) + { + $keys = array(); + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + if ($key = self::parseKey($v)) { + $keys[$kid] = $key; + } + } + + if (0 === \count($keys)) { + throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + } + + return $keys; + } + + /** + * Parse a JWK key + * + * @param array $jwk An individual JWK + * + * @return resource|array An associative array that represents the key + * + * @throws InvalidArgumentException Provided JWK is empty + * @throws UnexpectedValueException Provided JWK was invalid + * @throws DomainException OpenSSL failure + * + * @uses createPemFromModulusAndExponent + */ + public static function parseKey(array $jwk) + { + if (empty($jwk)) { + throw new InvalidArgumentException('JWK must not be empty'); + } + if (!isset($jwk['kty'])) { + throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + } + + switch ($jwk['kty']) { + case 'RSA': + if (!empty($jwk['d'])) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } + + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = \openssl_pkey_get_public($pem); + if (false === $publicKey) { + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + } + return $publicKey; + default: + // Currently only RSA is supported + break; + } + } + + /** + * Create a public key represented in PEM format from RSA modulus and exponent information + * + * @param string $n The RSA modulus encoded in Base64 + * @param string $e The RSA exponent encoded in Base64 + * + * @return string The RSA public key represented in PEM format + * + * @uses encodeLength + */ + private static function createPemFromModulusAndExponent($n, $e) + { + $modulus = JWT::urlsafeB64Decode($n); + $publicExponent = JWT::urlsafeB64Decode($e); + + $components = array( + 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), + 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) + ); + + $rsaPublicKey = \pack( + 'Ca*a*a*', + 48, + self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = \chr(0) . $rsaPublicKey; + $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; + + $rsaPublicKey = \pack( + 'Ca*a*', + 48, + self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), + $rsaOID . $rsaPublicKey + ); + + $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + \chunk_split(\base64_encode($rsaPublicKey), 64) . + '-----END PUBLIC KEY-----'; + + return $rsaPublicKey; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @param int $length + * @return string + */ + private static function encodeLength($length) + { + if ($length <= 0x7F) { + return \chr($length); + } + + $temp = \ltrim(\pack('N', $length), \chr(0)); + + return \pack('Ca*', 0x80 | \strlen($temp), $temp); + } +} diff --git a/vendor/firebase/php-jwt/src/JWT.php b/vendor/firebase/php-jwt/src/JWT.php new file mode 100644 index 0000000..ec1641b --- /dev/null +++ b/vendor/firebase/php-jwt/src/JWT.php @@ -0,0 +1,611 @@ + + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWT +{ + const ASN1_INTEGER = 0x02; + const ASN1_SEQUENCE = 0x10; + const ASN1_BIT_STRING = 0x03; + + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + */ + public static $leeway = 0; + + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * + * Will default to PHP time() value if null. + */ + public static $timestamp = null; + + public static $supported_algs = array( + 'ES384' => array('openssl', 'SHA384'), + 'ES256' => array('openssl', 'SHA256'), + 'HS256' => array('hash_hmac', 'SHA256'), + 'HS384' => array('hash_hmac', 'SHA384'), + 'HS512' => array('hash_hmac', 'SHA512'), + 'RS256' => array('openssl', 'SHA256'), + 'RS384' => array('openssl', 'SHA384'), + 'RS512' => array('openssl', 'SHA512'), + 'EdDSA' => array('sodium_crypto', 'EdDSA'), + ); + + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT + * @param Key|array|mixed $keyOrKeyArray The Key or array of Key objects. + * If the algorithm used is asymmetric, this is the public key + * Each Key object contains an algorithm and matching key. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only + * should be used for backwards compatibility. + * + * @return object The JWT's payload as a PHP object + * + * @throws InvalidArgumentException Provided JWT was empty + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @uses jsonDecode + * @uses urlsafeB64Decode + */ + public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) + { + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + + if (empty($keyOrKeyArray)) { + throw new InvalidArgumentException('Key may not be empty'); + } + $tks = \explode('.', $jwt); + if (\count($tks) != 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { + throw new UnexpectedValueException('Invalid signature encoding'); + } + if (empty($header->alg)) { + throw new UnexpectedValueException('Empty algorithm'); + } + if (empty(static::$supported_algs[$header->alg])) { + throw new UnexpectedValueException('Algorithm not supported'); + } + + list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( + $keyOrKeyArray, + empty($header->kid) ? null : $header->kid + ); + + if (empty($algorithm)) { + // Use deprecated "allowed_algs" to determine if the algorithm is supported. + // This opens up the possibility of an attack in some implementations. + // @see https://github.com/firebase/php-jwt/issues/351 + if (!\in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); + } + } else { + // Check the algorithm + if (!self::constantTimeEquals($algorithm, $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); + } + } + if ($header->alg === 'ES256' || $header->alg === 'ES384') { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures + $sig = self::signatureToDER($sig); + } + + if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } + + // Check the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) + ); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) + ); + } + + // Check if this token has expired. + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + throw new ExpiredException('Expired token'); + } + + return $payload; + } + + /** + * Converts and signs a PHP object or array into a JWT string. + * + * @param object|array $payload PHP object or array + * @param string|resource $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param mixed $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + * + * @uses jsonEncode + * @uses urlsafeB64Encode + */ + public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) + { + $header = array('typ' => 'JWT', 'alg' => $alg); + if ($keyId !== null) { + $header['kid'] = $keyId; + } + if (isset($head) && \is_array($head)) { + $header = \array_merge($head, $header); + } + $segments = array(); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $signing_input = \implode('.', $segments); + + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); + + return \implode('.', $segments); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm or bad key was specified + */ + public static function sign($msg, $key, $alg = 'HS256') + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'hash_hmac': + return \hash_hmac($algorithm, $msg, $key, true); + case 'openssl': + $signature = ''; + $success = \openssl_sign($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); + } elseif ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } + return $signature; + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_detached($msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + } + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm + * + * @return bool + * + * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure + */ + private static function verify($msg, $signature, $key, $alg) + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'openssl': + $success = \openssl_verify($msg, $signature, $key, $algorithm); + if ($success === 1) { + return true; + } elseif ($success === 0) { + return false; + } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException( + 'OpenSSL error: ' . \openssl_error_string() + ); + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + case 'hash_hmac': + default: + $hash = \hash_hmac($algorithm, $msg, $key, true); + return self::constantTimeEquals($signature, $hash); + } + } + + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string + * + * @return object Object representation of JSON string + * + * @throws DomainException Provided string was invalid JSON + */ + public static function jsonDecode($input) + { + if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { + /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + * to specify that large ints (like Steam Transaction IDs) should be treated as + * strings, rather than the PHP default behaviour of converting them to floats. + */ + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + } else { + /** Not all servers will support that, however, so for older versions we must + * manually detect large ints in the JSON string and quote them (thus converting + *them to strings) before decoding, hence the preg_replace() call. + */ + $max_int_length = \strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = \json_decode($json_without_bigints); + } + + if ($errno = \json_last_error()) { + static::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + + /** + * Encode a PHP object into a JSON string. + * + * @param object|array $input A PHP object or array + * + * @return string JSON representation of the PHP object or array + * + * @throws DomainException Provided object could not be encoded to valid JSON + */ + public static function jsonEncode($input) + { + $json = \json_encode($input); + if ($errno = \json_last_error()) { + static::handleJsonError($errno); + } elseif ($json === 'null' && $input !== null) { + throw new DomainException('Null result with non-null input'); + } + return $json; + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string A decoded string + */ + public static function urlsafeB64Decode($input) + { + $remainder = \strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= \str_repeat('=', $padlen); + } + return \base64_decode(\strtr($input, '-_', '+/')); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode($input) + { + return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); + } + + + /** + * Determine if an algorithm has been provided for each Key + * + * @param Key|array|mixed $keyOrKeyArray + * @param string|null $kid + * + * @throws UnexpectedValueException + * + * @return array containing the keyMaterial and algorithm + */ + private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) + { + if ( + is_string($keyOrKeyArray) + || is_resource($keyOrKeyArray) + || $keyOrKeyArray instanceof OpenSSLAsymmetricKey + ) { + return array($keyOrKeyArray, null); + } + + if ($keyOrKeyArray instanceof Key) { + return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); + } + + if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { + if (!isset($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + + $key = $keyOrKeyArray[$kid]; + + if ($key instanceof Key) { + return array($key->getKeyMaterial(), $key->getAlgorithm()); + } + + return array($key, null); + } + + throw new UnexpectedValueException( + '$keyOrKeyArray must be a string|resource key, an array of string|resource keys, ' + . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' + ); + } + + /** + * @param string $left + * @param string $right + * @return bool + */ + public static function constantTimeEquals($left, $right) + { + if (\function_exists('hash_equals')) { + return \hash_equals($left, $right); + } + $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (\ord($left[$i]) ^ \ord($right[$i])); + } + $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + + return ($status === 0); + } + + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() + * + * @return void + */ + private static function handleJsonError($errno) + { + $messages = array( + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 + ); + throw new DomainException( + isset($messages[$errno]) + ? $messages[$errno] + : 'Unknown JSON error: ' . $errno + ); + } + + /** + * Get the number of bytes in cryptographic strings. + * + * @param string $str + * + * @return int + */ + private static function safeStrlen($str) + { + if (\function_exists('mb_strlen')) { + return \mb_strlen($str, '8bit'); + } + return \strlen($str); + } + + /** + * Convert an ECDSA signature to an ASN.1 DER sequence + * + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object + */ + private static function signatureToDER($sig) + { + // Separate the signature into r-value and s-value + list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); + + // Trim leading zeros + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + + // Convert r-value and s-value from unsigned big-endian integers to + // signed two's complement + if (\ord($r[0]) > 0x7f) { + $r = "\x00" . $r; + } + if (\ord($s[0]) > 0x7f) { + $s = "\x00" . $s; + } + + return self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) + ); + } + + /** + * Encodes a value into a DER object. + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER($type, $value) + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = \chr($tag_header | $type); + + // Length + $der .= \chr(\strlen($value)); + + return $der . $value; + } + + /** + * Encodes signature from a DER object. + * + * @param string $der binary signature in DER format + * @param int $keySize the number of bits in the key + * @return string the signature + */ + private static function signatureFromDER($der, $keySize) + { + // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE + list($offset, $_) = self::readDER($der); + list($offset, $r) = self::readDER($der, $offset); + list($offset, $s) = self::readDER($der, $offset); + + // Convert r-value and s-value from signed two's compliment to unsigned + // big-endian integers + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + + // Pad out r and s so that they are $keySize bits long + $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + + return $r . $s; + } + + /** + * Reads binary DER-encoded data and decodes into a single object + * + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode + * @return array [$offset, $data] the new offset and the decoded object + */ + private static function readDER($der, $offset = 0) + { + $pos = $offset; + $size = \strlen($der); + $constructed = (\ord($der[$pos]) >> 5) & 0x01; + $type = \ord($der[$pos++]) & 0x1f; + + // Length + $len = \ord($der[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $size) { + $len = ($len << 8) | \ord($der[$pos++]); + } + } + + // Value + if ($type == self::ASN1_BIT_STRING) { + $pos++; // Skip the first contents octet (padding indicator) + $data = \substr($der, $pos, $len - 1); + $pos += $len - 1; + } elseif (!$constructed) { + $data = \substr($der, $pos, $len); + $pos += $len; + } else { + $data = null; + } + + return array($pos, $data); + } +} diff --git a/vendor/firebase/php-jwt/src/Key.php b/vendor/firebase/php-jwt/src/Key.php new file mode 100644 index 0000000..f1ede6f --- /dev/null +++ b/vendor/firebase/php-jwt/src/Key.php @@ -0,0 +1,59 @@ +keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; + } + + /** + * Return the algorithm valid for this key + * + * @return string + */ + public function getAlgorithm() + { + return $this->algorithm; + } + + /** + * @return string|resource|OpenSSLAsymmetricKey + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/vendor/firebase/php-jwt/src/SignatureInvalidException.php b/vendor/firebase/php-jwt/src/SignatureInvalidException.php new file mode 100644 index 0000000..d35dee9 --- /dev/null +++ b/vendor/firebase/php-jwt/src/SignatureInvalidException.php @@ -0,0 +1,7 @@ +