Initial import

This commit is contained in:
Nextcloud Team 2021-11-30 11:20:42 +01:00 committed by Lukas Reschke
commit 2295a33590
884 changed files with 93939 additions and 0 deletions

3
php/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/php/data/configuration.json
/php/data/containers.json

8
php/.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

34
php/.idea/aio.iml generated Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="AIO\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-middleware" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/http-interop/http-factory-guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/slim/slim" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/fast-route" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/opis/closure" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/slim-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/php-di" />
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/phpdoc-reader" />
<excludeFolder url="file://$MODULE_DIR$/vendor/slim/twig-view" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
php/.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/aio.iml" filepath="$PROJECT_DIR$/.idea/aio.iml" />
</modules>
</component>
</project>

34
php/.idea/php.xml generated Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/psr/http-server-middleware" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/http-interop/http-factory-guzzle" />
<path value="$PROJECT_DIR$/vendor/psr/http-server-handler" />
<path value="$PROJECT_DIR$/vendor/slim/slim" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/nikic/fast-route" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/opis/closure" />
<path value="$PROJECT_DIR$/vendor/php-di/slim-bridge" />
<path value="$PROJECT_DIR$/vendor/php-di/invoker" />
<path value="$PROJECT_DIR$/vendor/php-di/php-di" />
<path value="$PROJECT_DIR$/vendor/php-di/phpdoc-reader" />
<path value="$PROJECT_DIR$/vendor/slim/twig-view" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
</project>

6
php/.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

15
php/README.md Normal file
View file

@ -0,0 +1,15 @@
# PHP Docker Controller
This is the code for the PHP Docker controller.
## How to run
Running this locally requires Docker Engine on the same machine.
If this is the case, just execute the following command:
```
cd public/
php -S 0.0.0.0:8080
```
You can then access the web interface at `localhost:8080`.

20
php/composer.json Normal file
View file

@ -0,0 +1,20 @@
{
"autoload": {
"psr-4": {
"AIO\\": ["src/"]
}
},
"require": {
"ext-json": "*",
"ext-sodium": "*",
"ext-curl": "*",
"slim/slim": "4.*",
"php-di/slim-bridge": "^3.1",
"guzzlehttp/guzzle": "^7.3",
"guzzlehttp/psr7": "^1.8",
"http-interop/http-factory-guzzle": "^1.2",
"slim/twig-view": "^3.2",
"slim/csrf": "^1.2",
"ext-apcu": "*"
}
}

1529
php/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

288
php/containers.json Normal file
View file

@ -0,0 +1,288 @@
{
"production": [
{
"dependsOn": [
"nextcloud-aio-nextcloud",
"nextcloud-aio-collabora",
"nextcloud-aio-talk"
],
"identifier": "nextcloud-aio-apache",
"displayName": "Apache",
"containerName": "nextcloud/aio-apache",
"ports": [
"443/tcp"
],
"internalPorts": [
"443"
],
"secrets": [],
"environmentVariables": [
"NC_DOMAIN=%NC_DOMAIN%",
"NEXTCLOUD_HOST=nextcloud-aio-nextcloud",
"COLLABORA_HOST=nextcloud-aio-collabora",
"TALK_HOST=nextcloud-aio-talk"
],
"volumes": [
{
"name": "nextcloud_aio_nextcloud",
"location": "/var/www/html",
"writeable": false
},
{
"name": "nextcloud_aio_apache",
"location": "/mnt/data",
"writeable": true
}
],
"maxShutdownTime": 10,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-database",
"displayName": "Database",
"containerName": "nextcloud/aio-postgresql",
"ports": [],
"internalPorts": [
"5432"
],
"secrets": [
"DATABASE_PASSWORD"
],
"volumes": [
{
"name": "nextcloud_aio_database",
"location": "/var/lib/postgresql/data",
"writeable": true
},
{
"name": "nextcloud_aio_database_dump",
"location": "/mnt/data",
"writeable": true
}
],
"environmentVariables": [
"POSTGRES_PASSWORD=%DATABASE_PASSWORD%",
"POSTGRES_DB=nextcloud_database",
"POSTGRES_USER=nextcloud"
],
"maxShutdownTime": 1800,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [
"nextcloud-aio-database",
"nextcloud-aio-redis"
],
"identifier": "nextcloud-aio-nextcloud",
"displayName": "Nextcloud",
"containerName": "nextcloud/aio-nextcloud",
"ports": [],
"internalPorts": [
"9000"
],
"secrets": [
"DATABASE_PASSWORD",
"REDIS_PASSWORD",
"NEXTCLOUD_PASSWORD",
"TURN_SECRET",
"SIGNALING_SECRET"
],
"volumes": [
{
"name": "nextcloud_aio_nextcloud",
"location": "/var/www/html",
"writeable": true
},
{
"name": "nextcloud_aio_nextcloud_data",
"location": "/mnt/ncdata",
"writeable": true
}
],
"environmentVariables": [
"POSTGRES_HOST=nextcloud-aio-database",
"POSTGRES_PASSWORD=%DATABASE_PASSWORD%",
"POSTGRES_DB=nextcloud_database",
"POSTGRES_USER=nextcloud",
"REDIS_HOST=nextcloud-aio-redis",
"REDIS_HOST_PASSWORD=%REDIS_PASSWORD%",
"AIO_TOKEN=%AIO_TOKEN%",
"NC_DOMAIN=%NC_DOMAIN%",
"ADMIN_USER=admin",
"ADMIN_PASSWORD=%NEXTCLOUD_PASSWORD%",
"NEXTCLOUD_DATA_DIR=/mnt/ncdata",
"OVERWRITEHOST=%NC_DOMAIN%",
"OVERWRITEPROTOCOL=https",
"TRUSTED_PROXIES=127.0.0.1",
"TURN_SECRET=%TURN_SECRET%",
"SIGNALING_SECRET=%SIGNALING_SECRET%",
"AIO_URL=%AIO_URL%"
],
"maxShutdownTime": 10,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-redis",
"displayName": "Redis",
"containerName": "nextcloud/aio-redis",
"ports": [],
"internalPorts": [
"6379"
],
"environmentVariables": [
"REDIS_HOST_PASSWORD=%REDIS_PASSWORD%"
],
"volumes": [],
"secrets": [
"REDIS_PASSWORD"
],
"maxShutdownTime": 10,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-collabora",
"displayName": "Collabora",
"containerName": "nextcloud/aio-collabora",
"ports": [],
"internalPorts": [
"9980"
],
"environmentVariables": [
"domain=%NC_DOMAIN%"
],
"volumes": [],
"secrets": [],
"maxShutdownTime": 10,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-talk",
"displayName": "Talk",
"containerName": "nextcloud/aio-talk",
"ports": [
"3478/tcp",
"3478/udp"
],
"internalPorts": [
"3478"
],
"environmentVariables": [
"NC_DOMAIN=%NC_DOMAIN%",
"TURN_SECRET=%TURN_SECRET%",
"SIGNALING_SECRET=%SIGNALING_SECRET%",
"JANUS_API_KEY=%JANUS_API_KEY%"
],
"volumes": [],
"secrets": [
"TURN_SECRET",
"SIGNALING_SECRET",
"JANUS_API_KEY"
],
"maxShutdownTime": 10,
"restartPolicy": "unless-stopped"
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-borgbackup",
"displayName": "Borgbackup",
"containerName": "nextcloud/aio-borgbackup",
"ports": [],
"internalPorts": [],
"environmentVariables": [
"BORG_PASSWORD=%BORGBACKUP_PASSWORD%",
"BORG_MODE=%BORGBACKUP_MODE%"
],
"volumes": [
{
"name": "nextcloud_aio_backup_cache",
"location": "/root",
"writeable": true
},
{
"name": "nextcloud_aio_nextcloud",
"location": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud",
"writeable": true
},
{
"name": "nextcloud_aio_nextcloud_data",
"location": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data",
"writeable": true
},
{
"name": "nextcloud_aio_database",
"location": "/nextcloud_aio_volumes/nextcloud_aio_database",
"writeable": true
},
{
"name": "nextcloud_aio_database_dump",
"location": "/nextcloud_aio_volumes/nextcloud_aio_database_dump",
"writeable": true
},
{
"name": "nextcloud_aio_apache",
"location": "/nextcloud_aio_volumes/nextcloud_aio_apache",
"writeable": true
},
{
"name": "nextcloud_aio_mastercontainer",
"location": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer",
"writeable": true
},
{
"name": "%BORGBACKUP_HOST_LOCATION%",
"location": "/mnt/borgbackup",
"writeable": true
}
],
"secrets": [
"BORGBACKUP_PASSWORD"
],
"maxShutdownTime": 10,
"restartPolicy": ""
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-watchtower",
"displayName": "Watchtower",
"containerName": "nextcloud/aio-watchtower",
"ports": [],
"internalPorts": [],
"environmentVariables": [
"CONTAINER_TO_UPDATE=nextcloud-aio-mastercontainer"
],
"volumes": [
{
"name": "/var/run/docker.sock",
"location": "/var/run/docker.sock",
"writeable": false
}
],
"secrets": [],
"maxShutdownTime": 10,
"restartPolicy": ""
},
{
"dependsOn": [],
"identifier": "nextcloud-aio-domaincheck",
"displayName": "Domaincheck",
"containerName": "nextcloud/aio-domaincheck",
"ports": [
"443/tcp"
],
"internalPorts": [],
"environmentVariables": [
"INSTANCE_ID=%INSTANCE_ID%"
],
"volumes": [],
"secrets": [
"INSTANCE_ID"
],
"maxShutdownTime": 1,
"restartPolicy": ""
}
]
}

0
php/data/.gitkeep Normal file
View file

View file

@ -0,0 +1 @@
I_AM_HERE

79
php/public/forms.js Normal file
View file

@ -0,0 +1,79 @@
"use strict";
(function (){
var lastError;
function showError(message) {
const body = document.getElementsByTagName('body')[0]
const toast = document.createElement("div")
toast.className = "toast error"
toast.prepend(message)
if (lastError) {
lastError.remove()
}
lastError = toast
body.prepend(toast)
setTimeout(toast.remove.bind(toast), 3000)
}
function handleEvent(e) {
const xhr = e.target;
if (xhr.status === 201) {
window.location.replace(xhr.getResponseHeader('Location'));
}
if (xhr.status === 422) {
showError(xhr.response);
}
if (xhr.status === 500) {
showError("Server error. Please see the logs for details.");
}
}
function disable(element) {
document.getElementById('overlay').classList.add('loading');
element.classList.add('loading');
element.disabled = true;
}
function enable(element) {
document.getElementById('overlay').classList.remove('loading');
element.classList.remove('loading');
element.disabled = false;
}
function initForm(form) {
function submit(event)
{
if (lastError) {
lastError.remove()
}
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', handleEvent);
xhr.addEventListener('error', () => showError("Failed to talk to server."));
xhr.addEventListener('load', () => enable(event.submitter));
xhr.addEventListener('error', () => enable(event.submitter));
xhr.open(form.method, form.getAttribute("action"));
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
disable(event.submitter);
xhr.send(new URLSearchParams(new FormData(form)));
event.preventDefault();
}
form.onsubmit = submit;
console.info(form);
}
function initForms() {
const forms = document.querySelectorAll('form.xhr')
console.info("Making " + forms.length + " form(s) use XHR.");
for (const form of forms) {
initForm(form);
}
}
if (document.readyState === 'loading') {
// Loading hasn't finished yet
document.addEventListener('DOMContentLoaded', initForms);
} else { // `DOMContentLoaded` has already fired
initForms();
}
})()

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
php/public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

1
php/public/img/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg width="256" height="128" version="1.1" viewBox="0 0 256 128" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke-width="22"><circle cx="40" cy="64" r="26" stroke="#ffffff" fill="none"/><circle cx="216" cy="64" r="26" stroke="#ffffff" fill="none"/><circle cx="128" cy="64" r="46" stroke="#ffffff" fill="none"/></g></svg>

After

Width:  |  Height:  |  Size: 330 B

140
php/public/index.php Normal file
View file

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
// increase memory limit to 2GB
ini_set('memory_limit', '2048M');
// set max execution time to 2h just in case of a very slow internet connection
ini_set('max_execution_time', '7200');
use DI\Container;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
require __DIR__ . '/../vendor/autoload.php';
$container = \AIO\DependencyInjection::GetContainer();
$dataConst = $container->get(\AIO\Data\DataConst::class);
ini_set('session.save_path', $dataConst->GetSessionDirectory());
// Auto logout on browser close
ini_set('session.cookie_lifetime', '0');
// Make sure to delete all stale sessions after at least one day
ini_set('session.gc_maxlifetime', '86400');
ini_set('session.gc_probability', '1');
ini_set('session.gc_divisor', '1');
// Create app
AppFactory::setContainer($container);
$app = AppFactory::create();
$responseFactory = $app->getResponseFactory();
// Register Middleware On Container
$container->set(Guard::class, function () use ($responseFactory) {
$guard = new Guard($responseFactory);
$guard->setPersistentTokenMode(true);
return $guard;
});
// Register Middleware To Be Executed On All Routes
session_start();
$app->add(Guard::class);
// Create Twig
$twig = Twig::create(__DIR__ . '/../templates/', ['cache' => false]);
$app->add(TwigMiddleware::create($app, $twig));
$twig->addExtension(new \AIO\Twig\CsrfExtension($container->get(Guard::class)));
// Auth Middleware
$app->add(new \AIO\Middleware\AuthMiddleware($container->get(\AIO\Auth\AuthManager::class)));
// API
$app->post('/api/docker/watchtower', AIO\Controller\DockerController::class . ':StartWatchtowerContainer');
$app->post('/api/docker/start', AIO\Controller\DockerController::class . ':StartContainer');
$app->post('/api/docker/backup', AIO\Controller\DockerController::class . ':StartBackupContainerBackup');
$app->post('/api/docker/backup-check', AIO\Controller\DockerController::class . ':StartBackupContainerCheck');
$app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore');
$app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer');
$app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs');
$app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin');
$app->get('/api/auth/getlogin', AIO\Controller\LoginController::class . ':GetTryLogin');
$app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout');
$app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig');
// Views
$app->get('/containers', function ($request, $response, $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Data\ConfigurationManager $configurationManager */
$configurationManager = $container->get(\AIO\Data\ConfigurationManager::class);
$dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class);
$dockerActionManger->ConnectMasterContainerToNetwork();
$dockerController = $container->get(\AIO\Controller\DockerController::class);
$dockerController->StartDomaincheckContainer();
$view->addExtension(new \AIO\Twig\ClassExtension());
return $view->render($response, 'containers.twig', [
'domain' => $configurationManager->GetDomain(),
'borg_backup_host_location' => $configurationManager->GetBorgBackupHostLocation(),
'borg_backup_mode' => $configurationManager->GetBorgBackupMode(),
'nextcloud_password' => $configurationManager->GetSecret('NEXTCLOUD_PASSWORD'),
'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(),
'borgbackup_password' => $configurationManager->GetSecret('BORGBACKUP_PASSWORD'),
'is_mastercontainer_update_available' => $dockerActionManger->IsMastercontainerUpdateAvailable(),
'has_backup_run_once' => $configurationManager->hasBackupRunOnce(),
'backup_exit_code' => $dockerActionManger->GetBackupcontainerExitCode(),
'was_start_button_clicked' => $configurationManager->wasStartButtonClicked(),
'has_update_available' => $dockerActionManger->isAnyUpdateAvailable(),
'last_backup_time' => $configurationManager->GetLastBackupTime(),
]);
})->setName('profile');
$app->get('/login', function ($request, $response, $args) {
$view = Twig::fromRequest($request);
return $view->render($response, 'login.twig');
});
$app->get('/setup', function ($request, $response, $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if(!$setup->CanBeInstalled()) {
return $view->render(
$response,
'already-installed.twig'
);
}
return $view->render(
$response,
'setup.twig',
[
'password' => $setup->Setup(),
]
);
});
// Auth Redirector
$app->get('/', function (\Psr\Http\Message\RequestInterface $request, \Psr\Http\Message\ResponseInterface $response, $args) use ($container) {
$authManager = $container->get(\AIO\Auth\AuthManager::class);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if($setup->CanBeInstalled()) {
return $response
->withHeader('Location', '/setup')
->withStatus(302);
}
if($authManager->IsAuthenticated()) {
return $response
->withHeader('Location', '/containers')
->withStatus(302);
} else {
return $response
->withHeader('Location', '/login')
->withStatus(302);
}
});
$app->run();

206
php/public/style.css Normal file
View file

