初始化项目

main
落雨楓 2 years ago
commit 270eb211b5

@ -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"
}
}

@ -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
}

@ -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": "积分不足"
}

@ -0,0 +1,105 @@
<?php
namespace Isekai\ChatComplete\Api;
use ApiBase;
use ApiMain;
use ApiModuleManager;
use Isekai\ChatComplete\Api\ChatComplete\ApiCheckAccess;
use Isekai\ChatComplete\Api\ChatComplete\ApiCreateToken;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
class ApiChatComplete extends ApiBase {
public static $permissionMap = [
'embeddingpage' => '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;
}
}

@ -0,0 +1,100 @@
<?php
namespace Isekai\ChatComplete\Api;
use ApiBase;
use ApiMain;
use ApiModuleManager;
use Isekai\ChatComplete\Api\ChatCompleteBot\ApiReportUsage;
use Isekai\ChatComplete\Api\ChatCompleteBot\ApiUserInfo;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
class ApiChatCompleteBot extends ApiBase {
public static $methodModules = [
'reportusage' => [
'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;
}
}

@ -0,0 +1,115 @@
<?php
namespace Isekai\ChatComplete\Api\ChatComplete;
use ApiBase;
use Isekai\ChatComplete\Api\ApiChatComplete;
use Isekai\ChatComplete\ChatCompleteUtils;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
class ApiCheckAccess extends ApiBase {
/** @var MediaWikiServices */
private $services;
/** @var ApiChatComplete */
private $mParent;
public function __construct(ApiChatComplete $main, $method) {
parent::__construct($main->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;
}
}

@ -0,0 +1,80 @@
<?php
namespace Isekai\ChatComplete\Api\ChatComplete;
use ApiBase;
use Config;
use Isekai\ChatComplete\Api\ApiChatComplete;
use Isekai\ChatComplete\Api\ApiChatCompleteBot;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
use Firebase\JWT\JWT;
class ApiCreateToken extends ApiBase {
/** @var MediaWikiServices */
private $services;
/** @var Config */
private $config;
/** @var ApiChatComplete */
private $mParent;
public function __construct(ApiChatComplete $main, $method) {
parent::__construct($main->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';
}
}

@ -0,0 +1,379 @@
<?php
namespace Isekai\ChatComplete\Api\ChatCompleteBot;
use ApiBase;
use Isekai\ChatComplete\Api\ApiChatComplete;
use Isekai\ChatComplete\Api\ApiChatCompleteBot;
use Isekai\ChatComplete\ChatCompleteUtils;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
use Message;
class ApiReportUsage extends ApiBase {
const TRANSACTION_TIMEOUT = 60 * 10;
/** @var MediaWikiServices */
private $services;
/** @var \WANObjectCache */
private $cache;
/** @var ApiChatCompleteBot */
private $mParent;
public function __construct(ApiChatCompleteBot $main, $method) {
parent::__construct($main->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 '<var>' . $this->encodeParamName( $p ) . '</var>';
},
$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;
}
}

@ -0,0 +1,131 @@
<?php
namespace Isekai\ChatComplete\Api\ChatCompleteBot;
use ApiBase;
use Isekai\ChatComplete\Api\ApiChatCompleteBot;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
use Message;
class ApiUserInfo extends ApiBase {
const DEFAULT_CC_ACTION = 'chatcomplete';
/** @var MediaWikiServices */
private $services;
/** @var Config */
private $config;
/** @var ApiChatCompleteBot */
private $mParent;
public function __construct(ApiChatCompleteBot $main, $method) {
parent::__construct($main->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 '<var>' . $this->encodeParamName( $p ) . '</var>';
},
$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;
}
}

@ -0,0 +1,43 @@
<?php
namespace Isekai\ChatComplete;
use MediaWiki\MediaWikiServices;
class ChatCompleteUtils {
public static function getPointCost(string $action = '', int $tokens, int $extractLimit) {
$pointConfig = MediaWikiServices::getInstance()->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];
}
}

@ -0,0 +1,19 @@
<?php
namespace Isekai\ChatComplete;
use MediaWiki\MediaWikiServices;
use Config;
class Hooks {
public static function onLoad(\OutputPage $outputPage) {
$user = $outputPage->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');
}
}

@ -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();
}
});
}
});

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4a641ef89ab4ebc68d910ae89f3831d8::getLoader();

