Compare commits

...

6 Commits

Author SHA1 Message Date
Flatlogic Bot
199e46cb75 5 2025-10-05 18:34:00 +00:00
Flatlogic Bot
1201afcfd8 new 2025-10-05 18:16:57 +00:00
Flatlogic Bot
2e0e56a927 2 2025-10-05 18:08:48 +00:00
Flatlogic Bot
923befa860 Auto commit: 2025-10-05T17:44:42.404Z 2025-10-05 17:44:42 +00:00
Flatlogic Bot
6decd3c147 Revert to version e8307ed 2025-10-04 18:43:18 +00:00
8d402afd99 Edit index.php via Editor 2025-10-04 18:41:34 +00:00
1508 changed files with 234563 additions and 141 deletions

163
actions.php Normal file
View File

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Your Action - Chirivia</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-green: #2E8B57;
--secondary-blue: #4682B4;
--accent-color: #5F9EA0;
--background-light: #F0F8FF;
--surface-white: #FFFFFF;
--text-dark: #2F4F4F;
--font-headings: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
--spacing-base: 8px;
--border-radius: 0.5rem; /* 8px */
--danger-red: #DC3545;
--neutral-gray: #A9A9A9;
}
body {
font-family: var(--font-body);
line-height: 1.6;
margin: 0;
padding: 0;
background-color: var(--background-light);
color: var(--text-dark);
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
background: var(--surface-white);
padding: 1rem 2rem;
border-bottom: 1px solid #E0E0E0;
display: flex;
justify-content: space-between;
align-items: center;
}
.header .logo {
font-family: var(--font-headings);
font-size: 1.5rem;
font-weight: bold;
color: var(--primary-green);
text-decoration: none;
}
.main {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.action-container {
background: var(--surface-white);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
text-align: center;
max-width: 500px;
width: 100%;
}
h1 {
font-family: var(--font-headings);
color: var(--primary-green);
margin-bottom: 1.5rem;
}
.message {
font-size: 1.2rem;
margin-bottom: 2rem;
}
.form-container form {
display: flex;
justify-content: center;
gap: 1rem;
}
.btn {
font-family: var(--font-body);
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
cursor: pointer;
text-decoration: none;
display: inline-block;
color: var(--surface-white);
}
.btn-yes {
background-color: var(--primary-green);
}
.btn-no {
background-color: var(--neutral-gray);
}
.result-message {
margin-top: 2rem;
font-size: 1.2rem;
font-weight: bold;
}
.result-message .chick {
font-size: 2rem;
}
.footer {
text-align: center;
padding: 1.5rem;
background: var(--surface-white);
border-top: 1px solid #E0E0E0;
font-size: 0.9rem;
color: #6c757d;
}
</style>
</head>
<body>
<header class="header">
<a href="index.php" class="logo">Chirivia</a>
</header>
<main class="main">
<div class="action-container">
<h1>Log Your Green Action</h1>
<?php
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['answer'])) {
$answer = $_POST['answer'];
if ($answer === 'yes') {
$message = '<div class="chick">🐣</div> says thank you!';
} elseif ($answer === 'no') {
$message = '<div class="chick">🐣</div> says you should go do it asap!';
}
}
?>
<?php if ($message): ?>
<div class="result-message">
<?php echo $message; ?>
<br><br>
<a href="actions.php" class="btn btn-yes">Log another action</a>
</div>
<?php else: ?>
<p class="message">Did you do something good for the environment today?</p>
<div class="form-container">
<form method="POST" action="actions.php">
<button type="submit" name="answer" value="yes" class="btn btn-yes">Yes, I did!</button>
<button type="submit" name="answer" value="no" class="btn btn-no">Not yet</button>
</form>
</div>
<?php endif; ?>
</div>
</main>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> Chirivia. All rights reserved. | <a href="privacy.php">Privacy Policy</a></p>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

92
chatbot.php Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chatbot</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f2f5; }
#chat-container { width: 400px; height: 600px; border: 1px solid #ccc; border-radius: 8px; display: flex; flex-direction: column; background-color: #fff; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
#chat-log { flex-grow: 1; padding: 10px; overflow-y: auto; border-bottom: 1px solid #ccc; }
#chat-input-container { display: flex; padding: 10px; }
#chat-input { flex-grow: 1; border: 1px solid #ccc; border-radius: 4px; padding: 8px; }
#send-button { margin-left: 10px; padding: 8px 12px; border: none; background-color: #007bff; color: white; border-radius: 4px; cursor: pointer; }
.message { margin-bottom: 10px; padding: 8px 12px; border-radius: 18px; max-width: 70%; word-wrap: break-word; }
.user-message { background-color: #007bff; color: white; align-self: flex-end; margin-left: auto; }
.bot-message { background-color: #e9e9eb; color: black; align-self: flex-start; }
.chat-log-inner { display: flex; flex-direction: column; }
</style>
</head>
<body>
<div id="chat-container">
<div id="chat-log">
<div class="chat-log-inner"></div>
</div>
<div id="chat-input-container">
<input type="text" id="chat-input" placeholder="Type a message...">
<button id="send-button">Send</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const chatLog = document.querySelector('#chat-log .chat-log-inner');
const chatInput = document.getElementById('chat-input');
const sendButton = document.getElementById('send-button');
const appendMessage = (text, sender) => {
const messageElement = document.createElement('div');
messageElement.classList.add('message', sender + '-message');
messageElement.textContent = text;
chatLog.appendChild(messageElement);
chatLog.parentElement.scrollTop = chatLog.parentElement.scrollHeight;
};
const sendMessage = async () => {
const messageText = chatInput.value.trim();
if (messageText === '') return;
appendMessage(messageText, 'user');
chatInput.value = '';
try {
const response = await fetch('chatbot_api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageText })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const botResponse = data.reply || 'Sorry, I could not understand.';
appendMessage(botResponse, 'bot');
} catch (error) {
console.error('Fetch error:', error);
let errorMessage = 'Sorry, something went wrong. Please try again later.';
if (error instanceof TypeError) { // Network error
errorMessage = 'Could not connect to the server. Please check your network connection.';
} else if (error instanceof SyntaxError) { // JSON parsing error
errorMessage = 'Received an invalid response from the server.';
} else if (error.message.startsWith('HTTP error!')) {
errorMessage = 'There was a problem communicating with the server.';
}
appendMessage(errorMessage, 'bot');
}
};
sendButton.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
appendMessage("Hello! I'm a chatbot. How can I help you today?", "bot");
});
</script>
</body>
</html>

73
chatbot_api.php Normal file
View File

@ -0,0 +1,73 @@
<?php
// Set content type to JSON
header('Content-Type: application/json');
// Include the Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
use Google\Cloud\Dialogflow\V2\Client\SessionsClient;
use Google\Cloud\Dialogflow\V2\TextInput;
use Google\Cloud\Dialogflow\V2\QueryInput;
function detect_intent_texts($projectId, $text, $sessionId, $languageCode = 'en-US')
{
// Set the path to your service account key file
$credentialsPath = __DIR__ . '/gcp_creds/dialogflow_key.json';
// Check if the credentials file exists
if (!file_exists($credentialsPath)) {
return "Error: Service account key file not found.";
}
try {
// Create a new sessions client
$sessionsClient = new SessionsClient([
'credentials' => $credentialsPath
]);
// Format the session name
$session = $sessionsClient->sessionName($projectId, $sessionId);
// Create a new text input
$textInput = new TextInput();
$textInput->setText($text);
$textInput->setLanguageCode($languageCode);
// Create a new query input
$queryInput = new QueryInput();
$queryInput->setText($textInput);
// Detect the intent
$response = $sessionsClient->detectIntent($session, $queryInput);
$queryResult = $response->getQueryResult();
$fulfillmentText = $queryResult->getFulfillmentText();
// Close the sessions client
$sessionsClient->close();
return $fulfillmentText;
} catch (Exception $e) {
// Return a generic error message
error_log($e->getMessage());
return "Error processing your request.";
}
}
// Get the user's message from the POST request
$data = json_decode(file_get_contents('php://input'), true);
$userMessage = $data['message'] ?? '';
$sessionId = $data['sessionId'] ?? session_id(); // Use PHP session ID as Dialogflow session ID
// Your Google Cloud Project ID
$projectId = 'chrivia-asxi';
// Get the bot's reply from Dialogflow
if (!empty($userMessage)) {
$botReply = detect_intent_texts($projectId, $userMessage, $sessionId);
} else {
$botReply = 'Please say something.';
}
// Return the bot's reply as JSON
echo json_encode(['reply' => $botReply]);

1781
composer-setup.php Normal file

File diff suppressed because it is too large Load Diff

5
composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"google/cloud-dialogflow": "^2.2"
}
}

1395
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

1
gcp_creds/.htaccess Normal file
View File

@ -0,0 +1 @@
Deny from all

View File

@ -0,0 +1,28 @@
{ "type": "service_account", "project_id": "chrivia-asxi", "private_key_id": "6053badd24ceb066aba55c264b0dc5051bb3ce69", "private_key": "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMCXYJzgk2AR64
rxAVMq9Kf8yD4uYCjIxjtZcYoyrSSmC6V/i6GqQ/CRASzaY5L9NTRWEUN0Ukvlub
lYvIHQoBnsr+Zm28PmuougyJdjLr4GKH++HhviNellkQ/QvwGtBG8WAMwu4HuCyK
8yeEl6mJpdlJGthOvgt2w23BCsfLjOf6aA1dgZwohCzS5GJhsgwggREPjWnSyRIK
YDZaXyfYBgM4imcBSh3lA0ekR87yb+zUY03KLDEo3uWuOplI2d9heKt1ZZoYJCSe
UwZ0kFYwCsPDI8vDVV/dLjX7cB8eqZAHLlgYbGJyFBqTZ3qZv8ezAXUYRdOYcpgg
nyz8dt3nAgMBAAECggEALlil0dNVv0kg19WYIyCItbTy2TBki8auKwX4BNYnZ24S
q7FI48kibtkZqBPDgrDs4TjctNFbKN3+hAhDoJiMCdMui/vrSDuri797EoxhQ7gL
2ZSq+fKNKTKgl53LJOaKUdsJNMzgcatxnrxdyR4EGiqsgRESekxr4TXCC/vtZzw6
GVUBJMAxh8UY8sSCMO4QWBmxbfth722scCs0eCb1yp9K+1M5g24qFxan4fZVHmDE
z/0r+caLSE9KPf+oGnJaccKGz7mcAEOla24/GALrW3JMgnXlcgB1DYNQyUxul+/G
c3xT6TR5OG/T4MKDknN9eaHmRd8H5sMwdmj/BT5g2QKBgQDuBZwASn9LzPpwwbVq
gNLvo03NRwicLhoSe+Kl8Yao+m7GOKbpfzZgFej2zgq40jgBAJ2zSds1VKEBwKeX
CSaXJenKk3DN5trtzTsj3H8AvagNyZcV3YKlKLMc0HUPAWVKX5O6h322SJT3JV30
NZTM46rTAD7dFeb9m+JmZ8+B7wKBgQDbcrgCNcijvtSApVw/hXLyhqzPqLDUGGhS
XtXx0r6mVuMKOVkgr48/mO/xgSL4wjs4Ps4S0/yqtt5PSFloCOuytpWeTQrBCLL0
Or9dPuS+qLmJxkvWV1uoZkfdKpTY2s2ovyilQtovPdoACJnK83pdwArWp3VkrM9
Al6Hp0v7iQKBgAx27rx1KkVl7peJDV8Ob/1sp95gIetL3sGpCy11gH/I3ZQz00nX
B5nwi8qg757OI3Cp/5gr/fbE/8l/tUcLi6HOsneRUQ73T++0F6zBF0WKqQpPzEGw
3+6WOwr/P6IRiKRkbPAPuF2bX3Gx20G2rJwuL/vsv14Ej5woVarXNN6xAoGAaUP9
SmocRZfLfa5Uss/D1NyPVslXkVXn7OM7A1YRR99T51qdC1XLhDlLl/BXIzagi5ls
5pEzmXxA5Y0R/hqRXVfCK35PU0tl9Eud8g+yUFbFMXaieD1NZNkzTb8YSXGjx3dy
+ts3p+z66dNurUjQ7FW+Cg3cuk81lWVmjPHOO+kCgYEA5TJu0h6YSPr5dnNjPgrw
UxaGC0wtKX61w+tYOmpUKKCawi2K2tVTa3r/jJy27ClLHodwrNka4R/tNQD7EuRb
jD58iSXrHeaPVtbvBZCTASM+ssxEVrhDouz9KH8pKi7TLgA8WNQQjXoV9XRdg4Vj
7GFbXwUhhsoOgFWjAKjLol0=
-----END PRIVATE KEY-----", "client_email": "chirivia@chrivia-asxi.iam.gserviceaccount.com", "client_id": "109471356352632635196", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/chirivia%40chrivia-asxi.iam.gserviceaccount.com" }

554
index.php
View File

@ -1,150 +1,422 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
if (!isset($_SESSION['streak'])) {
$_SESSION['streak'] = 0;
}
?>
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chirivia - Save the Planet, Save Your Chircuit</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-green: #2E8B57;
--secondary-blue: #4682B4;
--accent-color: #5F9EA0;
--background-light: #F0F8FF;
--surface-white: #FFFFFF;
--text-dark: #2F4F4F;
--font-headings: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
--spacing-base: 8px;
--border-radius: 0.5rem; /* 8px */
}
body {
font-family: var(--font-body);
background-color: var(--background-light);
color: var(--text-dark);
margin: 0;
line-height: 1.6;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: calc(var(--spacing-base) * 2);
}
h1, h2, h3 {
font-family: var(--font-headings);
color: var(--primary-green);
}
.header {
background: var(--surface-white);
padding: calc(var(--spacing-base) * 2) 0;
border-bottom: 1px solid #eee;
text-align: center;
}
.header .logo {
font-family: var(--font-headings);
font-size: 2rem;
color: var(--primary-green);
text-decoration: none;
font-weight: bold;
}
.hero {
background: linear-gradient(135deg, var(--primary-green), var(--secondary-blue));
color: var(--surface-white);
padding: calc(var(--spacing-base) * 8) calc(var(--spacing-base) * 2);
text-align: center;
background-image: url('assets/pasted-20251004-214642-63e1d23c.webp');
background-size: 100% auto;
background-repeat: no-repeat;
background-position: center 25%;
position: relative;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
}
.hero-content {
position: relative;
z-index: 1;
}
.hero h1 {
font-size: 4rem;
margin-bottom: var(--spacing-base);
color: var(--surface-white);
}
.hero p {
font-size: 1.25rem;
margin-bottom: calc(var(--spacing-base) * 3);
}
.cta-button {
background-color: var(--accent-color);
color: var(--surface-white);
padding: calc(var(--spacing-base) * 1.5) calc(var(--spacing-base) * 3);
border: none;
border-radius: var(--border-radius);
font-size: 1.1rem;
font-weight: bold;
text-decoration: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.cta-button:hover {
background-color: #53868B;
}
.section {
padding: calc(var(--spacing-base) * 6) 0;
border-bottom: 1px solid #eee;
}
.section:last-child {
border-bottom: none;
}
.pet-section {
display: flex;
align-items: center;
gap: calc(var(--spacing-base) * 4);
}
.pet-section .pet-image-frame {
width: 300px;
height: 300px;
border-radius: 50%;
overflow: hidden;
border: 5px solid var(--surface-white);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.pet-section img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.2);
}
.pet-status h2 {
margin-top: 0;
font-size: 2.5rem;
margin-bottom: 0;
}
.streak-counter {
font-size: 1.5rem;
font-weight: bold;
color: var(--secondary-blue);
}
.about-section h2 {
font-size: 2.5rem;
}
.about-section {
display: flex;
align-items: center;
gap: calc(var(--spacing-base) * 4);
}
.about-section img {
max-width: 400px;
border-radius: var(--border-radius);
}
.footer {
background-color: var(--surface-white);
text-align: center;
padding: calc(var(--spacing-base) * 3) 0;
margin-top: calc(var(--spacing-base) * 4);
font-size: 0.9rem;
}
.footer a {
color: var(--primary-green);
text-decoration: none;
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0px);
}
}
.animated-pet {
animation: float 3s ease-in-out infinite;
}
.pet-image-frame img {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 1s ease-in-out;
}
.pet-image-frame img.active {
opacity: 1;
}
/* Chat Widget */
.chat-widget-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.chat-bubble {
width: 60px;
height: 60px;
background-color: var(--secondary-blue);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: transform 0.2s;
}
.chat-bubble:hover {
transform: scale(1.1);
}
.chat-window {
display: none;
position: absolute;
bottom: 80px;
right: 0;
width: 350px;
max-width: 90vw;
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
flex-direction: column;
}
.chat-header {
background: var(--secondary-blue);
color: white;
padding: 15px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header p {
margin: 0;
font-weight: bold;
}
.close-chat {
cursor: pointer;
font-size: 24px;
}
.chat-body {
padding: 15px;
height: 300px;
overflow-y: auto;
background: #f9f9f9;
}
.message {
margin-bottom: 15px;
}
.message p {
background: #e9e9eb;
padding: 10px 15px;
border-radius: 15px;
display: inline-block;
max-width: 80%;
margin: 0;
}
.chat-footer {
padding: 15px;
display: flex;
border-top: 1px solid #ddd;
}
.chat-footer input {
flex: 1;
border: 1px solid #ddd;
border-radius: 20px;
padding: 10px;
margin-right: 10px;
}
.chat-footer button {
background: var(--secondary-blue);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
}
.cta-button img {
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<main>
<section class="hero">
<div class="hero-content">
<h1>Chirivia</h1>
<p>Save the Planet, Save Your Chircuit.</p>
</div>
</section>
<div style="text-align: center; padding: 24px 0; background-color: var(--background-light); border-bottom: 1px solid #eee;">
<a href="quiz.php?v=<?php echo time(); ?>" class="cta-button" style="margin-right: 16px;">Play Trivia</a>
<a href="actions.php?v=<?php echo time(); ?>" class="cta-button" style="margin-right: 16px;">Log an Action</a>
<a href="streaks.php?v=<?php echo time(); ?>" class="cta-button" style="margin-right: 16px;">Streaks</a>
<a href="chatbot.php?v=<?php echo time(); ?>" class="cta-button">Chatbot</a>
</div>
<div class="container">
<section class="section pet-section">
<div class="pet-image-frame animated-pet" style="position: relative;">
<img src="assets/pasted-20251004-192255-8c7c39e6.webp" alt="A cute, healthy pet chircuit." class="active">
<img src="assets/pasted-20251004-222033-3b718197.webp" alt="A happy, playful pet chircuit.">
</div>
<div class="pet-status">
<h2>Your Pet Chircuit</h2>
<p>Keep me alive by learning about our planet!</p>
<div class="streak-counter">
🔥 Streak: <?php echo $_SESSION['streak']; ?> <?php echo ($_SESSION['streak'] == 1) ? 'day' : 'days'; ?> 🔥
</div>
</div>
</section>
<section class="section about-section">
<div>
<h2>About Chirivia</h2>
<p>Chirivia makes learning about environmental issues fun and engaging. Every correct answer helps you maintain your streak and contributes to a (virtual) healthier planet, keeping your pet happy and healthy.</p>
<p>You can also log real-world actions to get extra points!</p>
</div>
<img src="https://picsum.photos/seed/about/800/600" alt="A person planting a small tree, symbolizing environmental action.">
</section>
</div>
</main>
<footer class="footer">
<div class="container">
<p>&copy; 2025 Chirivia. All Rights Reserved. | <a href="privacy.php">Privacy Policy</a></p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
const petImageFrame = document.querySelector('.pet-image-frame');
const images = petImageFrame.querySelectorAll('img');
let currentIndex = 0;
setInterval(() => {
images[currentIndex].classList.remove('active');
currentIndex = (currentIndex + 1) % images.length;
images[currentIndex].classList.add('active');
}, 3000); // Change image every 3 seconds
});
</script>
<!-- Chat Widget -->
<div class="chat-widget-container">
<div id="chat-bubble" class="chat-bubble">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
</div>
<div id="chat-window" class="chat-window">
<div class="chat-header">
<p>Chat with us!</p>
<span id="close-chat" class="close-chat">&times;</span>
</div>
<div class="chat-body">
<div class="message received">
<p>Hi there! How can I help you today?</p>
</div>
</div>
<div class="chat-footer">
<input type="text" placeholder="Type a message...">
<button>Send</button>
</div>
</div>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<script>
const chatBubble = document.getElementById('chat-bubble');
const chatWindow = document.getElementById('chat-window');
const closeChat = document.getElementById('close-chat');
chatBubble.addEventListener('click', () => {
chatWindow.style.display = 'flex';
chatBubble.style.display = 'none';
});
closeChat.addEventListener('click', () => {
chatWindow.style.display = 'none';
chatBubble.style.display = 'flex';
});
</script>
</body>
</html>
</html>

51
privacy.php Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Chirivia</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-green: #2E8B57;
--secondary-blue: #4682B4;
--accent-color: #5F9EA0;
--background-light: #F0F8FF;
--surface-white: #FFFFFF;
--text-dark: #2F4F4F;
--font-headings: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
--spacing-base: 8px;
--border-radius: 0.5rem;
}
body {
font-family: var(--font-body);
background-color: var(--background-light);
color: var(--text-dark);
margin: 0;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: calc(var(--spacing-base) * 4);
}
h1 {
color: var(--primary-green);
}
a {
color: var(--primary-green);
}
</style>
</head>
<body>
<div class="container">
<h1>Privacy Policy</h1>
<p>This is a placeholder for the Privacy Policy.</p>
<p>Information about how user data is collected, used, and protected will be detailed here.</p>
<a href="index.php">Back to Home</a>
</div>
</body>
</html>

366
quiz.php Normal file
View File