@ -0,0 +1,206 @@
html, body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Cantarell, Ubuntu, Helvetica Neue, Arial, Noto Color Emoji, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;;
}
a {
text-decoration: none;
}
.button {
padding: 6px 16px;
width: auto;
min-height: 34px;
cursor: pointer;
background-color:#0082c9;
font-weight: bold;
border-radius: 100px;
margin: 3px 3px 3px 0;
font-size: 13px;
color: white;
border: 1px solid black;
outline: none;
}
ul {
list-style: none;
padding: 0;
}
li {
padding-bottom: 5px;
}
span.error {
background-color: #e9322d;
}
div.toast.error {
border-left-color: #e9322d;
}
.status {
display: inline-block;
height: 16px;
width: 16px;
vertical-align: text-bottom
}
.status {
border-radius: 50%
}
span.success {
background-color: #46ba61;
}
span.running {
background-color: rgb(255, 208, 0);
}
div.toast.success {
border-left-color: #46ba61;
}
div.toast {
border-left: 3px solid;
right: 10px;
min-width: 200px;
box-shadow: 0 0 6px 0 rgba(77, 77, 77, 0.3);
padding: 12px;
margin-top: 45px;
position: fixed;
z-index: 1;
border-radius: 3px;
background: none;
background-color: white;
}
.login {
padding: 50px;
background-color: white;
width: 500px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 16px;
}
.login > .monospace {
font-family: monospace;
font-size: 17px;
}
input {
padding: 10px;
margin-bottom: 15px;
}
.login > form > input {
width: 100%;
}
.login > img {
margin-left: auto;
margin-right: auto;
display: block;
}
.login > .button {
margin-left: auto;
margin-right: auto;
display: block;
text-align: center;
line-height: 33px;
margin-top: 20px;
}
.login-wrapper {
height: 100%;
width: 100%;
background-color: #0082c9;
background-image: linear-gradient(
40deg
, #0082c9 0%, #30b6ff 100%);
background-size: contain;
background-image: url('/img/background.png'), linear-gradient(
40deg
, #0082c9 0%, #30b6ff 100%);
position: relative;
}
.content {
padding: 20px;
max-width: 100%;
word-break: break-word;
max-width: 450px;
margin: 0 auto;
}
.logo {
background-image: url('/img/logo.svg');
height: 50px;
background-repeat: no-repeat;
display: inline-flex;
background-size: contain;
background-position: center center;
width: 62px;
position: absolute;
left: 12px;
top: 1px;
bottom: 1px;
}
header {
background-color: #0082c9;
background-image: linear-gradient(40deg, #0082c9 0%, #30b6ff 100%);
height: 50px;
justify-content: space-between;
display: flex;
}
.loading {
color: grey;
}
#overlay {
position: fixed; /* Sit on top of the page content */
display: none; /* Hidden by default */
width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5); /* Black background with opacity */
z-index: 2;
}
#overlay.loading {
display: block;
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
position: absolute;
top: calc(50% - 60px);
left: calc(50% - 60px);
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

0
php/session/.gitkeep Normal file
View file

View file

@ -0,0 +1 @@
csrf|a:1:{s:17:"csrf61a4cf452d1d1";s:32:"aca558848ae342fdf00f078576fbf5cb";}aio_authenticated|b:1;

View file

@ -0,0 +1,34 @@
<?php
namespace AIO\Auth;
use AIO\Data\ConfigurationManager;
class AuthManager {
private const SESSION_KEY = 'aio_authenticated';
private ConfigurationManager $configurationManager;
public function __construct(ConfigurationManager $configurationManager) {
$this->configurationManager = $configurationManager;
}
public function CheckCredentials(string $username, string $password) : bool {
if($username === $this->configurationManager->GetUserName()) {
return hash_equals($this->configurationManager->GetPassword(), $password);
}
return false;
}
public function CheckToken(string $token) : bool {
return hash_equals($this->configurationManager->GetToken(), $token);
}
public function SetAuthState(bool $isLoggedIn) : void {
$_SESSION[self::SESSION_KEY] = $isLoggedIn;
}
public function IsAuthenticated() : bool {
return isset($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY] === true;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
<?php
namespace AIO\Container;
use AIO\Container\State\IContainerState;
use AIO\Data\ConfigurationManager;
use AIO\Docker\DockerActionManager;
use AIO\ContainerDefinitionFetcher;
class Container {
private string $identifier;
private string $displayName;
private string $containerName;
private string $restartPolicy;
private int $maxShutdownTime;
private ContainerPorts $ports;
private ContainerInternalPorts $internalPorts;
private ContainerVolumes $volumes;
private ContainerEnvironmentVariables $containerEnvironmentVariables;
/** @var string[] */
private array $dependsOn;
/** @var string[] */
private array $secrets;
private DockerActionManager $dockerActionManager;
public function __construct(
string $identifier,
string $displayName,
string $containerName,
string $restartPolicy,
int $maxShutdownTime,
ContainerPorts $ports,
ContainerInternalPorts $internalPorts,
ContainerVolumes $volumes,
ContainerEnvironmentVariables $containerEnvironmentVariables,
array $dependsOn,
array $secrets,
DockerActionManager $dockerActionManager
) {
$this->identifier = $identifier;
$this->displayName = $displayName;
$this->containerName = $containerName;
$this->restartPolicy = $restartPolicy;
$this->maxShutdownTime = $maxShutdownTime;
$this->ports = $ports;
$this->internalPorts = $internalPorts;
$this->volumes = $volumes;
$this->containerEnvironmentVariables = $containerEnvironmentVariables;
$this->dependsOn = $dependsOn;
$this->secrets = $secrets;
$this->dockerActionManager = $dockerActionManager;
}
public function GetIdentifier() : string {
return $this->identifier;
}
public function GetDisplayName() : string {
return $this->displayName;
}
public function GetContainerName() : string {
return $this->containerName;
}
public function GetRestartPolicy() : string {
return $this->restartPolicy;
}
public function GetMaxShutdownTime() : int {
return $this->maxShutdownTime;
}
public function GetSecrets() : array {
return $this->secrets;
}
public function GetPorts() : ContainerPorts {
return $this->ports;
}
public function GetInternalPorts() : ContainerInternalPorts {
return $this->internalPorts;
}
public function GetVolumes() : ContainerVolumes {
return $this->volumes;
}
public function GetRunningState() : IContainerState {
return $this->dockerActionManager->GetContainerRunningState($this);
}
public function GetUpdateState() : IContainerState {
return $this->dockerActionManager->GetContainerUpdateState($this);
}
public function GetStartingState() : IContainerState {
return $this->dockerActionManager->GetContainerStartingState($this);
}
/**
* @return string[]
*/
public function GetDependsOn() : array {
return $this->dependsOn;
}
public function GetEnvironmentVariables() : ContainerEnvironmentVariables {
return $this->containerEnvironmentVariables;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerEnvironmentVariables {
/** @var string[] */
private array $variables = [];
public function AddVariable(string $variable) : void {
$this->variables[] = $variable;
}
/**
* @return string[]
*/
public function GetVariables() : array {
return $this->variables;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerInternalPorts {
/** @var string[] */
private array $internalPorts = [];
public function AddInternalPort(string $internalPort) : void {
$this->internalPorts[] = $internalPort;
}
/**
* @return string[]
*/
public function GetInternalPorts() : array {
return $this->internalPorts;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerPorts {
/** @var string[] */
private array $ports = [];
public function AddPort(string $port) : void {
$this->ports[] = $port;
}
/**
* @return string[]
*/
public function GetPorts() : array {
return $this->ports;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerVolume {
public string $name;
public string $mountPoint;
public bool $isWritable;
public function __construct(
string $name,
string $mountPoint,
bool $isWritable
) {
$this->name = $name;
$this->mountPoint = $mountPoint;
$this->isWritable = $isWritable;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerVolumes {
/** @var ContainerVolume[] */
private array $volumes = [];
public function AddVolume(ContainerVolume $volume) {
$this->volumes[] = $volume;
}
/**
* @return ContainerVolume[]
*/
public function GetVolumes() : array {
return $this->volumes;
}
}

View file

@ -0,0 +1,5 @@
<?php
namespace AIO\Container\State;
interface IContainerState {}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class ImageDoesNotExistState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class RunningState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class StartingState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class StoppedState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class VersionDifferentState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class VersionEqualState implements IContainerState
{}

View file

@ -0,0 +1,136 @@
<?php
namespace AIO;
use AIO\Container\Container;
use AIO\Container\ContainerEnvironmentVariables;
use AIO\Container\ContainerPorts;
use AIO\Container\ContainerInternalPorts;
use AIO\Container\ContainerVolume;
use AIO\Container\ContainerVolumes;
use AIO\Container\State\RunningState;
use AIO\Data\ConfigurationManager;
use AIO\Data\DataConst;
use AIO\Docker\DockerActionManager;
class ContainerDefinitionFetcher
{
private ConfigurationManager $configurationManager;
private \DI\Container $container;
public function __construct(
ConfigurationManager $configurationManager,
\DI\Container $container
)
{
$this->configurationManager = $configurationManager;
$this->container = $container;
}
public function GetContainerById(string $id): ?Container
{
$containers = $this->FetchDefinition();
foreach ($containers as $container) {
if ($container->GetIdentifier() === $id) {
return $container;
}
}
return null;
}
/**
* @return array
*/
private function GetDefinition(bool $latest): array
{
$data = json_decode(file_get_contents(__DIR__ . '/../containers.json'), true);
$containers = [];
foreach ($data['production'] as $entry) {
$ports = new ContainerPorts();
foreach ($entry['ports'] as $port) {
$ports->AddPort($port);
}
$internalPorts = new ContainerInternalPorts();
foreach ($entry['internalPorts'] as $internalPort) {
$internalPorts->AddInternalPort($internalPort);
}
$volumes = new ContainerVolumes();
foreach ($entry['volumes'] as $value) {
if($value['name'] === '%BORGBACKUP_HOST_LOCATION%') {
$value['name'] = $this->configurationManager->GetBorgBackupHostLocation();
if($value['name'] === '') {
continue;
}
}
$volumes->AddVolume(
new ContainerVolume(
$value['name'],
$value['location'],
$value['writeable']
)
);
}
$variables = new ContainerEnvironmentVariables();
foreach ($entry['environmentVariables'] as $value) {
$variables->AddVariable($value);
}
$containers[] = new Container(
$entry['identifier'],
$entry['displayName'],
$entry['containerName'],
$entry['restartPolicy'],
$entry['maxShutdownTime'],
$ports,
$internalPorts,
$volumes,
$variables,
$entry['dependsOn'],
$entry['secrets'],
$this->container->get(DockerActionManager::class)
);
}
return $containers;
}
public function FetchDefinition(): array
{
if (!file_exists(DataConst::GetDataDirectory() . '/containers.json')) {
$containers = $this->GetDefinition(true);
} else {
$containers = $this->GetDefinition(false);
}
$borgBackupMode = $this->configurationManager->GetBorgBackupMode();
$fetchLatest = false;
foreach ($containers as $container) {
if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') {
if ($container->GetRunningState() === RunningState::class) {
if ($borgBackupMode !== 'backup' && $borgBackupMode !== 'restore') {
$fetchLatest = true;
}
} else {
$fetchLatest = true;
}
} elseif ($container->GetIdentifier() === 'nextcloud-aio-watchtower' && $container->GetRunningState() === RunningState::class) {
return $containers;
}
}
if ($fetchLatest === true) {
$containers = $this->GetDefinition(true);
}
return $containers;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace AIO\Controller;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager;
use AIO\Data\InvalidSettingConfigurationException;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ConfigurationController
{
private ConfigurationManager $configurationManager;
public function __construct(
ConfigurationManager $configurationManager
) {
$this->configurationManager = $configurationManager;
}
public function SetConfig(Request $request, Response $response, $args) : Response {
try {
if (isset($request->getParsedBody()['domain'])) {
$this->configurationManager->SetDomain($request->getParsedBody()['domain']);
}
if (isset($request->getParsedBody()['borg_backup_host_location'])) {
$this->configurationManager->SetBorgBackupHostLocation($request->getParsedBody()['borg_backup_host_location']);
}
return $response->withStatus(201)->withHeader('Location', '/');
} catch (InvalidSettingConfigurationException $ex) {
$response->getBody()->write($ex->getMessage());
return $response->withStatus(422);
}
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace AIO\Controller;
use AIO\Container\State\RunningState;
use AIO\ContainerDefinitionFetcher;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use AIO\Data\ConfigurationManager;
class DockerController
{
private DockerActionManager $dockerActionManager;
private ContainerDefinitionFetcher $containerDefinitionFetcher;
private const TOP_CONTAINER = 'nextcloud-aio-apache';
private ConfigurationManager $configurationManager;
public function __construct(
DockerActionManager $dockerActionManager,
ContainerDefinitionFetcher $containerDefinitionFetcher,
ConfigurationManager $configurationManager
) {
$this->dockerActionManager = $dockerActionManager;
$this->containerDefinitionFetcher = $containerDefinitionFetcher;
$this->configurationManager = $configurationManager;
}
private function PerformRecursiveContainerStart(string $id) {
$container = $this->containerDefinitionFetcher->GetContainerById($id);
foreach($container->GetDependsOn() as $dependency) {
$this->PerformRecursiveContainerStart($dependency);
}
$this->dockerActionManager->DeleteContainer($container);
$this->dockerActionManager->CreateVolumes($container);
$this->dockerActionManager->PullContainer($container);
$this->dockerActionManager->CreateContainer($container);
$this->dockerActionManager->StartContainer($container);
$this->dockerActionManager->ConnectContainerToNetwork($container);
}
public function GetLogs(Request $request, Response $response, $args) : Response
{
$id = $request->getQueryParams()['id'];
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$logs = $this->dockerActionManager->GetLogs($container);
$body = $response->getBody();
$body->write($logs);
return $response
->withStatus(200)
->withHeader('Content-Type', 'text/plain; charset=utf-8')
->withHeader('Content-Disposition', 'inline');
}
public function StartBackupContainerBackup(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'backup';
$this->configurationManager->WriteConfig($config);
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartBackupContainerCheck(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'check';
$this->configurationManager->WriteConfig($config);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartBackupContainerRestore(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'restore';
$this->configurationManager->WriteConfig($config);
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartContainer(Request $request, Response $response, $args) : Response
{
$uri = $request->getUri();
$host = $uri->getHost();
$port = $uri->getPort();
$config = $this->configurationManager->GetConfig();
// set AIO_URL
$config['AIO_URL'] = $host . ':' . $port;
// set wasStartButtonClicked
$config['wasStartButtonClicked'] = 1;
// set AIO_TOKEN
$config['AIO_TOKEN'] = bin2hex(random_bytes(24));
$this->configurationManager->WriteConfig($config);
$this->configurationManager->SetIsContainerUpateAvailable(false);
// Stop domaincheck since apache would not be able to start otherwise
$this->StopDomaincheckContainer();
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartWatchtowerContainer(Request $request, Response $response, $args) : Response {
$id = 'nextcloud-aio-watchtower';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
private function PerformRecursiveContainerStop(string $id)
{
$container = $this->containerDefinitionFetcher->GetContainerById($id);
foreach($container->GetDependsOn() as $dependency) {
$this->PerformRecursiveContainerStop($dependency);
}
$this->dockerActionManager->DisconnectContainerFromNetwork($container);
$this->dockerActionManager->StopContainer($container);
}
public function StopContainer(Request $request, Response $response, $args) : Response
{
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartDomaincheckContainer()
{
# Don't start if domain is already set
if ($this->configurationManager->GetDomain() != '') {
return;
}
$id = 'nextcloud-aio-domaincheck';
$container = $this->containerDefinitionFetcher->GetContainerById($id);
// don't start if the domaincheck is already running
if ($container->GetIdentifier() === $id && $container->GetRunningState() instanceof RunningState) {
return;
// don't start if apache is already running
} elseif ($container->GetIdentifier() === self::TOP_CONTAINER && $container->GetRunningState() instanceof RunningState) {
return;
}
$this->PerformRecursiveContainerStart($id);
}
private function StopDomaincheckContainer()
{
$id = 'nextcloud-aio-domaincheck';
$this->PerformRecursiveContainerStop($id);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace AIO\Controller;
use AIO\Auth\AuthManager;
use AIO\Container\Container;
use AIO\ContainerDefinitionFetcher;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController
{
private AuthManager $authManager;
public function __construct(AuthManager $authManager) {
$this->authManager = $authManager;
}
public function TryLogin(Request $request, Response $response, $args) : Response {
$userName = $request->getParsedBody()['username'];
$password = $request->getParsedBody()['password'];
if($this->authManager->CheckCredentials($userName, $password)) {
$this->authManager->SetAuthState(true);
return $response->withHeader('Location', '/')->withStatus(302);
}
return $response->withHeader('Location', '/')->withStatus(302);
}
public function GetTryLogin(Request $request, Response $response, $args) : Response {
$token = $request->getQueryParams()['token'];
if($this->authManager->CheckToken($token)) {
$this->authManager->SetAuthState(true);
return $response->withHeader('Location', '/')->withStatus(302);
}
return $response->withHeader('Location', '/')->withStatus(302);
}
public function Logout(Request $request, Response $response, $args) : Response
{
$this->authManager->SetAuthState(false);
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}

View file

@ -0,0 +1,230 @@
<?php
namespace AIO\Data;
use AIO\Auth\PasswordGenerator;
use AIO\Controller\DockerController;
class ConfigurationManager
{
public function GetConfig() : array
{
if(file_exists(DataConst::GetConfigFile()))
{
$configContent = file_get_contents(DataConst::GetConfigFile());
return json_decode($configContent, true);
}
return [];
}
public function GetUserName() : string {
return $this->GetConfig()['username'];
}
public function GetPassword() : string {
return $this->GetConfig()['password'];
}
public function GetToken() : string {
return $this->GetConfig()['AIO_TOKEN'];
}
public function GetIsContainerUpateAvailable() : bool {
return isset($this->GetConfig()['isContainerUpateAvailable']) ? $this->GetConfig()['isContainerUpateAvailable'] : false;
}
public function SetIsContainerUpateAvailable(bool $value) : void {
$config = $this->GetConfig();
$config['isContainerUpateAvailable'] = $value;
$this->WriteConfig($config);
}
public function SetPassword(string $password) : void {
$config = $this->GetConfig();
$config['username'] = 'admin';
$config['password'] = $password;
$this->WriteConfig($config);
}
public function GetSecret(string $secretId) : string {
$config = $this->GetConfig();
if(!isset($config['secrets'][$secretId])) {
$config['secrets'][$secretId] = bin2hex(random_bytes(24));
$this->WriteConfig($config);
}
if ($secretId === 'BORGBACKUP_PASSWORD' && !file_exists(DataConst::GetBackupSecretFile())) {
$this->DoubleSafeBackupSecret($config['secrets'][$secretId]);
}
return $config['secrets'][$secretId];
}
private function DoubleSafeBackupSecret(string $borgBackupPassword) {
file_put_contents(DataConst::GetBackupSecretFile(), $borgBackupPassword);
}
public function hasBackupRunOnce() : bool {
if (!file_exists(DataConst::GetBackupKeyFile())) {
return false;
} else {
return true;
}
}
public function GetLastBackupTime() : string {
if (!file_exists(DataConst::GetBackupArchivesList())) {
return '';
}
$content = file_get_contents(DataConst::GetBackupArchivesList());
if ($content === "") {
return '';
}
$lastBackupLines = explode("\n", $content);
$lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2];
if ($lastBackupLine === "") {
return '';
}
$lastBackupTimes = explode(",", $lastBackupLine);
$lastBackupTime = $lastBackupTimes[1];
if ($lastBackupTime === "") {
return '';
}
return $lastBackupTime;
}
public function wasStartButtonClicked() : bool {
if (isset($this->GetConfig()['wasStartButtonClicked'])) {
return true;
} else {
return false;
}
}
/**
* @throws InvalidSettingConfigurationException
*/
public function SetDomain(string $domain) : void {
// Validate URL
if (!filter_var('http://' . $domain, FILTER_VALIDATE_URL)) {
throw new InvalidSettingConfigurationException("Domain is not in a valid format!");
}
$dnsRecordIP = gethostbyname($domain);
// Validate IP
if(!filter_var($dnsRecordIP, FILTER_VALIDATE_IP)) {
throw new InvalidSettingConfigurationException("DNS config is not set or domain is not in a valid format!");
}
$connection = @fsockopen($domain, 443, $errno, $errstr, 0.1);
if ($connection) {
fclose($connection);
} else {
throw new InvalidSettingConfigurationException("The server is not reachable on Port 443.");
}
// Get Instance ID
$instanceID = $this->GetSecret('INSTANCE_ID');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,'http://' . $domain . ':443');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
# Get rid of trailing \n
$response = str_replace("\n", "", $response);
if($response !== $instanceID) {
throw new InvalidSettingConfigurationException("Domain does not point to this server.");
}
// Write domain
$config = $this->GetConfig();
$config['domain'] = $domain;
$this->WriteConfig($config);
}
public function GetDomain() : string {
$config = $this->GetConfig();
if(!isset($config['domain'])) {
$config['domain'] = '';
}
return $config['domain'];
}
public function GetBackupMode() : string {
$config = $this->GetConfig();
if(!isset($config['backup-mode'])) {
$config['backup-mode'] = '';
}
return $config['backup-mode'];
}
public function GetAIOURL() : string {
$config = $this->GetConfig();
if(!isset($config['AIO_URL'])) {
$config['AIO_URL'] = '';
}
return $config['AIO_URL'];
}
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgBackupHostLocation(string $location) : void {
$allowedPrefixes = [
'/mnt/',
'/media/',
];
$isValidPath = false;
foreach($allowedPrefixes as $allowedPrefix) {
if(str_starts_with($location, $allowedPrefix)) {
$isValidPath = true;
break;
}
}
if(!$isValidPath) {
throw new InvalidSettingConfigurationException("Path must start with /mnt/ or /media/.");
}
$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$this->WriteConfig($config);
}
public function WriteConfig(array $config) : void {
if(!is_dir(DataConst::GetDataDirectory())) {
mkdir(DataConst::GetDataDirectory());
}
file_put_contents(DataConst::GetConfigFile(), json_encode($config));
}
public function GetBorgBackupHostLocation() : string {
$config = $this->GetConfig();
if(!isset($config['borg_backup_host_location'])) {
$config['borg_backup_host_location'] = '';
}
return $config['borg_backup_host_location'];
}
public function GetBorgBackupMode() : string {
$config = $this->GetConfig();
if(!isset($config['backup-mode'])) {
$config['backup-mode'] = '';
}
return $config['backup-mode'];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace AIO\Data;
class DataConst {
public static function GetDataDirectory() : string {
if(is_dir('/mnt/docker-aio-config/data/')) {
return '/mnt/docker-aio-config/data/';
}
return realpath(__DIR__ . '/../../data/');
}
public static function GetSessionDirectory() : string {
if(is_dir('/mnt/docker-aio-config/session/')) {
return '/mnt/docker-aio-config/session/';
}
return realpath(__DIR__ . '/../../session/');
}
public static function GetConfigFile() : string {
return self::GetDataDirectory() . '/configuration.json';
}
public static function GetBackupSecretFile() : string {
return self::GetDataDirectory() . '/backupsecret';
}
public static function GetBackupKeyFile() : string {
return self::GetDataDirectory() . '/borg.config';
}
public static function GetBackupArchivesList() : string {
return self::GetDataDirectory() . '/backup_archives.list';
}
}

View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace AIO\Data;
class InvalidSettingConfigurationException extends \Exception {}

32
php/src/Data/Setup.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace AIO\Data;
use AIO\Auth\PasswordGenerator;
class Setup
{
private PasswordGenerator $passwordGenerator;
private ConfigurationManager $configurationManager;
public function __construct(
PasswordGenerator $passwordGenerator,
ConfigurationManager $configurationManager) {
$this->passwordGenerator = $passwordGenerator;
$this->configurationManager = $configurationManager;
}
public function Setup() : string {
if(!$this->CanBeInstalled()) {
return '';
}
$password = $this->passwordGenerator->GeneratePassword(6);
$this->configurationManager->SetPassword($password);
return $password;
}
public function CanBeInstalled() : bool {
return !file_exists(DataConst::GetConfigFile());
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace AIO;
use AIO\Docker\DockerHubManager;
use DI\Container;
class DependencyInjection
{
public static function GetContainer() : Container {
$container = new Container();
$container->set(
DockerHubManager::class,
new DockerHubManager()
);
$container->set(
\AIO\Data\ConfigurationManager::class,
new \AIO\Data\ConfigurationManager()
);
$container->set(
\AIO\Docker\DockerActionManager::class,
new \AIO\Docker\DockerActionManager(
$container->get(\AIO\Data\ConfigurationManager::class),
$container->get(\AIO\ContainerDefinitionFetcher::class),
$container->get(DockerHubManager::class)
)
);
$container->set(
\AIO\Auth\PasswordGenerator::class,
new \AIO\Auth\PasswordGenerator()
);
$container->set(
\AIO\Auth\AuthManager::class,
new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class))
);
$container->set(
\AIO\Data\Setup::class,
new \AIO\Data\Setup(
$container->get(\AIO\Auth\PasswordGenerator::class),
$container->get(\AIO\Data\ConfigurationManager::class)
)
);
return $container;
}
}

View file

@ -0,0 +1,468 @@
<?php
namespace AIO\Docker;
use AIO\Container\Container;
use AIO\Container\State\IContainerState;
use AIO\Container\State\ImageDoesNotExistState;
use AIO\Container\State\StartingState;
use AIO\Container\State\RunningState;
use AIO\Container\State\VersionDifferentState;
use AIO\Container\State\StoppedState;
use AIO\Container\State\VersionEqualState;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use AIO\ContainerDefinitionFetcher;
use http\Env\Response;
class DockerActionManager
{
private const API_VERSION = 'v1.41';
private \GuzzleHttp\Client $guzzleClient;
private ConfigurationManager $configurationManager;
private ContainerDefinitionFetcher $containerDefinitionFetcher;
private DockerHubManager $dockerHubManager;
public function __construct(
ConfigurationManager $configurationManager,
ContainerDefinitionFetcher $containerDefinitionFetcher,
DockerHubManager $dockerHubManager
) {
$this->configurationManager = $configurationManager;
$this->containerDefinitionFetcher = $containerDefinitionFetcher;
$this->dockerHubManager = $dockerHubManager;
$this->guzzleClient = new \GuzzleHttp\Client(
[
'curl' => [
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
],
]
);
}
private function BuildApiUrl(string $url) : string {
return sprintf('http://localhost/%s/%s', self::API_VERSION, $url);
}
private function BuildImageName(Container $container) : string {
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
return $container->GetContainerName() . ':develop';
} else {
return $container->GetContainerName() . ':latest';
}
}
public function GetContainerRunningState(Container $container) : IContainerState
{
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier())));
try {
$response = $this->guzzleClient->get($url);
} catch (ClientException $ex) {
if($ex->getCode() === 404) {
return new ImageDoesNotExistState();
}
throw $ex;
}
$responseBody = json_decode((string)$response->getBody(), true);
if ($responseBody['State']['Running'] === true) {
return new RunningState();
} else {
return new StoppedState();
}
}
public function GetContainerUpdateState(Container $container) : IContainerState
{
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
$tag = 'develop';
} else {
$tag = 'latest';
}
$runningDigest = $this->GetRepoDigestOfContainer($container->GetIdentifier());
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($runningDigest === $remoteDigest) {
return new VersionEqualState();
} else {
return new VersionDifferentState();
}
}
public function GetContainerStartingState(Container $container) : IContainerState
{
$runningState = $this->GetContainerRunningState($container);
if ($runningState instanceof StoppedState) {
return new StoppedState();
} elseif ($runningState instanceof ImageDoesNotExistState) {
return new ImageDoesNotExistState();
}
$containerName = $container->GetIdentifier();
if ($container->GetInternalPorts() !== null) {
foreach($container->GetInternalPorts()->GetInternalPorts() as $internalPort) {
$connection = @fsockopen($containerName, $internalPort, $errno, $errstr, 0.1);
if ($connection) {
fclose($connection);
return new RunningState();
} else {
return new StartingState();
}
}
} else {
return new RunningState();
}
}
public function DeleteContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s', urlencode($container->GetIdentifier())));
try {
$this->guzzleClient->delete($url);
} catch (\Exception $e) {}
}
public function GetLogs(Container $container) : string
{
$url = $this->BuildApiUrl(
sprintf(
'containers/%s/logs?stdout=true&stderr=true',
urlencode($container->GetIdentifier())
));
$responseBody = (string)$this->guzzleClient->get($url)->getBody();
$response = "";
$separator = "\r\n";
$line = strtok($responseBody, $separator);
$response = substr($line, 8) . "\n";
while ($line !== false) {
$line = strtok($separator);
$response .= substr($line, 8) . "\n";
}
return $response;
}
public function StartContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier())));
$this->guzzleClient->post($url);
}
public function CreateVolumes(Container $container)
{
$url = $this->BuildApiUrl('volumes/create');
foreach($container->GetVolumes()->GetVolumes() as $volume) {
$forbiddenChars = [
'/',
];
$firstChar = substr($volume->name, 0, 1);
if(!in_array($firstChar, $forbiddenChars)) {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'name' => $volume->name,
],
]
);
}
}
}
public function CreateContainer(Container $container) {
$volumes = [];
foreach($container->GetVolumes()->GetVolumes() as $volume) {
$volumeEntry = $volume->name . ':' . $volume->mountPoint;
if($volume->isWritable) {
$volumeEntry = $volumeEntry . ':' . 'rw';
} else {
$volumeEntry = $volumeEntry . ':' . 'ro';
}
$volumes[] = $volumeEntry;
}
$exposedPorts = [];
foreach($container->GetPorts()->GetPorts() as $port) {
$exposedPorts[$port] = null;
}
$requestBody = [
'Image' => $this->BuildImageName($container),
];
if(count($volumes) > 0) {
$requestBody['HostConfig']['Binds'] = $volumes;
}
$envs = $container->GetEnvironmentVariables()->GetVariables();
foreach($envs as $key => $env) {
$patterns = ['/%(.*)%/'];
if(preg_match($patterns[0], $env, $out) === 1) {
$replacements = array();
if($out[1] === 'NC_DOMAIN') {
$replacements[1] = $this->configurationManager->GetDomain();
} elseif ($out[1] === 'AIO_TOKEN') {
$replacements[1] = $this->configurationManager->GetToken();
} elseif ($out[1] === 'BORGBACKUP_MODE') {
$replacements[1] = $this->configurationManager->GetBackupMode();
} elseif ($out[1] === 'AIO_URL') {
$replacements[1] = $this->configurationManager->GetAIOURL();
} else {
$replacements[1] = $this->configurationManager->GetSecret($out[1]);
}
$envs[$key] = preg_replace($patterns, $replacements, $env);
}
}
if(count($envs) > 0) {
$requestBody['Env'] = $envs;
}
$requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy();
if(count($exposedPorts) > 0) {
$requestBody['ExposedPorts'] = $exposedPorts;
foreach($container->GetPorts()->GetPorts() as $port) {
$portNumber = explode("/", $port);
$requestBody['HostConfig']['PortBindings'][$port] = [
[
'HostPort' => $portNumber[0],
]
];
}
}
// Special things for the backup container which should not be exposed in the containers.json
if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') {
$requestBody['HostConfig']['CapAdd'] = ["SYS_ADMIN"];
$requestBody['HostConfig']['Devices'] = [["PathOnHost" => "/dev/fuse", "PathInContainer" => "/dev/fuse", "CgroupPermissions" => "rwm"]];
$requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined"];
}
$url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier());
$this->guzzleClient->request(
'POST',
$url,
[
'json' => $requestBody
]
);
}
public function PullContainer(Container $container)
{
$url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', urlencode($this->BuildImageName($container))));
try {
$this->guzzleClient->post($url);
} catch (ClientException $ex) {
throw $ex;
}
}
private function isContainerUpdateAvailable(string $id) : string
{
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$updateAvailable = "";
if ($container->GetUpdateState() instanceof VersionDifferentState) {
$updateAvailable = '1';
}
foreach ($container->GetDependsOn() as $dependency) {
$updateAvailable .= $this->isContainerUpdateAvailable($dependency);
}
return $updateAvailable;
}
public function isAnyUpdateAvailable() {
$id = 'nextcloud-aio-apache';
if ($this->configurationManager->GetIsContainerUpateAvailable()) {
return true;
}
if ($this->isContainerUpdateAvailable($id) !== "") {
$this->configurationManager->SetIsContainerUpateAvailable(true);
return true;
} else {
return false;
}
}
private function GetRepoDigestOfContainer(string $containerName) : ?string {
try {
$containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
$containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true);
$imageName = $containerOutput['Image'];
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true);
if(isset($imageOutput['RepoDigests']) && count($imageOutput['RepoDigests']) === 1) {
$fullDigest = $imageOutput['RepoDigests'][0];
return substr($fullDigest, strpos($fullDigest, "@") + 1);
}
return null;
} catch (\Exception $e) {
return null;
}
}
public function GetCurrentChannel() : string {
$cacheKey = 'aio-ChannelName';
$channelName = apcu_fetch($cacheKey);
if($channelName !== false && is_string($channelName)) {
return $channelName;
}
$containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try {
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true);
$containerChecksum = $output['Image'];
$tagArray = explode(':', $output['Config']['Image']);
$tag = $tagArray[1];
apcu_add($cacheKey, $tag);
return $tag;
} catch (\Exception $ex) {
}
return 'latest';
}
public function IsMastercontainerUpdateAvailable() : bool
{
$imageName = 'nextcloud/all-in-one';
$containerName = 'nextcloud-aio-mastercontainer';
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
$tag = 'develop';
} else {
$tag = 'latest';
}
$runningDigest = $this->GetRepoDigestOfContainer($containerName);
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag);
if ($remoteDigest === $runningDigest) {
return false;
} else {
return true;
}
}
public function DisconnectContainerFromNetwork(Container $container)
{
$url = $this->BuildApiUrl(
sprintf('networks/%s/disconnect', 'nextcloud-aio')
);
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'container' => $container->GetIdentifier(),
],
]
);
} catch (ServerException $e) {}
}
private function ConnectContainerIdToNetwork(string $id)
{
$url = $this->BuildApiUrl('networks/create');
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'name' => 'nextcloud-aio',
'checkDuplicate' => true,
'internal' => true,
]
]
);
} catch (ClientException $e) {
if($e->getCode() !== 409) {
throw $e;
}
}
$url = $this->BuildApiUrl(
sprintf('networks/%s/connect', 'nextcloud-aio')
);
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'container' => $id,
]
]
);
} catch (ClientException $e) {
if($e->getCode() !== 403) {
throw $e;
}
}
}
public function ConnectMasterContainerToNetwork()
{
$this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer');
}
public function ConnectContainerToNetwork(Container $container)
{
$this->ConnectContainerIdToNetwork($container->GetIdentifier());
}
public function StopContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime()));
$this->guzzleClient->post($url);
}
public function GetBackupcontainerExitCode() : int
{
$containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
$response = $this->guzzleClient->get($url);
} catch (ClientException $ex) {
if ($ex->getCode() === 404) {
return -1;
}
throw $ex;
}
$responseBody = json_decode((string)$response->getBody(), true);
$exitCode = $responseBody['State']['ExitCode'];
if (is_int($exitCode)) {
return $exitCode;
} else {
return -1;
}
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace AIO\Docker;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Client;
class DockerHubManager
{
private Client $guzzleClient;
public function __construct()
{
$this->guzzleClient = new Client();
}
public function GetLatestDigestOfTag(string $name, string $tag) : ?string {
$cacheKey = 'dockerhub-manifest-' . $name . $tag;
$cachedVersion = apcu_fetch($cacheKey);
if($cachedVersion !== false && is_string($cachedVersion)) {
return $cachedVersion;
}
try {
$authTokenRequest = $this->guzzleClient->request(
'GET',
'https://auth.docker.io/token?service=registry.docker.io&scope=repository:' . $name . ':pull'
);
$body = $authTokenRequest->getBody()->getContents();
$decodedBody = json_decode($body, true);
if(isset($decodedBody['token'])) {
$authToken = $decodedBody['token'];
$manifestRequest = $this->guzzleClient->request(
'GET',
'https://registry-1.docker.io/v2/'.$name.'/manifests/' . $tag,
[
'headers' => [
'Accept' => 'application/vnd.docker.distribution.manifest.v2+json',
'Authorization' => 'Bearer ' . $authToken,
],
]
);
$responseHeaders = $manifestRequest->getHeader('docker-content-digest');
if(count($responseHeaders) === 1) {
$latestVersion = $responseHeaders[0];
apcu_add($cacheKey, $latestVersion, 600);
return $latestVersion;
}
}
return null;
} catch (\Exception $e) {
return null;
}
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace AIO\Middleware;
use AIO\Auth\AuthManager;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AuthMiddleware
{
private AuthManager $authManager;
public function __construct(AuthManager $authManager) {
$this->authManager = $authManager;
}
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$publicRoutes = [
'/api/auth/login',
'/api/auth/getlogin',
'/login',
'/setup',
'/',
];
if(!in_array($request->getUri()->getPath(), $publicRoutes)) {
if(!$this->authManager->IsAuthenticated()) {
$response = new Response();
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}
$response = $handler->handle($request);
return $response;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace AIO\Twig;
use Slim\Views\TwigExtension;
use Twig\TwigFunction;
class ClassExtension extends TwigExtension
{
public function getFunctions() : array
{
return array(
new TwigFunction('class', array($this, 'getClassName')),
);
}
public function getClassName($object) : ?string
{
if (!is_object($object)) {
return null;
}
return get_class($object);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace AIO\Twig;
use Slim\Csrf\Guard;
class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
/**
* @var Guard
*/
protected Guard $csrf;
public function __construct(Guard $csrf)
{
$this->csrf = $csrf;
}
public function getGlobals() : array
{
// CSRF token name and value
$csrfNameKey = $this->csrf->getTokenNameKey();
$csrfValueKey = $this->csrf->getTokenValueKey();
$csrfName = $this->csrf->getTokenName();
$csrfValue = $this->csrf->getTokenValue();
return [
'csrf' => [
'keys' => [
'name' => $csrfNameKey,
'value' => $csrfValueKey
],
'name' => $csrfName,
'value' => $csrfValue
]
];
}
}

View file

@ -0,0 +1,5 @@
{% extends "layout.twig" %}
{% block body %}
Already installed.
{% endblock %}

View file

@ -0,0 +1,245 @@
{% extends "layout.twig" %}
{% block body %}
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<header>
<div class="">
<div class="logo"></div>
</div>
<form method="POST" action="/api/auth/logout">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Log out" />
</form>
</header>
<div class="content">
<h1>Nextcloud AIO Beta</h1>
This is beta software and not production ready.<br><br>
{% set isAnyRunning = false %}
{% set isWatchtowerRunning = false %}
{% set isBackupContainerRunning = false %}
{% set isRestoreRunning = false %}
{% set isBackupOrRestoreRunning = false %}
{% set isApacheStarting = false %}
{% for container in containers %}
{% if class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' and container.GetIdentifier() != 'nextcloud-aio-domaincheck' and container.GetIdentifier() != 'nextcloud-aio-borgbackup' and container.GetIdentifier() != 'nextcloud-aio-watchtower' %}
{% set isAnyRunning = true %}
{% endif %}
{% if container.GetIdentifier() == 'nextcloud-aio-watchtower' and class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %}
{% set isWatchtowerRunning = true %}
{% endif %}
{% if container.GetIdentifier() == 'nextcloud-aio-apache' and class(container.GetStartingState()) == 'AIO\\Container\\State\\StartingState' %}
{% set isApacheStarting = true %}
{% endif %}
{% if container.GetIdentifier() == 'nextcloud-aio-borgbackup' and class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %}
{% set isBackupContainerRunning = true %}
{% if borg_backup_mode == 'restore' %}
{% set isRestoreRunning = true %}
{% endif %}
{% if borg_backup_mode == 'backup' or borg_backup_mode == 'restore' %}
{% set isBackupOrRestoreRunning = true %}
{% endif %}
{% endif %}
{% endfor %}
{% if isWatchtowerRunning == true %}
Mastercontainer updpate currently running. It will restart the mastercontainer soon which will make it unavailable for a moment. Please wait until thats done.<br /><br />
<a href="" class="button reload">Reload ↻</a><br/>
{% else %}
{% if isBackupOrRestoreRunning == false and domain == "" %}
Please type in the domain that will be used for Nextcloud:<br><br />
<form method="POST" action="/api/configuration" class="xhr">
<input type="text" name="domain" value="{{ domain }}" placeholder="nextcloud.yourdomain.com"/>
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Submit" />
</form>
Make sure that this server is reachable on Port 443 and you've correctly set up the DNS config for the domain that you enter. <br><br>
If you have a dynamic IP-adress, you can use e.g. <a href="https://ddclient.net/">DDclient</a> with a compatible domain provider for DNS updates.
{% endif %}
{% if domain != "" %}
{% if isAnyRunning == true %}
{% if isApacheStarting != true %}
Initial Nextcloud username: admin <br />
Initial Nextcloud password: {{ nextcloud_password }} <br /><br/>
<a href="https://{{ domain }}" class="button" target="_blank" rel="noopener">Open your Nextcloud ↗</a><br/>
{% else %}
Containers are currently starting.<br /><br />
<a href="" class="button reload">Reload ↻</a><br/>
{% endif %}
{% endif %}
{% if was_start_button_clicked == true %}
<h2>Containers</h2>
<ul>
{# @var containers \AIO\Container\Container[] #}
{% for container in containers %}
{% if container.GetIdentifier() != 'nextcloud-aio-borgbackup' and container.GetIdentifier() != 'nextcloud-aio-watchtower' and container.GetIdentifier() != 'nextcloud-aio-domaincheck' %}
<li>
{% if class(container.GetStartingState()) == 'AIO\\Container\\State\\StartingState' %}
<span class="status running"></span>
<span>{{container.GetDisplayName()}} (<a href="/api/docker/logs?id={{ container.GetIdentifier() }}">Starting</a>)</span>
{% elseif class(container.GetRunningState()) == 'AIO\\Container\\State\\RunningState' %}
<span class="status success"></span>
<span>{{container.GetDisplayName()}} (<a href="/api/docker/logs?id={{ container.GetIdentifier() }}">Running</a>)</span>
{% else %}
<span class="status error"></span>
<span>{{container.GetDisplayName()}} (<a href="/api/docker/logs?id={{ container.GetIdentifier() }}">Stopped</a>)</span>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
{% if has_update_available == true %}
Updates are available. Stop your containers and restart them to apply the update. You should consider creating a backup first.<br><br>
{% else %}
You are up-to-date.<br><br>
{% endif %}
{% endif %}
{% if isAnyRunning == true %}
{% if isApacheStarting != true %}
<form method="POST" action="/api/docker/stop" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Stop containers" />
</form>
{% endif %}
{% else %}
{% if isRestoreRunning == true %}
Restore currently running. Cannot start the containers until that's done.<br /><br />
{% elseif has_update_available == true and isBackupOrRestoreRunning == true %}
Restore or Backup currently running and container update available. Cannot start the containers until that's done.<br /><br />
{% else %}
{% if was_start_button_clicked == false %}
Clicking on the button below will download all docker containers and start them. This can take a lot of time depending on your internect connection. Since the overall size is a few GB, this will take around 5-10 min or more. So be aware and patient!<br><br>
{% endif %}
{% if was_start_button_clicked == false or has_update_available == false %}
<form method="POST" action="/api/docker/start" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Start containers" />
</form>
{% else %}
<form method="POST" action="/api/docker/start" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button " type="submit" value="Start and update containers" onclick="return confirm('Start and update containers? You should consider creating a backup first.')" />
</form>
{% endif %}
{% endif %}
{% endif %}
{% if is_mastercontainer_update_available == true %}
{% if isBackupOrRestoreRunning == false %}
<h2>Mastercontainer update</h2>
A new mastercontainer is available. Please click on the button below to update it.<br><br>
<form method="POST" action="/api/docker/watchtower" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Update mastercontainer" />
</form>
{% endif %}
{% endif %}
{% if was_start_button_clicked == true %}
{% if isBackupOrRestoreRunning == false and borg_backup_host_location == "" and isApacheStarting != true %}
<h2>Backup and restore</h2>
Please type in the directory where backups will get created on the host system:<br><br>
<form method="POST" action="/api/configuration" class="xhr">
<input type="text" name="borg_backup_host_location" value="/mnt/backup" placeholder="/mnt/backup"/>
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Submit" />
</form>
The folder path that you enter must start with /mnt/ or /media/ so e.g. /mnt/backup
{% endif %}
{% if borg_backup_host_location != "" %}
<h2>Backup and restore</h2>
{% if has_backup_run_once == true and isBackupContainerRunning == false %}
{% if backup_exit_code > 0 %}
<span class="status error"></span> Last {{ borg_backup_mode }} failed! (<a href="/api/docker/logs?id=nextcloud-aio-borgbackup">Logs</a>)<br /><br />
{% elseif backup_exit_code == 0 %}
{% if borg_backup_mode == "backup" %}
<span class="status success"></span> Last {{ borg_backup_mode }} succesful on {{ last_backup_time }}! (<a href="/api/docker/logs?id=nextcloud-aio-borgbackup">Logs</a>)<br /><br />
{% else %}
<span class="status success"></span> Last {{ borg_backup_mode }} succesful! (<a href="/api/docker/logs?id=nextcloud-aio-borgbackup">Logs</a>)<br /><br />
{% endif %}
{% endif %}
{% endif %}
{% if isBackupContainerRunning == false %}
This is your encryption password for backups: {{ borgbackup_password }} <br /><br/>
Please save it at a safe place since you won't be able to restore from backup if you loose this password! <br /><br/>
Backed up will get all important data of your Nextcloud AIO instance like the database, your files and configuration files of the mastercontainer and else. <br /><br/>
The backup itself will use a tool that is called <a href="https://github.com/borgbackup/borg#what-is-borgbackup">BorgBackup<a/> which is a well-known server backup tool that efficiently backs up your files and encrypts them on the fly. <br /><br/>
Backups get created in the following directory on the host: {{ borg_backup_host_location }}/borg <br /><br/>
{% if isApacheStarting != true %}
<form method="POST" action="/api/docker/backup" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Create backup" onclick="return confirm('Create backup? Are you sure that you want to create a backup? This will stop all running containers and create the backup.')" />
</form>
{% if has_backup_run_once == true %}
Click on the button below to perform a backup integrity check. This is an option that verifies that your backup is intact but it should't be needed in most situtations.<br><br/>
<form method="POST" action="/api/docker/backup-check" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Check backup integrity" onclick="return confirm('Check backup integrity? Are you sure that you want to check the backup? This can take a long time depending on the size of your backup.')" /><br/>
</form>
Click on the button below to restore the last backup from {{ last_backup_time }}. This will overwrite all your files with the state of the backup. It makes sense to run an integrity check before restoring your files but is not mandatory since it shouldn't be needed in most situations.<br><br>
<form method="POST" action="/api/docker/restore" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input class="button" type="submit" value="Restore last backup" onclick="return confirm('Restore last backup? Are you sure that you want to restore the last backup? This will stop all running containers and restore the last backup from {{ last_backup_time }}. You might want to check the backup integrity first.')" />
</form>
{% endif %}
{% endif %}
{% else %}
<span class="status running"></span> Backup container currently running. (<a href="/api/docker/logs?id=nextcloud-aio-borgbackup">Logs</a>)<br /><br />
<a href="" class="button reload">Reload ↻</a><br/>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% if isApacheStarting == true or isBackupContainerRunning == true or isWatchtowerRunning == true %}
<script>
if (document.hasFocus()) {
// hide reload button if the site reloads automatically
var list = document.getElementsByClassName("reload button");
for (var i = 0; i < list.length; i++) {
// list[i] is a node with the desired class name
list[i].style.display = 'none';
}
// set timeout for reload
setTimeout(function(){
window.location.reload(1);
}, 5000);
}
</script>
{% endif %}
</div>
<div id="overlay">
<div class="loader"></div>
</div>
{% endblock %}

12
php/templates/layout.twig Normal file
View file

@ -0,0 +1,12 @@
<html>
<head>
<title>AIO</title>
<link rel="stylesheet" href="/style.css" media="all" />
<link rel="icon" href="/img/favicon.png">
<script type="text/javascript" src="forms.js"></script>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

19
php/templates/login.twig Normal file
View file

@ -0,0 +1,19 @@
{% extends "layout.twig" %}
{% block body %}
<div class="login-wrapper">
<div class="login">
<img src="/img/logo-blue.svg" style="margin-left: auto;margin-right: auto;display: block;">
<h1>Nextcloud AIO Login</h1>
<p>Log in using your Nextcloud AIO credentials. If you don't have them you can also use the automatic login from your Nextcloud.</p>
<form method="POST" action="/api/auth/login">
<input type="text" name="username" placeholder="Username" />
<input type="text" name="password" placeholder="Password" />
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="submit" class="button" value="Login" />
</form>
</div>
</div>
{% endblock %}

15
php/templates/setup.twig Normal file
View file

@ -0,0 +1,15 @@
{% extends "layout.twig" %}
{% block body %}
<div class="login-wrapper">
<div class="login">
<img src="/img/logo-blue.svg" style="margin-left: auto;margin-right: auto;display: block;">
<h1>Your credentials for Nextcloud AIO Beta</h1>
<p>Please note down the details to access the management interface and don't loose them!</p>
<p>Nextcloud AIO stands for Nextcloud All In One and provides easy deployment and maintenance with most features included in this one Nextcloud instance.</p>
<strong>Username</strong><br/> <span class="monospace">admin</span> <br>
<strong>Password</strong><br/> <span class="monospace">{{ password }}</span><br>
<a href="/" class="button" target="_blank" rel="noopener">Open Nextcloud AIO login ↗</a>
</div>
</div>
{% endblock %}

7
php/vendor/autoload.php vendored Normal file
View file

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitd35e6be193b0f5ac4e01f20a44a2b9d6::getLoader();

479
php/vendor/composer/ClassLoader.php vendored Normal file
View file

@ -0,0 +1,479 @@
<?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
{
private $vendorDir;
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
private static $registeredLoaders = array();
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
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 array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
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 array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
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
*/
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
*/
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
*/
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
*/
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.
*/
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 bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
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;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View file

@ -0,0 +1,543 @@
<?php
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
class InstalledVersions
{
private static $installed = array (
'root' =>
array (
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'aliases' =>
array (
),
'reference' => '463633cabf721d3f0263945230c55a6d0199fa8f',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'aliases' =>
array (
),
'reference' => '463633cabf721d3f0263945230c55a6d0199fa8f',
),
'guzzlehttp/guzzle' =>
array (
'pretty_version' => '7.3.0',
'version' => '7.3.0.0',
'aliases' =>
array (
),
'reference' => '7008573787b430c1c1f650e3722d9bba59967628',
),
'guzzlehttp/promises' =>
array (
'pretty_version' => '1.4.1',
'version' => '1.4.1.0',
'aliases' =>
array (
),
'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d',
),
'guzzlehttp/psr7' =>
array (
'pretty_version' => '1.8.2',
'version' => '1.8.2.0',
'aliases' =>
array (
),
'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91',
),
'http-interop/http-factory-guzzle' =>
array (
'pretty_version' => '1.2.0',
'version' => '1.2.0.0',
'aliases' =>
array (
),
'reference' => '8f06e92b95405216b237521cc64c804dd44c4a81',
),
'nikic/fast-route' =>
array (
'pretty_version' => 'v1.3.0',
'version' => '1.3.0.0',
'aliases' =>
array (
),
'reference' => '181d480e08d9476e61381e04a71b34dc0432e812',
),
'opis/closure' =>
array (
'pretty_version' => '3.6.2',
'version' => '3.6.2.0',
'aliases' =>
array (
),
'reference' => '06e2ebd25f2869e54a306dda991f7db58066f7f6',
),
'php-di/invoker' =>
array (
'pretty_version' => '2.3.2',
'version' => '2.3.2.0',
'aliases' =>
array (
),
'reference' => '5214cbe5aad066022cd845dbf313f0e47aed928f',
),
'php-di/php-di' =>
array (
'pretty_version' => '6.3.4',
'version' => '6.3.4.0',
'aliases' =>
array (
),
'reference' => 'f53bcba06ab31b18e911b77c039377f4ccd1f7a5',
),
'php-di/phpdoc-reader' =>
array (
'pretty_version' => '2.2.1',
'version' => '2.2.1.0',
'aliases' =>
array (
),
'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c',
),
'php-di/slim-bridge' =>
array (
'pretty_version' => '3.1.1',
'version' => '3.1.1.0',
'aliases' =>
array (
),
'reference' => 'ad74ba03a3b97c717d58ac04b88671bafe4549c7',
),
'psr/container' =>
array (
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'aliases' =>
array (
),
'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf',
),
'psr/container-implementation' =>
array (
'provided' =>
array (
0 => '^1.0',
),
),
'psr/http-client' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
),
'psr/http-client-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-factory' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
),
'psr/http-factory-implementation' =>
array (
'provided' =>
array (
0 => '^1.0',
),
),
'psr/http-message' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
),
'psr/http-message-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-server-handler' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'aff2f80e33b7f026ec96bb42f63242dc50ffcae7',
),
'psr/http-server-middleware' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2296f45510945530b9dceb8bcedb5cb84d40c5f5',
),
'psr/log' =>
array (
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'aliases' =>
array (
),
'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
),
'ralouphie/getallheaders' =>
array (
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'aliases' =>
array (
),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
),
'slim/csrf' =>
array (
'pretty_version' => '1.2.1',
'version' => '1.2.1.0',
'aliases' =>
array (
),
'reference' => 'ee811a258ecee807846aefc51aabc1963ae0a400',
),
'slim/slim' =>
array (
'pretty_version' => '4.8.1',
'version' => '4.8.1.0',
'aliases' =>
array (
),
'reference' => 'c8934c35d9d98b1a1df9f99ee69b77a59e0aa820',
),
'slim/twig-view' =>
array (
'pretty_version' => '3.2.0',
'version' => '3.2.0.0',
'aliases' =>
array (
),
'reference' => '9ceaff0764ab8e70f9eeee825a9efd0b4e1dfc85',
),
'symfony/polyfill-ctype' =>
array (
'pretty_version' => 'v1.23.0',
'version' => '1.23.0.0',
'aliases' =>
array (
),
'reference' => '46cd95797e9df938fdd2b03693b5fca5e64b01ce',
),
'symfony/polyfill-mbstring' =>
array (
'pretty_version' => 'v1.23.1',
'version' => '1.23.1.0',
'aliases' =>
array (
),
'reference' => '9174a3d80210dca8daa7f31fec659150bbeabfc6',
),
'twig/twig' =>
array (
'pretty_version' => 'v3.3.2',
'version' => '3.3.2.0',
'aliases' =>
array (
),
'reference' => '21578f00e83d4a82ecfa3d50752b609f13de6790',
),
),
);
private static $canGetVendors;
private static $installedByVendor = array();
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)));
}
public static function isInstalled($packageName)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return true;
}
}
return false;
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
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');
}
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');
}
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');
}
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');
}
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
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);
return self::$installed;
}
public static function getAllRawData()
{
return self::getInstalled();
}
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
}
}
}
$installed[] = self::$installed;
return $installed;
}
}