@ -0,0 +1,585 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @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<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
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<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $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);
}
}

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
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<string>
*/
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<string>
*/
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
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<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

@ -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.

@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
);

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit4a641ef89ab4ebc68d910ae89f3831d8
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit4a641ef89ab4ebc68d910ae89f3831d8', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit4a641ef89ab4ebc68d910ae89f3831d8', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit4a641ef89ab4ebc68d910ae89f3831d8::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

@ -0,0 +1,36 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit4a641ef89ab4ebc68d910ae89f3831d8
{
public static $prefixLengthsPsr4 = array (
'F' =>
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);
}
}

@ -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": []
}

@ -0,0 +1,32 @@
<?php return array(
'root' => 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,
),
),
);

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 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
);
}

@ -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.

@ -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 = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC8kGa1pSjbSYZVebtTRBLxBz5H4i2p/llLCrEeQhta5kaQu/Rn
vuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t0tyazyZ8JXw+KgXTxldMPEL9
5+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4ehde/zUxo6UvS7UrBQIDAQAB
AoGAb/MXV46XxCFRxNuB8LyAtmLDgi/xRnTAlMHjSACddwkyKem8//8eZtw9fzxz
bWZ/1/doQOuHBGYZU8aDzzj59FZ78dyzNFoF91hbvZKkg+6wGyd/LrGVEB+Xre0J
Nil0GReM2AHDNZUYRv+HYJPIOrB0CRczLQsgFJ8K6aAD6F0CQQDzbpjYdx10qgK1
cP59UHiHjPZYC0loEsk7s+hUmT3QHerAQJMZWC11Qrn2N+ybwwNblDKv+s5qgMQ5
5tNoQ9IfAkEAxkyffU6ythpg/H0Ixe1I2rd0GbF05biIzO/i77Det3n4YsJVlDck
ZkcvY3SK2iRIL4c9yY6hlIhs+K9wXTtGWwJBAO9Dskl48mO7woPR9uD22jDpNSwe
k90OMepTjzSvlhjbfuPN1IdhqvSJTDychRwn1kIJ7LQZgQ8fVz9OCFZ/6qMCQGOb
qaGwHmUK6xzpUbbacnYrIM6nLSkXgOAwv7XXCojvY614ILTK3iXiLBOxPu5Eu13k
eUz9sHyD6vkgZzjtxXECQAkp4Xerf5TGfQXGXhxIX52yH+N2LtujCdkQZjXAsGdm
B2zNzvrlgRmgBrklMTrMYgm1NPcW+bRLGcwgW2PTvNM=
-----END RSA PRIVATE KEY-----
EOD;
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
-----END PUBLIC KEY-----
EOD;
$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";
$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).

@ -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"
}
}

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class BeforeValidException extends \UnexpectedValueException
{
}

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class ExpiredException extends \UnexpectedValueException
{
}

@ -0,0 +1,172 @@
<?php
namespace Firebase\JWT;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* JSON Web Key implementation, based on this spec:
* https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Bui Sy Nguyen <nguyenbs@gmail.com>
* @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);
}
}

@ -0,0 +1,611 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use DomainException;
use Exception;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use UnexpectedValueException;
use DateTime;
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Neuman Vong <neuman@twilio.com>
* @author Anant Narayanan <anant@php.net>
* @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<Key>|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<Key>|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);
}
}

@ -0,0 +1,59 @@
<?php
namespace Firebase\JWT;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
class Key
{
/** @var string $algorithm */
private $algorithm;
/** @var string|resource|OpenSSLAsymmetricKey $keyMaterial */
private $keyMaterial;
/**
* @param string|resource|OpenSSLAsymmetricKey $keyMaterial
* @param string $algorithm
*/
public function __construct($keyMaterial, $algorithm)
{
if (
!is_string($keyMaterial)
&& !is_resource($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
) {
throw new InvalidArgumentException('Type error: $keyMaterial must be a string, resource, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
throw new InvalidArgumentException('Type error: $keyMaterial must not be empty');
}
if (!is_string($algorithm)|| empty($keyMaterial)) {
throw new InvalidArgumentException('Type error: $algorithm must be a string');
}
$this->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;
}
}

@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class SignatureInvalidException extends \UnexpectedValueException
{
}
Loading…
Cancel
Save