@ -0,0 +1,366 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
if (!isset($_SESSION['streak'])) {
$_SESSION['streak'] = 0;
}
$questions = [
[
'question' => 'What is the impact of climate change on the planet?',
'options' => [
'Rising sea levels',
'More natural disasters',
'Increased temperatures globally',
'All of the above'
],
'answer' => 'All of the above'
],
[
'question' => 'Heat waves enter the atmosphere and are trapped because of the thick layer of gases that surround the planet. This is called…',
'options' => [
'Thick Atmosphere Effect',
'Greenhouse Effect',
'Increasing Heat Effect',
'None of the above'
],
'answer' => 'Greenhouse Effect'
],
[
'question' => 'When did climate change begin?',
'options' => [
'When the Industrial Revolution began',
'When the world population was larger than two billion people',
'It always has existed',
'Yesterday!'
],
'answer' => ['When the Industrial Revolution began', 'It always has existed']
]
];
$question_index = isset($_GET['q']) ? (int)$_GET['q'] : 0;
if ($question_index === 0) {
unset($_SESSION['quiz_completed_reward']);
}
$feedback = '';
if (isset($_POST['answer'])) {
$selected_answer = $_POST['answer'];
$correct_answer = $questions[$question_index]['answer'];
$is_correct = false;
if (is_array($correct_answer)) {
if (in_array($selected_answer, $correct_answer)) {
$is_correct = true;
}
} else {
if ($selected_answer == $correct_answer) {
$is_correct = true;
}
}
if ($is_correct) {
$feedback = '<div class="feedback correct" style="padding: 0;"><p style="padding: 16px 16px 0; margin: 0;">Correct! Great job.</p><img src="assets/pasted-20251004-232406-9a846679.png" alt="Happy Chircuit" class="feedback-image"></div>';
$question_index++;
} else {
$_SESSION['streak'] = 0;
$feedback = '<div class="feedback incorrect">Not quite! Try again.<br><img src="assets/pasted-20251005-145045-e02a01f3.png" alt="Sad Chircuit" class="feedback-image-incorrect"></div>';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trivia Quiz - Chirivia</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-green: #2E8B57;
--secondary-blue: #4682B4;
--accent-color: #5F9EA0;
--background-light: #F0F8FF;
--surface-white: #FFFFFF;
--text-dark: #2F4F4F;
--font-headings: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
--spacing-base: 8px;
--border-radius: 0.5rem;
}
body {
font-family: var(--font-body);
background-color: var(--background-light);
color: var(--text-dark);
margin: 0;
line-height: 1.6;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.quiz-container {
background: var(--surface-white);
padding: calc(var(--spacing-base) * 4);
border-radius: var(--border-radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
width: 100%;
max-width: 600px;
text-align: center;
}
h1 {
font-family: var(--font-headings);
color: var(--primary-green);
}
.question {
font-size: 1.25rem;
margin-bottom: calc(var(--spacing-base) * 2);
}
.options {
display: flex;
flex-direction: column;
gap: var(--spacing-base);
margin-bottom: calc(var(--spacing-base) * 3);
}
.option-button {
background-color: #eee;
border: 2px solid #ddd;
padding: calc(var(--spacing-base) * 1.5);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
font-size: 1rem;
}
.option-button:hover {
background-color: #ddd;
border-color: #ccc;
}
.feedback {
padding: calc(var(--spacing-base) * 2);
border-radius: var(--border-radius);
margin-top: calc(var(--spacing-base) * 2);
margin-bottom: 20px;
font-weight: bold;
}
.feedback.correct {
background-color: #d4edda;
color: #155724;
}
.feedback.incorrect {
background-color: #E0E0E0;
color: #2F4F4F;
}
.next-button {
background-color: var(--accent-color);
color: var(--surface-white);
padding: calc(var(--spacing-base) * 1.5) calc(var(--spacing-base) * 3);
border: none;
border-radius: var(--border-radius);
font-size: 1.1rem;
font-weight: bold;
text-decoration: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
a {
color: var(--primary-green);
display: inline-block;
margin-top: calc(var(--spacing-base) * 2);
}
.feedback-image {
max-width: 200px;
display: block;
margin: 10px auto;
}
.feedback-image-incorrect {
max-width: 100px;
display: block;
margin: 10px auto;
}
/* Chat Widget */
.chat-widget-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.chat-bubble {
width: 60px;
height: 60px;
background-color: var(--secondary-blue);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: transform 0.2s;
}
.chat-bubble:hover {
transform: scale(1.1);
}
.chat-window {
display: none;
position: absolute;
bottom: 80px;
right: 0;
width: 350px;
max-width: 90vw;
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
flex-direction: column;
}
.chat-header {
background: var(--secondary-blue);
color: white;
padding: 15px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header p {
margin: 0;
font-weight: bold;
}
.close-chat {
cursor: pointer;
font-size: 24px;
}
.chat-body {
padding: 15px;
height: 300px;
overflow-y: auto;
background: #f9f9f9;
}
.message {
margin-bottom: 15px;
}
.message p {
background: #e9e9eb;
padding: 10px 15px;
border-radius: 15px;
display: inline-block;
max-width: 80%;
margin: 0;
}
.chat-footer {
padding: 15px;
display: flex;
border-top: 1px solid #ddd;
}
.chat-footer input {
flex: 1;
border: 1px solid #ddd;
border-radius: 20px;
padding: 10px;
margin-right: 10px;
}
.chat-footer button {
background: var(--secondary-blue);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="quiz-container">
<h1>Trivia Quiz</h1>
<?php if ($question_index < count($questions)) {
$current_question = $questions[$question_index];
?>
<p class="question"><?php echo $current_question['question']; ?></p>
<?php echo $feedback; ?>
<form method="POST" action="quiz.php?q=<?php echo $question_index; ?>">
<div class="options">
<?php foreach ($current_question['options'] as $option) { ?>
<button type="submit" name="answer" value="<?php echo htmlspecialchars($option); ?>" class="option-button"><?php echo htmlspecialchars($option); ?></button>
<?php } ?>
</div>
</form>
<?php
} else {
if (!isset($_SESSION['quiz_completed_reward'])) {
$_SESSION['streak']++;
$_SESSION['quiz_completed_reward'] = true;
}
?>
<h2>Quiz Complete!</h2>
<p>You've answered all the questions! Your streak is now <?php echo $_SESSION['streak']; ?>!</p>
<div id="animation-container" style="position: relative; width: 200px; height: 200px; margin: 20px auto;">
<img src="assets/pasted-20251005-003819-6e811280.png" alt="Chircuit with a strawberry" class="anim-image" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; opacity: 1; transition: opacity 0.5s ease-in-out;">
<img src="assets/pasted-20251005-003524-d4bed2e1.png" alt="Chircuit eating a strawberry" class="anim-image" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.5s ease-in-out;">
<img src="assets/pasted-20251005-004414-8785f3f1.png" alt="Happy Chircuit" class="anim-image" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.5s ease-in-out;">
</div>
<a href="index.php" class="next-button" style="margin-top: 20px;">Back to Home</a>
<?php } ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const images = document.querySelectorAll('#animation-container .anim-image');
if (images.length > 0) {
let currentIndex = 0;
setInterval(() => {
images[currentIndex].style.opacity = 0;
currentIndex = (currentIndex + 1) % images.length;
images[currentIndex].style.opacity = 1;
}, 2000);
}
});
</script>
<!-- Chat Widget -->
<div class="chat-widget-container">
<div id="chat-bubble" class="chat-bubble">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
</div>
<div id="chat-window" class="chat-window">
<div class="chat-header">
<p>Chat with us!</p>
<span id="close-chat" class="close-chat">&times;</span>
</div>
<div class="chat-body">
<div class="message received">
<p>Hi there! How can I help you today?</p>
</div>
</div>
<div class="chat-footer">
<input type="text" placeholder="Type a message...">
<button>Send</button>
</div>
</div>
</div>
<script>
const chatBubble = document.getElementById('chat-bubble');
const chatWindow = document.getElementById('chat-window');
const closeChat = document.getElementById('close-chat');
chatBubble.addEventListener('click', () => {
chatWindow.style.display = 'flex';
chatBubble.style.display = 'none';
});
closeChat.addEventListener('click', () => {
chatWindow.style.display = 'none';
chatBubble.style.display = 'flex';
});
</script>
</body>
</html>

178
streaks.php Normal file
View File

@ -0,0 +1,178 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
if (!isset($_SESSION['streak'])) {
$_SESSION['streak'] = 0;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Streaks - Chirivia</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-green: #2E8B57;
--secondary-blue: #4682B4;
--accent-color: #5F9EA0;
--background-light: #F0F8FF;
--surface-white: #FFFFFF;
--text-dark: #2F4F4F;
--font-headings: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
--spacing-base: 8px;
--border-radius: 0.5rem; /* 8px */
}
body {
font-family: var(--font-body);
background-color: var(--background-light);
color: var(--text-dark);
margin: 0;
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: calc(var(--spacing-base) * 2);
}
h1, h2, h3 {
font-family: var(--font-headings);
color: var(--primary-green);
}
.header {
background: var(--surface-white);
padding: calc(var(--spacing-base) * 2) 0;
border-bottom: 1px solid #eee;
text-align: center;
}
.header .logo {
font-family: var(--font-headings);
font-size: 2rem;
color: var(--primary-green);
text-decoration: none;
font-weight: bold;
}
main {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
.content-wrapper {
display: flex;
align-items: center;
gap: calc(var(--spacing-base) * 8);
}
.image-container {
flex-shrink: 0;
}
.streak-image {
width: 250px;
height: 250px;
border-radius: 50%;
object-fit: contain;
background-color: var(--surface-white);
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
border: 5px solid var(--surface-white);
animation: bob 3s ease-in-out infinite;
}
.text-container {
text-align: left;
}
@keyframes bob {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
.footer {
background-color: var(--surface-white);
text-align: center;
padding: calc(var(--spacing-base) * 3) 0;
margin-top: auto;
font-size: 0.9rem;
}
.footer a {
color: var(--primary-green);
text-decoration: none;
}
.cta-button {
background-color: var(--accent-color);
color: var(--surface-white);
padding: calc(var(--spacing-base) * 1.5) calc(var(--spacing-base) * 3);
border: none;
border-radius: var(--border-radius);
font-size: 1.1rem;
font-weight: bold;
text-decoration: none;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: calc(var(--spacing-base) * 2);
display: inline-block;
}
.cta-button:hover {
background-color: #53868B;
}
</style>
</head>
<body>
<header class="header">
<div class="container">
<a href="index.php" class="logo">Chirivia</a>
</div>
</header>
<main class="container">
<div class="content-wrapper">
<div class="image-container">
<img src="assets/pasted-20251005-170401-70cc8986.png" alt="Streaks Calendar" class="streak-image">
</div>
<div class="text-container">
<h1>Track Your Progress</h1>
<p>Your commitment to learning and taking action creates a powerful impact. Keep the momentum going!</p>
<p style="font-size: 1.5rem; font-weight: bold; color: var(--secondary-blue);">
🔥 Current Streak: <?php echo $_SESSION['streak']; ?> <?php echo ($_SESSION['streak'] == 1) ? 'day' : 'days'; ?> 🔥
</p>
<a href="index.php" class="cta-button">Back to Home</a>
</div>
</div>
</main>
<footer class="footer">
<div class="container">
<p>&copy; 2025 Chirivia. All Rights Reserved. | <a href="privacy.php">Privacy Policy</a></p>
</div>
</footer>
</body>
</html>

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?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;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit2be695aecbd4dcc8ecce8c4845e9f2d7::getLoader();

513
vendor/brick/math/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,513 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.14.0](https://github.com/brick/math/releases/tag/0.14.0) - 2025-08-29
✨ **New features**
- New methods: `BigInteger::clamp()` and `BigDecimal::clamp()` (#96 by @JesterIruka)
✨ **Improvements**
- All pure methods in `BigNumber` classes are now marked as `@pure` for better static analysis
💥 **Breaking changes**
- Minimum PHP version is now 8.2
- `BigNumber` classes are now `readonly`
- `BigNumber` is now marked as sealed: it must not be extended outside of this package
- Exception classes are now `final`
## [0.13.1](https://github.com/brick/math/releases/tag/0.13.1) - 2025-03-29
✨ **Improvements**
- `__toString()` methods of `BigInteger` and `BigDecimal` are now type-hinted as returning `numeric-string` instead of `string` (#90 by @vudaltsov)
## [0.13.0](https://github.com/brick/math/releases/tag/0.13.0) - 2025-03-03
💥 **Breaking changes**
- `BigDecimal::ofUnscaledValue()` no longer throws an exception if the scale is negative
- `MathException` now extends `RuntimeException` instead of `Exception`; this reverts the change introduced in version `0.11.0` (#82)
✨ **New features**
- `BigDecimal::ofUnscaledValue()` allows a negative scale (and converts the values to create a zero scale number)
## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28
✨ **New features**
- `BigDecimal::getPrecision()` Returns the number of significant digits in a decimal number
## [0.12.2](https://github.com/brick/math/releases/tag/0.12.2) - 2025-02-26
⚡️ **Performance improvements**
- Division in `NativeCalculator` is now faster for small divisors, thanks to [@Izumi-kun](https://github.com/Izumi-kun) in [#87](https://github.com/brick/math/pull/87).
👌 **Improvements**
- Add missing `RoundingNecessaryException` to the `@throws` annotation of `BigNumber::of()`
## [0.12.1](https://github.com/brick/math/releases/tag/0.12.1) - 2023-11-29
⚡️ **Performance improvements**
- `BigNumber::of()` is now faster, thanks to [@SebastienDug](https://github.com/SebastienDug) in [#77](https://github.com/brick/math/pull/77).
## [0.12.0](https://github.com/brick/math/releases/tag/0.12.0) - 2023-11-26
💥 **Breaking changes**
- Minimum PHP version is now 8.1
- `RoundingMode` is now an `enum`; if you're type-hinting rounding modes, you need to type-hint against `RoundingMode` instead of `int` now
- `BigNumber` classes do not implement the `Serializable` interface anymore (they use the [new custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization))
- The following breaking changes only affect you if you're creating your own `BigNumber` subclasses:
- the return type of `BigNumber::of()` is now `static`
- `BigNumber` has a new abstract method `from()`
- all `public` and `protected` functions of `BigNumber` are now `final`
## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16
💥 **Breaking changes**
- Minimum PHP version is now 8.0
- Methods accepting a union of types are now strongly typed<sup>*</sup>
- `MathException` now extends `Exception` instead of `RuntimeException`
<sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods
internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string`
first.</sup>
## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11
👌 **Improvements**
- `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic
## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02
✨ **New features**
- `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers
## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18
💥 **Breaking changes**
- Minimum PHP version is now 7.4
## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
🚀 **Compatibility with PHP 8.1**
- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (#60) thanks @TRowbotham
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
🐛 **Bug fix**
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
✨ **New features**
- `BigInteger::not()` returns the bitwise `NOT` value
🐛 **Bug fixes**
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
👌 **Improvements**
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
💥 **Breaking changes**
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
🐛 **Bug fix**
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
🚑 **Critical fix**
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
✨ **New features**
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
💩 **Deprecations**
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
🐛 **Fixes**
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
⚡️ **Optimizations**
- additional optimization in `BigInteger::remainder()`
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
✨ **New features**
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
✨ **New features**
- `BigInteger::isEven()` tests whether the number is even
- `BigInteger::isOdd()` tests whether the number is odd
- `BigInteger::testBit()` tests if a bit is set
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
🛠️ **Maintenance release**
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
This is a maintenance release: no bug fixes, no new features, no breaking changes.
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
✨ **New feature**
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
✨ **New feature**
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
⚡️ **Performance improvements**
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
🐛 **Bug fixes**
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
✨ **New features**
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
💩 **Deprecations**
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
- the `$base` parameter is required, it does not default to `10`
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
**Improvements**
- Safer conversion from `float` when using custom locales
- **Much faster** `NativeCalculator` implementation 🚀
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
**New method**
`BigNumber::sum()` returns the sum of one or more numbers.
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
Thanks @manowark 👍
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
**New method**
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
**New method**
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
**New exception**
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
**Performance update**
- Further improvement of `toInt()` performance
- `NativeCalculator` can now perform some multiplications more efficiently
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
Performance optimization of `toInt()` methods.
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
**Breaking changes**
The following deprecated methods have been removed. Use the new method name instead:
| Method removed | Replacement method |
| --- | --- |
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
---
**New features**
`BigInteger` has been augmented with 5 new methods for bitwise operations:
| New method | Description |
| --- | --- |
| `and()` | performs a bitwise `AND` operation on two numbers |
| `or()` | performs a bitwise `OR` operation on two numbers |
| `xor()` | performs a bitwise `XOR` operation on two numbers |
| `shiftedLeft()` | returns the number shifted left by a number of bits |
| `shiftedRight()` | returns the number shifted right by a number of bits |
Thanks to @DASPRiD 👍
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
**Renamed/deprecated methods:**
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
**Performance update**
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
This is a maintenance release, no code has been changed.
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
- Tests and other files unnecessary for production are excluded from the dist package
This will help make installations more compact.
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
Methods renamed:
- `BigNumber:sign()` has been renamed to `getSign()`
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
- `BigDecimal::scale()` has been renamed to `getScale()`
- `BigDecimal::integral()` has been renamed to `getIntegral()`
- `BigDecimal::fraction()` has been renamed to `getFraction()`
- `BigRational::numerator()` has been renamed to `getNumerator()`
- `BigRational::denominator()` has been renamed to `getDenominator()`
Classes renamed:
- `ArithmeticException` has been renamed to `MathException`
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
The base class for all exceptions is now `MathException`.
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
A number of methods have been renamed:
- `BigNumber:sign()` is deprecated; use `getSign()` instead
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
The old methods will be removed in version 0.7.0.
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
The JSON output is always a string.
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
**New method: `BigNumber::toScale()`**
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
**New features**
- Common `BigNumber` interface for all classes, with the following methods:
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
- `toInteger()` and `toFloat()` conversion methods to native types
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
- New methods: `BigRational::quotient()` and `remainder()`
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
- Factory methods `zero()`, `one()` and `ten()` available in all classes
- Rounding mode reintroduced in `BigInteger::dividedBy()`
This release also comes with many performance improvements.
---
**Breaking changes**
- `BigInteger`:
- `getSign()` is renamed to `sign()`
- `toString()` is renamed to `toBase()`
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
- `BigDecimal`:
- `getSign()` is renamed to `sign()`
- `getUnscaledValue()` is renamed to `unscaledValue()`
- `getScale()` is renamed to `scale()`
- `getIntegral()` is renamed to `integral()`
- `getFraction()` is renamed to `fraction()`
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
- `toBigInteger()` does not accept a `$roundingMode` parameter anymore
- `toBigRational()` does not simplify the fraction anymore; explicitly add `->simplified()` to get the previous behaviour
- `BigRational`:
- `getSign()` is renamed to `sign()`
- `getNumerator()` is renamed to `numerator()`
- `getDenominator()` is renamed to `denominator()`
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
- Miscellaneous:
- `ArithmeticException` is moved to an `Exception\` sub-namespace
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
Backport of two bug fixes from the 0.5 branch:
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
New method: `BigDecimal::stripTrailingZeros()`
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
Introducing a `BigRational` class, to perform calculations on fractions of any size.
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
`BigInteger::dividedBy()` now always returns the quotient of the division.
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
Backport of two bug fixes from the 0.5 branch:
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
New methods:
- `BigInteger::remainder()` returns the remainder of a division only
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
Fix `toString()` not handling negative numbers.
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
- `-1` if the number is negative
- `0` if the number is zero
- `1` if the number is positive
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
Minor performance improvements
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
Added `BigDecimal::divideAndRemainder()`
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
- `min()` and `max()` do not accept an `array` anymore, but a variable number of parameters
- **minimum PHP version is now 5.6**
- continuous integration with PHP 7
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
- Added `BigInteger::power()`
- Added HHVM support
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
First beta release.

20
vendor/brick/math/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-present Benjamin Morel
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.

39
vendor/brick/math/composer.json vendored Normal file
View File

@ -0,0 +1,39 @@
{
"name": "brick/math",
"description": "Arbitrary-precision arithmetic library",
"type": "library",
"keywords": [
"Brick",
"Math",
"Mathematics",
"Arbitrary-precision",
"Arithmetic",
"BigInteger",
"BigDecimal",
"BigRational",
"BigNumber",
"Bignum",
"Decimal",
"Rational",
"Integer"
],
"license": "MIT",
"require": {
"php": "^8.2"
},
"require-dev": {
"phpunit/phpunit": "^11.5",
"php-coveralls/php-coveralls": "^2.2",
"phpstan/phpstan": "2.1.22"
},
"autoload": {
"psr-4": {
"Brick\\Math\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Brick\\Math\\Tests\\": "tests/"
}
}
}

14
vendor/brick/math/phpstan.neon vendored Normal file
View File

@ -0,0 +1,14 @@
parameters:
level: 10
checkUninitializedProperties: true
paths:
- src
ignoreErrors:
- '~Impure call to function array_shift\(\) in pure method~'
- '~Possibly impure call to function assert\(\) in pure method~'
- '~Possibly impure call to function hex2bin\(\) in pure method~'
- '~Possibly impure call to function preg_match\(\) in pure method~'
- '~Impure static variable in pure method Brick\\Math\\Big(Integer|Decimal|Rational)::(zero|one|ten)\(\)~'
-
message: '~Parameter #\d \$\S+ of function bc\S+ expects numeric-string, string given~'
path: src/Internal/Calculator/BcMathCalculator.php

862
vendor/brick/math/src/BigDecimal.php vendored Normal file
View File

@ -0,0 +1,862 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use Override;
/**
* Immutable, arbitrary-precision signed decimal numbers.
*/
final readonly class BigDecimal extends BigNumber
{
/**
* The unscaled value of this decimal number.
*
* This is a string of digits with an optional leading minus sign.
* No leading zero must be present.
* No leading minus sign must be present if the value is 0.
*/
private string $value;
/**
* The scale (number of digits after the decimal point) of this decimal number.
*
* This must be zero or more.
*/
private int $scale;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param string $value The unscaled value, validated.
* @param int $scale The scale, validated.
*
* @pure
*/
protected function __construct(string $value, int $scale = 0)
{
$this->value = $value;
$this->scale = $scale;
}
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigDecimal();
}
/**
* Creates a BigDecimal from an unscaled value and a scale.
*
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number. If negative, the scale will be set to zero
* and the unscaled value will be adjusted accordingly.
*
* @pure
*/
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
{
$value = (string) BigInteger::of($value);
if ($scale < 0) {
if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a BigDecimal representing zero, with a scale of zero.
*
* @pure
*/
public static function zero() : BigDecimal
{
/** @var BigDecimal|null $zero */
static $zero;
if ($zero === null) {
$zero = new BigDecimal('0');
}
return $zero;
}
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @pure
*/
public static function one() : BigDecimal
{
/** @var BigDecimal|null $one */
static $one;
if ($one === null) {
$one = new BigDecimal('1');
}
return $one;
}
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @pure
*/
public static function ten() : BigDecimal
{
/** @var BigDecimal|null $ten */
static $ten;
if ($ten === null) {
$ten = new BigDecimal('10');
}
return $ten;
}
/**
* Returns the sum of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/
public function plus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
if ($this->value === '0' && $this->scale <= $that->scale) {
return $that;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = CalculatorRegistry::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the difference of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/
public function minus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = CalculatorRegistry::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the product of this number and the given one.
*
* The result has a scale of `$this->scale + $that->scale`.
*
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
*
* @pure
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '1' && $that->scale === 0) {
return $this;
}
if ($this->value === '1' && $this->scale === 0) {
return $that;
}
$value = CalculatorRegistry::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the result of the division of this number by the given one, at the given scale.
*
* @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
*
* @pure
*/
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
if ($scale === null) {
$scale = $this->scale;
} elseif ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
return $this;
}
$p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale);
$result = CalculatorRegistry::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits.
*
* @pure
*/
public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0') {
throw DivisionByZeroException::divisionByZero();
}
[, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d);
$calculator = CalculatorRegistry::get();
foreach ([5, 2] as $prime) {
for (;;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $this->dividedBy($that, $scale)->stripTrailingZeros();
}
/**
* Limits (clamps) this number between the given minimum and maximum values.
*
* If the number is lower than $min, returns a copy of $min.
* If the number is greater than $max, returns a copy of $max.
* Otherwise, returns this number unchanged.
*
* @param BigNumber|int|float|string $min The minimum. Must be convertible to a BigDecimal.
* @param BigNumber|int|float|string $max The maximum. Must be convertible to a BigDecimal.
*
* @throws MathException If min/max are not convertible to a BigDecimal.
*/
public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max) : BigDecimal
{
if ($this->isLessThan($min)) {
return BigDecimal::of($min);
} elseif ($this->isGreaterThan($max)) {
return BigDecimal::of($max);
}
return $this;
}
/**
* Returns this number exponentiated to the given value.
*
* The result has a scale of `$this->scale * $exponent`.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/
public function power(int $exponent) : BigDecimal
{
if ($exponent === 0) {
return BigDecimal::one();
}
if ($exponent === 1) {
return $this;
}
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new \InvalidArgumentException(\sprintf(
'The exponent %d is not in the range 0 to %d.',
$exponent,
Calculator::MAX_POWER
));
}
return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
}
/**
* Returns the quotient of the division of this number by the given one.
*
* The quotient has a scale of `0`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/
public function quotient(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$quotient = CalculatorRegistry::get()->divQ($p, $q);
return new BigDecimal($quotient, 0);
}
/**
* Returns the remainder of the division of this number by the given one.
*
* The remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/
public function remainder(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$remainder = CalculatorRegistry::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($remainder, $scale);
}
/**
* Returns the quotient and remainder of the division of this number by the given one.
*
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*
* @pure
*/
public function quotientAndRemainder(BigNumber|int|float|string $that) : array
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale);
return [$quotient, $remainder];
}
/**
* Returns the square root of this number, rounded down to the given number of decimals.
*
* @throws \InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative.
*
* @pure
*/
public function sqrt(int $scale) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($this->value === '0') {
return new BigDecimal('0', $scale);
}
if ($this->value[0] === '-') {
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
}
$value = $this->value;
$addDigits = 2 * $scale - $this->scale;
if ($addDigits > 0) {
// add zeros
$value .= \str_repeat('0', $addDigits);
} elseif ($addDigits < 0) {
// trim digits
if (-$addDigits >= \strlen($this->value)) {
// requesting a scale too low, will always yield a zero result
return new BigDecimal('0', $scale);
}
$value = \substr($value, 0, $addDigits);
}
$value = CalculatorRegistry::get()->sqrt($value);
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
*
* @pure
*/
public function withPointMovedLeft(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedRight(-$n);
}
return new BigDecimal($this->value, $this->scale + $n);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
*
* @pure
*/
public function withPointMovedRight(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedLeft(-$n);
}
$value = $this->value;
$scale = $this->scale - $n;
if ($scale < 0) {
if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*
* @pure
*/
public function stripTrailingZeros() : BigDecimal
{
if ($this->scale === 0) {
return $this;
}
$trimmedValue = \rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
}
if ($trimmableZeros > $this->scale) {
$trimmableZeros = $this->scale;
}
$value = \substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
/**
* Returns the absolute value of this number.
*
* @pure
*/
public function abs() : BigDecimal
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*
* @pure
*/
public function negated() : BigDecimal
{
return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
}
#[Override]
public function compareTo(BigNumber|int|float|string $that) : int
{
$that = BigNumber::of($that);
if ($that instanceof BigInteger) {
$that = $that->toBigDecimal();
}
if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that);
return CalculatorRegistry::get()->cmp($a, $b);
}
return - $that->compareTo($this);
}
#[Override]
public function getSign() : int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
/**
* @pure
*/
public function getUnscaledValue() : BigInteger
{
return self::newBigInteger($this->value);
}
/**
* @pure
*/
public function getScale() : int
{
return $this->scale;
}
/**
* Returns the number of significant digits in the number.
*
* This is the number of digits to both sides of the decimal point, stripped of leading zeros.
* The sign has no impact on the result.
*
* Examples:
* 0 => 0
* 0.0 => 0
* 123 => 3
* 123.456 => 6
* 0.00123 => 3
* 0.0012300 => 5
*
* @pure
*/
public function getPrecision(): int
{
$value = $this->value;
if ($value === '0') {
return 0;
}
$length = \strlen($value);
return ($value[0] === '-') ? $length - 1 : $length;
}
/**
* Returns a string representing the integral part of this decimal number.
*
* Example: `-123.456` => `-123`.
*
* @pure
*/
public function getIntegralPart() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale);
}
/**
* Returns a string representing the fractional part of this decimal number.
*
* If the scale is zero, an empty string is returned.
*
* Examples: `-123.456` => '456', `123` => ''.
*
* @pure
*/
public function getFractionalPart() : string
{
if ($this->scale === 0) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale);
}
/**
* Returns whether this decimal number has a non-zero fractional part.
*
* @pure
*/
public function hasNonZeroFractionalPart() : bool
{
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
}
#[Override]
public function toBigInteger() : BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
#[Override]
public function toBigDecimal() : BigDecimal
{
return $this;
}
#[Override]
public function toBigRational() : BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
#[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
#[Override]
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat() : float
{
return (float) (string) $this;
}
/**
* @return numeric-string
*/
#[Override]
public function __toString() : string
{
if ($this->scale === 0) {
/** @var numeric-string */
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
/** @phpstan-ignore return.type */
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{value: string, scale: int}
*/
public function __serialize(): array
{
return ['value' => $this->value, 'scale' => $this->scale];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
*
* @param array{value: string, scale: int} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
/** @phpstan-ignore deadCode.unreachable */
$this->value = $data['value'];
$this->scale = $data['scale'];
}
/**
* Puts the internal values of the given decimal numbers on the same scale.
*
* @return array{string, string} The scaled integer values of $x and $y.
*
* @pure
*/
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
{
$a = $x->value;
$b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) {
$b .= \str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= \str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
/**
* @pure
*/
private function valueWithMinScale(int $scale) : string
{
$value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) {
$value .= \str_repeat('0', $scale - $this->scale);
}
return $value;
}
/**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
*
* @pure
*/
private function getUnscaledValueWithLeadingZeros() : string
{
$value = $this->value;
$targetLength = $this->scale + 1;
$negative = ($value[0] === '-');
$length = \strlen($value);
if ($negative) {
$length--;
}
if ($length >= $targetLength) {
return $this->value;
}
if ($negative) {
$value = \substr($value, 1);
}
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) {
$value = '-' . $value;
}
return $value;
}
}

1139
vendor/brick/math/src/BigInteger.php vendored Normal file

File diff suppressed because it is too large Load Diff

556
vendor/brick/math/src/BigNumber.php vendored Normal file
View File

@ -0,0 +1,556 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Override;
/**
* Base class for arbitrary-precision numbers.
*
* This class is sealed: it is part of the public API but should not be subclassed in userland.
* Protected methods may change in any version.
*
* @phpstan-sealed BigInteger|BigDecimal|BigRational
*/
abstract readonly class BigNumber implements \JsonSerializable, \Stringable
{
/**
* The regular expression used to parse integer or decimal numbers.
*/
private const PARSE_REGEXP_NUMERICAL =
'/^' .
'(?<sign>[\-\+])?' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
'$/';
/**
* The regular expression used to parse rational numbers.
*/
private const PARSE_REGEXP_RATIONAL =
'/^' .
'(?<sign>[\-\+])?' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
'$/';
/**
* Creates a BigNumber of the given value.
*
* When of() is called on BigNumber, the concrete return type is dependent on the given value, with the following
* rules:
*
* - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger
* - floating point numbers are converted to a string then parsed as such
* - strings containing a `/` character are returned as BigRational
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
*
* When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
*
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
*
* @pure
*/
final public static function of(BigNumber|int|float|string $value) : static
{
$value = self::_of($value);
if (static::class === BigNumber::class) {
assert($value instanceof static);
return $value;
}
return static::from($value);
}
/**
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
*
* @pure
*/
private static function _of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (\is_int($value)) {
return new BigInteger((string) $value);
}
if (is_float($value)) {
$value = (string) $value;
}
if (str_contains($value, '/')) {
// Rational number
if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$sign = $matches['sign'];
$numerator = $matches['numerator'];
$denominator = $matches['denominator'];
$numerator = self::cleanUp($sign, $numerator);
$denominator = self::cleanUp(null, $denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false
);
} else {
// Integer or decimal number
if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$sign = $matches['sign'];
$point = $matches['point'];
$integral = $matches['integral'];
$fractional = $matches['fractional'];
$exponent = $matches['exponent'];
if ($integral === null && $fractional === null) {
throw NumberFormatException::invalidFormat($value);
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional ??= '';
$exponent = ($exponent !== null) ? (int)$exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
}
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
}
$integral = self::cleanUp($sign, $integral);
return new BigInteger($integral);
}
}
/**
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
*
* @throws RoundingNecessaryException If the value cannot be converted.
*
* @pure
*/
abstract protected static function from(BigNumber $number): static;
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @pure
* @internal
*/
final protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @pure
* @internal
*/
final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @pure
* @internal
*/
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* Returns the minimum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @pure
*/
final public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @pure
*/
final public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
* the given values (BigInteger < BigDecimal < BigRational).
*
* When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
* specific subclass, and returns a result of the same type.
*
* @param BigNumber|int|float|string ...$values The values to add. All values must be convertible to the class on
* which this method is called.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @pure
*/
final public static function sum(BigNumber|int|float|string ...$values) : static
{
$first = array_shift($values);
if ($first === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
$sum = static::of($first);
foreach ($values as $value) {
$sum = self::add($sum, static::of($value));
}
assert($sum instanceof static);
return $sum;
}
/**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
*
* @pure
*/
private static function add(BigNumber $a, BigNumber $b) : BigNumber
{
if ($a instanceof BigRational) {
return $a->plus($b);
}
if ($b instanceof BigRational) {
return $b->plus($a);
}
if ($a instanceof BigDecimal) {
return $a->plus($b);
}
if ($b instanceof BigDecimal) {
return $b->plus($a);
}
return $a->plus($b);
}
/**
* Removes optional leading zeros and applies sign.
*
* @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
* @param string $number The number, validated as a string of digits.
*
* @pure
*/
private static function cleanUp(string|null $sign, string $number) : string
{
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
return $sign === '-' ? '-' . $number : $number;
}
/**
* Checks if this number is equal to the given one.
*
* @pure
*/
final public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly lower than the given one.
*
* @pure
*/
final public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is lower than or equal to the given one.
*
* @pure
*/
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*
* @pure
*/
final public function isGreaterThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*
* @pure
*/
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*
* @pure
*/
final public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*
* @pure
*/
final public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*
* @pure
*/
final public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*
* @pure
*/
final public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*
* @pure
*/
final public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* Returns -1 if the number is negative, 0 if zero, 1 if positive.
*
* @return -1|0|1
*
* @pure
*/
abstract public function getSign() : int;
/**
* Compares this number to the given one.
*
* Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
*
* @return -1|0|1
*
* @throws MathException If the number is not valid.
*
* @pure
*/
abstract public function compareTo(BigNumber|int|float|string $that) : int;
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*
* @pure
*/
abstract public function toBigInteger() : BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*
* @pure
*/
abstract public function toBigDecimal() : BigDecimal;
/**
* Converts this number to a BigRational.
*
* @pure
*/
abstract public function toBigRational() : BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*
* @pure
*/
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*
* @pure
*/
abstract public function toInt() : int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
*
* @pure
*/
abstract public function toFloat() : float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss.
*
* @pure
*/
abstract public function __toString() : string;
#[Override]
final public function jsonSerialize() : string
{
return $this->__toString();
}
}

441
vendor/brick/math/src/BigRational.php vendored Normal file
View File

@ -0,0 +1,441 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Override;
/**
* An arbitrarily large rational number.
*
* This class is immutable.
*/
final readonly class BigRational extends BigNumber
{
/**
* The numerator.
*/
private BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private BigInteger $denominator;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param BigInteger $numerator The numerator.
* @param BigInteger $denominator The denominator.
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
*
* @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
if ($checkDenominator) {
if ($denominator->isZero()) {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->denominator = $denominator;
}
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigRational();
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/
public static function nd(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
) : BigRational {
$numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns a BigRational representing zero.
*
* @pure
*/
public static function zero() : BigRational
{
/** @var BigRational|null $zero */
static $zero;
if ($zero === null) {
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
}
return $zero;
}
/**
* Returns a BigRational representing one.
*
* @pure
*/
public static function one() : BigRational
{
/** @var BigRational|null $one */
static $one;
if ($one === null) {
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
}
return $one;
}
/**
* Returns a BigRational representing ten.
*
* @pure
*/
public static function ten() : BigRational
{
/** @var BigRational|null $ten */
static $ten;
if ($ten === null) {
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
}
return $ten;
}
/**
* @pure
*/
public function getNumerator() : BigInteger
{
return $this->numerator;
}
/**
* @pure
*/
public function getDenominator() : BigInteger
{
return $this->denominator;
}
/**
* Returns the quotient of the division of the numerator by the denominator.
*
* @pure
*/
public function quotient() : BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the remainder of the division of the numerator by the denominator.
*
* @pure
*/
public function remainder() : BigInteger
{
return $this->numerator->remainder($this->denominator);
}
/**
* Returns the quotient and remainder of the division of the numerator by the denominator.
*
* @return array{BigInteger, BigInteger}
*
* @pure
*/
public function quotientAndRemainder() : array
{
return $this->numerator->quotientAndRemainder($this->denominator);
}
/**
* Returns the sum of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @throws MathException If the number is not valid.
*
* @pure
*/
public function plus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the difference of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to subtract.
*
* @throws MathException If the number is not valid.
*
* @pure
*/
public function minus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the product of this number and the given one.
*
* @param BigNumber|int|float|string $that The multiplier.
*
* @throws MathException If the multiplier is not a valid number.
*
* @pure
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->numerator);
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the result of the division of this number by the given one.
*
* @param BigNumber|int|float|string $that The divisor.
*
* @throws MathException If the divisor is not a valid number, or is zero.
*
* @pure
*/
public function dividedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns this number exponentiated to the given value.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/
public function power(int $exponent) : BigRational
{
if ($exponent === 0) {
$one = BigInteger::one();
return new BigRational($one, $one, false);
}
if ($exponent === 1) {
return $this;
}
return new BigRational(
$this->numerator->power($exponent),
$this->denominator->power($exponent),
false
);
}
/**
* Returns the reciprocal of this BigRational.
*
* The reciprocal has the numerator and denominator swapped.
*
* @throws DivisionByZeroException If the numerator is zero.
*
* @pure
*/
public function reciprocal() : BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
/**
* Returns the absolute value of this BigRational.
*
* @pure
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*
* @pure
*/
public function negated() : BigRational
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*
* @pure
*/
public function simplified() : BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
$numerator = $this->numerator->quotient($gcd);
$denominator = $this->denominator->quotient($gcd);
return new BigRational($numerator, $denominator, false);
}
#[Override]
public function compareTo(BigNumber|int|float|string $that) : int
{
return $this->minus($that)->getSign();
}
#[Override]
public function getSign() : int
{
return $this->numerator->getSign();
}
#[Override]
public function toBigInteger() : BigInteger
{
$simplified = $this->simplified();
if (! $simplified->denominator->isEqualTo(1)) {
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
}
return $simplified->numerator;
}
#[Override]
public function toBigDecimal() : BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
}
#[Override]
public function toBigRational() : BigRational
{
return $this;
}
#[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
#[Override]
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat() : float
{
$simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
}
#[Override]
public function __toString() : string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
if ($denominator === '1') {
return $numerator;
}
return $numerator . '/' . $denominator;
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{numerator: BigInteger, denominator: BigInteger}
*/
public function __serialize(): array
{
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
/** @phpstan-ignore deadCode.unreachable */
$this->numerator = $data['numerator'];
$this->denominator = $data['denominator'];
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
final class DivisionByZeroException extends MathException
{
/**
* @pure
*/
public static function divisionByZero() : DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @pure
*/
public static function denominatorMustNotBeZero() : DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
/**
* Exception thrown when an integer overflow occurs.
*/
final class IntegerOverflowException extends MathException
{
/**
* @pure
*/
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Base class for all math exceptions.
*/
class MathException extends \RuntimeException
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/
final class NegativeNumberException extends MathException
{
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to create a number from a string with an invalid format.
*/
final class NumberFormatException extends MathException
{
/**
* @pure
*/
public static function invalidFormat(string $value) : self
{
return new self(\sprintf(
'The given value "%s" does not represent a valid number.',
$value,
));
}
/**
* @param string $char The failing character.
*
* @pure
*/
public static function charNotInAlphabet(string $char) : self
{
$ord = \ord($char);
if ($ord < 32 || $ord > 126) {
$char = \strtoupper(\dechex($ord));
if ($ord < 10) {
$char = '0' . $char;
}
} else {
$char = '"' . $char . '"';
}
return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char));
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a number cannot be represented at the requested scale without rounding.
*/
final class RoundingNecessaryException extends MathException
{
/**
* @pure
*/
public static function roundingNecessary() : RoundingNecessaryException
{
return new self('Rounding is necessary to represent the result of the operation at this scale.');
}
}

View File

@ -0,0 +1,665 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
/**
* Performs basic operations on arbitrary size integers.
*
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
* without leading zero, and with an optional leading minus sign if the number is not zero.
*
* Any other parameter format will lead to undefined behaviour.
* All methods must return strings respecting this format, unless specified otherwise.
*
* @internal
*/
abstract readonly class Calculator
{
/**
* The maximum exponent value allowed for the pow() method.
*/
public const MAX_POWER = 1_000_000;
/**
* The alphabet for converting from and to base 2 to 36, lowercase.
*/
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*
* @pure
*/
final protected function init(string $a, string $b) : array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? \substr($a, 1) : $a,
$bNeg ? \substr($b, 1) : $b,
];
}
/**
* Returns the absolute value of a number.
*
* @pure
*/
final public function abs(string $n) : string
{
return ($n[0] === '-') ? \substr($n, 1) : $n;
}
/**
* Negates a number.
*
* @pure
*/
final public function neg(string $n) : string
{
if ($n === '0') {
return '0';
}
if ($n[0] === '-') {
return \substr($n, 1);
}
return '-' . $n;
}
/**
* Compares two numbers.
*
* Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
*
* @return -1|0|1
*
* @pure
*/
final public function cmp(string $a, string $b) : int
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
if ($aNeg && ! $bNeg) {
return -1;
}
if ($bNeg && ! $aNeg) {
return 1;
}
$aLen = \strlen($aDig);
$bLen = \strlen($bDig);
if ($aLen < $bLen) {
$result = -1;
} elseif ($aLen > $bLen) {
$result = 1;
} else {
$result = $aDig <=> $bDig;
}
return $aNeg ? -$result : $result;
}
/**
* Adds two numbers.
*
* @pure
*/
abstract public function add(string $a, string $b) : string;
/**
* Subtracts two numbers.
*
* @pure
*/
abstract public function sub(string $a, string $b) : string;
/**
* Multiplies two numbers.
*
* @pure
*/
abstract public function mul(string $a, string $b) : string;
/**
* Returns the quotient of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The quotient.
*
* @pure
*/
abstract public function divQ(string $a, string $b) : string;
/**
* Returns the remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The remainder.
*
* @pure
*/
abstract public function divR(string $a, string $b) : string;
/**
* Returns the quotient and remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return array{string, string} An array containing the quotient and remainder.
*
* @pure
*/
abstract public function divQR(string $a, string $b) : array;
/**
* Exponentiates a number.
*
* @param string $a The base number.
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
*
* @return string The power.
*
* @pure
*/
abstract public function pow(string $a, int $e) : string;
/**
* @param string $b The modulus; must not be zero.
*
* @pure
*/
public function mod(string $a, string $b) : string
{
return $this->divR($this->add($this->divR($a, $b), $b), $b);
}
/**
* Returns the modular multiplicative inverse of $x modulo $m.
*
* If $x has no multiplicative inverse mod m, this method must return null.
*
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
*
* @param string $m The modulus; must not be negative or zero.
*
* @pure
*/
public function modInverse(string $x, string $m) : ?string
{
if ($m === '1') {
return '0';
}
$modVal = $x;
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
$modVal = $this->mod($x, $m);
}
[$g, $x] = $this->gcdExtended($modVal, $m);
if ($g !== '1') {
return null;
}
return $this->mod($this->add($this->mod($x, $m), $m), $m);
}
/**
* Raises a number into power with modulo.
*
* @param string $base The base number; must be positive or zero.
* @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive.
*
* @pure
*/
abstract public function modPow(string $base, string $exp, string $mod) : string;
/**
* Returns the greatest common divisor of the two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for GCD calculations.
*
* @return string The GCD, always positive, or zero if both arguments are zero.
*
* @pure
*/
public function gcd(string $a, string $b) : string
{
if ($a === '0') {
return $this->abs($b);
}
if ($b === '0') {
return $this->abs($a);
}
return $this->gcd($b, $this->divR($a, $b));
}
/**
* @return array{string, string, string} GCD, X, Y
*
* @pure
*/
private function gcdExtended(string $a, string $b) : array
{
if ($a === '0') {
return [$b, '0', '1'];
}
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1;
return [$gcd, $x, $y];
}
/**
* Returns the square root of the given number, rounded down.
*
* The result is the largest x such that n.
* The input MUST NOT be negative.
*
* @pure
*/
abstract public function sqrt(string $n) : string;
/**
* Converts a number from an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
* @param int $base The base of the number, validated from 2 to 36.
*
* @return string The converted number, following the Calculator conventions.
*
* @pure
*/
public function fromBase(string $number, int $base) : string
{
return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
}
/**
* Converts a number to an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number to convert, following the Calculator conventions.
* @param int $base The base to convert to, validated from 2 to 36.
*
* @return string The converted number, lowercase.
*
* @pure
*/
public function toBase(string $number, int $base) : string
{
$negative = ($number[0] === '-');
if ($negative) {
$number = \substr($number, 1);
}
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
if ($negative) {
return '-' . $number;
}
return $number;
}
/**
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
*
* @param string $number The number to convert, validated as a non-empty string,
* containing only chars in the given alphabet/base.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base of the number, validated from 2 to alphabet length.
*
* @return string The number in base 10, following the Calculator conventions.
*
* @pure
*/
final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
{
// remove leading "zeros"
$number = \ltrim($number, $alphabet[0]);
if ($number === '') {
return '0';
}
// optimize for "one"
if ($number === $alphabet[1]) {
return '1';
}
$result = '0';
$power = '1';
$base = (string) $base;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$index = \strpos($alphabet, $number[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, $base);
}
}
return $result;
}
/**
* Converts a non-negative number to an arbitrary base using a custom alphabet.
*
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base to convert to, validated from 2 to alphabet length.
*
* @return string The converted number in the given alphabet.
*
* @pure
*/
final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
{
if ($number === '0') {
return $alphabet[0];
}
$base = (string) $base;
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, $base);
$remainder = (int) $remainder;
$result .= $alphabet[$remainder];
}
return \strrev($result);
}
/**
* Performs a rounded division.
*
* Rounding is performed when the remainder of the division is not zero.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
* @param RoundingMode $roundingMode The rounding mode.
*
* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
*
* @pure
*/
final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string
{
[$quotient, $remainder] = $this->divQR($a, $b);
$hasDiscardedFraction = ($remainder !== '0');
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
$discardedFractionSign = function() use ($remainder, $b) : int {
$r = $this->abs($this->mul($remainder, '2'));
$b = $this->abs($b);
return $this->cmp($r, $b);
};
$increment = false;
switch ($roundingMode) {
case RoundingMode::UNNECESSARY:
if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary();
}
break;
case RoundingMode::UP:
$increment = $hasDiscardedFraction;
break;
case RoundingMode::DOWN:
break;
case RoundingMode::CEILING:
$increment = $hasDiscardedFraction && $isPositiveOrZero;
break;
case RoundingMode::FLOOR:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break;
case RoundingMode::HALF_UP:
$increment = $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_DOWN:
$increment = $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_CEILING:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break;
case RoundingMode::HALF_FLOOR:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
case RoundingMode::HALF_EVEN:
$lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
}
if ($increment) {
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
}
return $quotient;
}
/**
* Calculates bitwise AND of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function and(string $a, string $b) : string
{
return $this->bitwise('and', $a, $b);
}
/**
* Calculates bitwise OR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function or(string $a, string $b) : string
{
return $this->bitwise('or', $a, $b);
}
/**
* Calculates bitwise XOR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function xor(string $a, string $b) : string
{
return $this->bitwise('xor', $a, $b);
}
/**
* Performs a bitwise operation on a decimal number.
*
* @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left operand.
* @param string $b The right operand.
*
* @pure
*/
private function bitwise(string $operator, string $a, string $b) : string
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig);
$aLen = \strlen($aBin);
$bLen = \strlen($bBin);
if ($aLen > $bLen) {
$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) {
$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
}
if ($aNeg) {
$aBin = $this->twosComplement($aBin);
}
if ($bNeg) {
$bBin = $this->twosComplement($bBin);
}
$value = match ($operator) {
'and' => $aBin & $bBin,
'or' => $aBin | $bBin,
'xor' => $aBin ^ $bBin,
};
$negative = match ($operator) {
'and' => $aNeg and $bNeg,
'or' => $aNeg or $bNeg,
'xor' => $aNeg xor $bNeg,
};
if ($negative) {
$value = $this->twosComplement($value);
}
$result = $this->toDecimal($value);
return $negative ? $this->neg($result) : $result;
}
/**
* @param string $number A positive, binary number.
*
* @pure
*/
private function twosComplement(string $number) : string
{
$xor = \str_repeat("\xff", \strlen($number));
$number ^= $xor;
for ($i = \strlen($number) - 1; $i >= 0; $i--) {
$byte = \ord($number[$i]);
if (++$byte !== 256) {
$number[$i] = \chr($byte);
break;
}
$number[$i] = "\x00";
if ($i === 0) {
$number = "\x01" . $number;
}
}
return $number;
}
/**
* Converts a decimal number to a binary string.
*
* @param string $number The number to convert, positive or zero, only digits.
*
* @pure
*/
private function toBinary(string $number) : string
{
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, '256');
$result .= \chr((int) $remainder);
}
return \strrev($result);
}
/**
* Returns the positive decimal representation of a binary number.
*
* @param string $bytes The bytes representing the number.
*
* @pure
*/
private function toDecimal(string $bytes) : string
{
$result = '0';
$power = '1';
for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
$index = \ord($bytes[$i]);
if ($index !== 0) {
$result = $this->add($result, ($index === 1)
? $power
: $this->mul($power, (string) $index)
);
}
if ($i !== 0) {
$power = $this->mul($power, '256');
}
}
return $result;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use Override;
/**
* Calculator implementation built around the bcmath library.
*
* @internal
*/
final readonly class BcMathCalculator extends Calculator
{
#[Override]
public function add(string $a, string $b) : string
{
return \bcadd($a, $b, 0);
}
#[Override]
public function sub(string $a, string $b) : string
{
return \bcsub($a, $b, 0);
}
#[Override]
public function mul(string $a, string $b) : string
{
return \bcmul($a, $b, 0);
}
#[Override]
public function divQ(string $a, string $b) : string
{
return \bcdiv($a, $b, 0);
}
#[Override]
public function divR(string $a, string $b) : string
{
return \bcmod($a, $b, 0);
}
#[Override]
public function divQR(string $a, string $b) : array
{
$q = \bcdiv($a, $b, 0);
$r = \bcmod($a, $b, 0);
return [$q, $r];
}
#[Override]
public function pow(string $a, int $e) : string
{
return \bcpow($a, (string) $e, 0);
}
#[Override]
public function modPow(string $base, string $exp, string $mod) : string
{
return \bcpowmod($base, $exp, $mod, 0);
}
#[Override]
public function sqrt(string $n) : string
{
return \bcsqrt($n, 0);
}
}

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use GMP;
use Override;
/**
* Calculator implementation built around the GMP library.
*
* @internal
*/
final readonly class GmpCalculator extends Calculator
{
#[Override]
public function add(string $a, string $b) : string
{
return \gmp_strval(\gmp_add($a, $b));
}
#[Override]
public function sub(string $a, string $b) : string
{
return \gmp_strval(\gmp_sub($a, $b));
}
#[Override]
public function mul(string $a, string $b) : string
{
return \gmp_strval(\gmp_mul($a, $b));
}
#[Override]
public function divQ(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_q($a, $b));
}
#[Override]
public function divR(string $a, string $b) : string
{
return \gmp_strval(\gmp_div_r($a, $b));
}
#[Override]
public function divQR(string $a, string $b) : array
{
[$q, $r] = \gmp_div_qr($a, $b);
/**
* @var GMP $q
* @var GMP $r
*/
return [
\gmp_strval($q),
\gmp_strval($r)
];
}
#[Override]
public function pow(string $a, int $e) : string
{
return \gmp_strval(\gmp_pow($a, $e));
}
#[Override]
public function modInverse(string $x, string $m) : ?string
{
$result = \gmp_invert($x, $m);
if ($result === false) {
return null;
}
return \gmp_strval($result);
}
#[Override]
public function modPow(string $base, string $exp, string $mod) : string
{
return \gmp_strval(\gmp_powm($base, $exp, $mod));
}
#[Override]
public function gcd(string $a, string $b) : string
{
return \gmp_strval(\gmp_gcd($a, $b));
}
#[Override]
public function fromBase(string $number, int $base) : string
{
return \gmp_strval(\gmp_init($number, $base));
}
#[Override]
public function toBase(string $number, int $base) : string
{
return \gmp_strval($number, $base);
}
#[Override]
public function and(string $a, string $b) : string
{
return \gmp_strval(\gmp_and($a, $b));
}
#[Override]
public function or(string $a, string $b) : string
{
return \gmp_strval(\gmp_or($a, $b));
}
#[Override]
public function xor(string $a, string $b) : string
{
return \gmp_strval(\gmp_xor($a, $b));
}
#[Override]
public function sqrt(string $n) : string
{
return \gmp_strval(\gmp_sqrt($n));
}
}

View File

@ -0,0 +1,603 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use Override;
/**
* Calculator implementation using only native PHP code.
*
* @internal
*/
final readonly class NativeCalculator extends Calculator
{
/**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
* For multiplication, this represents the max sum of the lengths of both operands.
*
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
*/
private int $maxDigits;
/**
* @pure
* @codeCoverageIgnore
*/
public function __construct()
{
$this->maxDigits = match (PHP_INT_SIZE) {
4 => 9,
8 => 18,
};
}
#[Override]
public function add(string $a, string $b) : string
{
/**
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a + $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0') {
return $b;
}
if ($b === '0') {
return $a;
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
if ($aNeg) {
$result = $this->neg($result);
}
return $result;
}
#[Override]
public function sub(string $a, string $b) : string
{
return $this->add($a, $this->neg($b));
}
#[Override]
public function mul(string $a, string $b) : string
{
/**
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a * $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0' || $b === '0') {
return '0';
}
if ($a === '1') {
return $b;
}
if ($b === '1') {
return $a;
}
if ($a === '-1') {
return $this->neg($b);
}
if ($b === '-1') {
return $this->neg($a);
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $this->doMul($aDig, $bDig);
if ($aNeg !== $bNeg) {
$result = $this->neg($result);
}
return $result;
}
#[Override]
public function divQ(string $a, string $b) : string
{
return $this->divQR($a, $b)[0];
}
#[Override]
public function divR(string $a, string $b): string
{
return $this->divQR($a, $b)[1];
}
#[Override]
public function divQR(string $a, string $b) : array
{
if ($a === '0') {
return ['0', '0'];
}
if ($a === $b) {
return ['1', '0'];
}
if ($b === '1') {
return [$a, '0'];
}
if ($b === '-1') {
return [$this->neg($a), '0'];
}
/** @var numeric-string $a */
$na = $a * 1; // cast to number
if (is_int($na)) {
/** @var numeric-string $b */
$nb = $b * 1;
if (is_int($nb)) {
// the only division that may overflow is PHP_INT_MIN / -1,
// which cannot happen here as we've already handled a divisor of -1 above.
$q = intdiv($na, $nb);
$r = $na % $nb;
return [
(string) $q,
(string) $r
];
}
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
[$q, $r] = $this->doDiv($aDig, $bDig);
if ($aNeg !== $bNeg) {
$q = $this->neg($q);
}
if ($aNeg) {
$r = $this->neg($r);
}
return [$q, $r];
}
#[Override]
public function pow(string $a, int $e) : string
{
if ($e === 0) {
return '1';
}
if ($e === 1) {
return $a;
}
$odd = $e % 2;
$e -= $odd;
$aa = $this->mul($a, $a);
$result = $this->pow($aa, $e / 2);
if ($odd === 1) {
$result = $this->mul($result, $a);
}
return $result;
}
/**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
*/
#[Override]
public function modPow(string $base, string $exp, string $mod) : string
{
// special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
if ($base === '0' && $exp === '0' && $mod === '1') {
return '0';
}
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
if ($exp === '0' && $mod === '1') {
return '0';
}
$x = $base;
$res = '1';
// numbers are positive, so we can use remainder instead of modulo
$x = $this->divR($x, $mod);
while ($exp !== '0') {
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
$res = $this->divR($this->mul($res, $x), $mod);
}
$exp = $this->divQ($exp, '2');
$x = $this->divR($this->mul($x, $x), $mod);
}
return $res;
}
/**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
*/
#[Override]
public function sqrt(string $n) : string
{
if ($n === '0') {
return '0';
}
// initial approximation
$x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
$decreased = false;
for (;;) {
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
break;
}
$decreased = $this->cmp($nx, $x) < 0;
$x = $nx;
}
return $x;
}
/**
* Performs the addition of two non-signed large integers.
*
* @pure
*/
private function doAdd(string $a, string $b) : string
{
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
$i = 0;
}
/** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry);
$sumLength = \strlen($sum);
if ($sumLength > $blockLength) {
$sum = \substr($sum, 1);
$carry = 1;
} else {
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$carry = 0;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
if ($carry === 1) {
$result = '1' . $result;
}
return $result;
}
/**
* Performs the subtraction of two non-signed large integers.
*
* @pure
*/
private function doSub(string $a, string $b) : string
{
if ($a === $b) {
return '0';
}
// Ensure that we always subtract to a positive result: biggest minus smallest.
$cmp = $this->doCmp($a, $b);
$invert = ($cmp === -1);
if ($invert) {
$c = $a;
$a = $b;
$b = $c;
}
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
$i = 0;
}
/** @var numeric-string $blockA */
$blockA = \substr($a, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = \substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
if ($sum < 0) {
$sum += $complement;
$carry = 1;
} else {
$carry = 0;
}
$sum = (string) $sum;
$sumLength = \strlen($sum);
if ($sumLength < $blockLength) {
$sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
// Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0);
$result = \ltrim($result, '0');
if ($invert) {
$result = $this->neg($result);
}
return $result;
}
/**
* Performs the multiplication of two non-signed large integers.
*
* @pure
*/
private function doMul(string $a, string $b) : string
{
$x = \strlen($a);
$y = \strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) {
$blockALength = $maxDigits;
if ($i < 0) {
$blockALength += $i;
$i = 0;
}
$blockA = (int) \substr($a, $i, $blockALength);
$line = '';
$carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) {
$blockBLength = $maxDigits;
if ($j < 0) {
$blockBLength += $j;
$j = 0;
}
$blockB = (int) \substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry;
$value = $mul % $complement;
$carry = ($mul - $value) / $complement;
$value = (string) $value;
$value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
if ($j === 0) {
break;
}
}
if ($carry !== 0) {
$line = $carry . $line;
}
$line = \ltrim($line, '0');
if ($line !== '') {
$line .= \str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line);
}
if ($i === 0) {
break;
}
}
return $result;
}
/**
* Performs the division of two non-signed large integers.
*
* @return string[] The quotient and remainder.
*
* @pure
*/
private function doDiv(string $a, string $b) : array
{
$cmp = $this->doCmp($a, $b);
if ($cmp === -1) {
return ['0', $a];
}
$x = \strlen($a);
$y = \strlen($b);
// we now know that a >= b && x >= y
$q = '0'; // quotient
$r = $a; // remainder
$z = $y; // focus length, always $y or $y+1
/** @var numeric-string $b */
$nb = $b * 1; // cast to number
// performance optimization in cases where the remainder will never cause int overflow
if (is_int(($nb - 1) * 10 + 9)) {
$r = (int) \substr($a, 0, $z - 1);
for ($i = $z - 1; $i < $x; $i++) {
$n = $r * 10 + (int) $a[$i];
/** @var int $nb */
$q .= \intdiv($n, $nb);
$r = $n % $nb;
}
return [\ltrim($q, '0') ?: '0', (string) $r];
}
for (;;) {
$focus = \substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
if ($cmp === -1) {
if ($z === $x) { // remainder < dividend
break;
}
$z++;
}
$zeros = \str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros);
$r = $a;
if ($r === '0') { // remainder == 0
break;
}
$x = \strlen($a);
if ($x < $y) { // remainder < dividend
break;
}
$z = $y;
}
return [$q, $r];
}
/**
* Compares two non-signed large numbers.
*
* @return -1|0|1
*
* @pure
*/
private function doCmp(string $a, string $b) : int
{
$x = \strlen($a);
$y = \strlen($b);
$cmp = $x <=> $y;
if ($cmp !== 0) {
return $cmp;
}
return \strcmp($a, $b) <=> 0; // enforce -1|0|1
}
/**
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
*
* The numbers must only consist of digits, without leading minus sign.
*
* @return array{string, string, int}
*
* @pure
*/
private function pad(string $a, string $b) : array
{
$x = \strlen($a);
$y = \strlen($b);
if ($x > $y) {
$b = \str_repeat('0', $x - $y) . $b;
return [$a, $b, $x];
}
if ($x < $y) {
$a = \str_repeat('0', $y - $x) . $a;
return [$a, $b, $y];
}
return [$a, $b, $x];
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use function extension_loaded;
/**
* Stores the current Calculator instance used by BigNumber classes.
*
* @internal
*/
final class CalculatorRegistry
{
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
*/
final public static function set(?Calculator $calculator) : void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
* only relying on autodetect.
*
* @pure
*/
final public static function get() : Calculator
{
/** @phpstan-ignore impure.staticPropertyAccess */
if (self::$instance === null) {
/** @phpstan-ignore impure.propertyAssign */
self::$instance = self::detect();
}
/** @phpstan-ignore impure.staticPropertyAccess */
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @pure
* @codeCoverageIgnore
*/
private static function detect() : Calculator
{
if (extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
}

98
vendor/brick/math/src/RoundingMode.php vendored Normal file
View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
/**
* Specifies a rounding behavior for numerical operations capable of discarding precision.
*
* Each rounding mode indicates how the least significant returned digit of a rounded result
* is to be calculated. If fewer digits are returned than the digits needed to represent the
* exact numerical result, the discarded digits will be referred to as the discarded fraction
* regardless the digits' contribution to the value of the number. In other words, considered
* as a numerical value, the discarded fraction could have an absolute value greater than one.
*/
enum RoundingMode
{
/**
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
*
* If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/
case UNNECESSARY;
/**
* Rounds away from zero.
*
* Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value.
*/
case UP;
/**
* Rounds towards zero.
*
* Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value.
*/
case DOWN;
/**
* Rounds towards positive infinity.
*
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
* Note that this rounding mode never decreases the calculated value.
*/
case CEILING;
/**
* Rounds towards negative infinity.
*
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
* Note that this rounding mode never increases the calculated value.
*/
case FLOOR;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
*
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
* Note that this is the rounding mode commonly taught at school.
*/
case HALF_UP;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
*
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
*/
case HALF_DOWN;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
*
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
*/
case HALF_CEILING;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
*
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
*/
case HALF_FLOOR;
/**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
*
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
* behaves as for HALF_DOWN if it's even.
*
* Note that this is the rounding mode that statistically minimizes
* cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/
case HALF_EVEN;
}

579
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,579 @@
<?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|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @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 list<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)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$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 list<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)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$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] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$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 list<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 list<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 keyed by their corresponding vendor directories.
*
* @return array<string, 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);
}
}

396
vendor/composer/InstalledVersions.php vendored Normal file
View File

@ -0,0 +1,396 @@
<?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 string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @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
*/
private static $installedIsLocalDir;
/**
* @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();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @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();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
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';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
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() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
vendor/composer/LICENSE vendored Normal file
View File

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

16
vendor/composer/autoload_classmap.php vendored Normal file
View File

@ -0,0 +1,16 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'GPBMetadata\\GrpcGcp' => $vendorDir . '/google/grpc-gcp/src/generated/GPBMetadata/GrpcGcp.php',
'Grpc\\Gcp\\AffinityConfig' => $vendorDir . '/google/grpc-gcp/src/generated/Grpc/Gcp/AffinityConfig.php',
'Grpc\\Gcp\\AffinityConfig_Command' => $vendorDir . '/google/grpc-gcp/src/generated/Grpc/Gcp/AffinityConfig_Command.php',
'Grpc\\Gcp\\ApiConfig' => $vendorDir . '/google/grpc-gcp/src/generated/Grpc/Gcp/ApiConfig.php',
'Grpc\\Gcp\\ChannelPoolConfig' => $vendorDir . '/google/grpc-gcp/src/generated/Grpc/Gcp/ChannelPoolConfig.php',
'Grpc\\Gcp\\MethodConfig' => $vendorDir . '/google/grpc-gcp/src/generated/Grpc/Gcp/MethodConfig.php',
);

13
vendor/composer/autoload_files.php vendored Normal file
View File

@ -0,0 +1,13 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
);

View File

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

43
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,43 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Grpc\\Gcp\\' => array($vendorDir . '/google/grpc-gcp/src'),
'Grpc\\' => array($vendorDir . '/grpc/grpc/src/lib'),
'Google\\Type\\' => array($vendorDir . '/google/common-protos/src/Type'),
'Google\\Rpc\\' => array($vendorDir . '/google/common-protos/src/Rpc'),
'Google\\Protobuf\\' => array($vendorDir . '/google/protobuf/src/Google/Protobuf'),
'Google\\LongRunning\\' => array($vendorDir . '/google/longrunning/src/LongRunning'),
'Google\\Iam\\' => array($vendorDir . '/google/common-protos/src/Iam'),
'Google\\Cloud\\Dialogflow\\' => array($vendorDir . '/google/cloud-dialogflow/src'),
'Google\\Cloud\\' => array($vendorDir . '/google/common-protos/src/Cloud'),
'Google\\Auth\\' => array($vendorDir . '/google/auth/src'),
'Google\\Api\\' => array($vendorDir . '/google/common-protos/src/Api'),
'Google\\ApiCore\\LongRunning\\' => array($vendorDir . '/google/longrunning/src/ApiCore/LongRunning'),
'Google\\ApiCore\\' => array($vendorDir . '/google/gax/src'),
'GPBMetadata\\Google\\Type\\' => array($vendorDir . '/google/common-protos/metadata/Type'),
'GPBMetadata\\Google\\Rpc\\' => array($vendorDir . '/google/common-protos/metadata/Rpc'),
'GPBMetadata\\Google\\Protobuf\\' => array($vendorDir . '/google/protobuf/src/GPBMetadata/Google/Protobuf'),
'GPBMetadata\\Google\\Longrunning\\' => array($vendorDir . '/google/longrunning/metadata/Longrunning'),
'GPBMetadata\\Google\\Logging\\' => array($vendorDir . '/google/common-protos/metadata/Logging'),
'GPBMetadata\\Google\\Iam\\' => array($vendorDir . '/google/common-protos/metadata/Iam'),
'GPBMetadata\\Google\\Cloud\\Dialogflow\\' => array($vendorDir . '/google/cloud-dialogflow/metadata'),
'GPBMetadata\\Google\\Cloud\\' => array($vendorDir . '/google/common-protos/metadata/Cloud'),
'GPBMetadata\\Google\\Api\\' => array($vendorDir . '/google/common-protos/metadata/Api'),
'GPBMetadata\\ApiCore\\' => array($vendorDir . '/google/gax/metadata/ApiCore'),
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
);

50
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit2be695aecbd4dcc8ecce8c4845e9f2d7
{
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('ComposerAutoloaderInit2be695aecbd4dcc8ecce8c4845e9f2d7', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit2be695aecbd4dcc8ecce8c4845e9f2d7', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

227
vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,227 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7
{
public static $files = array (
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
);
public static $prefixLengthsPsr4 = array (
'R' =>
array (
'Ramsey\\Uuid\\' => 12,
'Ramsey\\Collection\\' => 18,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17,
'Psr\\Http\\Client\\' => 16,
'Psr\\Cache\\' => 10,
),
'G' =>
array (
'GuzzleHttp\\Psr7\\' => 16,
'GuzzleHttp\\Promise\\' => 19,
'GuzzleHttp\\' => 11,
'Grpc\\Gcp\\' => 9,
'Grpc\\' => 5,
'Google\\Type\\' => 12,
'Google\\Rpc\\' => 11,
'Google\\Protobuf\\' => 16,
'Google\\LongRunning\\' => 19,
'Google\\Iam\\' => 11,
'Google\\Cloud\\Dialogflow\\' => 24,
'Google\\Cloud\\' => 13,
'Google\\Auth\\' => 12,
'Google\\Api\\' => 11,
'Google\\ApiCore\\LongRunning\\' => 27,
'Google\\ApiCore\\' => 15,
'GPBMetadata\\Google\\Type\\' => 24,
'GPBMetadata\\Google\\Rpc\\' => 23,
'GPBMetadata\\Google\\Protobuf\\' => 28,
'GPBMetadata\\Google\\Longrunning\\' => 31,
'GPBMetadata\\Google\\Logging\\' => 27,
'GPBMetadata\\Google\\Iam\\' => 23,
'GPBMetadata\\Google\\Cloud\\Dialogflow\\' => 36,
'GPBMetadata\\Google\\Cloud\\' => 25,
'GPBMetadata\\Google\\Api\\' => 23,
'GPBMetadata\\ApiCore\\' => 20,
),
'F' =>
array (
'Firebase\\JWT\\' => 13,
),
'B' =>
array (
'Brick\\Math\\' => 11,
),
);
public static $prefixDirsPsr4 = array (
'Ramsey\\Uuid\\' =>
array (
0 => __DIR__ . '/..' . '/ramsey/uuid/src',
),
'Ramsey\\Collection\\' =>
array (
0 => __DIR__ . '/..' . '/ramsey/collection/src',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\Http\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-client/src',
),
'Psr\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/cache/src',
),
'GuzzleHttp\\Psr7\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
),
'GuzzleHttp\\Promise\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
),
'GuzzleHttp\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
),
'Grpc\\Gcp\\' =>
array (
0 => __DIR__ . '/..' . '/google/grpc-gcp/src',
),
'Grpc\\' =>
array (
0 => __DIR__ . '/..' . '/grpc/grpc/src/lib',
),
'Google\\Type\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/src/Type',
),
'Google\\Rpc\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/src/Rpc',
),
'Google\\Protobuf\\' =>
array (
0 => __DIR__ . '/..' . '/google/protobuf/src/Google/Protobuf',
),
'Google\\LongRunning\\' =>
array (
0 => __DIR__ . '/..' . '/google/longrunning/src/LongRunning',
),
'Google\\Iam\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/src/Iam',
),
'Google\\Cloud\\Dialogflow\\' =>
array (
0 => __DIR__ . '/..' . '/google/cloud-dialogflow/src',
),
'Google\\Cloud\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/src/Cloud',
),
'Google\\Auth\\' =>
array (
0 => __DIR__ . '/..' . '/google/auth/src',
),
'Google\\Api\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/src/Api',
),
'Google\\ApiCore\\LongRunning\\' =>
array (
0 => __DIR__ . '/..' . '/google/longrunning/src/ApiCore/LongRunning',
),
'Google\\ApiCore\\' =>
array (
0 => __DIR__ . '/..' . '/google/gax/src',
),
'GPBMetadata\\Google\\Type\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Type',
),
'GPBMetadata\\Google\\Rpc\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Rpc',
),
'GPBMetadata\\Google\\Protobuf\\' =>
array (
0 => __DIR__ . '/..' . '/google/protobuf/src/GPBMetadata/Google/Protobuf',
),
'GPBMetadata\\Google\\Longrunning\\' =>
array (
0 => __DIR__ . '/..' . '/google/longrunning/metadata/Longrunning',
),
'GPBMetadata\\Google\\Logging\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Logging',
),
'GPBMetadata\\Google\\Iam\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Iam',
),
'GPBMetadata\\Google\\Cloud\\Dialogflow\\' =>
array (
0 => __DIR__ . '/..' . '/google/cloud-dialogflow/metadata',
),
'GPBMetadata\\Google\\Cloud\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Cloud',
),
'GPBMetadata\\Google\\Api\\' =>
array (
0 => __DIR__ . '/..' . '/google/common-protos/metadata/Api',
),
'GPBMetadata\\ApiCore\\' =>
array (
0 => __DIR__ . '/..' . '/google/gax/metadata/ApiCore',
),
'Firebase\\JWT\\' =>
array (
0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
),
'Brick\\Math\\' =>
array (
0 => __DIR__ . '/..' . '/brick/math/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'GPBMetadata\\GrpcGcp' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/GPBMetadata/GrpcGcp.php',
'Grpc\\Gcp\\AffinityConfig' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/Grpc/Gcp/AffinityConfig.php',
'Grpc\\Gcp\\AffinityConfig_Command' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/Grpc/Gcp/AffinityConfig_Command.php',
'Grpc\\Gcp\\ApiConfig' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/Grpc/Gcp/ApiConfig.php',
'Grpc\\Gcp\\ChannelPoolConfig' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/Grpc/Gcp/ChannelPoolConfig.php',
'Grpc\\Gcp\\MethodConfig' => __DIR__ . '/..' . '/google/grpc-gcp/src/generated/Grpc/Gcp/MethodConfig.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit2be695aecbd4dcc8ecce8c4845e9f2d7::$classMap;
}, null, ClassLoader::class);
}
}

1448
vendor/composer/installed.json vendored Normal file

File diff suppressed because it is too large Load Diff

245
vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,245 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '2e0e56a9271c9b4eb7c6014320398d8ef346bd90',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '2e0e56a9271c9b4eb7c6014320398d8ef346bd90',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'brick/math' => array(
'pretty_version' => '0.14.0',
'version' => '0.14.0.0',
'reference' => '113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2',
'type' => 'library',
'install_path' => __DIR__ . '/../brick/math',
'aliases' => array(),
'dev_requirement' => false,
),
'firebase/php-jwt' => array(
'pretty_version' => 'v6.11.1',
'version' => '6.11.1.0',
'reference' => 'd1e91ecf8c598d073d0995afa8cd5c75c6e19e66',
'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(),
'dev_requirement' => false,
),
'google/auth' => array(
'pretty_version' => 'v1.48.1',
'version' => '1.48.1.0',
'reference' => '023f41a2c80fb98a493dfb9dffcab643481a7ab0',
'type' => 'library',
'install_path' => __DIR__ . '/../google/auth',
'aliases' => array(),
'dev_requirement' => false,
),
'google/cloud-dialogflow' => array(
'pretty_version' => 'v2.2.0',
'version' => '2.2.0.0',
'reference' => 'b536e04b66e518505dbc0266c7f288f4d692cf7a',
'type' => 'library',
'install_path' => __DIR__ . '/../google/cloud-dialogflow',
'aliases' => array(),
'dev_requirement' => false,
),
'google/common-protos' => array(
'pretty_version' => '4.12.4',
'version' => '4.12.4.0',
'reference' => '0127156899af0df2681bd42024c60bd5360d64e3',
'type' => 'library',
'install_path' => __DIR__ . '/../google/common-protos',
'aliases' => array(),
'dev_requirement' => false,
),
'google/gax' => array(
'pretty_version' => 'v1.38.0',
'version' => '1.38.0.0',
'reference' => '0e1bce4a30722e85485bbb132b2fa811d66b397b',
'type' => 'library',
'install_path' => __DIR__ . '/../google/gax',
'aliases' => array(),
'dev_requirement' => false,
),
'google/grpc-gcp' => array(
'pretty_version' => 'v0.4.1',
'version' => '0.4.1.0',
'reference' => 'e585b7721bbe806ef45b5c52ae43dfc2bff89968',
'type' => 'library',
'install_path' => __DIR__ . '/../google/grpc-gcp',
'aliases' => array(),
'dev_requirement' => false,
),
'google/longrunning' => array(
'pretty_version' => '0.5.0',
'version' => '0.5.0.0',
'reference' => '715519ab4aaf3c4268adb2b551ee0f34135c8c5f',
'type' => 'library',
'install_path' => __DIR__ . '/../google/longrunning',
'aliases' => array(),
'dev_requirement' => false,
),
'google/protobuf' => array(
'pretty_version' => 'v4.32.1',
'version' => '4.32.1.0',
'reference' => 'c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb',
'type' => 'library',
'install_path' => __DIR__ . '/../google/protobuf',
'aliases' => array(),
'dev_requirement' => false,
),
'grpc/grpc' => array(
'pretty_version' => '1.74.0',
'version' => '1.74.0.0',
'reference' => '32bf4dba256d60d395582fb6e4e8d3936bcdb713',
'type' => 'library',
'install_path' => __DIR__ . '/../grpc/grpc',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/guzzle' => array(
'pretty_version' => '7.10.0',
'version' => '7.10.0.0',
'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/promises' => array(
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => '481557b130ef3790cf82b713667b43030dc9c957',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/psr7' => array(
'pretty_version' => '2.8.0',
'version' => '2.8.0.0',
'reference' => '21dc724a0583619cd1652f673303492272778051',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/cache',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-client' => array(
'pretty_version' => '1.0.3',
'version' => '1.0.3.0',
'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-client',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-client-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/http-factory' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/http-message' => array(
'pretty_version' => '2.0',
'version' => '2.0.0.0',
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
),
),
'psr/log' => array(
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
'type' => 'library',
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
'aliases' => array(),
'dev_requirement' => false,
),
'ramsey/collection' => array(
'pretty_version' => '2.1.1',
'version' => '2.1.1.0',
'reference' => '344572933ad0181accbf4ba763e85a0306a8c5e2',
'type' => 'library',
'install_path' => __DIR__ . '/../ramsey/collection',
'aliases' => array(),
'dev_requirement' => false,
),
'ramsey/uuid' => array(
'pretty_version' => '4.9.1',
'version' => '4.9.1.0',
'reference' => '81f941f6f729b1e3ceea61d9d014f8b6c6800440',
'type' => 'library',
'install_path' => __DIR__ . '/../ramsey/uuid',
'aliases' => array(),
'dev_requirement' => false,
),
'rhumsaa/uuid' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '4.9.1',
),
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

25
vendor/composer/platform_check.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.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;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}

205
vendor/firebase/php-jwt/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,205 @@
# Changelog
## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09)
### Bug Fixes
* update error text for consistency ([#528](https://github.com/firebase/php-jwt/issues/528)) ([c11113a](https://github.com/firebase/php-jwt/commit/c11113afa13265e016a669e75494b9203b8a7775))
## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23)
### Features
* support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad))
### Bug Fixes
* refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca))
## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24)
### Bug Fixes
* Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5))
* support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e))
## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18)
### Bug Fixes
* ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648))
* ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae))
## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28)
### Features
* allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9))
## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04)
### Features
* add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2))
## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14)
### Bug Fixes
* accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e))
* different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4))
## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14)
### Features
* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d))
### Bug Fixes
* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718))
## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14)
### Features
* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a))
## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13)
### Features
* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3))
### Bug Fixes
* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797))
## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)
### Bug Fixes
* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a))
### Miscellaneous Chores
* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495))
## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08)
### Features
* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95))
* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c))
## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01)
### Bug Fixes
* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd))
## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01)
### Bug Fixes
* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22))
* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2))
## 6.3.0 / 2022-07-15
- Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399))
- Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435))
## 6.2.0 / 2022-05-14
- Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397))
- Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)).
## 6.1.0 / 2022-03-23
- Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0
- Add parameter typing and return types where possible
## 6.0.0 / 2022-01-24
- **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information.
- New Key object to prevent key/algorithm type confusion (#365)
- Add JWK support (#273)
- Add ES256 support (#256)
- Add ES384 support (#324)
- Add Ed25519 support (#343)
## 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.

30
vendor/firebase/php-jwt/LICENSE vendored Normal file
View File

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

425
vendor/firebase/php-jwt/README.md vendored Normal file
View File

@ -0,0 +1,425 @@
![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg)
[![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 env 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 = [
'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);
// Pass a stdClass in as the third parameter to get the decoded header values
$headers = new stdClass();
$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers);
print_r($headers);
/*
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 encode/decode headers
-------
Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
this library. This is because without verifying the JWT, the header values could have been tampered with.
Any value pulled from an unverified header should be treated as if it could be any string sent in from an
attacker. If this is something you still want to do in your application for whatever reason, it's possible to
decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT
header part:
```php
use Firebase\JWT\JWT;
$key = 'example_key';
$payload = [
'iss' => 'http://example.org',
'aud' => 'http://example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$headers = [
'x-forwarded-for' => 'www.google.com'
];
// Encode headers in the JWT string
$jwt = JWT::encode($payload, $key, 'HS256', null, $headers);
// Decode headers from the JWT string WITHOUT validation
// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified.
// These headers could be any value sent by an attacker.
list($headersB64, $payloadB64, $sig) = explode('.', $jwt);
$decoded = json_decode(base64_decode($headersB64), true);
print_r($decoded);
```
Example with RS256 (openssl)
----------------------------
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew
M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S
JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM
78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5
HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ
WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k
6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc
VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2
oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b
c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW
h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK
bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M
39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l
3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG
vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC
6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb
OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP
nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y
xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG
8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L
hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15
YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44
DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI
RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek
2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og
-----END RSA PRIVATE KEY-----
EOD;
$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT
fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ
hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t
u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS
opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz
TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B
wQIDAQAB
-----END PUBLIC KEY-----
EOD;
$payload = [
'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 = [
'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 = [
'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";
````
Example with multiple keys
--------------------------
```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Example RSA keys from previous example
// $privateKey1 = '...';
// $publicKey1 = '...';
// Example EdDSA keys from previous example
// $privateKey2 = '...';
// $publicKey2 = '...';
$payload = [
'iss' => 'example.org',
'aud' => 'example.com',
'iat' => 1356999524,
'nbf' => 1357000000
];
$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1');
$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2');
echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
$keys = [
'kid1' => new Key($publicKey1, 'RS256'),
'kid2' => new Key($publicKey2, 'EdDSA'),
];
$decoded1 = JWT::decode($jwt1, $keys);
$decoded2 = JWT::decode($jwt2, $keys);
echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
echo "Decode 2:\n" . print_r((array) $decoded2, 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 Firebase\JWT\Key
// objects. Pass this as the second parameter to JWT::decode.
JWT::decode($jwt, JWK::parseKeySet($jwks));
```
Using Cached Key Sets
---------------------
The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
This has the following advantages:
1. The results are cached for performance.
2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation.
3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second.
```php
use Firebase\JWT\CachedKeySet;
use Firebase\JWT\JWT;
// The URI for the JWKS you wish to cache the results from
$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
// Create an HTTP client (can be any PSR-7 compatible HTTP client)
$httpClient = new GuzzleHttp\Client();
// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
$httpFactory = new GuzzleHttp\Psr\HttpFactory();
// Create a cache item pool (can be any PSR-6 compatible cache item pool)
$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
$keySet = new CachedKeySet(
$jwksUri,
$httpClient,
$httpFactory,
$cacheItemPool,
null, // $expiresAfter int seconds to set the JWKS to expire
true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys
);
$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above
$decoded = JWT::decode($jwt, $keySet);
```
Miscellaneous
-------------
#### Exception Handling
When a call to `JWT::decode` is invalid, it will throw one of the following exceptions:
```php
use Firebase\JWT\JWT;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
use Firebase\JWT\ExpiredException;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
try {
$decoded = JWT::decode($jwt, $keys);
} catch (InvalidArgumentException $e) {
// provided key/key-array is empty or malformed.
} catch (DomainException $e) {
// provided algorithm is unsupported OR
// provided key is invalid OR
// unknown error thrown in openSSL or libsodium OR
// libsodium is required but not available.
} catch (SignatureInvalidException $e) {
// provided JWT signature verification failed.
} catch (BeforeValidException $e) {
// provided JWT is trying to be used before "nbf" claim OR
// provided JWT is trying to be used before "iat" claim.
} catch (ExpiredException $e) {
// provided JWT is trying to be used after "exp" claim.
} catch (UnexpectedValueException $e) {
// provided JWT is malformed OR
// provided JWT is missing an algorithm / using an unsupported algorithm OR
// provided JWT algorithm does not match provided key OR
// provided key ID in key/key-array is empty or invalid.
}
```
All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified
like this:
```php
use Firebase\JWT\JWT;
use UnexpectedValueException;
try {
$decoded = JWT::decode($jwt, $keys);
} catch (LogicException $e) {
// errors having to do with environmental setup or malformed JWT Keys
} catch (UnexpectedValueException $e) {
// errors having to do with JWT signature and claims
}
```
#### Casting to array
The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays
instead, you can do the following:
```php
// return type is stdClass
$decoded = JWT::decode($jwt, $keys);
// cast to array
$decoded = json_decode(json_encode($decoded), true);
```
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).

42
vendor/firebase/php-jwt/composer.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
"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": "^8.0"
},
"suggest": {
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
"ext-sodium": "Support EdDSA (Ed25519) signatures"
},
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Firebase\JWT;
class BeforeValidException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
{
private object $payload;
public function setPayload(object $payload): void
{
$this->payload = $payload;
}
public function getPayload(): object
{
return $this->payload;
}
}

View File

@ -0,0 +1,274 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use UnexpectedValueException;
/**
* @implements ArrayAccess<string, Key>
*/
class CachedKeySet implements ArrayAccess
{
/**
* @var string
*/
private $jwksUri;
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $httpFactory;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @var ?int
*/
private $expiresAfter;
/**
* @var ?CacheItemInterface
*/
private $cacheItem;
/**
* @var array<string, array<mixed>>
*/
private $keySet;
/**
* @var string
*/
private $cacheKey;
/**
* @var string
*/
private $cacheKeyPrefix = 'jwks';
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var bool
*/
private $rateLimit;
/**
* @var string
*/
private $rateLimitCacheKey;
/**
* @var int
*/
private $maxCallsPerMinute = 10;
/**
* @var string|null
*/
private $defaultAlg;
public function __construct(
string $jwksUri,
ClientInterface $httpClient,
RequestFactoryInterface $httpFactory,
CacheItemPoolInterface $cache,
?int $expiresAfter = null,
bool $rateLimit = false,
?string $defaultAlg = null
) {
$this->jwksUri = $jwksUri;
$this->httpClient = $httpClient;
$this->httpFactory = $httpFactory;
$this->cache = $cache;
$this->expiresAfter = $expiresAfter;
$this->rateLimit = $rateLimit;
$this->defaultAlg = $defaultAlg;
$this->setCacheKeys();
}
/**
* @param string $keyId
* @return Key
*/
public function offsetGet($keyId): Key
{
if (!$this->keyIdExists($keyId)) {
throw new OutOfBoundsException('Key ID not found');
}
return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
}
/**
* @param string $keyId
* @return bool
*/
public function offsetExists($keyId): bool
{
return $this->keyIdExists($keyId);
}
/**
* @param string $offset
* @param Key $value
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Method not implemented');
}
/**
* @param string $offset
*/
public function offsetUnset($offset): void
{
throw new LogicException('Method not implemented');
}
/**
* @return array<mixed>
*/
private function formatJwksForCache(string $jwks): array
{
$jwks = json_decode($jwks, true);
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');
}
$keys = [];
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
$keys[(string) $kid] = $v;
}
return $keys;
}
private function keyIdExists(string $keyId): bool
{
if (null === $this->keySet) {
$item = $this->getCacheItem();
// Try to load keys from cache
if ($item->isHit()) {
// item found! retrieve it
$this->keySet = $item->get();
// If the cached item is a string, the JWKS response was cached (previous behavior).
// Parse this into expected format array<kid, jwk> instead.
if (\is_string($this->keySet)) {
$this->keySet = $this->formatJwksForCache($this->keySet);
}
}
}
if (!isset($this->keySet[$keyId])) {
if ($this->rateLimitExceeded()) {
return false;
}
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
$jwksResponse = $this->httpClient->sendRequest($request);
if ($jwksResponse->getStatusCode() !== 200) {
throw new UnexpectedValueException(
\sprintf('HTTP Error: %d %s for URI "%s"',
$jwksResponse->getStatusCode(),
$jwksResponse->getReasonPhrase(),
$this->jwksUri,
),
$jwksResponse->getStatusCode()
);
}
$this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
if (!isset($this->keySet[$keyId])) {
return false;
}
$item = $this->getCacheItem();
$item->set($this->keySet);
if ($this->expiresAfter) {
$item->expiresAfter($this->expiresAfter);
}
$this->cache->save($item);
}
return true;
}
private function rateLimitExceeded(): bool
{
if (!$this->rateLimit) {
return false;
}
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
$cacheItemData = [];
if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
$cacheItemData = $data;
}
$callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
$expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
if (++$callsPerMinute > $this->maxCallsPerMinute) {
return true;
}
$cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
$cacheItem->expiresAt($expiry);
$this->cache->save($cacheItem);
return false;
}
private function getCacheItem(): CacheItemInterface
{
if (\is_null($this->cacheItem)) {
$this->cacheItem = $this->cache->getItem($this->cacheKey);
}
return $this->cacheItem;
}
private function setCacheKeys(): void
{
if (empty($this->jwksUri)) {
throw new RuntimeException('JWKS URI is empty');
}
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
// add prefix
$key = $this->cacheKeyPrefix . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
$this->cacheKey = $key;
if ($this->rateLimit) {
// add prefix
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
}
$this->rateLimitCacheKey = $rateLimitKey;
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Firebase\JWT;
class ExpiredException extends \UnexpectedValueException implements JWTExceptionWithPayloadInterface
{
private object $payload;
public function setPayload(object $payload): void
{
$this->payload = $payload;
}
public function getPayload(): object
{
return $this->payload;
}
}

355
vendor/firebase/php-jwt/src/JWK.php vendored Normal file
View File

@ -0,0 +1,355 @@
<?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
{
private const OID = '1.2.840.10045.2.1';
private const ASN1_OBJECT_IDENTIFIER = 0x06;
private const ASN1_SEQUENCE = 0x10; // also defined in JWT
private const ASN1_BIT_STRING = 0x03;
private const EC_CURVES = [
'P-256' => '1.2.840.10045.3.1.7', // Len: 64
'secp256k1' => '1.3.132.0.10', // Len: 64
'P-384' => '1.3.132.0.34', // Len: 96
// 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
];
// For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
// This library supports the following subtypes:
private const OKP_SUBTYPES = [
'Ed25519' => true, // RFC 8037
];
/**
* Parse a set of JWK keys
*
* @param array<mixed> $jwks The JSON Web Key Set as an associative array
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return array<string, Key> An associative array of key IDs (kid) to Key objects
*
* @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, ?string $defaultAlg = null): array
{
$keys = [];
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, $defaultAlg)) {
$keys[(string) $kid] = $key;
}
}
if (0 === \count($keys)) {
throw new UnexpectedValueException('No supported algorithms found in JWK Set');
}
return $keys;
}
/**
* Parse a JWK key
*
* @param array<mixed> $jwk An individual JWK
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return Key The key object for the JWK
*
* @throws InvalidArgumentException Provided JWK is empty
* @throws UnexpectedValueException Provided JWK was invalid
* @throws DomainException OpenSSL failure
*
* @uses createPemFromModulusAndExponent
*/
public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key
{
if (empty($jwk)) {
throw new InvalidArgumentException('JWK must not be empty');
}
if (!isset($jwk['kty'])) {
throw new UnexpectedValueException('JWK must contain a "kty" parameter');
}
if (!isset($jwk['alg'])) {
if (\is_null($defaultAlg)) {
// The "alg" parameter is optional in a KTY, but an algorithm is required
// for parsing in this library. Use the $defaultAlg parameter when parsing the
// key set in order to prevent this error.
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
throw new UnexpectedValueException('JWK must contain an "alg" parameter');
}
$jwk['alg'] = $defaultAlg;
}
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 new Key($publicKey, $jwk['alg']);
case 'EC':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (empty($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (!isset(self::EC_CURVES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported EC curve');
}
if (empty($jwk['x']) || empty($jwk['y'])) {
throw new UnexpectedValueException('x and y not set');
}
$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
return new Key($publicKey, $jwk['alg']);
case 'OKP':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (!isset($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported OKP key subtype');
}
if (empty($jwk['x'])) {
throw new UnexpectedValueException('x not set');
}
// This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
$publicKey = JWT::convertBase64urlToBase64($jwk['x']);
return new Key($publicKey, $jwk['alg']);
case 'oct':
if (!isset($jwk['k'])) {
throw new UnexpectedValueException('k not set');
}
return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']);
default:
break;
}
return null;
}
/**
* Converts the EC JWK values to pem format.
*
* @param string $crv The EC curve (only P-256 & P-384 is supported)
* @param string $x The EC x-coordinate
* @param string $y The EC y-coordinate
*
* @return string
*/
private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
{
$pem =
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::OID)
)
. self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::EC_CURVES[$crv])
)
) .
self::encodeDER(
self::ASN1_BIT_STRING,
\chr(0x00) . \chr(0x04)
. JWT::urlsafeB64Decode($x)
. JWT::urlsafeB64Decode($y)
)
);
return \sprintf(
"-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
wordwrap(base64_encode($pem), 64, "\n", true)
);
}
/**
* 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(
string $n,
string $e
): string {
$mod = JWT::urlsafeB64Decode($n);
$exp = JWT::urlsafeB64Decode($e);
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
$rsaPublicKey = \pack(
'Ca*a*a*',
48,
self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
$modulus,
$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
);
return "-----BEGIN PUBLIC KEY-----\r\n" .
\chunk_split(\base64_encode($rsaPublicKey), 64) .
'-----END PUBLIC KEY-----';
}
/**
* 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(int $length): string
{
if ($length <= 0x7F) {
return \chr($length);
}
$temp = \ltrim(\pack('N', $length), \chr(0));
return \pack('Ca*', 0x80 | \strlen($temp), $temp);
}
/**
* Encodes a value into a DER object.
* Also defined in Firebase\JWT\JWT
*
* @param int $type DER tag
* @param string $value the value to encode
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$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 a string into a DER-encoded OID.
*
* @param string $oid the OID string
* @return string the binary DER-encoded OID
*/
private static function encodeOID(string $oid): string
{
$octets = explode('.', $oid);
// Get the first octet
$first = (int) array_shift($octets);
$second = (int) array_shift($octets);
$oid = \chr($first * 40 + $second);
// Iterate over subsequent octets
foreach ($octets as $octet) {
if ($octet == 0) {
$oid .= \chr(0x00);
continue;
}
$bin = '';
while ($octet) {
$bin .= \chr(0x80 | ($octet & 0x7f));
$octet >>= 7;
}
$bin[0] = $bin[0] & \chr(0x7f);
// Convert to big endian if necessary
if (pack('V', 65534) == pack('L', 65534)) {
$oid .= strrev($bin);
} else {
$oid .= $bin;
}
}
return $oid;
}
}

667
vendor/firebase/php-jwt/src/JWT.php vendored Normal file
View File

@ -0,0 +1,667 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use DateTime;
use DomainException;
use Exception;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use stdClass;
use UnexpectedValueException;
/**
* 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
{
private const ASN1_INTEGER = 0x02;
private const ASN1_SEQUENCE = 0x10;
private 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.
*
* @var int
*/
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.
*
* @var ?int
*/
public static $timestamp = null;
/**
* @var array<string, string[]>
*/
public static $supported_algs = [
'ES384' => ['openssl', 'SHA384'],
'ES256' => ['openssl', 'SHA256'],
'ES256K' => ['openssl', 'SHA256'],
'HS256' => ['hash_hmac', 'SHA256'],
'HS384' => ['hash_hmac', 'SHA384'],
'HS512' => ['hash_hmac', 'SHA512'],
'RS256' => ['openssl', 'SHA256'],
'RS384' => ['openssl', 'SHA384'],
'RS512' => ['openssl', 'SHA512'],
'EdDSA' => ['sodium_crypto', 'EdDSA'],
];
/**
* Decodes a JWT string into a PHP object.
*
* @param string $jwt The JWT
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray The Key or associative array of key IDs
* (kid) to 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 stdClass $headers Optional. Populates stdClass with headers.
*
* @return stdClass The JWT's payload as a PHP object
*
* @throws InvalidArgumentException Provided key/key-array was empty or malformed
* @throws DomainException Provided JWT is malformed
* @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(
string $jwt,
$keyOrKeyArray,
?stdClass &$headers = null
): stdClass {
// Validate JWT
$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;
$headerRaw = static::urlsafeB64Decode($headb64);
if (null === ($header = static::jsonDecode($headerRaw))) {
throw new UnexpectedValueException('Invalid header encoding');
}
if ($headers !== null) {
$headers = $header;
}
$payloadRaw = static::urlsafeB64Decode($bodyb64);
if (null === ($payload = static::jsonDecode($payloadRaw))) {
throw new UnexpectedValueException('Invalid claims encoding');
}
if (\is_array($payload)) {
// prevent PHP Fatal Error in edge-cases when payload is empty array
$payload = (object) $payload;
}
if (!$payload instanceof stdClass) {
throw new UnexpectedValueException('Payload must be a JSON object');
}
$sig = static::urlsafeB64Decode($cryptob64);
if (empty($header->alg)) {
throw new UnexpectedValueException('Empty algorithm');
}
if (empty(static::$supported_algs[$header->alg])) {
throw new UnexpectedValueException('Algorithm not supported');
}
$key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
// Check the algorithm
if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
// See issue #351
throw new UnexpectedValueException('Incorrect key for this algorithm');
}
if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
// OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
$sig = self::signatureToDER($sig);
}
if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $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) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) floor($payload->nbf))
);
$ex->setPayload($payload);
throw $ex;
}
// 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->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
$ex = new BeforeValidException(
'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) floor($payload->iat))
);
$ex->setPayload($payload);
throw $ex;
}
// Check if this token has expired.
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
$ex = new ExpiredException('Expired token');
$ex->setPayload($payload);
throw $ex;
}
return $payload;
}
/**
* Converts and signs a PHP array into a JWT string.
*
* @param array<mixed> $payload PHP array
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string $keyId
* @param array<string, string> $head An array with header elements to attach
*
* @return string A signed JWT
*
* @uses jsonEncode
* @uses urlsafeB64Encode
*/
public static function encode(
array $payload,
$key,
string $alg,
?string $keyId = null,
?array $head = null
): string {
$header = ['typ' => 'JWT'];
if (isset($head)) {
$header = \array_merge($header, $head);
}
$header['alg'] = $alg;
if ($keyId !== null) {
$header['kid'] = $keyId;
}
$segments = [];
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode((string) 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|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', '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(
string $msg,
$key,
string $alg
): string {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'hash_hmac':
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
return \hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
if (!\is_resource($key) && !openssl_pkey_get_private($key)) {
throw new DomainException('OpenSSL unable to validate key');
}
$success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
if (!$success) {
throw new DomainException('OpenSSL unable to sign data');
}
if ($alg === 'ES256' || $alg === 'ES256K') {
$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');
}
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $key));
$key = base64_decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
return sodium_crypto_sign_detached($msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
}
throw new DomainException('Algorithm not supported');
}
/**
* 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|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
*
* @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
*/
private static function verify(
string $msg,
string $signature,
$keyMaterial,
string $alg
): bool {
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, $keyMaterial, $algorithm); // @phpstan-ignore-line
if ($success === 1) {
return true;
}
if ($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');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $keyMaterial));
$key = base64_decode((string) end($lines));
if (\strlen($key) === 0) {
throw new DomainException('Key cannot be empty string');
}
if (\strlen($signature) === 0) {
throw new DomainException('Signature cannot be empty string');
}
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
case 'hash_hmac':
default:
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
return self::constantTimeEquals($hash, $signature);
}
}
/**
* Decode a JSON string into a PHP object.
*
* @param string $input JSON string
*
* @return mixed The decoded JSON string
*
* @throws DomainException Provided string was invalid JSON
*/
public static function jsonDecode(string $input)
{
$obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($obj === null && $input !== 'null') {
throw new DomainException('Null result with non-null input');
}
return $obj;
}
/**
* Encode a PHP array into a JSON string.
*
* @param array<mixed> $input A PHP array
*
* @return string JSON representation of the PHP array
*
* @throws DomainException Provided object could not be encoded to valid JSON
*/
public static function jsonEncode(array $input): string
{
$json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($json === 'null') {
throw new DomainException('Null result with non-null input');
}
if ($json === false) {
throw new DomainException('Provided object could not be encoded to valid JSON');
}
return $json;
}
/**
* Decode a string with URL-safe Base64.
*
* @param string $input A Base64 encoded string
*
* @return string A decoded string
*
* @throws InvalidArgumentException invalid base64 characters
*/
public static function urlsafeB64Decode(string $input): string
{
return \base64_decode(self::convertBase64UrlToBase64($input));
}
/**
* Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
*
* @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
*
* @return string A Base64 encoded string with standard characters (+/) and padding (=), when
* needed.
*
* @see https://www.rfc-editor.org/rfc/rfc4648
*/
public static function convertBase64UrlToBase64(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= \str_repeat('=', $padlen);
}
return \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(string $input): string
{
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
}
/**
* Determine if an algorithm has been provided for each Key
*
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray
* @param string|null $kid
*
* @throws UnexpectedValueException
*
* @return Key
*/
private static function getKey(
$keyOrKeyArray,
?string $kid
): Key {
if ($keyOrKeyArray instanceof Key) {
return $keyOrKeyArray;
}
if (empty($kid) && $kid !== '0') {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
if ($keyOrKeyArray instanceof CachedKeySet) {
// Skip "isset" check, as this will automatically refresh if not set
return $keyOrKeyArray[$kid];
}
if (!isset($keyOrKeyArray[$kid])) {
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
}
return $keyOrKeyArray[$kid];
}
/**
* @param string $left The string of known length to compare against
* @param string $right The user-supplied string
* @return bool
*/
public static function constantTimeEquals(string $left, string $right): bool
{
if (\function_exists('hash_equals')) {
return \hash_equals($left, $right);
}
$len = \min(self::safeStrlen($left), self::safeStrlen($right));
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= (\ord($left[$i]) ^ \ord($right[$i]));
}
$status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
return ($status === 0);
}
/**
* Helper method to create a JSON error.
*
* @param int $errno An error number from json_last_error()
*
* @throws DomainException
*
* @return void
*/
private static function handleJsonError(int $errno): void
{
$messages = [
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(string $str): int
{
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(string $sig): string
{
// Separate the signature into r-value and s-value
$length = max(1, (int) (\strlen($sig) / 2));
list($r, $s) = \str_split($sig, $length);
// 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(int $type, string $value): string
{
$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(string $der, int $keySize): string
{
// 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{int, string|null} the new offset and the decoded object
*/
private static function readDER(string $der, int $offset = 0): array
{
$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 [$pos, $data];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Firebase\JWT;
interface JWTExceptionWithPayloadInterface
{
/**
* Get the payload that caused this exception.
*
* @return object
*/
public function getPayload(): object;
/**
* Get the payload that caused this exception.
*
* @param object $payload
* @return void
*/
public function setPayload(object $payload): void;
}

55
vendor/firebase/php-jwt/src/Key.php vendored Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace Firebase\JWT;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use TypeError;
class Key
{
/**
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string $algorithm
*/
public function __construct(
private $keyMaterial,
private string $algorithm
) {
if (
!\is_string($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
&& !$keyMaterial instanceof OpenSSLCertificate
&& !\is_resource($keyMaterial)
) {
throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
throw new InvalidArgumentException('Key material must not be empty');
}
if (empty($algorithm)) {
throw new InvalidArgumentException('Algorithm must not be empty');
}
}
/**
* Return the algorithm valid for this key
*
* @return string
*/
public function getAlgorithm(): string
{
return $this->algorithm;
}
/**
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial()
{
return $this->keyMaterial;
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"language": "php",
"distribution_name": "google/auth",
"release_level": "stable",
"client_documentation": "https://cloud.google.com/php/docs/reference/auth/latest",
"library_type": "CORE"
}

202
vendor/google/auth/COPYING vendored Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

203
vendor/google/auth/LICENSE vendored Normal file
View File

@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

368
vendor/google/auth/README.md vendored Normal file
View File

@ -0,0 +1,368 @@
# Google Auth Library for PHP
<a href="https://cloud.google.com/php/docs/reference/auth/latest">Reference Docs</a>
## Description
This is Google's officially supported PHP client library for using OAuth 2.0
authorization and authentication with Google APIs.
### Installing via Composer
The recommended way to install the google auth library is through
[Composer](http://getcomposer.org).
```bash
# Install Composer
curl -sS https://getcomposer.org/installer | php
```
Next, run the Composer command to install the latest stable version:
```bash
composer.phar require google/auth
```
## Application Default Credentials
This library provides an implementation of
[Application Default Credentials (ADC)][application default credentials] for PHP.
Application Default Credentials provides a simple way to get authorization
credentials for use in calling Google APIs, and is
the recommended approach to authorize calls to Cloud APIs.
**Important**: If you accept a credential configuration (credential JSON/File/Stream) from an
external source for authentication to Google Cloud Platform, you must validate it before providing
it to any Google API or library. Providing an unvalidated credential configuration to Google APIs
can compromise the security of your systems and data. For more information, refer to
[Validate credential configurations from external sources][externally-sourced-credentials].
[externally-sourced-credentials]: https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
### Set up ADC
To use ADC, you must set it up by providing credentials.
How you set up ADC depends on the environment where your code is running,
and whether you are running code in a test or production environment.
For more information, see [Set up Application Default Credentials][set-up-adc].
### Enable the API you want to use
Before making your API call, you must be sure the API you're calling has been
enabled. Go to **APIs & Auth** > **APIs** in the
[Google Developers Console][developer console] and enable the APIs you'd like to
call. For the example below, you must enable the `Drive API`.
### Call the APIs
As long as you update the environment variable below to point to *your* JSON
credentials file, the following code should output a list of your Drive files.
```php
use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
// specify the path to your application credentials
putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');
// define the scopes for your API call
$scopes = ['https://www.googleapis.com/auth/drive.readonly'];
// create middleware
$middleware = ApplicationDefaultCredentials::getMiddleware($scopes);
$stack = HandlerStack::create();
$stack->push($middleware);
// create the HTTP client
$client = new Client([
'handler' => $stack,
'base_uri' => 'https://www.googleapis.com',
'auth' => 'google_auth' // authorize all requests
]);
// make the request
$response = $client->get('drive/v2/files');
// show the result!
print_r((string) $response->getBody());
```
##### Guzzle 5 Compatibility
If you are using [Guzzle 5][Guzzle 5], replace the `create middleware` and
`create the HTTP Client` steps with the following:
```php
// create the HTTP client
$client = new Client([
'base_url' => 'https://www.googleapis.com',
'auth' => 'google_auth' // authorize all requests
]);
// create subscriber
$subscriber = ApplicationDefaultCredentials::getSubscriber($scopes);
$client->getEmitter()->attach($subscriber);
```
#### Call using an ID Token
If your application is running behind Cloud Run, or using Cloud Identity-Aware
Proxy (IAP), you will need to fetch an ID token to access your application. For
this, use the static method `getIdTokenMiddleware` on
`ApplicationDefaultCredentials`.
```php
use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
// specify the path to your application credentials
putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');
// Provide the ID token audience. This can be a Client ID associated with an IAP application,
// Or the URL associated with a CloudRun App
// $targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';
// $targetAudience = 'https://service-1234-uc.a.run.app';
$targetAudience = 'YOUR_ID_TOKEN_AUDIENCE';
// create middleware
$middleware = ApplicationDefaultCredentials::getIdTokenMiddleware($targetAudience);
$stack = HandlerStack::create();
$stack->push($middleware);
// create the HTTP client
$client = new Client([
'handler' => $stack,
'auth' => 'google_auth',
// Cloud Run, IAP, or custom resource URL
'base_uri' => 'https://YOUR_PROTECTED_RESOURCE',
]);
// make the request
$response = $client->get('/');
// show the result!
print_r((string) $response->getBody());
```
For invoking Cloud Run services, your service account will need the
[`Cloud Run Invoker`](https://cloud.google.com/run/docs/authenticating/service-to-service)
IAM permission.
For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID
used when you set up your protected resource as the target audience. See how to
[secure your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto).
#### Call using a specific JSON key
If you want to use a specific JSON key instead of using `GOOGLE_APPLICATION_CREDENTIALS` environment variable, you can
do this:
```php
use Google\Auth\CredentialsLoader;
use Google\Auth\Middleware\AuthTokenMiddleware;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
// Define the Google Application Credentials array
$jsonKey = ['key' => 'value'];
// define the scopes for your API call
$scopes = ['https://www.googleapis.com/auth/drive.readonly'];
// Load credentials from JSON containing service account credentials.
$creds = new ServiceAccountCredentials($scopes, $jsonKey),
// For other credentials types, create those classes explicitly using the
// "type" field in the JSON key, for example:
$creds = match ($jsonKey['type']) {
'service_account' => new ServiceAccountCredentials($scope, $jsonKey),
'authorized_user' => new UserRefreshCredentials($scope, $jsonKey),
default => throw new InvalidArgumentException('This application only supports service account and user account credentials'),
};
// optional caching
$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
// create middleware
$middleware = new AuthTokenMiddleware($creds);
$stack = HandlerStack::create();
$stack->push($middleware);
// create the HTTP client
$client = new Client([
'handler' => $stack,
'base_uri' => 'https://www.googleapis.com',
'auth' => 'google_auth' // authorize all requests
]);
// make the request
$response = $client->get('drive/v2/files');
// show the result!
print_r((string) $response->getBody());
```
#### Call using Proxy-Authorization Header
If your application is behind a proxy such as [Google Cloud IAP][iap-proxy-header],
and your application occupies the `Authorization` request header,
you can include the ID token in a `Proxy-Authorization: Bearer`
header instead. If a valid ID token is found in a `Proxy-Authorization` header,
IAP authorizes the request with it. After authorizing the request, IAP passes
the Authorization header to your application without processing the content.
For this, use the static method `getProxyIdTokenMiddleware` on
`ApplicationDefaultCredentials`.
```php
use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
// specify the path to your application credentials
putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');
// Provide the ID token audience. This can be a Client ID associated with an IAP application
// $targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';
$targetAudience = 'YOUR_ID_TOKEN_AUDIENCE';
// create middleware
$middleware = ApplicationDefaultCredentials::getProxyIdTokenMiddleware($targetAudience);
$stack = HandlerStack::create();
$stack->push($middleware);
// create the HTTP client
$client = new Client([
'handler' => $stack,
'auth' => ['username', 'pass'], // auth option handled by your application
'proxy_auth' => 'google_auth',
]);
// make the request
$response = $client->get('/');
// show the result!
print_r((string) $response->getBody());
```
[iap-proxy-header]: https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_proxy-authorization_header
#### External credentials (Workload identity federation)
Using workload identity federation, your application can access Google Cloud resources from Amazon Web Services (AWS),
Microsoft Azure or any identity provider that supports OpenID Connect (OIDC).
Traditionally, applications running outside Google Cloud have used service account keys to access Google Cloud
resources. Using identity federation, you can allow your workload to impersonate a service account. This lets you access
Google Cloud resources directly, eliminating the maintenance and security burden associated with service account keys.
Follow the detailed instructions on how to
[Configure Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds).
#### Verifying JWTs
If you are [using Google ID tokens to authenticate users][google-id-tokens], use
the `Google\Auth\AccessToken` class to verify the ID token:
```php
use Google\Auth\AccessToken;
$auth = new AccessToken();
$auth->verify($idToken);
```
If your app is running behind [Google Identity-Aware Proxy][iap-id-tokens]
(IAP), you can verify the ID token coming from the IAP server by pointing to the
appropriate certificate URL for IAP. This is because IAP signs the ID
tokens with a different key than the Google Identity service:
```php
use Google\Auth\AccessToken;
$auth = new AccessToken();
$auth->verify($idToken, [
'certsLocation' => AccessToken::IAP_CERT_URL
]);
```
[google-id-tokens]: https://developers.google.com/identity/sign-in/web/backend-auth
[iap-id-tokens]: https://cloud.google.com/iap/docs/signed-headers-howto
## Caching
Caching is enabled by passing a PSR-6 `CacheItemPoolInterface`
instance to the constructor when instantiating the credentials.
We offer some caching classes out of the box under the `Google\Auth\Cache` namespace.
```php
use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\Cache\MemoryCacheItemPool;
// Cache Instance
$memoryCache = new MemoryCacheItemPool;
// Get the credentials
// From here, the credentials will cache the access token
$middleware = ApplicationDefaultCredentials::getCredentials($scope, cache: $memoryCache);
```
### FileSystemCacheItemPool Cache
The `FileSystemCacheItemPool` class is a `PSR-6` compliant cache that stores its
serialized objects on disk, caching data between processes and making it possible
to use data between different requests.
```php
use Google\Auth\Cache\FileSystemCacheItemPool;
use Google\Auth\ApplicationDefaultCredentials;
// Create a Cache pool instance
$cache = new FileSystemCacheItemPool(__DIR__ . '/cache');
// Pass your Cache to the Auth Library
$credentials = ApplicationDefaultCredentials::getCredentials($scope, cache: $cache);
// This token will be cached and be able to be used for the next request
$token = $credentials->fetchAuthToken();
```
### Integrating with a third party cache
You can use a third party that follows the `PSR-6` interface of your choice.
```php
// run "composer require symfony/cache"
use Google\Auth\ApplicationDefaultCredentials;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
// Create the cache instance
$filesystemCache = new FilesystemAdapter();
// Create Get the credentials
$credentials = ApplicationDefaultCredentials::getCredentials($targetAudience, cache: $filesystemCache);
```
## License
This library is licensed under Apache 2.0. Full license text is
available in [COPYING][copying].
## Contributing
See [CONTRIBUTING][contributing].
## Support
Please
[report bugs at the project on Github](https://github.com/google/google-auth-library-php/issues). Don't
hesitate to
[ask questions](http://stackoverflow.com/questions/tagged/google-auth-library-php)
about the client or APIs on [StackOverflow](http://stackoverflow.com).
[google-apis-php-client]: https://github.com/google/google-api-php-client
[application default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
[contributing]: https://github.com/google/google-auth-library-php/tree/main/.github/CONTRIBUTING.md
[copying]: https://github.com/google/google-auth-library-php/tree/main/COPYING
[Guzzle]: https://github.com/guzzle/guzzle
[Guzzle 5]: http://docs.guzzlephp.org/en/5.3
[developer console]: https://console.developers.google.com
[set-up-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc

7
vendor/google/auth/SECURITY.md vendored Normal file
View File

@ -0,0 +1,7 @@
# Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.

1
vendor/google/auth/VERSION vendored Normal file
View File

@ -0,0 +1 @@
1.48.1

44
vendor/google/auth/composer.json vendored Normal file
View File

@ -0,0 +1,44 @@
{
"name": "google/auth",
"type": "library",
"description": "Google Auth Library for PHP",
"keywords": ["google", "oauth2", "authentication"],
"homepage": "https://github.com/google/google-auth-library-php",
"license": "Apache-2.0",
"support": {
"docs": "https://cloud.google.com/php/docs/reference/auth/latest"
},
"require": {
"php": "^8.1",
"firebase/php-jwt": "^6.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.4.5",
"psr/http-message": "^1.1||^2.0",
"psr/cache": "^2.0||^3.0",
"psr/log": "^3.0"
},
"require-dev": {
"guzzlehttp/promises": "^2.0",
"squizlabs/php_codesniffer": "^4.0",
"phpunit/phpunit": "^9.6",
"phpspec/prophecy-phpunit": "^2.1",
"sebastian/comparator": ">=1.2.3",
"phpseclib/phpseclib": "^3.0.35",
"kelvinmo/simplejwt": "0.7.1",
"webmozart/assert": "^1.11",
"symfony/process": "^6.0||^7.0"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
},
"autoload": {
"psr-4": {
"Google\\Auth\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Google\\Auth\\Tests\\": "tests"
}
}
}

473
vendor/google/auth/src/AccessToken.php vendored Normal file
View File

@ -0,0 +1,473 @@
<?php
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use DateTime;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use Google\Auth\Cache\MemoryCacheItemPool;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
use phpseclib3\Math\BigInteger;
use Psr\Cache\CacheItemPoolInterface;
use RuntimeException;
use SimpleJWT\InvalidTokenException;
use SimpleJWT\JWT as SimpleJWT;
use SimpleJWT\Keys\KeyFactory;
use SimpleJWT\Keys\KeySet;
use TypeError;
use UnexpectedValueException;
/**
* Wrapper around Google Access Tokens which provides convenience functions.
*
* @experimental
*/
class AccessToken
{
const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
const IAP_CERT_URL = 'https://www.gstatic.com/iap/verify/public_key-jwk';
const IAP_ISSUER = 'https://cloud.google.com/iap';
const OAUTH2_ISSUER = 'accounts.google.com';
const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
const OAUTH2_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
/**
* @var callable
*/
private $httpHandler;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @param callable|null $httpHandler [optional] An HTTP Handler to deliver PSR-7 requests.
* @param CacheItemPoolInterface|null $cache [optional] A PSR-6 compatible cache implementation.
*/
public function __construct(
?callable $httpHandler = null,
?CacheItemPoolInterface $cache = null
) {
$this->httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
$this->cache = $cache ?: new MemoryCacheItemPool();
}
/**
* Verifies an id token and returns the authenticated apiLoginTicket.
* Throws an exception if the id token is not valid.
* The audience parameter can be used to control which id tokens are
* accepted. By default, the id token must have been issued to this OAuth2 client.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $options [optional] {
* Configuration options.
* @type string $audience The indended recipient of the token.
* @type string $issuer The intended issuer of the token.
* @type string $cacheKey The cache key of the cached certs. Defaults to
* the sha1 of $certsLocation if provided, otherwise is set to
* "federated_signon_certs_v3".
* @type string $certsLocation The location (remote or local) from which
* to retrieve certificates, if not cached. This value should only be
* provided in limited circumstances in which you are sure of the
* behavior.
* @type bool $throwException Whether the function should throw an
* exception if the verification fails. This is useful for
* determining the reason verification failed.
* }
* @return array<mixed>|false the token payload, if successful, or false if not.
* @throws InvalidArgumentException If certs could not be retrieved from a local file.
* @throws InvalidArgumentException If received certs are in an invalid format.
* @throws InvalidArgumentException If the cert alg is not supported.
* @throws RuntimeException If certs could not be retrieved from a remote location.
* @throws UnexpectedValueException If the token issuer does not match.
* @throws UnexpectedValueException If the token audience does not match.
*/
public function verify($token, array $options = [])
{
$audience = $options['audience'] ?? null;
$issuer = $options['issuer'] ?? null;
$certsLocation = $options['certsLocation'] ?? self::FEDERATED_SIGNON_CERT_URL;
$cacheKey = $options['cacheKey'] ?? $this->getCacheKeyFromCertLocation($certsLocation);
$throwException = $options['throwException'] ?? false; // for backwards compatibility
// Check signature against each available cert.
$certs = $this->getCerts($certsLocation, $cacheKey, $options);
$alg = $this->determineAlg($certs);
if (!in_array($alg, ['RS256', 'ES256'])) {
throw new InvalidArgumentException(
'unrecognized "alg" in certs, expected ES256 or RS256'
);
}
try {
if ($alg == 'RS256') {
return $this->verifyRs256($token, $certs, $audience, $issuer);
}
return $this->verifyEs256($token, $certs, $audience, $issuer);
} catch (ExpiredException $e) { // firebase/php-jwt 5+
} catch (SignatureInvalidException $e) { // firebase/php-jwt 5+
} catch (InvalidTokenException $e) { // simplejwt
} catch (InvalidArgumentException $e) {
} catch (UnexpectedValueException $e) {
}
if ($throwException) {
throw $e;
}
return false;
}
/**
* Identifies the expected algorithm to verify by looking at the "alg" key
* of the provided certs.
*
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @return string The expected algorithm, such as "ES256" or "RS256".
*/
private function determineAlg(array $certs)
{
$alg = null;
foreach ($certs as $cert) {
if (empty($cert['alg'])) {
throw new InvalidArgumentException(
'certs expects "alg" to be set'
);
}
$alg = $alg ?: $cert['alg'];
if ($alg != $cert['alg']) {
throw new InvalidArgumentException(
'More than one alg detected in certs'
);
}
}
return $alg;
}
/**
* Verifies an ES256-signed JWT.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @param string|null $audience If set, returns false if the provided
* audience does not match the "aud" claim on the JWT.
* @param string|null $issuer If set, returns false if the provided
* issuer does not match the "iss" claim on the JWT.
* @return array<mixed> the token payload, if successful, or false if not.
*/
private function verifyEs256($token, array $certs, $audience = null, $issuer = null)
{
$this->checkSimpleJwt();
$jwkset = new KeySet();
foreach ($certs as $cert) {
$jwkset->add(KeyFactory::create($cert, 'php'));
}
// Validate the signature using the key set and ES256 algorithm.
$jwt = $this->callSimpleJwtDecode([$token, $jwkset, 'ES256']);
$payload = $jwt->getClaims();
if ($audience) {
if (!isset($payload['aud']) || $payload['aud'] != $audience) {
throw new UnexpectedValueException('Audience does not match');
}
}
// @see https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
$issuer = $issuer ?: self::IAP_ISSUER;
if (!isset($payload['iss']) || $payload['iss'] !== $issuer) {
throw new UnexpectedValueException('Issuer does not match');
}
return $payload;
}
/**
* Verifies an RS256-signed JWT.
*
* @param string $token The JSON Web Token to be verified.
* @param array<mixed> $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @param string|null $audience If set, returns false if the provided
* audience does not match the "aud" claim on the JWT.
* @param string|null $issuer If set, returns false if the provided
* issuer does not match the "iss" claim on the JWT.
* @return array<mixed> the token payload, if successful, or false if not.
*/
private function verifyRs256($token, array $certs, $audience = null, $issuer = null)
{
$this->checkAndInitializePhpsec();
$keys = [];
foreach ($certs as $cert) {
if (empty($cert['kid'])) {
throw new InvalidArgumentException(
'certs expects "kid" to be set'
);
}
if (empty($cert['n']) || empty($cert['e'])) {
throw new InvalidArgumentException(
'RSA certs expects "n" and "e" to be set'
);
}
$publicKey = $this->loadPhpsecPublicKey($cert['n'], $cert['e']);
// create an array of key IDs to certs for the JWT library
$keys[$cert['kid']] = new Key($publicKey, 'RS256');
}
$payload = $this->callJwtStatic('decode', [
$token,
$keys,
]);
if ($audience) {
if (!property_exists($payload, 'aud') || $payload->aud != $audience) {
throw new UnexpectedValueException('Audience does not match');
}
}
// support HTTP and HTTPS issuers
// @see https://developers.google.com/identity/sign-in/web/backend-auth
$issuers = $issuer ? [$issuer] : [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
throw new UnexpectedValueException('Issuer does not match');
}
return (array) $payload;
}
/**
* Revoke an OAuth2 access token or refresh token. This method will revoke the current access
* token, if a token isn't provided.
*
* @param string|array<mixed> $token The token (access token or a refresh token) that should be revoked.
* @param array<mixed> $options [optional] Configuration options.
* @return bool Returns True if the revocation was successful, otherwise False.
*/
public function revoke($token, array $options = [])
{
if (is_array($token)) {
if (isset($token['refresh_token'])) {
$token = $token['refresh_token'];
} else {
$token = $token['access_token'];
}
}
$body = Utils::streamFor(http_build_query(['token' => $token]));
$request = new Request('POST', self::OAUTH2_REVOKE_URI, [
'Cache-Control' => 'no-store',
'Content-Type' => 'application/x-www-form-urlencoded',
], $body);
$httpHandler = $this->httpHandler;
$response = $httpHandler($request, $options);
return $response->getStatusCode() == 200;
}
/**
* Gets federated sign-on certificates to use for verifying identity tokens.
* Returns certs as array structure, where keys are key ids, and values
* are PEM encoded certificates.
*
* @param string $location The location from which to retrieve certs.
* @param string $cacheKey The key under which to cache the retrieved certs.
* @param array<mixed> $options [optional] Configuration options.
* @return array<mixed>
* @throws InvalidArgumentException If received certs are in an invalid format.
*/
private function getCerts($location, $cacheKey, array $options = [])
{
$cacheItem = $this->cache->getItem($cacheKey);
$certs = $cacheItem ? $cacheItem->get() : null;
$expireTime = null;
if (!$certs) {
list($certs, $expireTime) = $this->retrieveCertsFromLocation($location, $options);
}
if (!isset($certs['keys'])) {
if ($location !== self::IAP_CERT_URL) {
throw new InvalidArgumentException(
'federated sign-on certs expects "keys" to be set'
);
}
throw new InvalidArgumentException(
'certs expects "keys" to be set'
);
}
// Push caching off until after verifying certs are in a valid format.
// Don't want to cache bad data.
if ($expireTime) {
$cacheItem->expiresAt(new DateTime($expireTime));
$cacheItem->set($certs);
$this->cache->save($cacheItem);
}
return $certs['keys'];
}
/**
* Retrieve and cache a certificates file.
*
* @param string $url location
* @param array<mixed> $options [optional] Configuration options.
* @return array{array<mixed>, string}
* @throws InvalidArgumentException If certs could not be retrieved from a local file.
* @throws RuntimeException If certs could not be retrieved from a remote location.
*/
private function retrieveCertsFromLocation($url, array $options = [])
{
// If we're retrieving a local file, just grab it.
$expireTime = '+1 hour';
if (strpos($url, 'http') !== 0) {
if (!file_exists($url)) {
throw new InvalidArgumentException(sprintf(
'Failed to retrieve verification certificates from path: %s.',
$url
));
}
return [
json_decode((string) file_get_contents($url), true),
$expireTime
];
}
$httpHandler = $this->httpHandler;
$response = $httpHandler(new Request('GET', $url), $options);
if ($response->getStatusCode() == 200) {
if ($cacheControl = $response->getHeaderLine('Cache-Control')) {
array_map(function ($value) use (&$expireTime) {
list($key, $value) = explode('=', $value) + [null, null];
if (trim($key) == 'max-age') {
$expireTime = '+' . $value . ' seconds';
}
}, explode(',', $cacheControl));
}
return [
json_decode((string) $response->getBody(), true),
$expireTime
];
}
throw new RuntimeException(sprintf(
'Failed to retrieve verification certificates: "%s".',
$response->getBody()->getContents()
), $response->getStatusCode());
}
/**
* @return void
*/
private function checkAndInitializePhpsec()
{
if (!class_exists(RSA::class)) {
throw new RuntimeException('Please require phpseclib/phpseclib v3 to use this utility.');
}
}
/**
* @return string
* @throws TypeError If the key cannot be initialized to a string.
*/
private function loadPhpsecPublicKey(string $modulus, string $exponent): string
{
$key = PublicKeyLoader::load([
'n' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
$modulus,
]), 256),
'e' => new BigInteger($this->callJwtStatic('urlsafeB64Decode', [
$exponent
]), 256),
]);
$formattedPublicKey = $key->toString('PKCS8');
if (!is_string($formattedPublicKey)) {
throw new TypeError('Failed to initialize the key');
}
return $formattedPublicKey;
}
/**
* @return void
*/
private function checkSimpleJwt()
{
// @codeCoverageIgnoreStart
if (!class_exists(SimpleJwt::class)) {
throw new RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.');
}
// @codeCoverageIgnoreEnd
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param string $method
* @param array<mixed> $args
* @return mixed
*/
protected function callJwtStatic($method, array $args = [])
{
return call_user_func_array([JWT::class, $method], $args); // @phpstan-ignore-line
}
/**
* Provide a hook to mock calls to the JWT static methods.
*
* @param array<mixed> $args
* @return mixed
*/
protected function callSimpleJwtDecode(array $args = [])
{
return call_user_func_array([SimpleJwt::class, 'decode'], $args);
}
/**
* Generate a cache key based on the cert location using sha1 with the
* exception of using "federated_signon_certs_v3" to preserve BC.
*
* @param string $certsLocation
* @return string
*/
private function getCacheKeyFromCertLocation($certsLocation)
{
$key = $certsLocation === self::FEDERATED_SIGNON_CERT_URL
? 'federated_signon_certs_v3'
: sha1($certsLocation);
return 'google_auth_certs_cache|' . $key;
}
}

View File

@ -0,0 +1,391 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use DomainException;
use Google\Auth\Credentials\AppIdentityCredentials;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Logging\StdOutLogger;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
use Google\Auth\Subscriber\AuthTokenSubscriber;
use GuzzleHttp\Client;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
/**
* ApplicationDefaultCredentials obtains the default credentials for
* authorizing a request to a Google service.
*
* Application Default Credentials are described here:
* https://developers.google.com/accounts/docs/application-default-credentials
*
* This class implements the search for the application default credentials as
* described in the link.
*
* It provides three factory methods:
* - #get returns the computed credentials object
* - #getSubscriber returns an AuthTokenSubscriber built from the credentials object
* - #getMiddleware returns an AuthTokenMiddleware built from the credentials object
*
* This allows it to be used as follows with GuzzleHttp\Client:
*
* ```
* use Google\Auth\ApplicationDefaultCredentials;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $middleware = ApplicationDefaultCredentials::getMiddleware(
* 'https://www.googleapis.com/auth/taskqueue'
* );
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
* ```
*/
class ApplicationDefaultCredentials
{
private const SDK_DEBUG_ENV_VAR = 'GOOGLE_SDK_PHP_LOGGING';
/**
* @deprecated
*
* Obtains an AuthTokenSubscriber that uses the default FetchAuthTokenInterface
* implementation to use in this environment.
*
* If supplied, $scope is used to in creating the credentials instance if
* this does not fallback to the compute engine defaults.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @return AuthTokenSubscriber
* @throws DomainException if no implementation can be obtained.
*/
public static function getSubscriber(// @phpstan-ignore-line
$scope = null,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
) {
$creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache);
/** @phpstan-ignore-next-line */
return new AuthTokenSubscriber($creds, $httpHandler);
}
/**
* Obtains an AuthTokenMiddleware that uses the default FetchAuthTokenInterface
* implementation to use in this environment.
*
* If supplied, $scope is used to in creating the credentials instance if
* this does not fallback to the compute engine defaults.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @param string $quotaProject specifies a project to bill for access
* charges associated with the request.
* @return AuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getMiddleware(
$scope = null,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null,
$quotaProject = null
) {
$creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache, $quotaProject);
return new AuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment.
*
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @param string|null $quotaProject specifies a project to bill for access
* charges associated with the request.
* @param string|string[]|null $defaultScope The default scope to use if no
* user-defined scopes exist, expressed either as an Array or as a
* space-delimited string.
* @param string|null $universeDomain Specifies a universe domain to use for the
* calling client library.
* @param null|false|LoggerInterface $logger A PSR3 compliant LoggerInterface.
*
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
*/
public static function getCredentials(
$scope = null,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null,
$quotaProject = null,
$defaultScope = null,
?string $universeDomain = null,
null|false|LoggerInterface $logger = null,
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();
$anyScope = $scope ?: $defaultScope;
if (!$httpHandler) {
if (!($client = HttpClientCache::getHttpClient())) {
$client = new Client();
HttpClientCache::setHttpClient($client);
}
$httpHandler = HttpHandlerFactory::build($client, $logger);
}
if (is_null($quotaProject)) {
// if a quota project isn't specified, try to get one from the env var
$quotaProject = CredentialsLoader::quotaProjectFromEnv();
}
if (!is_null($jsonKey)) {
if ($quotaProject) {
$jsonKey['quota_project_id'] = $quotaProject;
}
if ($universeDomain) {
$jsonKey['universe_domain'] = $universeDomain;
}
$creds = CredentialsLoader::makeCredentials(
$scope,
$jsonKey,
$defaultScope
);
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
$creds = new AppIdentityCredentials($anyScope);
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}
if (is_null($creds)) {
throw new DomainException(self::notFound());
}
if (!is_null($cache)) {
$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
}
return $creds;
}
/**
* Obtains an AuthTokenMiddleware which will fetch an ID token to use in the
* Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $targetAudience is used to set the "aud" on the resulting
* ID token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @return AuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getIdTokenMiddleware(
$targetAudience,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
) {
$creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
return new AuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains an ProxyAuthTokenMiddleware which will fetch an ID token to use in the
* Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $targetAudience is used to set the "aud" on the resulting
* ID token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @return ProxyAuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getProxyIdTokenMiddleware(
$targetAudience,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
) {
$creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
return new ProxyAuthTokenMiddleware($creds, $httpHandler);
}
/**
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment, configured with a $targetAudience for fetching an ID
* token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed>|null $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface|null $cache A cache implementation, may be
* provided if you have one already available for use.
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
* @throws InvalidArgumentException if JSON "type" key is invalid
*/
public static function getIdTokenCredentials(
$targetAudience,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();
if (!$httpHandler) {
if (!($client = HttpClientCache::getHttpClient())) {
$client = new Client();
HttpClientCache::setHttpClient($client);
}
$httpHandler = HttpHandlerFactory::build($client);
}
if (!is_null($jsonKey)) {
if (!array_key_exists('type', $jsonKey)) {
throw new \InvalidArgumentException('json key is missing the type field');
}
$creds = match ($jsonKey['type']) {
'authorized_user' => new UserRefreshCredentials(null, $jsonKey, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(null, $jsonKey, $targetAudience),
'service_account' => new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience),
default => throw new InvalidArgumentException('invalid value in the type field')
};
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, null, $targetAudience);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}
if (is_null($creds)) {
throw new DomainException(self::notFound());
}
if (!is_null($cache)) {
$creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
}
return $creds;
}
/**
* Returns a StdOutLogger instance
*
* @internal
*
* @return null|LoggerInterface
*/
public static function getDefaultLogger(): null|LoggerInterface
{
$loggingFlag = getenv(self::SDK_DEBUG_ENV_VAR);
// Env var is not set
if (empty($loggingFlag)) {
return null;
}
$loggingFlag = strtolower($loggingFlag);
// Env Var is not true
if ($loggingFlag !== 'true') {
if ($loggingFlag !== 'false') {
trigger_error('The ' . self::SDK_DEBUG_ENV_VAR . ' is set, but it is set to another value than false or true. Logging is disabled');
}
return null;
}
return new StdOutLogger();
}
/**
* @return string
*/
private static function notFound()
{
$msg = 'Your default credentials were not found. To set up ';
$msg .= 'Application Default Credentials, see ';
$msg .= 'https://cloud.google.com/docs/authentication/external/set-up-adc';
return $msg;
}
/**
* @param callable|null $httpHandler
* @param array<mixed>|null $cacheConfig
* @param CacheItemPoolInterface|null $cache
* @return bool
*/
private static function onGce(
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
) {
$gceCacheConfig = [];
foreach (['lifetime', 'prefix'] as $key) {
if (isset($cacheConfig['gce_' . $key])) {
$gceCacheConfig[$key] = $cacheConfig['gce_' . $key];
}
}
return (new GCECache($gceCacheConfig, $cache))->onGce($httpHandler);
}
}

View File

@ -0,0 +1,230 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use ErrorException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
class FileSystemCacheItemPool implements CacheItemPoolInterface
{
/**
* @var string
*/
private string $cachePath;
/**
* @var array<CacheItemInterface>
*/
private array $buffer = [];
/**
* Creates a FileSystemCacheItemPool cache that stores values in local storage
*
* @param string $path The string representation of the path where the cache will store the serialized objects.
*/
public function __construct(string $path)
{
$this->cachePath = $path;
if (is_dir($this->cachePath)) {
return;
}
if (!mkdir($this->cachePath)) {
throw new ErrorException("Cache folder couldn't be created.");
}
}
/**
* {@inheritdoc}
*/
public function getItem(string $key): CacheItemInterface
{
if (!$this->validKey($key)) {
throw new InvalidArgumentException("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}
$item = new TypedItem($key);
$itemPath = $this->cacheFilePath($key);
if (!file_exists($itemPath)) {
return $item;
}
$serializedItem = file_get_contents($itemPath);
if ($serializedItem === false) {
return $item;
}
$item->set(unserialize($serializedItem));
return $item;
}
/**
* {@inheritdoc}
*
* @return iterable<CacheItemInterface> An iterable object containing all the
* A traversable collection of Cache Items keyed by the cache keys of
* each item. A Cache item will be returned for each key, even if that
* key is not found. However, if no keys are specified then an empty
* traversable MUST be returned instead.
*/
public function getItems(array $keys = []): iterable
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->getItem($key);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item): bool
{
if (!$this->validKey($item->getKey())) {
return false;
}
$itemPath = $this->cacheFilePath($item->getKey());
$serializedItem = serialize($item->get());
$result = file_put_contents($itemPath, $serializedItem);
// 0 bytes write is considered a successful operation
if ($result === false) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function hasItem(string $key): bool
{
return $this->getItem($key)->isHit();
}
/**
* {@inheritdoc}
*/
public function clear(): bool
{
$this->buffer = [];
if (!is_dir($this->cachePath)) {
return false;
}
$files = scandir($this->cachePath);
if (!$files) {
return false;
}
foreach ($files as $fileName) {
if ($fileName === '.' || $fileName === '..') {
continue;
}
if (!unlink($this->cachePath . '/' . $fileName)) {
return false;
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function deleteItem(string $key): bool
{
if (!$this->validKey($key)) {
throw new InvalidArgumentException("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}
$itemPath = $this->cacheFilePath($key);
if (!file_exists($itemPath)) {
return true;
}
return unlink($itemPath);
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
$result = true;
foreach ($keys as $key) {
if (!$this->deleteItem($key)) {
$result = false;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item): bool
{
array_push($this->buffer, $item);
return true;
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
$result = true;
foreach ($this->buffer as $item) {
if (!$this->save($item)) {
$result = false;
}
}
return $result;
}
private function cacheFilePath(string $key): string
{
return $this->cachePath . '/' . $key;
}
private function validKey(string $key): bool
{
return (bool) preg_match('|^[a-zA-Z0-9_\.]+$|', $key);
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\InvalidArgumentException as PsrInvalidArgumentException;
class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException
{
}

View File

@ -0,0 +1,182 @@
<?php
/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* Simple in-memory cache implementation.
*/
final class MemoryCacheItemPool implements CacheItemPoolInterface
{
/**
* @var CacheItemInterface[]
*/
private $items;
/**
* @var CacheItemInterface[]
*/
private $deferredItems;
/**
* {@inheritdoc}
*
* @return CacheItemInterface The corresponding Cache Item.
*/
public function getItem($key): CacheItemInterface
{
return current($this->getItems([$key])); // @phpstan-ignore-line
}
/**
* {@inheritdoc}
*
* @return iterable<CacheItemInterface>
* A traversable collection of Cache Items keyed by the cache keys of
* each item. A Cache item will be returned for each key, even if that
* key is not found. However, if no keys are specified then an empty
* traversable MUST be returned instead.
*/
public function getItems(array $keys = []): iterable
{
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->hasItem($key) ? clone $this->items[$key] : new TypedItem($key);
}
return $items;
}
/**
* {@inheritdoc}
*
* @return bool
* True if item exists in the cache, false otherwise.
*/
public function hasItem($key): bool
{
$this->isValidKey($key);
return isset($this->items[$key]) && $this->items[$key]->isHit();
}
/**
* {@inheritdoc}
*
* @return bool
* True if the pool was successfully cleared. False if there was an error.
*/
public function clear(): bool
{
$this->items = [];
$this->deferredItems = [];
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if the item was successfully removed. False if there was an error.
*/
public function deleteItem($key): bool
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*
* @return bool
* True if the items were successfully removed. False if there was an error.
*/
public function deleteItems(array $keys): bool
{
array_walk($keys, [$this, 'isValidKey']);
foreach ($keys as $key) {
unset($this->items[$key]);
}
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if the item was successfully persisted. False if there was an error.
*/
public function save(CacheItemInterface $item): bool
{
$this->items[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* False if the item could not be queued or if a commit was attempted and failed. True otherwise.
*/
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferredItems[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
* True if all not-yet-saved items were successfully saved or there were none. False otherwise.
*/
public function commit(): bool
{
foreach ($this->deferredItems as $item) {
$this->save($item);
}
$this->deferredItems = [];
return true;
}
/**
* Determines if the provided key is valid.
*
* @param string $key
* @return bool
* @throws InvalidArgumentException
*/
private function isValidKey($key)
{
$invalidCharacters = '{}()/\\\\@:';
if (!is_string($key) || preg_match("#[$invalidCharacters]#", $key)) {
throw new InvalidArgumentException('The provided key is not valid: ' . var_export($key, true));
}
return true;
}
}

View File

@ -0,0 +1,248 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
/**
* SystemV shared memory based CacheItemPool implementation.
*
* This CacheItemPool implementation can be used among multiple processes, but
* it doesn't provide any locking mechanism. If multiple processes write to
* this ItemPool, you have to avoid race condition manually in your code.
*/
class SysVCacheItemPool implements CacheItemPoolInterface
{
const VAR_KEY = 1;
const DEFAULT_PROJ = 'A';
const DEFAULT_MEMSIZE = 10000;
const DEFAULT_PERM = 0600;
/**
* @var int
*/
private $sysvKey;
/**
* @var CacheItemInterface[]
*/
private $items;
/**
* @var CacheItemInterface[]
*/
private $deferredItems;
/**
* @var array<mixed>
*/
private $options;
/**
* @var bool
*/
private $hasLoadedItems = false;
/**
* Create a SystemV shared memory based CacheItemPool.
*
* @param array<mixed> $options {
* [optional] Configuration options.
*
* @type int $variableKey The variable key for getting the data from the shared memory. **Defaults to** 1.
* @type string $proj The project identifier for ftok. This needs to be a one character string.
* **Defaults to** 'A'.
* @type int $memsize The memory size in bytes for shm_attach. **Defaults to** 10000.
* @type int $perm The permission for shm_attach. **Defaults to** 0600.
* }
*/
public function __construct($options = [])
{
if (! extension_loaded('sysvshm')) {
throw new \RuntimeException(
'sysvshm extension is required to use this ItemPool'
);
}
$this->options = $options + [
'variableKey' => self::VAR_KEY,
'proj' => self::DEFAULT_PROJ,
'memsize' => self::DEFAULT_MEMSIZE,
'perm' => self::DEFAULT_PERM
];
$this->items = [];
$this->deferredItems = [];
$this->sysvKey = ftok(__FILE__, $this->options['proj']);
}
/**
* @param mixed $key
* @return CacheItemInterface
*/
public function getItem($key): CacheItemInterface
{
$this->loadItems();
return current($this->getItems([$key])); // @phpstan-ignore-line
}
/**
* @param array<mixed> $keys
* @return iterable<CacheItemInterface>
*/
public function getItems(array $keys = []): iterable
{
$this->loadItems();
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->hasItem($key) ?
clone $this->items[$key] :
new TypedItem($key);
}
return $items;
}
/**
* {@inheritdoc}
*/
public function hasItem($key): bool
{
$this->loadItems();
return isset($this->items[$key]) && $this->items[$key]->isHit();
}
/**
* {@inheritdoc}
*/
public function clear(): bool
{
$this->items = [];
$this->deferredItems = [];
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function deleteItem($key): bool
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
if (!$this->hasLoadedItems) {
$this->loadItems();
}
foreach ($keys as $key) {
unset($this->items[$key]);
}
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item): bool
{
if (!$this->hasLoadedItems) {
$this->loadItems();
}
$this->items[$item->getKey()] = $item;
return $this->saveCurrentItems();
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item): bool
{
$this->deferredItems[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
foreach ($this->deferredItems as $item) {
if ($this->save($item) === false) {
return false;
}
}
$this->deferredItems = [];
return true;
}
/**
* Save the current items.
*
* @return bool true when success, false upon failure
*/
private function saveCurrentItems()
{
$shmid = shm_attach(
$this->sysvKey,
$this->options['memsize'],
$this->options['perm']
);
if ($shmid !== false) {
$ret = shm_put_var(
$shmid,
$this->options['variableKey'],
$this->items
);
shm_detach($shmid);
return $ret;
}
return false;
}
/**
* Load the items from the shared memory.
*
* @return bool true when success, false upon failure
*/
private function loadItems()
{
$shmid = shm_attach(
$this->sysvKey,
$this->options['memsize'],
$this->options['perm']
);
if ($shmid !== false) {
$data = @shm_get_var($shmid, $this->options['variableKey']);
if (!empty($data)) {
$this->items = $data;
} else {
$this->items = [];
}
shm_detach($shmid);
$this->hasLoadedItems = true;
return true;
}
return false;
}
}

View File

@ -0,0 +1,170 @@
<?php
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Cache;
use Psr\Cache\CacheItemInterface;
/**
* A cache item.
*
* This class will be used by MemoryCacheItemPool and SysVCacheItemPool
* on PHP 8.0 and above. It is compatible with psr/cache 3.0 (PSR-6).
* @see Item for compatiblity with previous versions of PHP.
*/
final class TypedItem implements CacheItemInterface
{
/**
* @var mixed
*/
private mixed $value;
/**
* @var \DateTimeInterface|null
*/
private ?\DateTimeInterface $expiration;
/**
* @var bool
*/
private bool $isHit = false;
/**
* @param string $key
*/
public function __construct(
private string $key
) {
$this->key = $key;
$this->expiration = null;
}
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return $this->key;
}
/**
* {@inheritdoc}
*/
public function get(): mixed
{
return $this->isHit() ? $this->value : null;
}
/**
* {@inheritdoc}
*/
public function isHit(): bool
{
if (!$this->isHit) {
return false;
}
if ($this->expiration === null) {
return true;
}
return $this->currentTime()->getTimestamp() < $this->expiration->getTimestamp();
}
/**
* {@inheritdoc}
*/
public function set(mixed $value): static
{
$this->isHit = true;
$this->value = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function expiresAt($expiration): static
{
if ($this->isValidExpiration($expiration)) {
$this->expiration = $expiration;
return $this;
}
$error = sprintf(
'Argument 1 passed to %s::expiresAt() must implement interface DateTimeInterface, %s given',
get_class($this),
gettype($expiration)
);
throw new \TypeError($error);
}
/**
* {@inheritdoc}
*/
public function expiresAfter($time): static
{
if (is_int($time)) {
$this->expiration = $this->currentTime()->add(new \DateInterval("PT{$time}S"));
} elseif ($time instanceof \DateInterval) {
$this->expiration = $this->currentTime()->add($time);
} elseif ($time === null) {
$this->expiration = $time;
} else {
$message = 'Argument 1 passed to %s::expiresAfter() must be an ' .
'instance of DateInterval or of the type integer, %s given';
$error = sprintf($message, get_class($this), gettype($time));
throw new \TypeError($error);
}
return $this;
}
/**
* Determines if an expiration is valid based on the rules defined by PSR6.
*
* @param mixed $expiration
* @return bool
*/
private function isValidExpiration($expiration)
{
if ($expiration === null) {
return true;
}
// We test for two types here due to the fact the DateTimeInterface
// was not introduced until PHP 5.5. Checking for the DateTime type as
// well allows us to support 5.4.
if ($expiration instanceof \DateTimeInterface) {
return true;
}
return false;
}
/**
* @return \DateTime
*/
protected function currentTime()
{
return new \DateTime('now', new \DateTimeZone('UTC'));
}
}

110
vendor/google/auth/src/CacheTrait.php vendored Normal file
View File

@ -0,0 +1,110 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Psr\Cache\CacheItemPoolInterface;
trait CacheTrait
{
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var array<mixed>
*/
private $cacheConfig;
/**
* @var ?CacheItemPoolInterface
*/
private $cache;
/**
* Gets the cached value if it is present in the cache when that is
* available.
*
* @param mixed $k
*
* @return mixed
*/
private function getCachedValue($k)
{
if (is_null($this->cache)) {
return null;
}
$key = $this->getFullCacheKey($k);
if (is_null($key)) {
return null;
}
$cacheItem = $this->cache->getItem($key);
if ($cacheItem->isHit()) {
return $cacheItem->get();
}
}
/**
* Saves the value in the cache when that is available.
*
* @param mixed $k
* @param mixed $v
* @return mixed
*/
private function setCachedValue($k, $v)
{
if (is_null($this->cache)) {
return null;
}
$key = $this->getFullCacheKey($k);
if (is_null($key)) {
return null;
}
$cacheItem = $this->cache->getItem($key);
$cacheItem->set($v);
$cacheItem->expiresAfter($this->cacheConfig['lifetime']);
return $this->cache->save($cacheItem);
}
/**
* @param null|string $key
* @return null|string
*/
private function getFullCacheKey($key)
{
if (is_null($key)) {
return null;
}
$key = ($this->cacheConfig['prefix'] ?? '') . $key;
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $key);
// Hash keys if they exceed $maxKeyLength (defaults to 64)
if ($this->maxKeyLength && strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
return $key;
}
}

View File

@ -0,0 +1,375 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
/**
* Authenticates requests using AWS credentials.
*/
class AwsNativeSource implements ExternalAccountCredentialSourceInterface
{
private const CRED_VERIFICATION_QUERY = 'Action=GetCallerIdentity&Version=2011-06-15';
private string $audience;
private string $regionalCredVerificationUrl;
private ?string $regionUrl;
private ?string $securityCredentialsUrl;
private ?string $imdsv2SessionTokenUrl;
/**
* @param string $audience The audience for the credential.
* @param string $regionalCredVerificationUrl The regional AWS GetCallerIdentity action URL used to determine the
* AWS account ID and its roles. This is not called by this library, but
* is sent in the subject token to be called by the STS token server.
* @param string|null $regionUrl This URL should be used to determine the current AWS region needed for the signed
* request construction.
* @param string|null $securityCredentialsUrl The AWS metadata server URL used to retrieve the access key, secret
* key and security token needed to sign the GetCallerIdentity request.
* @param string|null $imdsv2SessionTokenUrl Presence of this URL enforces the auth libraries to fetch a Session
* Token from AWS. This field is required for EC2 instances using IMDSv2.
*/
public function __construct(
string $audience,
string $regionalCredVerificationUrl,
?string $regionUrl = null,
?string $securityCredentialsUrl = null,
?string $imdsv2SessionTokenUrl = null
) {
$this->audience = $audience;
$this->regionalCredVerificationUrl = $regionalCredVerificationUrl;
$this->regionUrl = $regionUrl;
$this->securityCredentialsUrl = $securityCredentialsUrl;
$this->imdsv2SessionTokenUrl = $imdsv2SessionTokenUrl;
}
public function fetchSubjectToken(?callable $httpHandler = null): string
{
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$headers = [];
if ($this->imdsv2SessionTokenUrl) {
$headers = [
'X-aws-ec2-metadata-token' => self::getImdsV2SessionToken($this->imdsv2SessionTokenUrl, $httpHandler)
];
}
if (!$signingVars = self::getSigningVarsFromEnv()) {
if (!$this->securityCredentialsUrl) {
throw new \LogicException('Unable to get credentials from ENV, and no security credentials URL provided');
}
$signingVars = self::getSigningVarsFromUrl(
$httpHandler,
$this->securityCredentialsUrl,
self::getRoleName($httpHandler, $this->securityCredentialsUrl, $headers),
$headers
);
}
if (!$region = self::getRegionFromEnv()) {
if (!$this->regionUrl) {
throw new \LogicException('Unable to get region from ENV, and no region URL provided');
}
$region = self::getRegionFromUrl($httpHandler, $this->regionUrl, $headers);
}
$url = str_replace('{region}', $region, $this->regionalCredVerificationUrl);
$host = parse_url($url)['host'] ?? '';
// From here we use the signing vars to create the signed request to receive a token
[$accessKeyId, $secretAccessKey, $securityToken] = $signingVars;
$headers = self::getSignedRequestHeaders($region, $host, $accessKeyId, $secretAccessKey, $securityToken);
// Inject x-goog-cloud-target-resource into header
$headers['x-goog-cloud-target-resource'] = $this->audience;
// Format headers as they're expected in the subject token
$formattedHeaders = array_map(
fn ($k, $v) => ['key' => $k, 'value' => $v],
array_keys($headers),
$headers,
);
$request = [
'headers' => $formattedHeaders,
'method' => 'POST',
'url' => $url,
];
return urlencode(json_encode($request) ?: '');
}
/**
* @internal
*/
public static function getImdsV2SessionToken(string $imdsV2Url, callable $httpHandler): string
{
$headers = [
'X-aws-ec2-metadata-token-ttl-seconds' => '21600'
];
$request = new Request(
'PUT',
$imdsV2Url,
$headers
);
$response = $httpHandler($request);
return (string) $response->getBody();
}
/**
* @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
*
* @internal
*
* @return array<string, string>
*/
public static function getSignedRequestHeaders(
string $region,
string $host,
string $accessKeyId,
string $secretAccessKey,
?string $securityToken
): array {
$service = 'sts';
# Create a date for headers and the credential string in ISO-8601 format
$amzdate = gmdate('Ymd\THis\Z');
$datestamp = gmdate('Ymd'); # Date w/o time, used in credential scope
# Create the canonical headers and signed headers. Header names
# must be trimmed and lowercase, and sorted in code point order from
# low to high. Note that there is a trailing \n.
$canonicalHeaders = sprintf("host:%s\nx-amz-date:%s\n", $host, $amzdate);
if ($securityToken) {
$canonicalHeaders .= sprintf("x-amz-security-token:%s\n", $securityToken);
}
# Step 5: Create the list of signed headers. This lists the headers
# in the canonicalHeaders list, delimited with ";" and in alpha order.
# Note: The request can include any headers; $canonicalHeaders and
# $signedHeaders lists those that you want to be included in the
# hash of the request. "Host" and "x-amz-date" are always required.
$signedHeaders = 'host;x-amz-date';
if ($securityToken) {
$signedHeaders .= ';x-amz-security-token';
}
# Step 6: Create payload hash (hash of the request body content). For GET
# requests, the payload is an empty string ("").
$payloadHash = hash('sha256', '');
# Step 7: Combine elements to create canonical request
$canonicalRequest = implode("\n", [
'POST', // method
'/', // canonical URL
self::CRED_VERIFICATION_QUERY, // query string
$canonicalHeaders,
$signedHeaders,
$payloadHash
]);
# ************* TASK 2: CREATE THE STRING TO SIGN*************
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
# SHA-256 (recommended)
$algorithm = 'AWS4-HMAC-SHA256';
$scope = implode('/', [$datestamp, $region, $service, 'aws4_request']);
$stringToSign = implode("\n", [$algorithm, $amzdate, $scope, hash('sha256', $canonicalRequest)]);
# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
// (done above)
$signingKey = self::getSignatureKey($secretAccessKey, $datestamp, $region, $service);
# Sign the string_to_sign using the signing_key
$signature = bin2hex(self::hmacSign($signingKey, $stringToSign));
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
# The signing information can be either in a query string value or in
# a header named Authorization. This code shows how to use a header.
# Create authorization header and add to request headers
$authorizationHeader = sprintf(
'%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
$algorithm,
$accessKeyId,
$scope,
$signedHeaders,
$signature
);
# The request can include any headers, but MUST include "host", "x-amz-date",
# and (for this scenario) "Authorization". "host" and "x-amz-date" must
# be included in the canonical_headers and signed_headers, as noted
# earlier. Order here is not significant.
$headers = [
'host' => $host,
'x-amz-date' => $amzdate,
'Authorization' => $authorizationHeader,
];
if ($securityToken) {
$headers['x-amz-security-token'] = $securityToken;
}
return $headers;
}
/**
* @internal
*/
public static function getRegionFromEnv(): ?string
{
$region = getenv('AWS_REGION');
if (empty($region)) {
$region = getenv('AWS_DEFAULT_REGION');
}
return $region ?: null;
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $regionUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
*/
public static function getRegionFromUrl(callable $httpHandler, string $regionUrl, array $headers): string
{
// get the region/zone from the region URL
$regionRequest = new Request('GET', $regionUrl, $headers);
$regionResponse = $httpHandler($regionRequest);
// Remove last character. For example, if us-east-2b is returned,
// the region would be us-east-2.
return substr((string) $regionResponse->getBody(), 0, -1);
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $securityCredentialsUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
*/
public static function getRoleName(callable $httpHandler, string $securityCredentialsUrl, array $headers): string
{
// Get the AWS role name
$roleRequest = new Request('GET', $securityCredentialsUrl, $headers);
$roleResponse = $httpHandler($roleRequest);
$roleName = (string) $roleResponse->getBody();
return $roleName;
}
/**
* @internal
*
* @param callable $httpHandler
* @param string $securityCredentialsUrl
* @param array<string, string|string[]> $headers Request headers to send in with the request.
* @return array{string, string, ?string}
*/
public static function getSigningVarsFromUrl(
callable $httpHandler,
string $securityCredentialsUrl,
string $roleName,
array $headers
): array {
// Get the AWS credentials
$credsRequest = new Request(
'GET',
$securityCredentialsUrl . '/' . $roleName,
$headers
);
$credsResponse = $httpHandler($credsRequest);
$awsCreds = json_decode((string) $credsResponse->getBody(), true);
return [
$awsCreds['AccessKeyId'], // accessKeyId
$awsCreds['SecretAccessKey'], // secretAccessKey
$awsCreds['Token'], // token
];
}
/**
* @internal
*
* @return array{string, string, ?string}
*/
public static function getSigningVarsFromEnv(): ?array
{
$accessKeyId = getenv('AWS_ACCESS_KEY_ID');
$secretAccessKey = getenv('AWS_SECRET_ACCESS_KEY');
if ($accessKeyId && $secretAccessKey) {
return [
$accessKeyId,
$secretAccessKey,
getenv('AWS_SESSION_TOKEN') ?: null, // session token (can be null)
];
}
return null;
}
/**
* Gets the unique key for caching
* For AwsNativeSource the values are:
* Imdsv2SessionTokenUrl.SecurityCredentialsUrl.RegionUrl.RegionalCredVerificationUrl
*
* @return string
*/
public function getCacheKey(): string
{
return ($this->imdsv2SessionTokenUrl ?? '') .
'.' . ($this->securityCredentialsUrl ?? '') .
'.' . $this->regionUrl .
'.' . $this->regionalCredVerificationUrl;
}
/**
* Return HMAC hash in binary string
*/
private static function hmacSign(string $key, string $msg): string
{
return hash_hmac('sha256', self::utf8Encode($msg), $key, true);
}
/**
* @TODO add a fallback when mbstring is not available
*/
private static function utf8Encode(string $string): string
{
return (string) mb_convert_encoding($string, 'UTF-8', 'ISO-8859-1');
}
private static function getSignatureKey(
string $key,
string $dateStamp,
string $regionName,
string $serviceName
): string {
$kDate = self::hmacSign(self::utf8Encode('AWS4' . $key), $dateStamp);
$kRegion = self::hmacSign($kDate, $regionName);
$kService = self::hmacSign($kRegion, $serviceName);
$kSigning = self::hmacSign($kService, 'aws4_request');
return $kSigning;
}
}

View File

@ -0,0 +1,272 @@
<?php
/*
* Copyright 2024 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExecutableHandler\ExecutableHandler;
use Google\Auth\ExecutableHandler\ExecutableResponseError;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use RuntimeException;
/**
* ExecutableSource enables the exchange of workload identity pool external credentials for
* Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
* scripts/executables are completely independent of the Google Cloud Auth libraries. These
* credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
* to be exchanged for a Google access token.
*
* To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
* must be set to '1'. This is for security reasons.
*
* Both OIDC and SAML are supported. The executable must adhere to a specific response format
* defined below.
*
* The executable must print out the 3rd party token to STDOUT in JSON format. When an
* output_file is specified in the credential configuration, the executable must also handle writing the
* JSON response to this file.
*
* <pre>
* OIDC response sample:
* {
* "version": 1,
* "success": true,
* "token_type": "urn:ietf:params:oauth:token-type:id_token",
* "id_token": "HEADER.PAYLOAD.SIGNATURE",
* "expiration_time": 1620433341
* }
*
* SAML2 response sample:
* {
* "version": 1,
* "success": true,
* "token_type": "urn:ietf:params:oauth:token-type:saml2",
* "saml_response": "...",
* "expiration_time": 1620433341
* }
*
* Error response sample:
* {
* "version": 1,
* "success": false,
* "code": "401",
* "message": "Error message."
* }
* </pre>
*
* The "expiration_time" field in the JSON response is only required for successful
* responses when an output file was specified in the credential configuration
*
* The auth libraries will populate certain environment variables that will be accessible by the
* executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
* GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
* GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
*/
class ExecutableSource implements ExternalAccountCredentialSourceInterface
{
private const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = 'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES';
private const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2';
private const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token';
private const OIDC_SUBJECT_TOKEN_TYPE2 = 'urn:ietf:params:oauth:token-type:jwt';
private string $command;
private ExecutableHandler $executableHandler;
private ?string $outputFile;
/**
* @param string $command The string command to run to get the subject token.
* @param string|null $outputFile
*/
public function __construct(
string $command,
?string $outputFile,
?ExecutableHandler $executableHandler = null,
) {
$this->command = $command;
$this->outputFile = $outputFile;
$this->executableHandler = $executableHandler ?: new ExecutableHandler();
}
/**
* Gets the unique key for caching
* The format for the cache key is:
* Command.OutputFile
*
* @return ?string
*/
public function getCacheKey(): ?string
{
return $this->command . '.' . $this->outputFile;
}
/**
* @param callable|null $httpHandler unused.
* @return string
* @throws RuntimeException if the executable is not allowed to run.
* @throws ExecutableResponseError if the executable response is invalid.
*/
public function fetchSubjectToken(?callable $httpHandler = null): string
{
// Check if the executable is allowed to run.
if (getenv(self::GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES) !== '1') {
throw new RuntimeException(
'Pluggable Auth executables need to be explicitly allowed to run by '
. 'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment '
. 'Variable to 1.'
);
}
if (!$executableResponse = $this->getCachedExecutableResponse()) {
// Run the executable.
$exitCode = ($this->executableHandler)($this->command);
$output = $this->executableHandler->getOutput();
// If the exit code is not 0, throw an exception with the output as the error details
if ($exitCode !== 0) {
throw new ExecutableResponseError(
'The executable failed to run'
. ($output ? ' with the following error: ' . $output : '.'),
(string) $exitCode
);
}
$executableResponse = $this->parseExecutableResponse($output);
// Validate expiration.
if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
throw new ExecutableResponseError('Executable response is expired.');
}
}
// Throw error when the request was unsuccessful
if ($executableResponse['success'] === false) {
throw new ExecutableResponseError($executableResponse['message'], (string) $executableResponse['code']);
}
// Return subject token field based on the token type
return $executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE
? $executableResponse['saml_response']
: $executableResponse['id_token'];
}
/**
* @return array<string, mixed>|null
*/
private function getCachedExecutableResponse(): ?array
{
if (
$this->outputFile
&& file_exists($this->outputFile)
&& !empty(trim($outputFileContents = (string) file_get_contents($this->outputFile)))
) {
try {
$executableResponse = $this->parseExecutableResponse($outputFileContents);
} catch (ExecutableResponseError $e) {
throw new ExecutableResponseError(
'Error in output file: ' . $e->getMessage(),
'INVALID_OUTPUT_FILE'
);
}
if ($executableResponse['success'] === false) {
// If the cached token was unsuccessful, run the executable to get a new one.
return null;
}
if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
// If the cached token is expired, run the executable to get a new one.
return null;
}
return $executableResponse;
}
return null;
}
/**
* @return array<string, mixed>
*/
private function parseExecutableResponse(string $response): array
{
$executableResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ExecutableResponseError(
'The executable returned an invalid response: ' . $response,
'INVALID_RESPONSE'
);
}
if (!array_key_exists('version', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "version" field.');
}
if (!array_key_exists('success', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "success" field.');
}
// Validate required fields for a successful response.
if ($executableResponse['success']) {
// Validate token type field.
$tokenTypes = [self::SAML_SUBJECT_TOKEN_TYPE, self::OIDC_SUBJECT_TOKEN_TYPE1, self::OIDC_SUBJECT_TOKEN_TYPE2];
if (!isset($executableResponse['token_type'])) {
throw new ExecutableResponseError(
'Executable response must contain a "token_type" field when successful'
);
}
if (!in_array($executableResponse['token_type'], $tokenTypes)) {
throw new ExecutableResponseError(sprintf(
'Executable response "token_type" field must be one of %s.',
implode(', ', $tokenTypes)
));
}
// Validate subject token for SAML and OIDC.
if ($executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE) {
if (empty($executableResponse['saml_response'])) {
throw new ExecutableResponseError(sprintf(
'Executable response must contain a "saml_response" field when token_type=%s.',
self::SAML_SUBJECT_TOKEN_TYPE
));
}
} elseif (empty($executableResponse['id_token'])) {
throw new ExecutableResponseError(sprintf(
'Executable response must contain a "id_token" field when '
. 'token_type=%s.',
$executableResponse['token_type']
));
}
// Validate expiration exists when an output file is specified.
if ($this->outputFile) {
if (!isset($executableResponse['expiration_time'])) {
throw new ExecutableResponseError(
'The executable response must contain a "expiration_time" field for successful responses ' .
'when an output_file has been specified in the configuration.'
);
}
}
} else {
// Both code and message must be provided for unsuccessful responses.
if (!array_key_exists('code', $executableResponse)) {
throw new ExecutableResponseError('Executable response must contain a "code" field when unsuccessful.');
}
if (empty($executableResponse['message'])) {
throw new ExecutableResponseError('Executable response must contain a "message" field when unsuccessful.');
}
}
return $executableResponse;
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Retrieve a token from a file.
*/
class FileSource implements ExternalAccountCredentialSourceInterface
{
private string $file;
private ?string $format;
private ?string $subjectTokenFieldName;
/**
* @param string $file The file to read the subject token from.
* @param string|null $format The format of the token in the file. Can be null or "json".
* @param string|null $subjectTokenFieldName The name of the field containing the token in the file. This is required
* when format is "json".
*/
public function __construct(
string $file,
?string $format = null,
?string $subjectTokenFieldName = null
) {
$this->file = $file;
if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}
$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
}
public function fetchSubjectToken(?callable $httpHandler = null): string
{
$contents = file_get_contents($this->file);
if ($this->format === 'json') {
if (!$json = json_decode((string) $contents, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON file'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$contents = $json[$this->subjectTokenFieldName];
}
return $contents;
}
/**
* Gets the unique key for caching.
* The format for the cache key one of the following:
* Filename
*
* @return string
*/
public function getCacheKey(): ?string
{
return $this->file;
}
}

View File

@ -0,0 +1,109 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\CredentialSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Retrieve a token from a URL.
*/
class UrlSource implements ExternalAccountCredentialSourceInterface
{
private string $url;
private ?string $format;
private ?string $subjectTokenFieldName;
/**
* @var array<string, string|string[]>
*/
private ?array $headers;
/**
* @param string $url The URL to fetch the subject token from.
* @param string|null $format The format of the token in the response. Can be null or "json".
* @param string|null $subjectTokenFieldName The name of the field containing the token in the response. This is required
* when format is "json".
* @param array<string, string|string[]>|null $headers Request headers to send in with the request to the URL.
*/
public function __construct(
string $url,
?string $format = null,
?string $subjectTokenFieldName = null,
?array $headers = null
) {
$this->url = $url;
if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}
$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
$this->headers = $headers;
}
public function fetchSubjectToken(?callable $httpHandler = null): string
{
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$request = new Request(
'GET',
$this->url,
$this->headers ?: []
);
$response = $httpHandler($request);
$body = (string) $response->getBody();
if ($this->format === 'json') {
if (!$json = json_decode((string) $body, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON response'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$body = $json[$this->subjectTokenFieldName];
}
return $body;
}
/**
* Get the cache key for the credentials.
* The format for the cache key is:
* URL
*
* @return ?string
*/
public function getCacheKey(): ?string
{
return $this->url;
}
}

View File

@ -0,0 +1,238 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
/*
* The AppIdentityService class is automatically defined on App Engine,
* so including this dependency is not necessary, and will result in a
* PHP fatal error in the App Engine environment.
*/
use google\appengine\api\app_identity\AppIdentityService;
use Google\Auth\CredentialsLoader;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\SignBlobInterface;
/**
* @deprecated
*
* AppIdentityCredentials supports authorization on Google App Engine.
*
* It can be used to authorize requests using the AuthTokenMiddleware or
* AuthTokenSubscriber, but will only succeed if being run on App Engine:
*
* Example:
* ```
* use Google\Auth\Credentials\AppIdentityCredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $gae = new AppIdentityCredentials('https://www.googleapis.com/auth/books');
* $middleware = new AuthTokenMiddleware($gae);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/books/v1',
* 'auth' => 'google_auth'
* ]);
*
* $res = $client->get('volumes?q=Henry+David+Thoreau&country=US');
* ```
*/
class AppIdentityCredentials extends CredentialsLoader implements
SignBlobInterface,
ProjectIdProviderInterface
{
/**
* Result of fetchAuthToken.
*
* @var array<mixed>
*/
protected $lastReceivedToken;
/**
* Array of OAuth2 scopes to be requested.
*
* @var string[]
*/
private $scope;
/**
* @var string
*/
private $clientName;
/**
* @param string|string[] $scope One or more scopes.
*/
public function __construct($scope = [])
{
$this->scope = is_array($scope) ? $scope : explode(' ', (string) $scope);
}
/**
* Determines if this an App Engine instance, by accessing the
* SERVER_SOFTWARE environment variable (prod) or the APPENGINE_RUNTIME
* environment variable (dev).
*
* @return bool true if this an App Engine Instance, false otherwise
*/
public static function onAppEngine()
{
$appEngineProduction = isset($_SERVER['SERVER_SOFTWARE']) &&
0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine');
if ($appEngineProduction) {
return true;
}
$appEngineDevAppServer = isset($_SERVER['APPENGINE_RUNTIME']) &&
$_SERVER['APPENGINE_RUNTIME'] == 'php';
if ($appEngineDevAppServer) {
return true;
}
return false;
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* Fetches the auth tokens using the AppIdentityService if available.
* As the AppIdentityService uses protobufs to fetch the access token,
* the GuzzleHttp\ClientInterface instance passed in will not be used.
*
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type string $expiration_time
* }
*/
public function fetchAuthToken(?callable $httpHandler = null)
{
try {
$this->checkAppEngineContext();
} catch (\Exception $e) {
return [];
}
/** @phpstan-ignore-next-line */
$token = AppIdentityService::getAccessToken($this->scope);
$this->lastReceivedToken = $token;
return $token;
}
/**
* Sign a string using AppIdentityService.
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenSsl [optional] Does not apply to this credentials
* type.
* @return string The signature, base64-encoded.
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function signBlob($stringToSign, $forceOpenSsl = false)
{
$this->checkAppEngineContext();
/** @phpstan-ignore-next-line */
return base64_encode(AppIdentityService::signForApp($stringToSign)['signature']);
}
/**
* Get the project ID from AppIdentityService.
*
* Returns null if AppIdentityService is unavailable.
*
* @param callable|null $httpHandler Not used by this type.
* @return string|null
*/
public function getProjectId(?callable $httpHandler = null)
{
try {
$this->checkAppEngineContext();
} catch (\Exception $e) {
return null;
}
/** @phpstan-ignore-next-line */
return AppIdentityService::getApplicationId();
}
/**
* Get the client name from AppIdentityService.
*
* Subsequent calls to this method will return a cached value.
*
* @param callable|null $httpHandler Not used in this implementation.
* @return string
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function getClientName(?callable $httpHandler = null)
{
$this->checkAppEngineContext();
if (!$this->clientName) {
/** @phpstan-ignore-next-line */
$this->clientName = AppIdentityService::getServiceAccountName();
}
return $this->clientName;
}
/**
* @return array{access_token:string,expires_at:int}|null
*/
public function getLastReceivedToken()
{
if ($this->lastReceivedToken) {
return [
'access_token' => $this->lastReceivedToken['access_token'],
'expires_at' => $this->lastReceivedToken['expiration_time'],
];
}
return null;
}
/**
* Caching is handled by the underlying AppIdentityService, return empty string
* to prevent caching.
*
* @return string
*/
public function getCacheKey()
{
return '';
}
/**
* @return void
*/
private function checkAppEngineContext()
{
if (!self::onAppEngine() || !class_exists('google\appengine\api\app_identity\AppIdentityService')) {
throw new \Exception(
'This class must be run in App Engine, or you must include the AppIdentityService '
. 'mock class defined in tests/mocks/AppIdentityService.php'
);
}
}
}

View File

@ -0,0 +1,394 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialSource\AwsNativeSource;
use Google\Auth\CredentialSource\ExecutableSource;
use Google\Auth\CredentialSource\FileSource;
use Google\Auth\CredentialSource\UrlSource;
use Google\Auth\ExecutableHandler\ExecutableHandler;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\UpdateMetadataInterface;
use Google\Auth\UpdateMetadataTrait;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
/**
* **IMPORTANT**:
* This class does not validate the credential configuration. A security
* risk occurs when a credential configuration configured with malicious urls
* is used.
* When the credential configuration is accepted from an
* untrusted source, you should validate it before creating this class.
* @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
*/
class ExternalAccountCredentials implements
FetchAuthTokenInterface,
UpdateMetadataInterface,
GetQuotaProjectInterface,
GetUniverseDomainInterface,
ProjectIdProviderInterface
{
use UpdateMetadataTrait;
private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';
private OAuth2 $auth;
private ?string $quotaProject;
private ?string $serviceAccountImpersonationUrl;
private ?string $workforcePoolUserProject;
private ?string $projectId;
/** @var array<mixed> */
private ?array $lastImpersonatedAccessToken;
private string $universeDomain;
/**
* @param string|string[] $scope The scope of the access request, expressed either as an array
* or as a space-delimited string.
* @param array<mixed> $jsonKey JSON credentials as an associative array.
*/
public function __construct(
$scope,
array $jsonKey
) {
if (!array_key_exists('type', $jsonKey)) {
throw new InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
throw new InvalidArgumentException(sprintf(
'expected "%s" type but received "%s"',
self::EXTERNAL_ACCOUNT_TYPE,
$jsonKey['type']
));
}
if (!array_key_exists('token_url', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the token_url field'
);
}
if (!array_key_exists('audience', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the audience field'
);
}
if (!array_key_exists('subject_token_type', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the subject_token_type field'
);
}
if (!array_key_exists('credential_source', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the credential_source field'
);
}
$this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null;
$this->quotaProject = $jsonKey['quota_project_id'] ?? null;
$this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
$this->auth = new OAuth2([
'tokenCredentialUri' => $jsonKey['token_url'],
'audience' => $jsonKey['audience'],
'scope' => $scope,
'subjectTokenType' => $jsonKey['subject_token_type'],
'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
'additionalOptions' => $this->workforcePoolUserProject
? ['userProject' => $this->workforcePoolUserProject]
: [],
]);
if (!$this->isWorkforcePool() && $this->workforcePoolUserProject) {
throw new InvalidArgumentException(
'workforce_pool_user_project should not be set for non-workforce pool credentials.'
);
}
}
/**
* @param array<mixed> $jsonKey
*/
private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
{
$credentialSource = $jsonKey['credential_source'];
if (isset($credentialSource['file'])) {
return new FileSource(
$credentialSource['file'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null
);
}
if (
isset($credentialSource['environment_id'])
&& 1 === preg_match('/^aws(\d+)$/', $credentialSource['environment_id'], $matches)
) {
if ($matches[1] !== '1') {
throw new InvalidArgumentException(
"aws version \"$matches[1]\" is not supported in the current build."
);
}
if (!array_key_exists('regional_cred_verification_url', $credentialSource)) {
throw new InvalidArgumentException(
'The regional_cred_verification_url field is required for aws1 credential source.'
);
}
return new AwsNativeSource(
$jsonKey['audience'],
$credentialSource['regional_cred_verification_url'], // $regionalCredVerificationUrl
$credentialSource['region_url'] ?? null, // $regionUrl
$credentialSource['url'] ?? null, // $securityCredentialsUrl
$credentialSource['imdsv2_session_token_url'] ?? null, // $imdsV2TokenUrl
);
}
if (isset($credentialSource['url'])) {
return new UrlSource(
$credentialSource['url'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null,
$credentialSource['headers'] ?? null,
);
}
if (isset($credentialSource['executable'])) {
if (!array_key_exists('command', $credentialSource['executable'])) {
throw new InvalidArgumentException(
'executable source requires a command to be set in the JSON file.'
);
}
// Build command environment variables
$env = [
'GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE' => $jsonKey['audience'],
'GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE' => $jsonKey['subject_token_type'],
// Always set to 0 because interactive mode is not supported.
'GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE' => '0',
];
if ($outputFile = $credentialSource['executable']['output_file'] ?? null) {
$env['GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE'] = $outputFile;
}
if ($serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null) {
// Parse email from URL. The formal looks as follows:
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
$env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $matches['email'];
}
}
$timeoutMs = $credentialSource['executable']['timeout_millis'] ?? null;
return new ExecutableSource(
$credentialSource['executable']['command'],
$outputFile,
$timeoutMs ? new ExecutableHandler($env, $timeoutMs) : new ExecutableHandler($env)
);
}
throw new InvalidArgumentException('Unable to determine credential source from json key.');
}
/**
* @param string $stsToken
* @param callable|null $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_at
* }
*/
private function getImpersonatedAccessToken(string $stsToken, ?callable $httpHandler = null): array
{
if (!isset($this->serviceAccountImpersonationUrl)) {
throw new InvalidArgumentException(
'service_account_impersonation_url must be set in JSON credentials.'
);
}
$request = new Request(
'POST',
$this->serviceAccountImpersonationUrl,
[
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $stsToken,
],
(string) json_encode([
'lifetime' => sprintf('%ss', OAuth2::DEFAULT_EXPIRY_SECONDS),
'scope' => explode(' ', $this->auth->getScope()),
]),
);
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);
return [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
];
}
/**
* @param callable|null $httpHandler
* @param array<mixed> $headers [optional] Metrics headers to be inserted
* into the token endpoint request present.
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_at (impersonated service accounts only)
* @type int $expires_in (identity pool only)
* @type string $issued_token_type (identity pool only)
* @type string $token_type (identity pool only)
* }
*/
public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
{
$stsToken = $this->auth->fetchAuthToken($httpHandler, $headers);
if (isset($this->serviceAccountImpersonationUrl)) {
return $this->lastImpersonatedAccessToken = $this->getImpersonatedAccessToken(
$stsToken['access_token'],
$httpHandler
);
}
return $stsToken;
}
/**
* Get the cache token key for the credentials.
* The cache token key format depends on the type of source
* The format for the cache key one of the following:
* FetcherCacheKey.Scope.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
* FetcherCacheKey.Audience.[ServiceAccount].[TokenType].[WorkforcePoolUserProject]
*
* @return ?string;
*/
public function getCacheKey(): ?string
{
$scopeOrAudience = $this->auth->getAudience();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getScope();
}
return $this->auth->getSubjectTokenFetcher()->getCacheKey() .
'.' . $scopeOrAudience .
'.' . ($this->serviceAccountImpersonationUrl ?? '') .
'.' . ($this->auth->getSubjectTokenType() ?? '') .
'.' . ($this->workforcePoolUserProject ?? '');
}
public function getLastReceivedToken()
{
return $this->lastImpersonatedAccessToken ?? $this->auth->getLastReceivedToken();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Get the universe domain used for this API request
*
* @return string
*/
public function getUniverseDomain(): string
{
return $this->universeDomain;
}
/**
* Get the project ID.
*
* @param callable|null $httpHandler Callback which delivers psr7 request
* @param string|null $accessToken The access token to use to sign the blob. If
* provided, saves a call to the metadata server for a new access
* token. **Defaults to** `null`.
* @return string|null
*/
public function getProjectId(?callable $httpHandler = null, ?string $accessToken = null)
{
if (isset($this->projectId)) {
return $this->projectId;
}
$projectNumber = $this->getProjectNumber() ?: $this->workforcePoolUserProject;
if (!$projectNumber) {
return null;
}
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}
$url = str_replace(
'UNIVERSE_DOMAIN',
$this->getUniverseDomain(),
sprintf(self::CLOUD_RESOURCE_MANAGER_URL, $projectNumber)
);
if (is_null($accessToken)) {
$accessToken = $this->fetchAuthToken($httpHandler)['access_token'];
}
$request = new Request('GET', $url, ['authorization' => 'Bearer ' . $accessToken]);
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);
return $this->projectId = $body['projectId'];
}
private function getProjectNumber(): ?string
{
$parts = explode('/', $this->auth->getAudience());
$i = array_search('projects', $parts);
return $parts[$i + 1] ?? null;
}
private function isWorkforcePool(): bool
{
$regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
return preg_match($regex, $this->auth->getAudience()) === 1;
}
}

View File

@ -0,0 +1,685 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use COM;
use com_exception;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Iam;
use Google\Auth\IamSignerTrait;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\SignBlobInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
/**
* GCECredentials supports authorization on Google Compute Engine.
*
* It can be used to authorize requests using the AuthTokenMiddleware, but will
* only succeed if being run on GCE:
*
* use Google\Auth\Credentials\GCECredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $gce = new GCECredentials();
* $middleware = new AuthTokenMiddleware($gce);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth'
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*/
class GCECredentials extends CredentialsLoader implements
SignBlobInterface,
ProjectIdProviderInterface,
GetQuotaProjectInterface
{
use IamSignerTrait;
// phpcs:disable
const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
// phpcs:enable
/**
* The metadata IP address on appengine instances.
*
* The IP is used instead of the domain 'metadata' to avoid slow responses
* when not on Compute Engine.
*/
const METADATA_IP = '169.254.169.254';
/**
* The metadata path of the default token.
*/
const TOKEN_URI_PATH = 'v1/instance/service-accounts/default/token';
/**
* The metadata path of the default id token.
*/
const ID_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/identity';
/**
* The metadata path of the client ID.
*/
const CLIENT_ID_URI_PATH = 'v1/instance/service-accounts/default/email';
/**
* The metadata path of the project ID.
*/
const PROJECT_ID_URI_PATH = 'v1/project/project-id';
/**
* The metadata path of the project ID.
*/
const UNIVERSE_DOMAIN_URI_PATH = 'v1/universe/universe-domain';
/**
* The header whose presence indicates GCE presence.
*/
const FLAVOR_HEADER = 'Metadata-Flavor';
/**
* The Linux file which contains the product name.
*/
private const GKE_PRODUCT_NAME_FILE = '/sys/class/dmi/id/product_name';
/**
* The Windows Registry key path to the product name
*/
private const WINDOWS_REGISTRY_KEY_PATH = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
/**
* The Windows registry key name for the product name
*/
private const WINDOWS_REGISTRY_KEY_NAME = 'SystemProductName';
/**
* The Name of the product expected from the windows registry
*/
private const PRODUCT_NAME = 'Google';
private const CRED_TYPE = 'mds';
/**
* Note: the explicit `timeout` and `tries` below is a workaround. The underlying
* issue is that resolving an unknown host on some networks will take
* 20-30 seconds; making this timeout short fixes the issue, but
* could lead to false negatives in the event that we are on GCE, but
* the metadata resolution was particularly slow. The latter case is
* "unlikely" since the expected 4-nines time is about 0.5 seconds.
* This allows us to limit the total ping maximum timeout to 1.5 seconds
* for developer desktop scenarios.
*/
const MAX_COMPUTE_PING_TRIES = 3;
const COMPUTE_PING_CONNECTION_TIMEOUT_S = 0.5;
/**
* Flag used to ensure that the onGCE test is only done once;.
*
* @var bool
*/
private $hasCheckedOnGce = false;
/**
* Flag that stores the value of the onGCE check.
*
* @var bool
*/
private $isOnGce = false;
/**
* Result of fetchAuthToken.
*
* @var array<mixed>
*/
protected $lastReceivedToken;
/**
* @var string|null
*/
private $clientName;
/**
* @var string|null
*/
private $projectId;
/**
* @var string
*/
private $tokenUri;
/**
* @var string
*/
private $targetAudience;
/**
* @var string|null
*/
private $quotaProject;
/**
* @var string|null
*/
private $serviceAccountIdentity;
/**
* @var string
*/
private ?string $universeDomain;
/**
* @param Iam|null $iam [optional] An IAM instance.
* @param string|string[] $scope [optional] the scope of the access request,
* expressed either as an array or as a space-delimited string.
* @param string $targetAudience [optional] The audience for the ID token.
* @param string $quotaProject [optional] Specifies a project to bill for access
* charges associated with the request.
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @param string|null $universeDomain [optional] Specify a universe domain to use
* instead of fetching one from the metadata server.
*/
public function __construct(
?Iam $iam = null,
$scope = null,
$targetAudience = null,
$quotaProject = null,
$serviceAccountIdentity = null,
?string $universeDomain = null
) {
$this->iam = $iam;
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
$tokenUri = self::getTokenUri($serviceAccountIdentity);
if ($scope) {
if (is_string($scope)) {
$scope = explode(' ', $scope);
}
$scope = implode(',', $scope);
$tokenUri = $tokenUri . '?scopes=' . $scope;
} elseif ($targetAudience) {
$tokenUri = self::getIdTokenUri($serviceAccountIdentity);
$tokenUri = $tokenUri . '?audience=' . $targetAudience;
$this->targetAudience = $targetAudience;
}
$this->tokenUri = $tokenUri;
$this->quotaProject = $quotaProject;
$this->serviceAccountIdentity = $serviceAccountIdentity;
$this->universeDomain = $universeDomain;
}
/**
* The full uri for accessing the default token.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
public static function getTokenUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::TOKEN_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accessing the default service account.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
public static function getClientNameUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::CLIENT_ID_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accesesing the default identity token.
*
* @param string $serviceAccountIdentity [optional] Specify a service
* account identity name to use instead of "default".
* @return string
*/
private static function getIdTokenUri($serviceAccountIdentity = null)
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
$base .= self::ID_TOKEN_URI_PATH;
if ($serviceAccountIdentity) {
return str_replace(
'/default/',
'/' . $serviceAccountIdentity . '/',
$base
);
}
return $base;
}
/**
* The full uri for accessing the default project ID.
*
* @return string
*/
private static function getProjectIdUri()
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
return $base . self::PROJECT_ID_URI_PATH;
}
/**
* The full uri for accessing the default universe domain.
*
* @return string
*/
private static function getUniverseDomainUri()
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';
return $base . self::UNIVERSE_DOMAIN_URI_PATH;
}
/**
* Determines if this an App Engine Flexible instance, by accessing the
* GAE_INSTANCE environment variable.
*
* @return bool true if this an App Engine Flexible Instance, false otherwise
*/
public static function onAppEngineFlexible()
{
return substr((string) getenv('GAE_INSTANCE'), 0, 4) === 'aef-';
}
/**
* Determines if this a GCE instance, by accessing the expected metadata
* host.
* If $httpHandler is not specified a the default HttpHandler is used.
*
* @param callable|null $httpHandler callback which delivers psr7 request
* @return bool True if this a GCEInstance, false otherwise
*/
public static function onGce(?callable $httpHandler = null)
{
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
$checkUri = 'http://' . self::METADATA_IP;
for ($i = 1; $i <= self::MAX_COMPUTE_PING_TRIES; $i++) {
try {
// Comment from: oauth2client/client.py
//
// Note: the explicit `timeout` below is a workaround. The underlying
// issue is that resolving an unknown host on some networks will take
// 20-30 seconds; making this timeout short fixes the issue, but
// could lead to false negatives in the event that we are on GCE, but
// the metadata resolution was particularly slow. The latter case is
// "unlikely".
$resp = $httpHandler(
new Request(
'GET',
$checkUri,
[
self::FLAVOR_HEADER => 'Google',
self::$metricMetadataKey => self::getMetricsHeader('', 'mds')
]
),
['timeout' => self::COMPUTE_PING_CONNECTION_TIMEOUT_S]
);
return $resp->getHeaderLine(self::FLAVOR_HEADER) == 'Google';
} catch (ClientException $e) {
} catch (ServerException $e) {
} catch (RequestException $e) {
} catch (ConnectException $e) {
}
}
if (PHP_OS === 'Windows' || PHP_OS === 'WINNT') {
return self::detectResidencyWindows(
self::WINDOWS_REGISTRY_KEY_PATH . self::WINDOWS_REGISTRY_KEY_NAME
);
}
// Detect GCE residency on Linux
return self::detectResidencyLinux(self::GKE_PRODUCT_NAME_FILE);
}
private static function detectResidencyLinux(string $productNameFile): bool
{
if (file_exists($productNameFile)) {
$productName = trim((string) file_get_contents($productNameFile));
return 0 === strpos($productName, self::PRODUCT_NAME);
}
return false;
}
private static function detectResidencyWindows(string $registryProductKey): bool
{
if (!class_exists(COM::class)) {
// the COM extension must be installed and enabled to detect Windows residency
// see https://www.php.net/manual/en/book.com.php
return false;
}
$shell = new COM('WScript.Shell');
$productName = null;
try {
$productName = $shell->regRead($registryProductKey);
} catch (com_exception) {
// This means that we tried to read a key that doesn't exist on the registry
// which might mean that it is a windows instance that is not on GCE
return false;
}
return 0 === strpos($productName, self::PRODUCT_NAME);
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* Fetches the auth tokens from the GCE metadata host if it is available.
* If $httpHandler is not specified a the default HttpHandler is used.
*
* @param callable|null $httpHandler callback which delivers psr7 request
* @param array<mixed> $headers [optional] Headers to be inserted
* into the token endpoint request present.
*
* @return array<mixed> {
* A set of auth related metadata, based on the token type.
*
* @type string $access_token for access tokens
* @type int $expires_in for access tokens
* @type string $token_type for access tokens
* @type string $id_token for ID tokens
* }
* @throws \Exception
*/
public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
{
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return []; // return an empty array with no access token
}
$response = $this->getFromMetadata(
$httpHandler,
$this->tokenUri,
$this->applyTokenEndpointMetrics($headers, $this->targetAudience ? 'it' : 'at')
);
if ($this->targetAudience) {
return $this->lastReceivedToken = ['id_token' => $response];
}
if (null === $json = json_decode($response, true)) {
throw new \Exception('Invalid JSON response');
}
$json['expires_at'] = time() + $json['expires_in'];
// store this so we can retrieve it later
$this->lastReceivedToken = $json;
return $json;
}
/**
* Returns the Cache Key for the credential token.
* The format for the cache key is:
* TokenURI
*
* @return string
*/
public function getCacheKey()
{
return $this->tokenUri;
}
/**
* @return array<mixed>|null
*/
public function getLastReceivedToken()
{
if ($this->lastReceivedToken) {
if (array_key_exists('id_token', $this->lastReceivedToken)) {
return $this->lastReceivedToken;
}
return [
'access_token' => $this->lastReceivedToken['access_token'],
'expires_at' => $this->lastReceivedToken['expires_at']
];
}
return null;
}
/**
* Get the client name from GCE metadata.
*
* Subsequent calls will return a cached value.
*
* @param callable|null $httpHandler callback which delivers psr7 request
* @return string
*/
public function getClientName(?callable $httpHandler = null)
{
if ($this->clientName) {
return $this->clientName;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return '';
}
$this->clientName = $this->getFromMetadata(
$httpHandler,
self::getClientNameUri($this->serviceAccountIdentity)
);
return $this->clientName;
}
/**
* Fetch the default Project ID from compute engine.
*
* Returns null if called outside GCE.
*
* @param callable|null $httpHandler Callback which delivers psr7 request
* @return string|null
*/
public function getProjectId(?callable $httpHandler = null)
{
if ($this->projectId) {
return $this->projectId;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
if (!$this->isOnGce) {
return null;
}
$this->projectId = $this->getFromMetadata($httpHandler, self::getProjectIdUri());
return $this->projectId;
}
/**
* Fetch the default universe domain from the metadata server.
*
* @param callable|null $httpHandler Callback which delivers psr7 request
* @return string
*/
public function getUniverseDomain(?callable $httpHandler = null): string
{
if (null !== $this->universeDomain) {
return $this->universeDomain;
}
$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}
try {
$this->universeDomain = $this->getFromMetadata(
$httpHandler,
self::getUniverseDomainUri()
);
} catch (ClientException $e) {
// If the metadata server exists, but returns a 404 for the universe domain, the auth
// libraries should safely assume this is an older metadata server running in GCU, and
// should return the default universe domain.
if (!$e->hasResponse() || 404 != $e->getResponse()->getStatusCode()) {
throw $e;
}
$this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
}
// We expect in some cases the metadata server will return an empty string for the universe
// domain. In this case, the auth library MUST return the default universe domain.
if ('' === $this->universeDomain) {
$this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
}
return $this->universeDomain;
}
/**
* Fetch the value of a GCE metadata server URI.
*
* @param callable $httpHandler An HTTP Handler to deliver PSR7 requests.
* @param string $uri The metadata URI.
* @param array<mixed> $headers [optional] If present, add these headers to the token
* endpoint request.
*
* @return string
*/
private function getFromMetadata(callable $httpHandler, $uri, array $headers = [])
{
$resp = $httpHandler(
new Request(
'GET',
$uri,
[self::FLAVOR_HEADER => 'Google'] + $headers
)
);
return (string) $resp->getBody();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Set whether or not we've already checked the GCE environment.
*
* @param bool $isOnGce
*
* @return void
*/
public function setIsOnGce($isOnGce)
{
// Implicitly set hasCheckedGce to true
$this->hasCheckedOnGce = true;
// Set isOnGce
$this->isOnGce = $isOnGce;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
/**
* Authenticates requests using IAM credentials.
*/
class IAMCredentials
{
const SELECTOR_KEY = 'x-goog-iam-authority-selector';
const TOKEN_KEY = 'x-goog-iam-authorization-token';
/**
* @var string
*/
private $selector;
/**
* @var string
*/
private $token;
/**
* @param string $selector the IAM selector
* @param string $token the IAM token
*/
public function __construct($selector, $token)
{
if (!is_string($selector)) {
throw new \InvalidArgumentException(
'selector must be a string'
);
}
if (!is_string($token)) {
throw new \InvalidArgumentException(
'token must be a string'
);
}
$this->selector = $selector;
$this->token = $token;
}
/**
* export a callback function which updates runtime metadata.
*
* @return callable updateMetadata function
*/
public function getUpdateMetadataFunc()
{
return [$this, 'updateMetadata'];
}
/**
* Updates metadata with the appropriate header metadata.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $unusedAuthUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* Note: this param is unused here, only included here for
* consistency with other credentials class
*
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$unusedAuthUri = null,
?callable $httpHandler = null
) {
$metadata_copy = $metadata;
$metadata_copy[self::SELECTOR_KEY] = $this->selector;
$metadata_copy[self::TOKEN_KEY] = $this->token;
return $metadata_copy;
}
}

View File

@ -0,0 +1,295 @@
<?php
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CacheTrait;
use Google\Auth\CredentialsLoader;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\IamSignerTrait;
use Google\Auth\SignBlobInterface;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use LogicException;
/**
* **IMPORTANT**:
* This class does not validate the credential configuration. A security
* risk occurs when a credential configuration configured with malicious urls
* is used.
* When the credential configuration is accepted from an
* untrusted source, you should validate it before creating this class.
* @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
*/
class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
SignBlobInterface,
GetUniverseDomainInterface
{
use CacheTrait;
use IamSignerTrait;
private const CRED_TYPE = 'imp';
private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
private const ID_TOKEN_IMPERSONATION_URL =
'https://iamcredentials.UNIVERSE_DOMAIN/v1/projects/-/serviceAccounts/%s:generateIdToken';
/**
* @var string
*/
protected $impersonatedServiceAccountName;
protected FetchAuthTokenInterface $sourceCredentials;
private string $serviceAccountImpersonationUrl;
/**
* @var string[]
*/
private array $delegates;
/**
* @var string|string[]
*/
private string|array $targetScope;
private int $lifetime;
/**
* Instantiate an instance of ImpersonatedServiceAccountCredentials from a credentials file that
* has be created with the --impersonate-service-account flag.
*
* @param string|string[]|null $scope The scope of the access request, expressed either as an
* array or as a space-delimited string.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON array credentials {
* JSON credentials as an associative array.
*
* @type string $service_account_impersonation_url The URL to the service account
* @type string|FetchAuthTokenInterface $source_credentials The source credentials to impersonate
* @type int $lifetime The lifetime of the impersonated credentials
* @type string[] $delegates The delegates to impersonate
* }
* @param string|null $targetAudience The audience to request an ID token.
*/
public function __construct(
string|array|null $scope,
string|array $jsonKey,
private ?string $targetAudience = null
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new InvalidArgumentException('file does not exist');
}
$json = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $json, true)) {
throw new LogicException('invalid json for auth config');
}
}
if (!array_key_exists('service_account_impersonation_url', $jsonKey)) {
throw new LogicException(
'json key is missing the service_account_impersonation_url field'
);
}
if (!array_key_exists('source_credentials', $jsonKey)) {
throw new LogicException('json key is missing the source_credentials field');
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
if (is_array($jsonKey['source_credentials'])) {
if (!array_key_exists('type', $jsonKey['source_credentials'])) {
throw new InvalidArgumentException('json key source credentials are missing the type field');
}
if (
$targetAudience !== null
&& $jsonKey['source_credentials']['type'] === 'service_account'
) {
// Service account tokens MUST request a scope, and as this token is only used to impersonate
// an ID token, the narrowest scope we can request is `iam`.
$scope = self::IAM_SCOPE;
}
$jsonKey['source_credentials'] = match ($jsonKey['source_credentials']['type'] ?? null) {
// Do not pass $defaultScope to ServiceAccountCredentials
'service_account' => new ServiceAccountCredentials($scope, $jsonKey['source_credentials']),
'authorized_user' => new UserRefreshCredentials($scope, $jsonKey['source_credentials']),
'external_account' => new ExternalAccountCredentials($scope, $jsonKey['source_credentials']),
default => throw new \InvalidArgumentException('invalid value in the type field'),
};
}
$this->targetScope = $scope ?? [];
$this->lifetime = $jsonKey['lifetime'] ?? 3600;
$this->delegates = $jsonKey['delegates'] ?? [];
$this->serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'];
$this->impersonatedServiceAccountName = $this->getImpersonatedServiceAccountNameFromUrl(
$this->serviceAccountImpersonationUrl
);
$this->sourceCredentials = $jsonKey['source_credentials'];
}
/**
* Helper function for extracting the Server Account Name from the URL saved in the account
* credentials file.
*
* @param $serviceAccountImpersonationUrl string URL from "service_account_impersonation_url"
* @return string Service account email or ID.
*/
private function getImpersonatedServiceAccountNameFromUrl(
string $serviceAccountImpersonationUrl
): string {
$fields = explode('/', $serviceAccountImpersonationUrl);
$lastField = end($fields);
$splitter = explode(':', $lastField);
return $splitter[0];
}
/**
* Get the client name from the keyfile
*
* In this implementation, it will return the issuers email from the oauth token.
*
* @param callable|null $unusedHttpHandler not used by this credentials type.
* @return string Token issuer email
*/
public function getClientName(?callable $unusedHttpHandler = null)
{
return $this->impersonatedServiceAccountName;
}
/**
* @param callable|null $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $scope
* @type string $token_type
* @type string $id_token
* }
*/
public function fetchAuthToken(?callable $httpHandler = null)
{
$httpHandler = $httpHandler ?? HttpHandlerFactory::build(HttpClientCache::getHttpClient());
// The FetchAuthTokenInterface technically does not have a "headers" argument, but all of
// the implementations do. Additionally, passing in more parameters than the function has
// defined is allowed in PHP. So we'll just ignore the phpstan error here.
// @phpstan-ignore-next-line
$authToken = $this->sourceCredentials->fetchAuthToken(
$httpHandler,
$this->applyTokenEndpointMetrics([], 'at')
);
$headers = $this->applyTokenEndpointMetrics([
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store',
'Authorization' => sprintf('Bearer %s', $authToken['access_token'] ?? $authToken['id_token']),
], $this->isIdTokenRequest() ? 'it' : 'at');
$body = match ($this->isIdTokenRequest()) {
true => [
'audience' => $this->targetAudience,
'includeEmail' => true,
],
false => [
'scope' => $this->targetScope,
'delegates' => $this->delegates,
'lifetime' => sprintf('%ss', $this->lifetime),
]
};
$url = $this->serviceAccountImpersonationUrl;
if ($this->isIdTokenRequest()) {
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (!preg_match($regex, $url, $matches)) {
throw new InvalidArgumentException(
'Invalid service account impersonation URL - unable to parse service account email'
);
}
$url = str_replace(
'UNIVERSE_DOMAIN',
$this->getUniverseDomain(),
sprintf(self::ID_TOKEN_IMPERSONATION_URL, $matches['email'])
);
}
$request = new Request(
'POST',
$url,
$headers,
(string) json_encode($body)
);
$response = $httpHandler($request);
$body = json_decode((string) $response->getBody(), true);
return match ($this->isIdTokenRequest()) {
true => ['id_token' => $body['token']],
false => [
'access_token' => $body['accessToken'],
'expires_at' => strtotime($body['expireTime']),
]
};
}
/**
* Returns the Cache Key for the credentials
* The cache key is the same as the UserRefreshCredentials class
*
* @return string
*/
public function getCacheKey()
{
return $this->getFullCacheKey(
$this->serviceAccountImpersonationUrl . $this->sourceCredentials->getCacheKey()
);
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->sourceCredentials->getLastReceivedToken();
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
private function isIdTokenRequest(): bool
{
return !is_null($this->targetAudience);
}
public function getUniverseDomain(): string
{
return $this->sourceCredentials instanceof GetUniverseDomainInterface
? $this->sourceCredentials->getUniverseDomain()
: self::DEFAULT_UNIVERSE_DOMAIN;
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\FetchAuthTokenInterface;
/**
* Provides a set of credentials that will always return an empty access token.
* This is useful for APIs which do not require authentication, for local
* service emulators, and for testing.
*/
class InsecureCredentials implements FetchAuthTokenInterface
{
/**
* @var array{access_token:string}
*/
private $token = [
'access_token' => ''
];
/**
* Fetches the auth token. In this case it returns an empty string.
*
* @param callable|null $httpHandler
* @return array{access_token:string} A set of auth related metadata
*/
public function fetchAuthToken(?callable $httpHandler = null)
{
return $this->token;
}
/**
* Returns the cache key. In this case it returns a null value, disabling
* caching.
*
* @return string|null
*/
public function getCacheKey()
{
return null;
}
/**
* Fetches the last received token. In this case, it returns the same empty string
* auth token.
*
* @return array{access_token:string}
*/
public function getLastReceivedToken()
{
return $this->token;
}
}

View File

@ -0,0 +1,457 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Firebase\JWT\JWT;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\Iam;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\ServiceAccountSignerTrait;
use Google\Auth\SignBlobInterface;
use InvalidArgumentException;
/**
* ServiceAccountCredentials supports authorization using a Google service
* account.
*
* (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
*
* It's initialized using the json key file that's downloadable from developer
* console, which should contain a private_key and client_email fields that it
* uses.
*
* Use it with AuthTokenMiddleware to authorize http requests:
*
* use Google\Auth\Credentials\ServiceAccountCredentials;
* use Google\Auth\Middleware\AuthTokenMiddleware;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $sa = new ServiceAccountCredentials(
* 'https://www.googleapis.com/auth/taskqueue',
* '/path/to/your/json/key_file.json'
* );
* $middleware = new AuthTokenMiddleware($sa);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*/
class ServiceAccountCredentials extends CredentialsLoader implements
GetQuotaProjectInterface,
SignBlobInterface,
ProjectIdProviderInterface
{
use ServiceAccountSignerTrait;
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'sa';
private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* @var string|null
*/
protected $projectId;
/**
* @var array<mixed>|null
*/
private $lastReceivedJwtAccessToken;
/**
* @var bool
*/
private $useJwtAccessWithScope = false;
/**
* @var ServiceAccountJwtAccessCredentials|null
*/
private $jwtAccessCredentials;
/**
* @var string
*/
private string $universeDomain;
/**
* Whether this is an ID token request or an access token request. Used when
* building the metric header.
*/
private bool $isIdTokenRequest = false;
/**
* Create a new ServiceAccountCredentials.
*
* @param string|string[]|null $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
* @param string $sub an email address account to impersonate, in situations when
* the service account has been delegated domain wide access.
* @param string $targetAudience The audience for the ID token.
*/
public function __construct(
$scope,
$jsonKey,
$sub = null,
$targetAudience = null
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$jsonKeyStream = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_email', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_email field'
);
}
if (!array_key_exists('private_key', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the private_key field'
);
}
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
$additionalClaims = [];
if ($targetAudience) {
$additionalClaims = ['target_audience' => $targetAudience];
$this->isIdTokenRequest = true;
}
$this->auth = new OAuth2([
'audience' => self::TOKEN_CREDENTIAL_URI,
'issuer' => $jsonKey['client_email'],
'scope' => $scope,
'signingAlgorithm' => 'RS256',
'signingKey' => $jsonKey['private_key'],
'signingKeyId' => $jsonKey['private_key_id'] ?? null,
'sub' => $sub,
'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
'additionalClaims' => $additionalClaims,
]);
$this->projectId = $jsonKey['project_id'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? self::DEFAULT_UNIVERSE_DOMAIN;
}
/**
* When called, the ServiceAccountCredentials will use an instance of
* ServiceAccountJwtAccessCredentials to fetch (self-sign) an access token
* even when only scopes are supplied. Otherwise,
* ServiceAccountJwtAccessCredentials is only called when no scopes and an
* authUrl (audience) is suppled.
*
* @return void
*/
public function useJwtAccessWithScope()
{
$this->useJwtAccessWithScope = true;
}
/**
* @param callable|null $httpHandler
* @param array<mixed> $headers [optional] Headers to be inserted
* into the token endpoint request present.
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $token_type
* }
*/
public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
{
if ($this->useSelfSignedJwt()) {
$jwtCreds = $this->createJwtAccessCredentials();
$accessToken = $jwtCreds->fetchAuthToken($httpHandler);
if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
// Keep self-signed JWTs in memory as the last received token
$this->lastReceivedJwtAccessToken = $lastReceivedToken;
}
return $accessToken;
}
if ($this->isIdTokenRequest && $this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
$now = time();
$jwt = Jwt::encode(
[
'iss' => $this->auth->getIssuer(),
'sub' => $this->auth->getIssuer(),
'scope' => self::IAM_SCOPE,
'exp' => ($now + $this->auth->getExpiry()),
'iat' => ($now - OAuth2::DEFAULT_SKEW_SECONDS),
],
$this->auth->getSigningKey(),
$this->auth->getSigningAlgorithm(),
$this->auth->getSigningKeyId()
);
// We create a new instance of Iam each time because the `$httpHandler` might change.
$idToken = (new Iam($httpHandler, $this->getUniverseDomain()))->generateIdToken(
$this->auth->getIssuer(),
$this->auth->getAdditionalClaims()['target_audience'],
$jwt,
$this->applyTokenEndpointMetrics($headers, 'it')
);
return ['id_token' => $idToken];
}
return $this->auth->fetchAuthToken(
$httpHandler,
$this->applyTokenEndpointMetrics($headers, $this->isIdTokenRequest ? 'it' : 'at')
);
}
/**
* Return the Cache Key for the credentials.
* For the cache key format is one of the following:
* ClientEmail.Scope[.Sub]
* ClientEmail.Audience[.Sub]
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
$key = $this->auth->getIssuer() . '.' . $scopeOrAudience;
if ($sub = $this->auth->getSub()) {
$key .= '.' . $sub;
}
return $key;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
// If self-signed JWTs are being used, fetch the last received token
// from memory. Else, fetch it from OAuth2
return $this->useSelfSignedJwt()
? $this->lastReceivedJwtAccessToken
: $this->auth->getLastReceivedToken();
}
/**
* Get the project ID from the service account keyfile.
*
* Returns null if the project ID does not exist in the keyfile.
*
* @param callable|null $httpHandler Not used by this credentials type.
* @return string|null
*/
public function getProjectId(?callable $httpHandler = null)
{
return $this->projectId;
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
// scope exists. use oauth implementation
if (!$this->useSelfSignedJwt()) {
return parent::updateMetadata($metadata, $authUri, $httpHandler);
}
$jwtCreds = $this->createJwtAccessCredentials();
if ($this->auth->getScope()) {
// Prefer user-provided "scope" to "audience"
$updatedMetadata = $jwtCreds->updateMetadata($metadata, null, $httpHandler);
} else {
$updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler);
}
if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
// Keep self-signed JWTs in memory as the last received token
$this->lastReceivedJwtAccessToken = $lastReceivedToken;
}
return $updatedMetadata;
}
/**
* @return ServiceAccountJwtAccessCredentials
*/
private function createJwtAccessCredentials()
{
if (!$this->jwtAccessCredentials) {
// Create credentials for self-signing a JWT (JwtAccess)
$credJson = [
'private_key' => $this->auth->getSigningKey(),
'client_email' => $this->auth->getIssuer(),
];
$this->jwtAccessCredentials = new ServiceAccountJwtAccessCredentials(
$credJson,
$this->auth->getScope()
);
}
return $this->jwtAccessCredentials;
}
/**
* @param string $sub an email address account to impersonate, in situations when
* the service account has been delegated domain wide access.
* @return void
*/
public function setSub($sub)
{
$this->auth->setSub($sub);
}
/**
* Get the client name from the keyfile.
*
* In this case, it returns the keyfile's client_email key.
*
* @param callable|null $httpHandler Not used by this credentials type.
* @return string
*/
public function getClientName(?callable $httpHandler = null)
{
return $this->auth->getIssuer();
}
/**
* Get the private key from the keyfile.
*
* In this case, it returns the keyfile's private_key key, needed for JWT signing.
*
* @return string
*/
public function getPrivateKey()
{
return $this->auth->getSigningKey();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Get the universe domain configured in the JSON credential.
*
* @return string
*/
public function getUniverseDomain(): string
{
return $this->universeDomain;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
/**
* @return bool
*/
private function useSelfSignedJwt()
{
// When a sub is supplied, the user is using domain-wide delegation, which not available
// with self-signed JWTs
if (null !== $this->auth->getSub()) {
// If we are outside the GDU, we can't use domain-wide delegation
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
throw new \LogicException(sprintf(
'Service Account subject is configured for the credential. Domain-wide ' .
'delegation is not supported in universes other than %s.',
self::DEFAULT_UNIVERSE_DOMAIN
));
}
return false;
}
// Do not use self-signed JWT for ID tokens
if ($this->isIdTokenRequest) {
return false;
}
// When true, ServiceAccountCredentials will always use JwtAccess for access tokens
if ($this->useJwtAccessWithScope) {
return true;
}
// If the universe domain is outside the GDU, use JwtAccess for access tokens
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
return true;
}
return is_null($this->auth->getScope());
}
}

View File

@ -0,0 +1,246 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\OAuth2;
use Google\Auth\ProjectIdProviderInterface;
use Google\Auth\ServiceAccountSignerTrait;
use Google\Auth\SignBlobInterface;
/**
* Authenticates requests using Google's Service Account credentials via
* JWT Access.
*
* This class allows authorizing requests for service accounts directly
* from credentials from a json key file downloaded from the developer
* console (via 'Generate new Json Key'). It is not part of any OAuth2
* flow, rather it creates a JWT and sends that as a credential.
*/
class ServiceAccountJwtAccessCredentials extends CredentialsLoader implements
GetQuotaProjectInterface,
SignBlobInterface,
ProjectIdProviderInterface
{
use ServiceAccountSignerTrait;
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'jwt';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* @var string
*/
public $projectId;
/**
* Create a new ServiceAccountJwtAccessCredentials.
*
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
* @param string|string[] $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
*/
public function __construct($jsonKey, $scope = null)
{
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new \InvalidArgumentException('file does not exist');
}
$jsonKeyStream = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
throw new \LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_email', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the client_email field'
);
}
if (!array_key_exists('private_key', $jsonKey)) {
throw new \InvalidArgumentException(
'json key is missing the private_key field'
);
}
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
$this->auth = new OAuth2([
'issuer' => $jsonKey['client_email'],
'sub' => $jsonKey['client_email'],
'signingAlgorithm' => 'RS256',
'signingKey' => $jsonKey['private_key'],
'scope' => $scope,
]);
$this->projectId = $jsonKey['project_id'] ?? null;
}
/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
$scope = $this->auth->getScope();
if (empty($authUri) && empty($scope)) {
return $metadata;
}
$this->auth->setAudience($authUri);
return parent::updateMetadata($metadata, $authUri, $httpHandler);
}
/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
* @param callable|null $httpHandler
*
* @return null|array{access_token:string} A set of auth related metadata
*/
public function fetchAuthToken(?callable $httpHandler = null)
{
$audience = $this->auth->getAudience();
$scope = $this->auth->getScope();
if (empty($audience) && empty($scope)) {
return null;
}
if (!empty($audience) && !empty($scope)) {
throw new \UnexpectedValueException(
'Cannot sign both audience and scope in JwtAccess'
);
}
$access_token = $this->auth->toJwt();
// Set the self-signed access token in OAuth2 for getLastReceivedToken
$this->auth->setAccessToken($access_token);
return [
'access_token' => $access_token,
'expires_in' => $this->auth->getExpiry(),
'token_type' => 'Bearer'
];
}
/**
* Return the cache key for the credentials.
* The format for the Cache Key one of the following:
* ClientEmail.Scope
* ClientEmail.Audience
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
return $this->auth->getIssuer() . '.' . $scopeOrAudience;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
/**
* Get the project ID from the service account keyfile.
*
* Returns null if the project ID does not exist in the keyfile.
*
* @param callable|null $httpHandler Not used by this credentials type.
* @return string|null
*/
public function getProjectId(?callable $httpHandler = null)
{
return $this->projectId;
}
/**
* Get the client name from the keyfile.
*
* In this case, it returns the keyfile's client_email key.
*
* @param callable|null $httpHandler Not used by this credentials type.
* @return string
*/
public function getClientName(?callable $httpHandler = null)
{
return $this->auth->getIssuer();
}
/**
* Get the private key from the keyfile.
*
* In this case, it returns the keyfile's private_key key, needed for JWT signing.
*
* @return string
*/
public function getPrivateKey()
{
return $this->auth->getSigningKey();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@ -0,0 +1,202 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth\Credentials;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\OAuth2;
use InvalidArgumentException;
use LogicException;
/**
* Authenticates requests using User Refresh credentials.
*
* This class allows authorizing requests from user refresh tokens.
*
* This the end of the result of a 3LO flow. E.g, the end result of
* 'gcloud auth login' saves a file with these contents in well known
* location
*
* @see [Application Default Credentials](http://goo.gl/mkAHpZ)
*/
class UserRefreshCredentials extends CredentialsLoader implements GetQuotaProjectInterface
{
/**
* Used in observability metric headers
*
* @var string
*/
private const CRED_TYPE = 'u';
/**
* The OAuth2 instance used to conduct authorization.
*
* @var OAuth2
*/
protected $auth;
/**
* The quota project associated with the JSON credentials
*
* @var string
*/
protected $quotaProject;
/**
* Whether this is an ID token request or an access token request. Used when
* building the metric header.
*/
private bool $isIdTokenRequest = false;
/**
* Create a new UserRefreshCredentials.
*
* @param string|string[]|null $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
* @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
* as an associative array
* @param string|null $targetAudience The audience for the ID token.
*/
public function __construct(
$scope,
$jsonKey,
?string $targetAudience = null
) {
if (is_string($jsonKey)) {
if (!file_exists($jsonKey)) {
throw new InvalidArgumentException('file does not exist or is unreadable');
}
$json = file_get_contents($jsonKey);
if (!$jsonKey = json_decode((string) $json, true)) {
throw new LogicException('invalid json for auth config');
}
}
if (!array_key_exists('client_id', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the client_id field'
);
}
if (!array_key_exists('client_secret', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the client_secret field'
);
}
if (!array_key_exists('refresh_token', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the refresh_token field'
);
}
if ($scope && $targetAudience) {
throw new InvalidArgumentException(
'Scope and targetAudience cannot both be supplied'
);
}
$additionalClaims = [];
if ($targetAudience) {
$additionalClaims = ['target_audience' => $targetAudience];
$this->isIdTokenRequest = true;
}
$this->auth = new OAuth2([
'clientId' => $jsonKey['client_id'],
'clientSecret' => $jsonKey['client_secret'],
'refresh_token' => $jsonKey['refresh_token'],
'scope' => $scope,
'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
'additionalClaims' => $additionalClaims,
]);
if (array_key_exists('quota_project_id', $jsonKey)) {
$this->quotaProject = (string) $jsonKey['quota_project_id'];
}
}
/**
* @param callable|null $httpHandler
* @param array<mixed> $headers [optional] Metrics headers to be inserted
* into the token endpoint request present.
* This could be passed from ImersonatedServiceAccountCredentials as it uses
* UserRefreshCredentials as source credentials.
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $scope
* @type string $token_type
* @type string $id_token
* }
*/
public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
{
return $this->auth->fetchAuthToken(
$httpHandler,
$this->applyTokenEndpointMetrics($headers, $this->isIdTokenRequest ? 'it' : 'at')
);
}
/**
* Return the Cache Key for the credentials.
* The format for the Cache key is one of the following:
* ClientId.Scope
* ClientId.Audience
*
* @return string
*/
public function getCacheKey()
{
$scopeOrAudience = $this->auth->getScope();
if (!$scopeOrAudience) {
$scopeOrAudience = $this->auth->getAudience();
}
return $this->auth->getClientId() . '.' . $scopeOrAudience;
}
/**
* @return array<mixed>
*/
public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
/**
* Get the quota project used for this API request
*
* @return string|null
*/
public function getQuotaProject()
{
return $this->quotaProject;
}
/**
* Get the granted scopes (if they exist) for the last fetched token.
*
* @return string|null
*/
public function getGrantedScope()
{
return $this->auth->getGrantedScope();
}
protected function getCredType(): string
{
return self::CRED_TYPE;
}
}

View File

@ -0,0 +1,319 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Auth;
use Google\Auth\Credentials\ExternalAccountCredentials;
use Google\Auth\Credentials\ImpersonatedServiceAccountCredentials;
use Google\Auth\Credentials\InsecureCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use RuntimeException;
use UnexpectedValueException;
/**
* CredentialsLoader contains the behaviour used to locate and find default
* credentials files on the file system.
*/
abstract class CredentialsLoader implements
GetUniverseDomainInterface,
FetchAuthTokenInterface,
UpdateMetadataInterface
{
use UpdateMetadataTrait;
const TOKEN_CREDENTIAL_URI = 'https://oauth2.googleapis.com/token';
const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS';
const QUOTA_PROJECT_ENV_VAR = 'GOOGLE_CLOUD_QUOTA_PROJECT';
const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json';
const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config';
const MTLS_WELL_KNOWN_PATH = '.secureConnect/context_aware_metadata.json';
const MTLS_CERT_ENV_VAR = 'GOOGLE_API_USE_CLIENT_CERTIFICATE';
/**
* @param string $cause
* @return string
*/
private static function unableToReadEnv($cause)
{
$msg = 'Unable to read the credential file specified by ';
$msg .= ' GOOGLE_APPLICATION_CREDENTIALS: ';
$msg .= $cause;
return $msg;
}
/**
* @return bool
*/
private static function isOnWindows()
{
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
/**
* Load a JSON key from the path specified in the environment.
*
* Load a JSON key from the path specified in the environment
* variable GOOGLE_APPLICATION_CREDENTIALS. Return null if
* GOOGLE_APPLICATION_CREDENTIALS is not specified.
*
* @return array<mixed>|null JSON key | null
*/
public static function fromEnv()
{
$path = self::getEnv(self::ENV_VAR);
if (empty($path)) {
return null;
}
if (!file_exists($path)) {
$cause = 'file ' . $path . ' does not exist';
throw new \DomainException(self::unableToReadEnv($cause));
}
$jsonKey = file_get_contents($path);
return json_decode((string) $jsonKey, true);
}
/**
* Load a JSON key from a well known path.
*
* The well known path is OS dependent:
*
* * windows: %APPDATA%/gcloud/application_default_credentials.json
* * others: $HOME/.config/gcloud/application_default_credentials.json
*
* If the file does not exist, this returns null.
*
* @return array<mixed>|null JSON key | null
*/
public static function fromWellKnownFile()
{
$rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME';
$path = [self::getEnv($rootEnv)];
if (!self::isOnWindows()) {
$path[] = self::NON_WINDOWS_WELL_KNOWN_PATH_BASE;
}
$path[] = self::WELL_KNOWN_PATH;
$path = implode(DIRECTORY_SEPARATOR, $path);
if (!file_exists($path)) {
return null;
}
$jsonKey = file_get_contents($path);
return json_decode((string) $jsonKey, true);
}
/**
* Create a new Credentials instance.
*
* @deprecated This method is being deprecated because of a potential security risk.
*
* This method does not validate the credential configuration. The security
* risk occurs when a credential configuration is accepted from a source
* that is not under your control and used without validation on your side.
*
* If you know that you will be loading credential configurations of a
* specific type, it is recommended to use a credential-type-specific
* method.
* This will ensure that an unexpected credential type with potential for
* malicious intent is not loaded unintentionally. You might still have to do
* validation for certain credential types. Please follow the recommendation
* for that method. For example, if you want to load only service accounts,
* you can create the {@see ServiceAccountCredentials} explicitly:
*
* ```
* use Google\Auth\Credentials\ServiceAccountCredentials;
* $creds = new ServiceAccountCredentials($scopes, $json);
* ```
*
* If you are loading your credential configuration from an untrusted source and have
* not mitigated the risks (e.g. by validating the configuration yourself), make
* these changes as soon as possible to prevent security risks to your environment.
*
* Regardless of the method used, it is always your responsibility to validate
* configurations received from external sources.
*
* @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
*
* @param string|string[] $scope
* @param array<mixed> $jsonKey
* @param string|string[] $defaultScope
* @return ServiceAccountCredentials|UserRefreshCredentials|ImpersonatedServiceAccountCredentials|ExternalAccountCredentials
*/
public static function makeCredentials(
$scope,
array $jsonKey,
$defaultScope = null
) {
if (!array_key_exists('type', $jsonKey)) {
throw new \InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] == 'service_account') {
// Do not pass $defaultScope to ServiceAccountCredentials
return new ServiceAccountCredentials($scope, $jsonKey);
}
if ($jsonKey['type'] == 'authorized_user') {
$anyScope = $scope ?: $defaultScope;
return new UserRefreshCredentials($anyScope, $jsonKey);
}
if ($jsonKey['type'] == 'impersonated_service_account') {
$anyScope = $scope ?: $defaultScope;
return new ImpersonatedServiceAccountCredentials($anyScope, $jsonKey);
}
if ($jsonKey['type'] == 'external_account') {
$anyScope = $scope ?: $defaultScope;
return new ExternalAccountCredentials($anyScope, $jsonKey);
}
throw new \InvalidArgumentException('invalid value in the type field');
}
/**
* Create an authorized HTTP Client from an instance of FetchAuthTokenInterface.
*
* @param FetchAuthTokenInterface $fetcher is used to fetch the auth token
* @param array<mixed> $httpClientOptions (optional) Array of request options to apply.
* @param callable|null $httpHandler (optional) http client to fetch the token.
* @param callable|null $tokenCallback (optional) function to be called when a new token is fetched.
* @return \GuzzleHttp\Client
*/
public static function makeHttpClient(
FetchAuthTokenInterface $fetcher,
array $httpClientOptions = [],
?callable $httpHandler = null,
?callable $tokenCallback = null
) {
$middleware = new Middleware\AuthTokenMiddleware(
$fetcher,
$httpHandler,
$tokenCallback
);
$stack = \GuzzleHttp\HandlerStack::create();
$stack->push($middleware);
return new \GuzzleHttp\Client([
'handler' => $stack,
'auth' => 'google_auth',
] + $httpClientOptions);
}
/**
* Create a new instance of InsecureCredentials.
*
* @return InsecureCredentials
*/
public static function makeInsecureCredentials()
{
return new InsecureCredentials();
}
/**
* Fetch a quota project from the environment variable
* GOOGLE_CLOUD_QUOTA_PROJECT. Return null if
* GOOGLE_CLOUD_QUOTA_PROJECT is not specified.
*
* @return string|null
*/
public static function quotaProjectFromEnv()
{
return self::getEnv(self::QUOTA_PROJECT_ENV_VAR) ?: null;
}
/**
* Gets a callable which returns the default device certification.
*
* @throws UnexpectedValueException
* @return callable|null
*/
public static function getDefaultClientCertSource()
{
if (!$clientCertSourceJson = self::loadDefaultClientCertSourceFile()) {
return null;
}
$clientCertSourceCmd = $clientCertSourceJson['cert_provider_command'];
return function () use ($clientCertSourceCmd) {
$cmd = array_map('escapeshellarg', $clientCertSourceCmd);
exec(implode(' ', $cmd), $output, $returnVar);
if (0 === $returnVar) {
return implode(PHP_EOL, $output);
}
throw new RuntimeException(
'"cert_provider_command" failed with a nonzero exit code'
);
};
}
/**
* Determines whether or not the default device certificate should be loaded.
*
* @return bool
*/
public static function shouldLoadClientCertSource()
{
return filter_var(self::getEnv(self::MTLS_CERT_ENV_VAR), FILTER_VALIDATE_BOOLEAN);
}
/**
* @return array{cert_provider_command:string[]}|null
*/
private static function loadDefaultClientCertSourceFile()
{
$rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME';
$path = sprintf('%s/%s', self::getEnv($rootEnv), self::MTLS_WELL_KNOWN_PATH);
if (!file_exists($path)) {
return null;
}
$jsonKey = file_get_contents($path);
$clientCertSourceJson = json_decode((string) $jsonKey, true);
if (!$clientCertSourceJson) {
throw new UnexpectedValueException('Invalid client cert source JSON');
}
if (!isset($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source requires "cert_provider_command"'
);
}
if (!is_array($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source expects "cert_provider_command" to be an array'
);
}
return $clientCertSourceJson;
}
/**
* Get the universe domain from the credential. Defaults to "googleapis.com"
* for all credential types which do not support universe domain.
*
* @return string
*/
public function getUniverseDomain(): string
{
return self::DEFAULT_UNIVERSE_DOMAIN;
}
private static function getEnv(string $env): mixed
{
return getenv($env) ?: $_ENV[$env] ?? null;
}
}

Some files were not shown because too many files have changed in this diff Show More