21
php/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.

View file

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

18
php/vendor/composer/autoload_files.php vendored Normal file
View file

@ -0,0 +1,18 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
'538ca81a9a966a6716601ecf48f4eaef' => $vendorDir . '/opis/closure/functions.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
);

View file

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

31
php/vendor/composer/autoload_psr4.php vendored Normal file
View file

@ -0,0 +1,31 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Slim\\Views\\' => array($vendorDir . '/slim/twig-view/src'),
'Slim\\Csrf\\' => array($vendorDir . '/slim/csrf/src'),
'Slim\\' => array($vendorDir . '/slim/slim/Slim'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Server\\' => array($vendorDir . '/psr/http-server-handler/src', $vendorDir . '/psr/http-server-middleware/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\\Container\\' => array($vendorDir . '/psr/container/src'),
'PhpDocReader\\' => array($vendorDir . '/php-di/phpdoc-reader/src/PhpDocReader'),
'Opis\\Closure\\' => array($vendorDir . '/opis/closure/src'),
'Invoker\\' => array($vendorDir . '/php-di/invoker/src'),
'Http\\Factory\\Guzzle\\' => array($vendorDir . '/http-interop/http-factory-guzzle/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'FastRoute\\' => array($vendorDir . '/nikic/fast-route/src'),
'DI\\Bridge\\Slim\\' => array($vendorDir . '/php-di/slim-bridge/src'),
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
'AIO\\' => array($baseDir . '/src'),
);

75
php/vendor/composer/autoload_real.php vendored Normal file
View file

@ -0,0 +1,75 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitd35e6be193b0f5ac4e01f20a44a2b9d6
{
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('ComposerAutoloaderInitd35e6be193b0f5ac4e01f20a44a2b9d6', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInitd35e6be193b0f5ac4e01f20a44a2b9d6', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequired35e6be193b0f5ac4e01f20a44a2b9d6($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequired35e6be193b0f5ac4e01f20a44a2b9d6($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

182
php/vendor/composer/autoload_static.php vendored Normal file
View file

@ -0,0 +1,182 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6
{
public static $files = array (
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
'253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
'538ca81a9a966a6716601ecf48f4eaef' => __DIR__ . '/..' . '/opis/closure/functions.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'b33e3d135e5d9e47d845c576147bda89' => __DIR__ . '/..' . '/php-di/php-di/src/functions.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
);
public static $prefixLengthsPsr4 = array (
'T' =>
array (
'Twig\\' => 5,
),
'S' =>
array (
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
'Slim\\Views\\' => 11,
'Slim\\Csrf\\' => 10,
'Slim\\' => 5,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Server\\' => 16,
'Psr\\Http\\Message\\' => 17,
'Psr\\Http\\Client\\' => 16,
'Psr\\Container\\' => 14,
'PhpDocReader\\' => 13,
),
'O' =>
array (
'Opis\\Closure\\' => 13,
),
'I' =>
array (
'Invoker\\' => 8,
),
'H' =>
array (
'Http\\Factory\\Guzzle\\' => 20,
),
'G' =>
array (
'GuzzleHttp\\Psr7\\' => 16,
'GuzzleHttp\\Promise\\' => 19,
'GuzzleHttp\\' => 11,
),
'F' =>
array (
'FastRoute\\' => 10,
),
'D' =>
array (
'DI\\Bridge\\Slim\\' => 15,
'DI\\' => 3,
),
'A' =>
array (
'AIO\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'Twig\\' =>
array (
0 => __DIR__ . '/..' . '/twig/twig/src',
),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Slim\\Views\\' =>
array (
0 => __DIR__ . '/..' . '/slim/twig-view/src',
),
'Slim\\Csrf\\' =>
array (
0 => __DIR__ . '/..' . '/slim/csrf/src',
),
'Slim\\' =>
array (
0 => __DIR__ . '/..' . '/slim/slim/Slim',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Psr\\Http\\Server\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-server-handler/src',
1 => __DIR__ . '/..' . '/psr/http-server-middleware/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\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'PhpDocReader\\' =>
array (
0 => __DIR__ . '/..' . '/php-di/phpdoc-reader/src/PhpDocReader',
),
'Opis\\Closure\\' =>
array (
0 => __DIR__ . '/..' . '/opis/closure/src',
),
'Invoker\\' =>
array (
0 => __DIR__ . '/..' . '/php-di/invoker/src',
),
'Http\\Factory\\Guzzle\\' =>
array (
0 => __DIR__ . '/..' . '/http-interop/http-factory-guzzle/src',
),
'GuzzleHttp\\Psr7\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
),
'GuzzleHttp\\Promise\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
),
'GuzzleHttp\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
),
'FastRoute\\' =>
array (
0 => __DIR__ . '/..' . '/nikic/fast-route/src',
),
'DI\\Bridge\\Slim\\' =>
array (
0 => __DIR__ . '/..' . '/php-di/slim-bridge/src',
),
'DI\\' =>
array (
0 => __DIR__ . '/..' . '/php-di/php-di/src',
),
'AIO\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitd35e6be193b0f5ac4e01f20a44a2b9d6::$classMap;
}, null, ClassLoader::class);
}
}

1585
php/vendor/composer/installed.json vendored Normal file

File diff suppressed because it is too large Load diff

268
php/vendor/composer/installed.php vendored Normal file
View file

@ -0,0 +1,268 @@
<?php return array (
'root' =>
array (
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'aliases' =>
array (
),
'reference' => '463633cabf721d3f0263945230c55a6d0199fa8f',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'aliases' =>
array (
),
'reference' => '463633cabf721d3f0263945230c55a6d0199fa8f',
),
'guzzlehttp/guzzle' =>
array (
'pretty_version' => '7.3.0',
'version' => '7.3.0.0',
'aliases' =>
array (
),
'reference' => '7008573787b430c1c1f650e3722d9bba59967628',
),
'guzzlehttp/promises' =>
array (
'pretty_version' => '1.4.1',
'version' => '1.4.1.0',
'aliases' =>
array (
),
'reference' => '8e7d04f1f6450fef59366c399cfad4b9383aa30d',
),
'guzzlehttp/psr7' =>
array (
'pretty_version' => '1.8.2',
'version' => '1.8.2.0',
'aliases' =>
array (
),
'reference' => 'dc960a912984efb74d0a90222870c72c87f10c91',
),
'http-interop/http-factory-guzzle' =>
array (
'pretty_version' => '1.2.0',
'version' => '1.2.0.0',
'aliases' =>
array (
),
'reference' => '8f06e92b95405216b237521cc64c804dd44c4a81',
),
'nikic/fast-route' =>
array (
'pretty_version' => 'v1.3.0',
'version' => '1.3.0.0',
'aliases' =>
array (
),
'reference' => '181d480e08d9476e61381e04a71b34dc0432e812',
),
'opis/closure' =>
array (
'pretty_version' => '3.6.2',
'version' => '3.6.2.0',
'aliases' =>
array (
),
'reference' => '06e2ebd25f2869e54a306dda991f7db58066f7f6',
),
'php-di/invoker' =>
array (
'pretty_version' => '2.3.2',
'version' => '2.3.2.0',
'aliases' =>
array (
),
'reference' => '5214cbe5aad066022cd845dbf313f0e47aed928f',
),
'php-di/php-di' =>
array (
'pretty_version' => '6.3.4',
'version' => '6.3.4.0',
'aliases' =>
array (
),
'reference' => 'f53bcba06ab31b18e911b77c039377f4ccd1f7a5',
),
'php-di/phpdoc-reader' =>
array (
'pretty_version' => '2.2.1',
'version' => '2.2.1.0',
'aliases' =>
array (
),
'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c',
),
'php-di/slim-bridge' =>
array (
'pretty_version' => '3.1.1',
'version' => '3.1.1.0',
'aliases' =>
array (
),
'reference' => 'ad74ba03a3b97c717d58ac04b88671bafe4549c7',
),
'psr/container' =>
array (
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'aliases' =>
array (
),
'reference' => '8622567409010282b7aeebe4bb841fe98b58dcaf',
),
'psr/container-implementation' =>
array (
'provided' =>
array (
0 => '^1.0',
),
),
'psr/http-client' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
),
'psr/http-client-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-factory' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
),
'psr/http-factory-implementation' =>
array (
'provided' =>
array (
0 => '^1.0',
),
),
'psr/http-message' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
),
'psr/http-message-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/http-server-handler' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => 'aff2f80e33b7f026ec96bb42f63242dc50ffcae7',
),
'psr/http-server-middleware' =>
array (
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'aliases' =>
array (
),
'reference' => '2296f45510945530b9dceb8bcedb5cb84d40c5f5',
),
'psr/log' =>
array (
'pretty_version' => '1.1.4',
'version' => '1.1.4.0',
'aliases' =>
array (
),
'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
),
'ralouphie/getallheaders' =>
array (
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'aliases' =>
array (
),
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
),
'slim/csrf' =>
array (
'pretty_version' => '1.2.1',
'version' => '1.2.1.0',
'aliases' =>
array (
),
'reference' => 'ee811a258ecee807846aefc51aabc1963ae0a400',
),
'slim/slim' =>
array (
'pretty_version' => '4.8.1',
'version' => '4.8.1.0',
'aliases' =>
array (
),
'reference' => 'c8934c35d9d98b1a1df9f99ee69b77a59e0aa820',
),
'slim/twig-view' =>
array (
'pretty_version' => '3.2.0',
'version' => '3.2.0.0',
'aliases' =>
array (
),
'reference' => '9ceaff0764ab8e70f9eeee825a9efd0b4e1dfc85',
),
'symfony/polyfill-ctype' =>
array (
'pretty_version' => 'v1.23.0',
'version' => '1.23.0.0',
'aliases' =>
array (
),
'reference' => '46cd95797e9df938fdd2b03693b5fca5e64b01ce',
),
'symfony/polyfill-mbstring' =>
array (
'pretty_version' => 'v1.23.1',
'version' => '1.23.1.0',
'aliases' =>
array (
),
'reference' => '9174a3d80210dca8daa7f31fec659150bbeabfc6',
),
'twig/twig' =>
array (
'pretty_version' => 'v3.3.2',
'version' => '3.3.2.0',
'aliases' =>
array (
),
'reference' => '21578f00e83d4a82ecfa3d50752b609f13de6790',
),
),
);

26
php/vendor/composer/platform_check.php vendored Normal file
View file

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

1462
php/vendor/guzzlehttp/guzzle/CHANGELOG.md vendored Normal file

File diff suppressed because it is too large Load diff

19
php/vendor/guzzlehttp/guzzle/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2011 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
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.

79
php/vendor/guzzlehttp/guzzle/README.md vendored Normal file
View file

@ -0,0 +1,79 @@
![Guzzle](.github/logo.png?raw=true)
# Guzzle, PHP HTTP client
[![Latest Version](https://img.shields.io/github/release/guzzle/guzzle.svg?style=flat-square)](https://github.com/guzzle/guzzle/releases)
[![Build Status](https://img.shields.io/github/workflow/status/guzzle/guzzle/CI?label=ci%20build&style=flat-square)](https://github.com/guzzle/guzzle/actions?query=workflow%3ACI)
[![Total Downloads](https://img.shields.io/packagist/dt/guzzlehttp/guzzle.svg?style=flat-square)](https://packagist.org/packages/guzzlehttp/guzzle)
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and
trivial to integrate with web services.
- Simple interface for building query strings, POST requests, streaming large
uploads, streaming large downloads, using HTTP cookies, uploading JSON data,
etc...
- Can send both synchronous and asynchronous requests using the same interface.
- Uses PSR-7 interfaces for requests, responses, and streams. This allows you
to utilize other PSR-7 compatible libraries with Guzzle.
- Supports PSR-18 allowing interoperability between other PSR-18 HTTP Clients.
- Abstracts away the underlying HTTP transport, allowing you to write
environment and transport agnostic code; i.e., no hard dependency on cURL,
PHP streams, sockets, or non-blocking event loops.
- Middleware system allows you to augment and compose client behavior.
```php
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle');
echo $response->getStatusCode(); // 200
echo $response->getHeaderLine('content-type'); // 'application/json; charset=utf8'
echo $response->getBody(); // '{"id": 1420053, "name": "guzzle", ...}'
// Send an asynchronous request.
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org');
$promise = $client->sendAsync($request)->then(function ($response) {
echo 'I completed! ' . $response->getBody();
});
$promise->wait();
```
## Help and docs
We use GitHub issues only to discuss bugs and new features. For support please refer to:
- [Documentation](http://guzzlephp.org/)
- [Stack Overflow](http://stackoverflow.com/questions/tagged/guzzle)
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](http://slack.httplug.io/)
- [Gitter](https://gitter.im/guzzle/guzzle)
## Installing Guzzle
The recommended way to install Guzzle is through
[Composer](https://getcomposer.org/).
```bash
composer require guzzlehttp/guzzle
```
## Version Guidance
| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version |
|---------|------------|---------------------|--------------|---------------------|---------------------|-------|-------------|
| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >= 5.3.3 |
| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >= 5.4 |
| 5.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >= 5.4 |
| 6.x | Security fixes | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >= 5.5 |
| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >= 7.2 |
[guzzle-3-repo]: https://github.com/guzzle/guzzle3
[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
[guzzle-7-repo]: https://github.com/guzzle/guzzle
[guzzle-3-docs]: http://guzzle3.readthedocs.org
[guzzle-5-docs]: http://docs.guzzlephp.org/en/5.3/
[guzzle-6-docs]: http://docs.guzzlephp.org/en/6.5/
[guzzle-7-docs]: http://docs.guzzlephp.org/en/latest/

1253
php/vendor/guzzlehttp/guzzle/UPGRADING.md vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
{
"name": "guzzlehttp/guzzle",
"type": "library",
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"framework",
"http",
"rest",
"web service",
"curl",
"client",
"HTTP client",
"PSR-7",
"PSR-18"
],
"homepage": "http://guzzlephp.org/",
"license": "MIT",
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"ext-json": "*",
"guzzlehttp/promises": "^1.4",
"guzzlehttp/psr7": "^1.7 || ^2.0",
"psr/http-client": "^1.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"ext-curl": "*",
"bamarni/composer-bin-plugin": "^1.4.1",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"psr/log": "^1.1"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "7.3-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"autoload-dev": {
"psr-4": {
"GuzzleHttp\\Tests\\": "tests/"
}
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace GuzzleHttp;
use Psr\Http\Message\MessageInterface;
final class BodySummarizer implements BodySummarizerInterface
{
/**
* @var int|null
*/
private $truncateAt;
public function __construct(int $truncateAt = null)
{
$this->truncateAt = $truncateAt;
}
/**
* Returns a summarized message body.
*/
public function summarize(MessageInterface $message): ?string
{
return $this->truncateAt === null
? \GuzzleHttp\Psr7\Message::bodySummary($message)
: \GuzzleHttp\Psr7\Message::bodySummary($message, $this->truncateAt);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace GuzzleHttp;
use Psr\Http\Message\MessageInterface;
interface BodySummarizerInterface
{
/**
* Returns a summarized message body.
*/
public function summarize(MessageInterface $message): ?string;
}

View file

@ -0,0 +1,474 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\InvalidArgumentException;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* @final
*/
class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
{
use ClientTrait;
/**
* @var array Default request options
*/
private $config;
/**
* Clients accept an array of constructor parameters.
*
* Here's an example of creating a client using a base_uri and an array of
* default request options to apply to each request:
*
* $client = new Client([
* 'base_uri' => 'http://www.foo.com/1.0/',
* 'timeout' => 0,
* 'allow_redirects' => false,
* 'proxy' => '192.168.16.1:10'
* ]);
*
* Client configuration settings include the following options:
*
* - handler: (callable) Function that transfers HTTP requests over the
* wire. The function is called with a Psr7\Http\Message\RequestInterface
* and array of transfer options, and must return a
* GuzzleHttp\Promise\PromiseInterface that is fulfilled with a
* Psr7\Http\Message\ResponseInterface on success.
* If no handler is provided, a default handler will be created
* that enables all of the request options below by attaching all of the
* default middleware to the handler.
* - base_uri: (string|UriInterface) Base URI of the client that is merged
* into relative URIs. Can be a string or instance of UriInterface.
* - **: any request option
*
* @param array $config Client configuration settings.
*
* @see \GuzzleHttp\RequestOptions for a list of available request options.
*/
public function __construct(array $config = [])
{
if (!isset($config['handler'])) {
$config['handler'] = HandlerStack::create();
} elseif (!\is_callable($config['handler'])) {
throw new InvalidArgumentException('handler must be a callable');
}
// Convert the base_uri to a UriInterface
if (isset($config['base_uri'])) {
$config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']);
}
$this->configureDefaults($config);
}
/**
* @param string $method
* @param array $args
*
* @return PromiseInterface|ResponseInterface
*
* @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
*/
public function __call($method, $args)
{
if (\count($args) < 1) {
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
}
$uri = $args[0];
$opts = $args[1] ?? [];
return \substr($method, -5) === 'Async'
? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)
: $this->request($method, $uri, $opts);
}
/**
* Asynchronously send an HTTP request.
*
* @param array $options Request options to apply to the given
* request and to the transfer. See \GuzzleHttp\RequestOptions.
*/
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
{
// Merge the base URI into the request URI if needed.
$options = $this->prepareDefaults($options);
return $this->transfer(
$request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')),
$options
);
}
/**
* Send an HTTP request.
*
* @param array $options Request options to apply to the given
* request and to the transfer. See \GuzzleHttp\RequestOptions.
*
* @throws GuzzleException
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface
{
$options[RequestOptions::SYNCHRONOUS] = true;
return $this->sendAsync($request, $options)->wait();
}
/**
* The HttpClient PSR (PSR-18) specify this method.
*
* @inheritDoc
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
$options[RequestOptions::SYNCHRONOUS] = true;
$options[RequestOptions::ALLOW_REDIRECTS] = false;
$options[RequestOptions::HTTP_ERRORS] = false;
return $this->sendAsync($request, $options)->wait();
}
/**
* Create and send an asynchronous HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string $method HTTP method
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
*/
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{
$options = $this->prepareDefaults($options);
// Remove request modifying parameter because it can be done up-front.
$headers = $options['headers'] ?? [];
$body = $options['body'] ?? null;
$version = $options['version'] ?? '1.1';
// Merge the URI into the base URI.
$uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options);
if (\is_array($body)) {
throw $this->invalidBody();
}
$request = new Psr7\Request($method, $uri, $headers, $body, $version);
// Remove the option so that they are not doubly-applied.
unset($options['headers'], $options['body'], $options['version']);
return $this->transfer($request, $options);
}
/**
* Create and send an HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string $method HTTP method.
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
*
* @throws GuzzleException
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
$options[RequestOptions::SYNCHRONOUS] = true;
return $this->requestAsync($method, $uri, $options)->wait();
}
/**
* Get a client configuration option.
*
* These options include default request options of the client, a "handler"
* (if utilized by the concrete client), and a "base_uri" if utilized by
* the concrete client.
*
* @param string|null $option The config option to retrieve.
*
* @return mixed
*
* @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0.
*/
public function getConfig(?string $option = null)
{
return $option === null
? $this->config
: ($this->config[$option] ?? null);
}
private function buildUri(UriInterface $uri, array $config): UriInterface
{
if (isset($config['base_uri'])) {
$uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri);
}
if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
$idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion'];
$uri = Utils::idnUriConvert($uri, $idnOptions);
}
return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
}
/**
* Configures the default options for a client.
*/
private function configureDefaults(array $config): void
{
$defaults = [
'allow_redirects' => RedirectMiddleware::$defaultSettings,
'http_errors' => true,
'decode_content' => true,
'verify' => true,
'cookies' => false,
'idn_conversion' => false,
];
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
// We can only trust the HTTP_PROXY environment variable in a CLI
// process due to the fact that PHP has no reliable mechanism to
// get environment variables that start with "HTTP_".
if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) {
$defaults['proxy']['http'] = $proxy;
}
if ($proxy = Utils::getenv('HTTPS_PROXY')) {
$defaults['proxy']['https'] = $proxy;
}
if ($noProxy = Utils::getenv('NO_PROXY')) {
$cleanedNoProxy = \str_replace(' ', '', $noProxy);
$defaults['proxy']['no'] = \explode(',', $cleanedNoProxy);
}
$this->config = $config + $defaults;
if (!empty($config['cookies']) && $config['cookies'] === true) {
$this->config['cookies'] = new CookieJar();
}
// Add the default user-agent header.
if (!isset($this->config['headers'])) {
$this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()];
} else {
// Add the User-Agent header if one was not already set.
foreach (\array_keys($this->config['headers']) as $name) {
if (\strtolower($name) === 'user-agent') {
return;
}
}
$this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
}
}
/**
* Merges default options into the array.
*
* @param array $options Options to modify by reference
*/
private function prepareDefaults(array $options): array
{
$defaults = $this->config;
if (!empty($defaults['headers'])) {
// Default headers are only added if they are not present.
$defaults['_conditional'] = $defaults['headers'];
unset($defaults['headers']);
}
// Special handling for headers is required as they are added as
// conditional headers and as headers passed to a request ctor.
if (\array_key_exists('headers', $options)) {
// Allows default headers to be unset.
if ($options['headers'] === null) {
$defaults['_conditional'] = [];
unset($options['headers']);
} elseif (!\is_array($options['headers'])) {
throw new InvalidArgumentException('headers must be an array');
}
}
// Shallow merge defaults underneath options.
$result = $options + $defaults;
// Remove null values.
foreach ($result as $k => $v) {
if ($v === null) {
unset($result[$k]);
}
}
return $result;
}
/**
* Transfers the given request and applies request options.
*
* The URI of the request is not modified and the request options are used
* as-is without merging in default options.
*
* @param array $options See \GuzzleHttp\RequestOptions.
*/
private function transfer(RequestInterface $request, array $options): PromiseInterface
{
$request = $this->applyOptions($request, $options);
/** @var HandlerStack $handler */
$handler = $options['handler'];
try {
return P\Create::promiseFor($handler($request, $options));
} catch (\Exception $e) {
return P\Create::rejectionFor($e);
}
}
/**
* Applies the array of request options to a request.
*/
private function applyOptions(RequestInterface $request, array &$options): RequestInterface
{
$modify = [
'set_headers' => [],
];
if (isset($options['headers'])) {
$modify['set_headers'] = $options['headers'];
unset($options['headers']);
}
if (isset($options['form_params'])) {
if (isset($options['multipart'])) {
throw new InvalidArgumentException('You cannot use '
. 'form_params and multipart at the same time. Use the '
. 'form_params option if you want to send application/'
. 'x-www-form-urlencoded requests, and the multipart '
. 'option to send multipart/form-data requests.');
}
$options['body'] = \http_build_query($options['form_params'], '', '&');
unset($options['form_params']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (isset($options['multipart'])) {
$options['body'] = new Psr7\MultipartStream($options['multipart']);
unset($options['multipart']);
}
if (isset($options['json'])) {
$options['body'] = Utils::jsonEncode($options['json']);
unset($options['json']);
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'application/json';
}
if (!empty($options['decode_content'])
&& $options['decode_content'] !== true
) {
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
}
if (isset($options['body'])) {
if (\is_array($options['body'])) {
throw $this->invalidBody();
}
$modify['body'] = Psr7\Utils::streamFor($options['body']);
unset($options['body']);
}
if (!empty($options['auth']) && \is_array($options['auth'])) {
$value = $options['auth'];
$type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
switch ($type) {
case 'basic':
// Ensure that we don't have the header in different case and set the new value.
$modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
$modify['set_headers']['Authorization'] = 'Basic '
. \base64_encode("$value[0]:$value[1]");
break;
case 'digest':
// @todo: Do not rely on curl
$options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
case 'ntlm':
$options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
break;
}
}
if (isset($options['query'])) {
$value = $options['query'];
if (\is_array($value)) {
$value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986);
}
if (!\is_string($value)) {
throw new InvalidArgumentException('query must be a string or array');
}
$modify['query'] = $value;
unset($options['query']);
}
// Ensure that sink is not an invalid value.
if (isset($options['sink'])) {
// TODO: Add more sink validation?
if (\is_bool($options['sink'])) {
throw new InvalidArgumentException('sink must not be a boolean');
}
}
$request = Psr7\Utils::modifyRequest($request, $modify);
if ($request->getBody() instanceof Psr7\MultipartStream) {
// Use a multipart/form-data POST if a Content-Type is not set.
// Ensure that we don't have the header in different case and set the new value.
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
. $request->getBody()->getBoundary();
}
// Merge in conditional headers if they are not present.
if (isset($options['_conditional'])) {
// Build up the changes so it's in a single clone of the message.
$modify = [];
foreach ($options['_conditional'] as $k => $v) {
if (!$request->hasHeader($k)) {
$modify['set_headers'][$k] = $v;
}
}
$request = Psr7\Utils::modifyRequest($request, $modify);
// Don't pass this internal value along to middleware/handlers.
unset($options['_conditional']);
}
return $request;
}
/**
* Return an InvalidArgumentException with pre-set message.
*/
private function invalidBody(): InvalidArgumentException
{
return new InvalidArgumentException('Passing in the "body" request '
. 'option as an array to send a request is not supported. '
. 'Please use the "form_params" request option to send a '
. 'application/x-www-form-urlencoded request, or the "multipart" '
. 'request option to send a multipart/form-data request.');
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* Client interface for sending HTTP requests.
*/
interface ClientInterface
{
/**
* The Guzzle major version.
*/
const MAJOR_VERSION = 7;
/**
* Send an HTTP request.
*
* @param RequestInterface $request Request to send
* @param array $options Request options to apply to the given
* request and to the transfer.
*
* @throws GuzzleException
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface;
/**
* Asynchronously send an HTTP request.
*
* @param RequestInterface $request Request to send
* @param array $options Request options to apply to the given
* request and to the transfer.
*/
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface;
/**
* Create and send an HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string $method HTTP method.
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function request(string $method, $uri, array $options = []): ResponseInterface;
/**
* Create and send an asynchronous HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string $method HTTP method
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function requestAsync(string $method, $uri, array $options = []): PromiseInterface;
/**
* Get a client configuration option.
*
* These options include default request options of the client, a "handler"
* (if utilized by the concrete client), and a "base_uri" if utilized by
* the concrete client.
*
* @param string|null $option The config option to retrieve.
*
* @return mixed
*
* @deprecated ClientInterface::getConfig will be removed in guzzlehttp/guzzle:8.0.
*/
public function getConfig(?string $option = null);
}

View file

@ -0,0 +1,241 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* Client interface for sending HTTP requests.
*/
trait ClientTrait
{
/**
* Create and send an HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string $method HTTP method.
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
abstract public function request(string $method, $uri, array $options = []): ResponseInterface;
/**
* Create and send an HTTP GET request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function get($uri, array $options = []): ResponseInterface
{
return $this->request('GET', $uri, $options);
}
/**
* Create and send an HTTP HEAD request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function head($uri, array $options = []): ResponseInterface
{
return $this->request('HEAD', $uri, $options);
}
/**
* Create and send an HTTP PUT request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function put($uri, array $options = []): ResponseInterface
{
return $this->request('PUT', $uri, $options);
}
/**
* Create and send an HTTP POST request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function post($uri, array $options = []): ResponseInterface
{
return $this->request('POST', $uri, $options);
}
/**
* Create and send an HTTP PATCH request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function patch($uri, array $options = []): ResponseInterface
{
return $this->request('PATCH', $uri, $options);
}
/**
* Create and send an HTTP DELETE request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @throws GuzzleException
*/
public function delete($uri, array $options = []): ResponseInterface
{
return $this->request('DELETE', $uri, $options);
}
/**
* Create and send an asynchronous HTTP request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string $method HTTP method
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
abstract public function requestAsync(string $method, $uri, array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP GET request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function getAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('GET', $uri, $options);
}
/**
* Create and send an asynchronous HTTP HEAD request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function headAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('HEAD', $uri, $options);
}
/**
* Create and send an asynchronous HTTP PUT request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function putAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('PUT', $uri, $options);
}
/**
* Create and send an asynchronous HTTP POST request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function postAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('POST', $uri, $options);
}
/**
* Create and send an asynchronous HTTP PATCH request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function patchAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('PATCH', $uri, $options);
}
/**
* Create and send an asynchronous HTTP DELETE request.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*/
public function deleteAsync($uri, array $options = []): PromiseInterface
{
return $this->requestAsync('DELETE', $uri, $options);
}
}

View file

@ -0,0 +1,313 @@
<?php
namespace GuzzleHttp\Cookie;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Cookie jar that stores cookies as an array
*/
class CookieJar implements CookieJarInterface
{
/**
* @var SetCookie[] Loaded cookie data
*/
private $cookies = [];
/**
* @var bool
*/
private $strictMode;
/**
* @param bool $strictMode Set to true to throw exceptions when invalid
* cookies are added to the cookie jar.
* @param array $cookieArray Array of SetCookie objects or a hash of
* arrays that can be used with the SetCookie
* constructor
*/
public function __construct(bool $strictMode = false, array $cookieArray = [])
{
$this->strictMode = $strictMode;
foreach ($cookieArray as $cookie) {
if (!($cookie instanceof SetCookie)) {
$cookie = new SetCookie($cookie);
}
$this->setCookie($cookie);
}
}
/**
* Create a new Cookie jar from an associative array and domain.
*
* @param array $cookies Cookies to create the jar from
* @param string $domain Domain to set the cookies to
*/
public static function fromArray(array $cookies, string $domain): self
{
$cookieJar = new self();
foreach ($cookies as $name => $value) {
$cookieJar->setCookie(new SetCookie([
'Domain' => $domain,
'Name' => $name,
'Value' => $value,
'Discard' => true
]));
}
return $cookieJar;
}
/**
* Evaluate if this cookie should be persisted to storage
* that survives between requests.
*
* @param SetCookie $cookie Being evaluated.
* @param bool $allowSessionCookies If we should persist session cookies
*/
public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool
{
if ($cookie->getExpires() || $allowSessionCookies) {
if (!$cookie->getDiscard()) {
return true;
}
}
return false;
}
/**
* Finds and returns the cookie based on the name
*
* @param string $name cookie name to search for
*
* @return SetCookie|null cookie that was found or null if not found
*/
public function getCookieByName(string $name): ?SetCookie
{
foreach ($this->cookies as $cookie) {
if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
return $cookie;
}
}
return null;
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return \array_map(static function (SetCookie $cookie): array {
return $cookie->toArray();
}, $this->getIterator()->getArrayCopy());
}
/**
* @inheritDoc
*/
public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
{
if (!$domain) {
$this->cookies = [];
return;
} elseif (!$path) {
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($domain): bool {
return !$cookie->matchesDomain($domain);
}
);
} elseif (!$name) {
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($path, $domain): bool {
return !($cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
} else {
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie) use ($path, $domain, $name) {
return !($cookie->getName() == $name &&
$cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
}
}
/**
* @inheritDoc
*/
public function clearSessionCookies(): void
{
$this->cookies = \array_filter(
$this->cookies,
static function (SetCookie $cookie): bool {
return !$cookie->getDiscard() && $cookie->getExpires();
}
);
}
/**
* @inheritDoc
*/
public function setCookie(SetCookie $cookie): bool
{
// If the name string is empty (but not 0), ignore the set-cookie
// string entirely.
$name = $cookie->getName();
if (!$name && $name !== '0') {
return false;
}
// Only allow cookies with set and valid domain, name, value
$result = $cookie->validate();
if ($result !== true) {
if ($this->strictMode) {
throw new \RuntimeException('Invalid cookie: ' . $result);
}
$this->removeCookieIfEmpty($cookie);
return false;
}
// Resolve conflicts with previously set cookies
foreach ($this->cookies as $i => $c) {
// Two cookies are identical, when their path, and domain are
// identical.
if ($c->getPath() != $cookie->getPath() ||
$c->getDomain() != $cookie->getDomain() ||
$c->getName() != $cookie->getName()
) {
continue;
}
// The previously set cookie is a discard cookie and this one is
// not so allow the new cookie to be set
if (!$cookie->getDiscard() && $c->getDiscard()) {
unset($this->cookies[$i]);
continue;
}
// If the new cookie's expiration is further into the future, then
// replace the old cookie
if ($cookie->getExpires() > $c->getExpires()) {
unset($this->cookies[$i]);
continue;
}
// If the value has changed, we better change it
if ($cookie->getValue() !== $c->getValue()) {
unset($this->cookies[$i]);
continue;
}
// The cookie exists, so no need to continue
return false;
}
$this->cookies[] = $cookie;
return true;
}
public function count(): int
{
return \count($this->cookies);
}
/**
* @return \ArrayIterator<int, SetCookie>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator(\array_values($this->cookies));
}
public function extractCookies(RequestInterface $request, ResponseInterface $response): void
{
if ($cookieHeader = $response->getHeader('Set-Cookie')) {
foreach ($cookieHeader as $cookie) {
$sc = SetCookie::fromString($cookie);
if (!$sc->getDomain()) {
$sc->setDomain($request->getUri()->getHost());
}
if (0 !== \strpos($sc->getPath(), '/')) {
$sc->setPath($this->getCookiePathFromRequest($request));
}
$this->setCookie($sc);
}
}
}
/**
* Computes cookie path following RFC 6265 section 5.1.4
*
* @link https://tools.ietf.org/html/rfc6265#section-5.1.4
*/
private function getCookiePathFromRequest(RequestInterface $request): string
{
$uriPath = $request->getUri()->getPath();
if ('' === $uriPath) {
return '/';
}
if (0 !== \strpos($uriPath, '/')) {
return '/';
}
if ('/' === $uriPath) {
return '/';
}
$lastSlashPos = \strrpos($uriPath, '/');
if (0 === $lastSlashPos || false === $lastSlashPos) {
return '/';
}
return \substr($uriPath, 0, $lastSlashPos);
}
public function withCookieHeader(RequestInterface $request): RequestInterface
{
$values = [];
$uri = $request->getUri();
$scheme = $uri->getScheme();
$host = $uri->getHost();
$path = $uri->getPath() ?: '/';
foreach ($this->cookies as $cookie) {
if ($cookie->matchesPath($path) &&
$cookie->matchesDomain($host) &&
!$cookie->isExpired() &&
(!$cookie->getSecure() || $scheme === 'https')
) {
$values[] = $cookie->getName() . '='
. $cookie->getValue();
}
}
return $values
? $request->withHeader('Cookie', \implode('; ', $values))
: $request;
}
/**
* If a cookie already exists and the server asks to set it again with a
* null value, the cookie must be deleted.
*/
private function removeCookieIfEmpty(SetCookie $cookie): void
{
$cookieValue = $cookie->getValue();
if ($cookieValue === null || $cookieValue === '') {
$this->clear(
$cookie->getDomain(),
$cookie->getPath(),
$cookie->getName()
);
}
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace GuzzleHttp\Cookie;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Stores HTTP cookies.
*
* It extracts cookies from HTTP requests, and returns them in HTTP responses.
* CookieJarInterface instances automatically expire contained cookies when
* necessary. Subclasses are also responsible for storing and retrieving
* cookies from a file, database, etc.
*
* @link https://docs.python.org/2/library/cookielib.html Inspiration
* @extends \IteratorAggregate<SetCookie>
*/
interface CookieJarInterface extends \Countable, \IteratorAggregate
{
/**
* Create a request with added cookie headers.
*
* If no matching cookies are found in the cookie jar, then no Cookie
* header is added to the request and the same request is returned.
*
* @param RequestInterface $request Request object to modify.
*
* @return RequestInterface returns the modified request.
*/
public function withCookieHeader(RequestInterface $request): RequestInterface;
/**
* Extract cookies from an HTTP response and store them in the CookieJar.
*
* @param RequestInterface $request Request that was sent
* @param ResponseInterface $response Response that was received
*/
public function extractCookies(RequestInterface $request, ResponseInterface $response): void;
/**
* Sets a cookie in the cookie jar.
*
* @param SetCookie $cookie Cookie to set.
*
* @return bool Returns true on success or false on failure
*/
public function setCookie(SetCookie $cookie): bool;
/**
* Remove cookies currently held in the cookie jar.
*
* Invoking this method without arguments will empty the whole cookie jar.
* If given a $domain argument only cookies belonging to that domain will
* be removed. If given a $domain and $path argument, cookies belonging to
* the specified path within that domain are removed. If given all three
* arguments, then the cookie with the specified name, path and domain is
* removed.
*
* @param string|null $domain Clears cookies matching a domain
* @param string|null $path Clears cookies matching a domain and path
* @param string|null $name Clears cookies matching a domain, path, and name
*/
public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void;
/**
* Discard all sessions cookies.
*
* Removes cookies that don't have an expire field or a have a discard
* field set to true. To be called when the user agent shuts down according
* to RFC 2965.
*/
public function clearSessionCookies(): void;
/**
* Converts the cookie jar to an array.
*/
public function toArray(): array;
}

View file

@ -0,0 +1,101 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Utils;
/**
* Persists non-session cookies using a JSON formatted file
*/
class FileCookieJar extends CookieJar
{
/**
* @var string filename
*/
private $filename;
/**
* @var bool Control whether to persist session cookies or not.
*/
private $storeSessionCookies;
/**
* Create a new FileCookieJar object
*
* @param string $cookieFile File to store the cookie data
* @param bool $storeSessionCookies Set to true to store session cookies
* in the cookie jar.
*
* @throws \RuntimeException if the file cannot be found or created
*/
public function __construct(string $cookieFile, bool $storeSessionCookies = false)
{
parent::__construct();
$this->filename = $cookieFile;
$this->storeSessionCookies = $storeSessionCookies;
if (\file_exists($cookieFile)) {
$this->load($cookieFile);
}
}
/**
* Saves the file when shutting down
*/
public function __destruct()
{
$this->save($this->filename);
}
/**
* Saves the cookies to a file.
*
* @param string $filename File to save
*
* @throws \RuntimeException if the file cannot be found or created
*/
public function save(string $filename): void
{
$json = [];
/** @var SetCookie $cookie */
foreach ($this as $cookie) {
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$jsonStr = Utils::jsonEncode($json);
if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
throw new \RuntimeException("Unable to save file {$filename}");
}
}
/**
* Load cookies from a JSON formatted file.
*
* Old cookies are kept unless overwritten by newly loaded ones.
*
* @param string $filename Cookie file to load.
*
* @throws \RuntimeException if the file cannot be loaded.
*/
public function load(string $filename): void
{
$json = \file_get_contents($filename);
if (false === $json) {
throw new \RuntimeException("Unable to load file {$filename}");
}
if ($json === '') {
return;
}
$data = Utils::jsonDecode($json, true);
if (\is_array($data)) {
foreach ($data as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (\is_scalar($data) && !empty($data)) {
throw new \RuntimeException("Invalid cookie file: {$filename}");
}
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace GuzzleHttp\Cookie;
/**
* Persists cookies in the client session
*/
class SessionCookieJar extends CookieJar
{
/**
* @var string session key
*/
private $sessionKey;
/**
* @var bool Control whether to persist session cookies or not.
*/
private $storeSessionCookies;
/**
* Create a new SessionCookieJar object
*
* @param string $sessionKey Session key name to store the cookie
* data in session
* @param bool $storeSessionCookies Set to true to store session cookies
* in the cookie jar.
*/
public function __construct(string $sessionKey, bool $storeSessionCookies = false)
{
parent::__construct();
$this->sessionKey = $sessionKey;
$this->storeSessionCookies = $storeSessionCookies;
$this->load();
}
/**
* Saves cookies to session when shutting down
*/
public function __destruct()
{
$this->save();
}
/**
* Save cookies to the client session
*/
public function save(): void
{
$json = [];
/** @var SetCookie $cookie */
foreach ($this as $cookie) {
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$_SESSION[$this->sessionKey] = \json_encode($json);
}
/**
* Load the contents of the client session into the data array
*/
protected function load(): void
{
if (!isset($_SESSION[$this->sessionKey])) {
return;
}
$data = \json_decode($_SESSION[$this->sessionKey], true);
if (\is_array($data)) {
foreach ($data as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (\strlen($data)) {
throw new \RuntimeException("Invalid cookie data");
}
}
}

View file

@ -0,0 +1,410 @@
<?php
namespace GuzzleHttp\Cookie;
/**
* Set-Cookie object
*/
class SetCookie
{
/**
* @var array
*/
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
/**
* @var array Cookie data
*/
private $data;
/**
* Create a new SetCookie object from a string.
*
* @param string $cookie Set-Cookie header string
*/
public static function fromString(string $cookie): self
{
// Create the default return array
$data = self::$defaults;
// Explode the cookie string using a series of semicolons
$pieces = \array_filter(\array_map('trim', \explode(';', $cookie)));
// The name of the cookie (first kvp) must exist and include an equal sign.
if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
return new self($data);
}
// Add the cookie pieces into the parsed data array
foreach ($pieces as $part) {
$cookieParts = \explode('=', $part, 2);
$key = \trim($cookieParts[0]);
$value = isset($cookieParts[1])
? \trim($cookieParts[1], " \n\r\t\0\x0B")
: true;
// Only check for non-cookies when cookies have been found
if (!isset($data['Name'])) {
$data['Name'] = $key;
$data['Value'] = $value;
} else {
foreach (\array_keys(self::$defaults) as $search) {
if (!\strcasecmp($search, $key)) {
$data[$search] = $value;
continue 2;
}
}
$data[$key] = $value;
}
}
return new self($data);
}
/**
* @param array $data Array of cookie data provided by a Cookie parser
*/
public function __construct(array $data = [])
{
/** @var array|null $replaced will be null in case of replace error */
$replaced = \array_replace(self::$defaults, $data);
if ($replaced === null) {
throw new \InvalidArgumentException('Unable to replace the default values for the Cookie.');
}
$this->data = $replaced;
// Extract the Expires value and turn it into a UNIX timestamp if needed
if (!$this->getExpires() && $this->getMaxAge()) {
// Calculate the Expires date
$this->setExpires(\time() + $this->getMaxAge());
} elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
$this->setExpires($expires);
}
}
public function __toString()
{
$str = $this->data['Name'] . '=' . $this->data['Value'] . '; ';
foreach ($this->data as $k => $v) {
if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
if ($k === 'Expires') {
$str .= 'Expires=' . \gmdate('D, d M Y H:i:s \G\M\T', $v) . '; ';
} else {
$str .= ($v === true ? $k : "{$k}={$v}") . '; ';
}
}
}
return \rtrim($str, '; ');
}
public function toArray(): array
{
return $this->data;
}
/**
* Get the cookie name.
*
* @return string
*/
public function getName()
{
return $this->data['Name'];
}
/**
* Set the cookie name.
*
* @param string $name Cookie name
*/
public function setName($name): void
{
$this->data['Name'] = $name;
}
/**
* Get the cookie value.
*
* @return string|null
*/
public function getValue()
{
return $this->data['Value'];
}
/**
* Set the cookie value.
*
* @param string $value Cookie value
*/
public function setValue($value): void
{
$this->data['Value'] = $value;
}
/**
* Get the domain.
*
* @return string|null
*/
public function getDomain()
{
return $this->data['Domain'];
}
/**
* Set the domain of the cookie.
*
* @param string $domain
*/
public function setDomain($domain): void
{
$this->data['Domain'] = $domain;
}
/**
* Get the path.
*
* @return string
*/
public function getPath()
{
return $this->data['Path'];
}
/**
* Set the path of the cookie.
*
* @param string $path Path of the cookie
*/
public function setPath($path): void
{
$this->data['Path'] = $path;
}
/**
* Maximum lifetime of the cookie in seconds.
*
* @return int|null
*/
public function getMaxAge()
{
return $this->data['Max-Age'];
}
/**
* Set the max-age of the cookie.
*
* @param int $maxAge Max age of the cookie in seconds
*/
public function setMaxAge($maxAge): void
{
$this->data['Max-Age'] = $maxAge;
}
/**
* The UNIX timestamp when the cookie Expires.
*
* @return string|int|null
*/
public function getExpires()
{
return $this->data['Expires'];
}
/**
* Set the unix timestamp for which the cookie will expire.
*
* @param int|string $timestamp Unix timestamp or any English textual datetime description.
*/
public function setExpires($timestamp): void
{
$this->data['Expires'] = \is_numeric($timestamp)
? (int) $timestamp
: \strtotime($timestamp);
}
/**
* Get whether or not this is a secure cookie.
*
* @return bool|null
*/
public function getSecure()
{
return $this->data['Secure'];
}
/**
* Set whether or not the cookie is secure.
*
* @param bool $secure Set to true or false if secure
*/
public function setSecure($secure): void
{
$this->data['Secure'] = $secure;
}
/**
* Get whether or not this is a session cookie.
*
* @return bool|null
*/
public function getDiscard()
{
return $this->data['Discard'];
}
/**
* Set whether or not this is a session cookie.
*
* @param bool $discard Set to true or false if this is a session cookie
*/
public function setDiscard($discard): void
{
$this->data['Discard'] = $discard;
}
/**
* Get whether or not this is an HTTP only cookie.
*
* @return bool
*/
public function getHttpOnly()
{
return $this->data['HttpOnly'];
}
/**
* Set whether or not this is an HTTP only cookie.
*
* @param bool $httpOnly Set to true or false if this is HTTP only
*/
public function setHttpOnly($httpOnly): void
{
$this->data['HttpOnly'] = $httpOnly;
}
/**
* Check if the cookie matches a path value.
*
* A request-path path-matches a given cookie-path if at least one of
* the following conditions holds:
*
* - The cookie-path and the request-path are identical.
* - The cookie-path is a prefix of the request-path, and the last
* character of the cookie-path is %x2F ("/").
* - The cookie-path is a prefix of the request-path, and the first
* character of the request-path that is not included in the cookie-
* path is a %x2F ("/") character.
*
* @param string $requestPath Path to check against
*/
public function matchesPath(string $requestPath): bool
{
$cookiePath = $this->getPath();
// Match on exact matches or when path is the default empty "/"
if ($cookiePath === '/' || $cookiePath == $requestPath) {
return true;
}
// Ensure that the cookie-path is a prefix of the request path.
if (0 !== \strpos($requestPath, $cookiePath)) {
return false;
}
// Match if the last character of the cookie-path is "/"
if (\substr($cookiePath, -1, 1) === '/') {
return true;
}
// Match if the first character not included in cookie path is "/"
return \substr($requestPath, \strlen($cookiePath), 1) === '/';
}
/**
* Check if the cookie matches a domain value.
*
* @param string $domain Domain to check against
*/
public function matchesDomain(string $domain): bool
{
$cookieDomain = $this->getDomain();
if (null === $cookieDomain) {
return true;
}
// Remove the leading '.' as per spec in RFC 6265.
// https://tools.ietf.org/html/rfc6265#section-5.2.3
$cookieDomain = \ltrim($cookieDomain, '.');
// Domain not set or exact match.
if (!$cookieDomain || !\strcasecmp($domain, $cookieDomain)) {
return true;
}
// Matching the subdomain according to RFC 6265.
// https://tools.ietf.org/html/rfc6265#section-5.1.3
if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
return false;
}
return (bool) \preg_match('/\.' . \preg_quote($cookieDomain, '/') . '$/', $domain);
}
/**
* Check if the cookie is expired.
*/
public function isExpired(): bool
{
return $this->getExpires() !== null && \time() > $this->getExpires();
}
/**
* Check if the cookie is valid according to RFC 6265.
*
* @return bool|string Returns true if valid or an error message if invalid
*/
public function validate()
{
$name = $this->getName();
if ($name === '') {
return 'The cookie name must not be empty';
}
// Check if any of the invalid characters are present in the cookie name
if (\preg_match(
'/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/',
$name
)) {
return 'Cookie name must not contain invalid characters: ASCII '
. 'Control characters (0-31;127), space, tab and the '
. 'following characters: ()<>@,;:\"/?={}';
}
// Value must not be null. 0 and empty string are valid. Empty strings
// are technically against RFC 6265, but known to happen in the wild.
$value = $this->getValue();
if ($value === null) {
return 'The cookie value must not be empty';
}
// Domains must not be empty, but can be 0. "0" is not a valid internet
// domain, but may be used as server name in a private network.
$domain = $this->getDomain();
if ($domain === null || $domain === '') {
return 'The cookie domain must not be empty';
}
return true;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace GuzzleHttp\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Exception when an HTTP error occurs (4xx or 5xx error)
*/
class BadResponseException extends RequestException
{
public function __construct(
string $message,
RequestInterface $request,
ResponseInterface $response,
\Throwable $previous = null,
array $handlerContext = []
) {
parent::__construct($message, $request, $response, $previous, $handlerContext);
}
/**
* Current exception and the ones that extend it will always have a response.
*/
public function hasResponse(): bool
{
return true;
}
/**
* This function narrows the return type from the parent class and does not allow it to be nullable.
*/
public function getResponse(): ResponseInterface
{
/** @var ResponseInterface */
return parent::getResponse();
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when a client error is encountered (4xx codes)
*/
class ClientException extends BadResponseException
{
}

View file

@ -0,0 +1,56 @@
<?php
namespace GuzzleHttp\Exception;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;
/**
* Exception thrown when a connection cannot be established.
*
* Note that no response is present for a ConnectException
*/
class ConnectException extends TransferException implements NetworkExceptionInterface
{
/**
* @var RequestInterface
*/
private $request;
/**
* @var array
*/
private $handlerContext;
public function __construct(
string $message,
RequestInterface $request,
\Throwable $previous = null,
array $handlerContext = []
) {
parent::__construct($message, 0, $previous);
$this->request = $request;
$this->handlerContext = $handlerContext;
}
/**
* Get the request that caused the exception
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
/**
* Get contextual information about the error from the underlying handler.
*
* The contents of this array will vary depending on which handler you are
* using. It may also be just an empty array. Relying on this data will
* couple you to a specific handler, but can give more debug information
* when needed.
*/
public function getHandlerContext(): array
{
return $this->handlerContext;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace GuzzleHttp\Exception;
use Psr\Http\Client\ClientExceptionInterface;
interface GuzzleException extends ClientExceptionInterface
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements GuzzleException
{
}

View file

@ -0,0 +1,166 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\BodySummarizer;
use GuzzleHttp\BodySummarizerInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
/**
* HTTP Request exception
*/
class RequestException extends TransferException implements RequestExceptionInterface
{
/**
* @var RequestInterface
*/
private $request;
/**
* @var ResponseInterface|null
*/
private $response;
/**
* @var array
*/
private $handlerContext;
public function __construct(
string $message,
RequestInterface $request,
ResponseInterface $response = null,
\Throwable $previous = null,
array $handlerContext = []
) {
// Set the code of the exception if the response is set and not future.
$code = $response ? $response->getStatusCode() : 0;
parent::__construct($message, $code, $previous);
$this->request = $request;
$this->response = $response;
$this->handlerContext = $handlerContext;
}
/**
* Wrap non-RequestExceptions with a RequestException
*/
public static function wrapException(RequestInterface $request, \Throwable $e): RequestException
{
return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
}
/**
* Factory method to create a new exception with a normalized error message
*
* @param RequestInterface $request Request sent
* @param ResponseInterface $response Response received
* @param \Throwable|null $previous Previous exception
* @param array $handlerContext Optional handler context
* @param BodySummarizerInterface|null $bodySummarizer Optional body summarizer
*/
public static function create(
RequestInterface $request,
ResponseInterface $response = null,
\Throwable $previous = null,
array $handlerContext = [],
BodySummarizerInterface $bodySummarizer = null
): self {
if (!$response) {
return new self(
'Error completing request',
$request,
null,
$previous,
$handlerContext
);
}
$level = (int) \floor($response->getStatusCode() / 100);
if ($level === 4) {
$label = 'Client error';
$className = ClientException::class;
} elseif ($level === 5) {
$label = 'Server error';
$className = ServerException::class;
} else {
$label = 'Unsuccessful request';
$className = __CLASS__;
}
$uri = $request->getUri();
$uri = static::obfuscateUri($uri);
// Client Error: `GET /` resulted in a `404 Not Found` response:
// <html> ... (truncated)
$message = \sprintf(
'%s: `%s %s` resulted in a `%s %s` response',
$label,
$request->getMethod(),
$uri,
$response->getStatusCode(),
$response->getReasonPhrase()
);
$summary = ($bodySummarizer ?? new BodySummarizer())->summarize($response);
if ($summary !== null) {
$message .= ":\n{$summary}\n";
}
return new $className($message, $request, $response, $previous, $handlerContext);
}
/**
* Obfuscates URI if there is a username and a password present
*/
private static function obfuscateUri(UriInterface $uri): UriInterface
{
$userInfo = $uri->getUserInfo();
if (false !== ($pos = \strpos($userInfo, ':'))) {
return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***');
}
return $uri;
}
/**
* Get the request that caused the exception
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
/**
* Get the associated response
*/
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
/**
* Check if a response was received
*/
public function hasResponse(): bool
{
return $this->response !== null;
}
/**
* Get contextual information about the error from the underlying handler.
*
* The contents of this array will vary depending on which handler you are
* using. It may also be just an empty array. Relying on this data will
* couple you to a specific handler, but can give more debug information
* when needed.
*/
public function getHandlerContext(): array
{
return $this->handlerContext;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when a server error is encountered (5xx codes)
*/
class ServerException extends BadResponseException
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Exception;
class TooManyRedirectsException extends RequestException
{
}

View file

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Exception;
class TransferException extends \RuntimeException implements GuzzleException
{
}

View file

@ -0,0 +1,592 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\TransferStats;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
/**
* Creates curl resources from a request
*
* @final
*/
class CurlFactory implements CurlFactoryInterface
{
public const CURL_VERSION_STR = 'curl_version';
/**
* @deprecated
*/
public const LOW_CURL_VERSION_NUMBER = '7.21.2';
/**
* @var resource[]|\CurlHandle[]
*/
private $handles = [];
/**
* @var int Total number of idle handles to keep in cache
*/
private $maxHandles;
/**
* @param int $maxHandles Maximum number of idle handles.
*/
public function __construct(int $maxHandles)
{
$this->maxHandles = $maxHandles;
}
public function create(RequestInterface $request, array $options): EasyHandle
{
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
}
$easy = new EasyHandle;
$easy->request = $request;
$easy->options = $options;
$conf = $this->getDefaultConf($easy);
$this->applyMethod($easy, $conf);
$this->applyHandlerOptions($easy, $conf);
$this->applyHeaders($easy, $conf);
unset($conf['_headers']);
// Add handler options from the request configuration options
if (isset($options['curl'])) {
$conf = \array_replace($conf, $options['curl']);
}
$conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
$easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
curl_setopt_array($easy->handle, $conf);
return $easy;
}
public function release(EasyHandle $easy): void
{
$resource = $easy->handle;
unset($easy->handle);
if (\count($this->handles) >= $this->maxHandles) {
\curl_close($resource);
} else {
// Remove all callback functions as they can hold onto references
// and are not cleaned up by curl_reset. Using curl_setopt_array
// does not work for some reason, so removing each one
// individually.
\curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
\curl_setopt($resource, \CURLOPT_READFUNCTION, null);
\curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
\curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
\curl_reset($resource);
$this->handles[] = $resource;
}
}
/**
* Completes a cURL transaction, either returning a response promise or a
* rejected promise.
*
* @param callable(RequestInterface, array): PromiseInterface $handler
* @param CurlFactoryInterface $factory Dictates how the handle is released
*/
public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
{
if (isset($easy->options['on_stats'])) {
self::invokeStats($easy);
}
if (!$easy->response || $easy->errno) {
return self::finishError($handler, $easy, $factory);
}
// Return the response if it is present and there is no error.
$factory->release($easy);
// Rewind the body of the response if possible.
$body = $easy->response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
return new FulfilledPromise($easy->response);
}
private static function invokeStats(EasyHandle $easy): void
{
$curlStats = \curl_getinfo($easy->handle);
$curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
$stats = new TransferStats(
$easy->request,
$easy->response,
$curlStats['total_time'],
$easy->errno,
$curlStats
);
($easy->options['on_stats'])($stats);
}
/**
* @param callable(RequestInterface, array): PromiseInterface $handler
*/
private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
{
// Get error information and release the handle to the factory.
$ctx = [
'errno' => $easy->errno,
'error' => \curl_error($easy->handle),
'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
] + \curl_getinfo($easy->handle);
$ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
$factory->release($easy);
// Retry when nothing is present or when curl failed to rewind.
if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
return self::retryFailedRewind($handler, $easy, $ctx);
}
return self::createRejection($easy, $ctx);
}
private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
{
static $connectionErrors = [
\CURLE_OPERATION_TIMEOUTED => true,
\CURLE_COULDNT_RESOLVE_HOST => true,
\CURLE_COULDNT_CONNECT => true,
\CURLE_SSL_CONNECT_ERROR => true,
\CURLE_GOT_NOTHING => true,
];
if ($easy->createResponseException) {
return P\Create::rejectionFor(
new RequestException(
'An error was encountered while creating the response',
$easy->request,
$easy->response,
$easy->createResponseException,
$ctx
)
);
}
// If an exception was encountered during the onHeaders event, then
// return a rejected promise that wraps that exception.
if ($easy->onHeadersException) {
return P\Create::rejectionFor(
new RequestException(
'An error was encountered during the on_headers event',
$easy->request,
$easy->response,
$easy->onHeadersException,
$ctx
)
);
}
$message = \sprintf(
'cURL error %s: %s (%s)',
$ctx['errno'],
$ctx['error'],
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
);
$uriString = (string) $easy->request->getUri();
if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) {
$message .= \sprintf(' for %s', $uriString);
}
// Create a connection exception if it was a specific error code.
$error = isset($connectionErrors[$easy->errno])
? new ConnectException($message, $easy->request, null, $ctx)
: new RequestException($message, $easy->request, $easy->response, null, $ctx);
return P\Create::rejectionFor($error);
}
/**
* @return array<int|string, mixed>
*/
private function getDefaultConf(EasyHandle $easy): array
{
$conf = [
'_headers' => $easy->request->getHeaders(),
\CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
\CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
\CURLOPT_RETURNTRANSFER => false,
\CURLOPT_HEADER => false,
\CURLOPT_CONNECTTIMEOUT => 150,
];
if (\defined('CURLOPT_PROTOCOLS')) {
$conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
}
$version = $easy->request->getProtocolVersion();
if ($version == 1.1) {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
} elseif ($version == 2.0) {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
} else {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
}
return $conf;
}
private function applyMethod(EasyHandle $easy, array &$conf): void
{
$body = $easy->request->getBody();
$size = $body->getSize();
if ($size === null || $size > 0) {
$this->applyBody($easy->request, $easy->options, $conf);
return;
}
$method = $easy->request->getMethod();
if ($method === 'PUT' || $method === 'POST') {
// See https://tools.ietf.org/html/rfc7230#section-3.3.2
if (!$easy->request->hasHeader('Content-Length')) {
$conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
}
} elseif ($method === 'HEAD') {
$conf[\CURLOPT_NOBODY] = true;
unset(
$conf[\CURLOPT_WRITEFUNCTION],
$conf[\CURLOPT_READFUNCTION],
$conf[\CURLOPT_FILE],
$conf[\CURLOPT_INFILE]
);
}
}
private function applyBody(RequestInterface $request, array $options, array &$conf): void
{
$size = $request->hasHeader('Content-Length')
? (int) $request->getHeaderLine('Content-Length')
: null;
// Send the body as a string if the size is less than 1MB OR if the
// [curl][body_as_string] request value is set.
if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) {
$conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
// Don't duplicate the Content-Length header
$this->removeHeader('Content-Length', $conf);
$this->removeHeader('Transfer-Encoding', $conf);
} else {
$conf[\CURLOPT_UPLOAD] = true;
if ($size !== null) {
$conf[\CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $conf);
}
$body = $request->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
$conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
}
// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
}
// cURL sometimes adds a content-type by default. Prevent this.
if (!$request->hasHeader('Content-Type')) {
$conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
}
}
private function applyHeaders(EasyHandle $easy, array &$conf): void
{
foreach ($conf['_headers'] as $name => $values) {
foreach ($values as $value) {
$value = (string) $value;
if ($value === '') {
// cURL requires a special format for empty headers.
// See https://github.com/guzzle/guzzle/issues/1882 for more details.
$conf[\CURLOPT_HTTPHEADER][] = "$name;";
} else {
$conf[\CURLOPT_HTTPHEADER][] = "$name: $value";
}
}
}
// Remove the Accept header if one was not set
if (!$easy->request->hasHeader('Accept')) {
$conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
}
}
/**
* Remove a header from the options array.
*
* @param string $name Case-insensitive header to remove
* @param array $options Array of options to modify
*/
private function removeHeader(string $name, array &$options): void
{
foreach (\array_keys($options['_headers']) as $key) {
if (!\strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
return;
}
}
}
private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
{
$options = $easy->options;
if (isset($options['verify'])) {
if ($options['verify'] === false) {
unset($conf[\CURLOPT_CAINFO]);
$conf[\CURLOPT_SSL_VERIFYHOST] = 0;
$conf[\CURLOPT_SSL_VERIFYPEER] = false;
} else {
$conf[\CURLOPT_SSL_VERIFYHOST] = 2;
$conf[\CURLOPT_SSL_VERIFYPEER] = true;
if (\is_string($options['verify'])) {
// Throw an error if the file/folder/link path is not valid or doesn't exist.
if (!\file_exists($options['verify'])) {
throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
}
// If it's a directory or a link to a directory use CURLOPT_CAPATH.
// If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
if (
\is_dir($options['verify']) ||
(
\is_link($options['verify']) === true &&
($verifyLink = \readlink($options['verify'])) !== false &&
\is_dir($verifyLink)
)
) {
$conf[\CURLOPT_CAPATH] = $options['verify'];
} else {
$conf[\CURLOPT_CAINFO] = $options['verify'];
}
}
}
}
if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
$accept = $easy->request->getHeaderLine('Accept-Encoding');
if ($accept) {
$conf[\CURLOPT_ENCODING] = $accept;
} else {
$conf[\CURLOPT_ENCODING] = '';
// Don't let curl send the header over the wire
$conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
}
}
if (!isset($options['sink'])) {
// Use a default temp stream if no sink was set.
$options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
}
$sink = $options['sink'];
if (!\is_string($sink)) {
$sink = \GuzzleHttp\Psr7\Utils::streamFor($sink);
} elseif (!\is_dir(\dirname($sink))) {
// Ensure that the directory exists before failing in curl.
throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
} else {
$sink = new LazyOpenStream($sink, 'w+');
}
$easy->sink = $sink;
$conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
return $sink->write($write);
};
$timeoutRequiresNoSignal = false;
if (isset($options['timeout'])) {
$timeoutRequiresNoSignal |= $options['timeout'] < 1;
$conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
// CURL default value is CURL_IPRESOLVE_WHATEVER
if (isset($options['force_ip_resolve'])) {
if ('v4' === $options['force_ip_resolve']) {
$conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
} elseif ('v6' === $options['force_ip_resolve']) {
$conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
}
}
if (isset($options['connect_timeout'])) {
$timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
$conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}
if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
$conf[\CURLOPT_NOSIGNAL] = true;
}
if (isset($options['proxy'])) {
if (!\is_array($options['proxy'])) {
$conf[\CURLOPT_PROXY] = $options['proxy'];
} else {
$scheme = $easy->request->getUri()->getScheme();
if (isset($options['proxy'][$scheme])) {
$host = $easy->request->getUri()->getHost();
if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
$conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
}
}
}
}
if (isset($options['cert'])) {
$cert = $options['cert'];
if (\is_array($cert)) {
$conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
$cert = $cert[0];
}
if (!\file_exists($cert)) {
throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
}
# OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
# see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
$ext = pathinfo($cert, \PATHINFO_EXTENSION);
if (preg_match('#^(der|p12)$#i', $ext)) {
$conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
}
$conf[\CURLOPT_SSLCERT] = $cert;
}
if (isset($options['ssl_key'])) {
if (\is_array($options['ssl_key'])) {
if (\count($options['ssl_key']) === 2) {
[$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
} else {
[$sslKey] = $options['ssl_key'];
}
}
$sslKey = $sslKey ?? $options['ssl_key'];
if (!\file_exists($sslKey)) {
throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
}
$conf[\CURLOPT_SSLKEY] = $sslKey;
}
if (isset($options['progress'])) {
$progress = $options['progress'];
if (!\is_callable($progress)) {
throw new \InvalidArgumentException('progress client option must be callable');
}
$conf[\CURLOPT_NOPROGRESS] = false;
$conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
$progress($downloadSize, $downloaded, $uploadSize, $uploaded);
};
}
if (!empty($options['debug'])) {
$conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
$conf[\CURLOPT_VERBOSE] = true;
}
}
/**
* This function ensures that a response was set on a transaction. If one
* was not set, then the request is retried if possible. This error
* typically means you are sending a payload, curl encountered a
* "Connection died, retrying a fresh connect" error, tried to rewind the
* stream, and then encountered a "necessary data rewind wasn't possible"
* error, causing the request to be sent through curl_multi_info_read()
* without an error status.
*
* @param callable(RequestInterface, array): PromiseInterface $handler
*/
private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
{
try {
// Only rewind if the body has been read from.
$body = $easy->request->getBody();
if ($body->tell() > 0) {
$body->rewind();
}
} catch (\RuntimeException $e) {
$ctx['error'] = 'The connection unexpectedly failed without '
. 'providing an error. The request would have been retried, '
. 'but attempting to rewind the request body failed. '
. 'Exception: ' . $e;
return self::createRejection($easy, $ctx);
}
// Retry no more than 3 times before giving up.
if (!isset($easy->options['_curl_retries'])) {
$easy->options['_curl_retries'] = 1;
} elseif ($easy->options['_curl_retries'] == 2) {
$ctx['error'] = 'The cURL request was retried 3 times '
. 'and did not succeed. The most likely reason for the failure '
. 'is that cURL was unable to rewind the body of the request '
. 'and subsequent retries resulted in the same error. Turn on '
. 'the debug option to see what went wrong. See '
. 'https://bugs.php.net/bug.php?id=47204 for more information.';
return self::createRejection($easy, $ctx);
} else {
$easy->options['_curl_retries']++;
}
return $handler($easy->request, $easy->options);
}
private function createHeaderFn(EasyHandle $easy): callable
{
if (isset($easy->options['on_headers'])) {
$onHeaders = $easy->options['on_headers'];
if (!\is_callable($onHeaders)) {
throw new \InvalidArgumentException('on_headers must be callable');
}
} else {
$onHeaders = null;
}
return static function ($ch, $h) use (
$onHeaders,
$easy,
&$startingResponse
) {
$value = \trim($h);
if ($value === '') {
$startingResponse = true;
try {
$easy->createResponse();
} catch (\Exception $e) {
$easy->createResponseException = $e;
return -1;
}
if ($onHeaders !== null) {
try {
$onHeaders($easy->response);
} catch (\Exception $e) {
// Associate the exception with the handle and trigger
// a curl header write error by returning 0.
$easy->onHeadersException = $e;
return -1;
}
}
} elseif ($startingResponse) {
$startingResponse = false;
$easy->headers = [$value];
} else {
$easy->headers[] = $value;
}
return \strlen($h);
};
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace GuzzleHttp\Handler;
use Psr\Http\Message\RequestInterface;
interface CurlFactoryInterface
{
/**
* Creates a cURL handle resource.
*
* @param RequestInterface $request Request
* @param array $options Transfer options
*
* @throws \RuntimeException when an option cannot be applied
*/
public function create(RequestInterface $request, array $options): EasyHandle;
/**
* Release an easy handle, allowing it to be reused or closed.
*
* This function must call unset on the easy handle's "handle" property.
*/
public function release(EasyHandle $easy): void;
}

View file

@ -0,0 +1,49 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
/**
* HTTP handler that uses cURL easy handles as a transport layer.
*
* When using the CurlHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*
* @final
*/
class CurlHandler
{
/**
* @var CurlFactoryInterface
*/
private $factory;
/**
* Accepts an associative array of options:
*
* - factory: Optional curl factory used to create cURL handles.
*
* @param array $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
$this->factory = $options['handle_factory']
?? new CurlFactory(3);
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
if (isset($options['delay'])) {
\usleep($options['delay'] * 1000);
}
$easy = $this->factory->create($request, $options);
\curl_exec($easy->handle);
$easy->errno = \curl_errno($easy->handle);
return CurlFactory::finish($this, $easy, $this->factory);
}
}

View file

@ -0,0 +1,253 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
/**
* Returns an asynchronous response using curl_multi_* functions.
*
* When using the CurlMultiHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the provided request options.
*
* @property resource|\CurlMultiHandle $_mh Internal use only. Lazy loaded multi-handle.
*
* @final
*/
class CurlMultiHandler
{
/**
* @var CurlFactoryInterface
*/
private $factory;
/**
* @var int
*/
private $selectTimeout;
/**
* @var resource|\CurlMultiHandle|null the currently executing resource in `curl_multi_exec`.
*/
private $active;
/**
* @var array Request entry handles, indexed by handle id in `addRequest`.
*
* @see CurlMultiHandler::addRequest
*/
private $handles = [];
/**
* @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
*
* @see CurlMultiHandler::addRequest
*/
private $delays = [];
/**
* @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
*/
private $options = [];
/**
* This handler accepts the following options:
*
* - handle_factory: An optional factory used to create curl handles
* - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second.
* - options: An associative array of CURLMOPT_* options and
* corresponding values for curl_multi_setopt()
*/
public function __construct(array $options = [])
{
$this->factory = $options['handle_factory'] ?? new CurlFactory(50);
if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
@trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED);
$this->selectTimeout = (int) $selectTimeout;
} else {
$this->selectTimeout = 1;
}
$this->options = $options['options'] ?? [];
}
/**
* @param string $name
*
* @return resource|\CurlMultiHandle
*
* @throws \BadMethodCallException when another field as `_mh` will be gotten
* @throws \RuntimeException when curl can not initialize a multi handle
*/
public function __get($name)
{
if ($name !== '_mh') {
throw new \BadMethodCallException("Can not get other property as '_mh'.");
}
$multiHandle = \curl_multi_init();
if (false === $multiHandle) {
throw new \RuntimeException('Can not initialize curl multi handle.');
}
$this->_mh = $multiHandle;
foreach ($this->options as $option => $value) {
// A warning is raised in case of a wrong option.
curl_multi_setopt($this->_mh, $option, $value);
}
return $this->_mh;
}
public function __destruct()
{
if (isset($this->_mh)) {
\curl_multi_close($this->_mh);
unset($this->_mh);
}
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
$easy = $this->factory->create($request, $options);
$id = (int) $easy->handle;
$promise = new Promise(
[$this, 'execute'],
function () use ($id) {
return $this->cancel($id);
}
);
$this->addRequest(['easy' => $easy, 'deferred' => $promise]);
return $promise;
}
/**
* Ticks the curl event loop.
*/
public function tick(): void
{
// Add any delayed handles if needed.
if ($this->delays) {
$currentTime = Utils::currentTime();
foreach ($this->delays as $id => $delay) {
if ($currentTime >= $delay) {
unset($this->delays[$id]);
\curl_multi_add_handle(
$this->_mh,
$this->handles[$id]['easy']->handle
);
}
}
}
// Step through the task queue which may add additional requests.
P\Utils::queue()->run();
if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
\usleep(250);
}
while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
/**
* Runs until all outstanding connections have completed.
*/
public function execute(): void
{
$queue = P\Utils::queue();
while ($this->handles || !$queue->isEmpty()) {
// If there are no transfers, then sleep for the next delay
if (!$this->active && $this->delays) {
\usleep($this->timeToNext());
}
$this->tick();
}
}
private function addRequest(array $entry): void
{
$easy = $entry['easy'];
$id = (int) $easy->handle;
$this->handles[$id] = $entry;
if (empty($easy->options['delay'])) {
\curl_multi_add_handle($this->_mh, $easy->handle);
} else {
$this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
}
}
/**
* Cancels a handle from sending and removes references to it.
*
* @param int $id Handle ID to cancel and remove.
*
* @return bool True on success, false on failure.
*/
private function cancel($id): bool
{
// Cannot cancel if it has been processed.
if (!isset($this->handles[$id])) {
return false;
}
$handle = $this->handles[$id]['easy']->handle;
unset($this->delays[$id], $this->handles[$id]);
\curl_multi_remove_handle($this->_mh, $handle);
\curl_close($handle);
return true;
}
private function processMessages(): void
{
while ($done = \curl_multi_info_read($this->_mh)) {
$id = (int) $done['handle'];
\curl_multi_remove_handle($this->_mh, $done['handle']);
if (!isset($this->handles[$id])) {
// Probably was cancelled.
continue;
}
$entry = $this->handles[$id];
unset($this->handles[$id], $this->delays[$id]);
$entry['easy']->errno = $done['result'];
$entry['deferred']->resolve(
CurlFactory::finish($this, $entry['easy'], $this->factory)
);
}
}
private function timeToNext(): int
{
$currentTime = Utils::currentTime();
$nextTime = \PHP_INT_MAX;
foreach ($this->delays as $time) {
if ($time < $nextTime) {
$nextTime = $time;
}
}
return ((int) \max(0, $nextTime - $currentTime)) * 1000000;
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Represents a cURL easy handle and the data it populates.
*
* @internal
*/
final class EasyHandle
{
/**
* @var resource|\CurlHandle cURL resource
*/
public $handle;
/**
* @var StreamInterface Where data is being written
*/
public $sink;
/**
* @var array Received HTTP headers so far
*/
public $headers = [];
/**
* @var ResponseInterface|null Received response (if any)
*/
public $response;
/**
* @var RequestInterface Request being sent
*/
public $request;
/**
* @var array Request options
*/
public $options = [];
/**
* @var int cURL error number (if any)
*/
public $errno = 0;
/**
* @var \Throwable|null Exception during on_headers (if any)
*/
public $onHeadersException;
/**
* @var \Exception|null Exception during createResponse (if any)
*/
public $createResponseException;
/**
* Attach a response to the easy handle based on the received headers.
*
* @throws \RuntimeException if no headers have been received or the first
* header line is invalid.
*/
public function createResponse(): void
{
[$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers);
$normalizedKeys = Utils::normalizeHeaderKeys($headers);
if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) {
$headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
unset($headers[$normalizedKeys['content-encoding']]);
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
$bodyLength = (int) $this->sink->getSize();
if ($bodyLength) {
$headers[$normalizedKeys['content-length']] = $bodyLength;
} else {
unset($headers[$normalizedKeys['content-length']]);
}
}
}
// Attach a response to the easy handle with the parsed headers.
$this->response = new Response(
$status,
$headers,
$this->sink,
$ver,
$reason
);
}
/**
* @param string $name
*
* @return void
*
* @throws \BadMethodCallException
*/
public function __get($name)
{
$msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: ' . $name;
throw new \BadMethodCallException($msg);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Utils;
/**
* @internal
*/
final class HeaderProcessor
{
/**
* Returns the HTTP version, status code, reason phrase, and headers.
*
* @param string[] $headers
*
* @throws \RuntimeException
*
* @return array{0:string, 1:int, 2:?string, 3:array}
*/
public static function parseHeaders(array $headers): array
{
if ($headers === []) {
throw new \RuntimeException('Expected a non-empty array of header data');
}
$parts = \explode(' ', \array_shift($headers), 3);
$version = \explode('/', $parts[0])[1] ?? null;
if ($version === null) {
throw new \RuntimeException('HTTP version missing from header data');
}
$status = $parts[1] ?? null;
if ($status === null) {
throw new \RuntimeException('HTTP status code missing from header data');
}
return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
}
}

View file

@ -0,0 +1,211 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransferStats;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Handler that returns responses or throw exceptions from a queue.
*
* @final
*/
class MockHandler implements \Countable
{
/**
* @var array
*/
private $queue = [];
/**
* @var RequestInterface|null
*/
private $lastRequest;
/**
* @var array
*/
private $lastOptions = [];
/**
* @var callable|null
*/
private $onFulfilled;
/**
* @var callable|null
*/
private $onRejected;
/**
* Creates a new MockHandler that uses the default handler stack list of
* middlewares.
*
* @param array|null $queue Array of responses, callables, or exceptions.
* @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable|null $onRejected Callback to invoke when the return value is rejected.
*/
public static function createWithMiddleware(array $queue = null, callable $onFulfilled = null, callable $onRejected = null): HandlerStack
{
return HandlerStack::create(new self($queue, $onFulfilled, $onRejected));
}
/**
* The passed in value must be an array of
* {@see \Psr\Http\Message\ResponseInterface} objects, Exceptions,
* callables, or Promises.
*
* @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array.
* @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable|null $onRejected Callback to invoke when the return value is rejected.
*/
public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null)
{
$this->onFulfilled = $onFulfilled;
$this->onRejected = $onRejected;
if ($queue) {
// array_values included for BC
$this->append(...array_values($queue));
}
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
if (!$this->queue) {
throw new \OutOfBoundsException('Mock queue is empty');
}
if (isset($options['delay']) && \is_numeric($options['delay'])) {
\usleep((int) $options['delay'] * 1000);
}
$this->lastRequest = $request;
$this->lastOptions = $options;
$response = \array_shift($this->queue);
if (isset($options['on_headers'])) {
if (!\is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
}
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$response = new RequestException($msg, $request, $response, $e);
}
}
if (\is_callable($response)) {
$response = $response($request, $options);
}
$response = $response instanceof \Throwable
? P\Create::rejectionFor($response)
: P\Create::promiseFor($response);
return $response->then(
function (?ResponseInterface $value) use ($request, $options) {
$this->invokeStats($request, $options, $value);
if ($this->onFulfilled) {
($this->onFulfilled)($value);
}
if ($value !== null && isset($options['sink'])) {
$contents = (string) $value->getBody();
$sink = $options['sink'];
if (\is_resource($sink)) {
\fwrite($sink, $contents);
} elseif (\is_string($sink)) {
\file_put_contents($sink, $contents);
} elseif ($sink instanceof StreamInterface) {
$sink->write($contents);
}
}
return $value;
},
function ($reason) use ($request, $options) {
$this->invokeStats($request, $options, null, $reason);
if ($this->onRejected) {
($this->onRejected)($reason);
}
return P\Create::rejectionFor($reason);
}
);
}
/**
* Adds one or more variadic requests, exceptions, callables, or promises
* to the queue.
*
* @param mixed ...$values
*/
public function append(...$values): void
{
foreach ($values as $value) {
if ($value instanceof ResponseInterface
|| $value instanceof \Throwable
|| $value instanceof PromiseInterface
|| \is_callable($value)
) {
$this->queue[] = $value;
} else {
throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found ' . Utils::describeType($value));
}
}
}
/**
* Get the last received request.
*/
public function getLastRequest(): ?RequestInterface
{
return $this->lastRequest;
}
/**
* Get the last received request options.
*/
public function getLastOptions(): array
{
return $this->lastOptions;
}
/**
* Returns the number of remaining items in the queue.
*/
public function count(): int
{
return \count($this->queue);
}
public function reset(): void
{
$this->queue = [];
}
/**
* @param mixed $reason Promise or reason.
*/
private function invokeStats(
RequestInterface $request,
array $options,
ResponseInterface $response = null,
$reason = null
): void {
if (isset($options['on_stats'])) {
$transferTime = $options['transfer_time'] ?? 0;
$stats = new TransferStats($request, $response, $transferTime, $reason);
($options['on_stats'])($stats);
}
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\RequestInterface;
/**
* Provides basic proxies for handlers.
*
* @final
*/
class Proxy
{
/**
* Sends synchronous requests to a specific handler while sending all other
* requests to another handler.
*
* @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $default Handler used for normal responses
* @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $sync Handler used for synchronous responses.
*
* @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the composed handler.
*/
public static function wrapSync(callable $default, callable $sync): callable
{
return static function (RequestInterface $request, array $options) use ($default, $sync): PromiseInterface {
return empty($options[RequestOptions::SYNCHRONOUS]) ? $default($request, $options) : $sync($request, $options);
};
}
/**
* Sends streaming requests to a streaming compatible handler while sending
* all other requests to a default handler.
*
* This, for example, could be useful for taking advantage of the
* performance benefits of curl while still supporting true streaming
* through the StreamHandler.
*
* @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $default Handler used for non-streaming responses
* @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $streaming Handler used for streaming responses
*
* @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the composed handler.
*/
public static function wrapStreaming(callable $default, callable $streaming): callable
{
return static function (RequestInterface $request, array $options) use ($default, $streaming): PromiseInterface {
return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options);
};
}
}

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