mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-08-07 15:41:28 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 238452c104 | |||
| 91f7a2ec8d | |||
| 62823aef63 | |||
| 2ceec61e17 | |||
| 8fc5f3ccbf | |||
| 6a45d44f69 | |||
| 327af60d1c | |||
| a38ecd4864 | |||
| 1f7432fa26 | |||
| 55bda9daf0 | |||
| 667dd4c129 | |||
| d16b7f60f3 | |||
| 57fa9f26fb | |||
| bdf9160fdc | |||
| 93a321ff5b | |||
| 3f483c9ca7 | |||
| 1c7ae3e335 | |||
| 2555ab1b3c | |||
| 49c29c8681 | |||
| 6f932918aa | |||
| 13642d59d9 | |||
| 25d1dc3c6a | |||
| f1eec81591 | |||
| 46e09d172a | |||
| 9cfa2548fa | |||
| 8a622545ff | |||
| 39191885e5 | |||
| cb59689ead | |||
| 6d0242dead | |||
| caa4f39edd | |||
| a1f0d8f75d | |||
| 076348478e | |||
| ad9baffa62 | |||
| c0c202c5ff | |||
| f69cbba6cd | |||
| ec229ea0af | |||
| d1c52b886b | |||
| f547ae42bb | |||
| fb9af1b216 |
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Setup PHP, with composer and extensions
|
||||
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
|
||||
with:
|
||||
php-version: '8.3'
|
||||
php-version: '8.4'
|
||||
extensions: mbstring, dom, fileinfo, mysql, libxml, xml, xmlwriter, dom, tokenizer, filter, json, phar, pcre, openssl, pdo, intl, curl
|
||||
|
||||
- name: Cache composer dependencies
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Release
|
||||
|
||||
# Stage 1 of the release flow: opens a version-bump PR against 2.x.
|
||||
# When that PR is merged, tag-release.yml (stage 2) creates the tag and
|
||||
# the GitHub release automatically.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
version:
|
||||
description: 'Explicit version (e.g. 2.9.0) — overrides bump type'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-pr:
|
||||
name: Open version bump PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 2.x
|
||||
fetch-depth: 0
|
||||
# Optional: set a RELEASE_TOKEN repo secret (fine-grained PAT with
|
||||
# contents + pull-requests write) so the bump PR triggers CI checks.
|
||||
# PRs created with the default github.token do not trigger workflows.
|
||||
token: ${{ secrets.RELEASE_TOKEN || github.token }}
|
||||
|
||||
- name: Determine new version
|
||||
id: version
|
||||
env:
|
||||
EXPLICIT_VERSION: ${{ inputs.version }}
|
||||
BUMP: ${{ inputs.bump }}
|
||||
run: |
|
||||
current=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
|
||||
if [ -z "$current" ]; then
|
||||
echo "::error::Could not read current version from config/app.php"
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$EXPLICIT_VERSION" ]; then
|
||||
new="$EXPLICIT_VERSION"
|
||||
if ! echo "$new" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Invalid version '$new' — expected X.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
IFS=. read -r major minor patch <<< "$current"
|
||||
case "$BUMP" in
|
||||
major) new="$((major + 1)).0.0" ;;
|
||||
minor) new="$major.$((minor + 1)).0" ;;
|
||||
patch) new="$major.$minor.$((patch + 1))" ;;
|
||||
esac
|
||||
fi
|
||||
if git rev-parse -q --verify "refs/tags/v$new" > /dev/null; then
|
||||
echo "::error::Tag v$new already exists"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bumping $current -> $new"
|
||||
echo "new=$new" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Push bump commit to release branch
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.version.outputs.new }}
|
||||
run: |
|
||||
sed -i -E "s/^([[:space:]]*'version' => ')[0-9.]+(',)/\1$NEW_VERSION\2/" config/app.php
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -am "Bump version to $NEW_VERSION"
|
||||
git push --force origin "HEAD:refs/heads/release/v$NEW_VERSION"
|
||||
|
||||
- name: Open pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
|
||||
NEW_VERSION: ${{ steps.version.outputs.new }}
|
||||
run: |
|
||||
existing=$(gh pr list --head "release/v$NEW_VERSION" --base 2.x --state open --json number -q '.[0].number')
|
||||
if [ -n "$existing" ]; then
|
||||
echo "PR #$existing is already open for release/v$NEW_VERSION"
|
||||
else
|
||||
gh pr create \
|
||||
--base 2.x \
|
||||
--head "release/v$NEW_VERSION" \
|
||||
--title "Bump version to $NEW_VERSION" \
|
||||
--body "Automated version bump. Merging this PR will tag v$NEW_VERSION and publish the GitHub release. Merge it last, once everything for the release is on 2.x."
|
||||
fi
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Tag and release
|
||||
|
||||
# Stage 2 of the release flow: whenever the version in config/app.php changes
|
||||
# on 2.x (normally by merging the PR opened by release.yml, but a hand-made
|
||||
# bump PR works too), create the matching tag and GitHub release.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 2.x
|
||||
paths:
|
||||
- config/app.php
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
tag-release:
|
||||
name: Tag and publish release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create release if version is untagged
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
version=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
|
||||
if [ -z "$version" ]; then
|
||||
echo "::error::Could not read version from config/app.php"
|
||||
exit 1
|
||||
fi
|
||||
if git rev-parse -q --verify "refs/tags/v$version" > /dev/null; then
|
||||
echo "Tag v$version already exists — nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
gh release create "v$version" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "v$version" \
|
||||
--generate-notes
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Tag version check
|
||||
|
||||
# Safety net for manually pushed tags: fails if the tag doesn't match the
|
||||
# version in config/app.php. Tags created by tag-release.yml use GITHUB_TOKEN
|
||||
# and therefore don't trigger this (they always match anyway).
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Tag matches config/app.php
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compare tag with app version
|
||||
run: |
|
||||
tag="${GITHUB_REF_NAME#v}"
|
||||
version=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Tag v$tag does not match config/app.php version $version — bump the version (or use the Release workflow, which does it for you)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Tag v$tag matches config/app.php"
|
||||
@@ -29,3 +29,4 @@ yarn-error.log
|
||||
storage/app/public/avatars/*
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Phrity\Net\Context;
|
||||
use WebSocket\Client;
|
||||
use WebSocket\Exception\Exception as WebSocketException;
|
||||
|
||||
/**
|
||||
* TrueNAS JSON-RPC 2.0 WebSocket Client
|
||||
*
|
||||
* Handles WebSocket communication with TrueNAS using the JSON-RPC 2.0 protocol.
|
||||
* Required for TrueNAS 25.04+ as the REST API is deprecated.
|
||||
*
|
||||
* @see https://api.truenas.com/v25.10/jsonrpc.html
|
||||
*/
|
||||
class TrueNASWebSocketClient
|
||||
{
|
||||
private ?Client $client = null;
|
||||
private string $url;
|
||||
private string $apiKey;
|
||||
private bool $ignoreTls;
|
||||
private bool $authenticated = false;
|
||||
private int $requestId = 1;
|
||||
|
||||
/**
|
||||
* Create a new TrueNAS WebSocket client instance.
|
||||
*
|
||||
* @param string $baseUrl The base URL of the TrueNAS instance (e.g., https://truenas.local)
|
||||
* @param string $apiKey The API key for authentication
|
||||
* @param bool $ignoreTls Whether to skip TLS certificate verification
|
||||
*/
|
||||
public function __construct(string $baseUrl, string $apiKey, bool $ignoreTls = false)
|
||||
{
|
||||
$baseUrl = rtrim($baseUrl, '/');
|
||||
$scheme = parse_url($baseUrl, PHP_URL_SCHEME);
|
||||
$host = parse_url($baseUrl, PHP_URL_HOST);
|
||||
$port = parse_url($baseUrl, PHP_URL_PORT);
|
||||
|
||||
$wsScheme = ($scheme === 'https') ? 'wss' : 'ws';
|
||||
$portPart = $port ? ':' . $port : '';
|
||||
|
||||
$this->url = "{$wsScheme}://{$host}{$portPart}/api/current";
|
||||
$this->apiKey = $apiKey;
|
||||
$this->ignoreTls = $ignoreTls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the TrueNAS WebSocket API and authenticate.
|
||||
*
|
||||
* @return bool True if connection and authentication succeeded
|
||||
* @throws \Exception If connection or authentication fails
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->client !== null && $this->authenticated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build SSL options - always force HTTP/1.1 via ALPN for WebSocket compatibility
|
||||
// TrueNAS nginx defaults to HTTP/2 which doesn't support WebSocket upgrade
|
||||
$sslOptions = [
|
||||
'alpn_protocols' => 'http/1.1',
|
||||
];
|
||||
|
||||
if ($this->ignoreTls) {
|
||||
$sslOptions['verify_peer'] = false;
|
||||
$sslOptions['verify_peer_name'] = false;
|
||||
$sslOptions['allow_self_signed'] = true;
|
||||
}
|
||||
|
||||
// Create context using phrity/net-stream Context class (required by phrity/websocket v3.x)
|
||||
$streamContext = stream_context_create(['ssl' => $sslOptions]);
|
||||
$context = new Context($streamContext);
|
||||
|
||||
try {
|
||||
$this->client = new Client($this->url);
|
||||
$this->client->setTimeout(15);
|
||||
$this->client->setContext($context);
|
||||
|
||||
$authResult = $this->call('auth.login_with_api_key', [$this->apiKey]);
|
||||
|
||||
if ($authResult === true) {
|
||||
$this->authenticated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new \Exception('Authentication failed: Invalid API key');
|
||||
} catch (WebSocketException $e) {
|
||||
Log::error('TrueNAS WebSocket connection failed: ' . $e->getMessage());
|
||||
$this->disconnect();
|
||||
throw new \Exception('WebSocket connection failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a JSON-RPC 2.0 call to the TrueNAS API.
|
||||
*
|
||||
* @param string $method The JSON-RPC method name (e.g., 'system.info')
|
||||
* @param array $params Optional parameters for the method
|
||||
* @return mixed The result from the API call
|
||||
* @throws \Exception If the call fails or returns an error
|
||||
*/
|
||||
public function call(string $method, array $params = [])
|
||||
{
|
||||
if ($this->client === null) {
|
||||
throw new \Exception('WebSocket client not connected');
|
||||
}
|
||||
|
||||
$request = [
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => $method,
|
||||
'id' => $this->requestId++,
|
||||
];
|
||||
|
||||
if (!empty($params)) {
|
||||
$request['params'] = $params;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->client->text(json_encode($request));
|
||||
$response = $this->client->receive();
|
||||
$decoded = json_decode($response->getContent(), true);
|
||||
|
||||
if (isset($decoded['error'])) {
|
||||
$errorMsg = $decoded['error']['message'] ?? 'Unknown error';
|
||||
$errorCode = $decoded['error']['code'] ?? 0;
|
||||
throw new \Exception("API error ({$errorCode}): {$errorMsg}");
|
||||
}
|
||||
|
||||
return $decoded['result'] ?? null;
|
||||
} catch (WebSocketException $e) {
|
||||
Log::error('TrueNAS WebSocket call failed: ' . $e->getMessage());
|
||||
throw new \Exception('WebSocket call failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the WebSocket connection.
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
if ($this->client !== null) {
|
||||
try {
|
||||
$this->client->close();
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('Error closing WebSocket: ' . $e->getMessage());
|
||||
}
|
||||
$this->client = null;
|
||||
$this->authenticated = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the client is connected and authenticated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return $this->client !== null && $this->authenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection by calling core.ping.
|
||||
*
|
||||
* @return bool True if the ping succeeds
|
||||
*/
|
||||
public function ping(): bool
|
||||
{
|
||||
try {
|
||||
$result = $this->call('core.ping');
|
||||
return $result === 'pong';
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up on destruction.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
}
|
||||
@@ -328,12 +328,18 @@ class ItemController extends Controller
|
||||
$config = json_encode($configObject);
|
||||
}
|
||||
|
||||
$current_user = User::currentUser();
|
||||
$request->merge([
|
||||
'description' => $config,
|
||||
'user_id' => $current_user->getId(),
|
||||
]);
|
||||
|
||||
// Only assign ownership when creating; updates must keep the existing owner.
|
||||
if ($id === null) {
|
||||
$current_user = User::currentUser();
|
||||
$request->merge([
|
||||
'user_id' => $current_user->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->input('appid') === 'null' || $request->input('appid') === null) {
|
||||
$request->merge([
|
||||
'class' => null,
|
||||
@@ -348,7 +354,8 @@ class ItemController extends Controller
|
||||
$item = Item::create($request->all());
|
||||
} else {
|
||||
$item = Item::find($id);
|
||||
$item->update($request->all());
|
||||
// Exclude user_id so an update can never reassign ownership
|
||||
$item->update($request->except(['user_id']));
|
||||
}
|
||||
|
||||
$item->parents()->sync($request->tags);
|
||||
@@ -430,7 +437,7 @@ class ItemController extends Controller
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function appload(Request $request): ?string
|
||||
public function appload(Request $request): \Illuminate\Http\JsonResponse|string|null
|
||||
{
|
||||
$output = [];
|
||||
$appid = $request->input('app');
|
||||
@@ -587,18 +594,46 @@ class ItemController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return void
|
||||
* Return live stats for an enhanced application tile.
|
||||
*
|
||||
* Always responds with HTTP 200 and valid JSON so the frontend refresh
|
||||
* loop (liveStatRefresh.js) keeps re-queueing the tile. On any failure we
|
||||
* degrade gracefully to an inactive, empty tile instead of a 500.
|
||||
*
|
||||
* @param int|string $id
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
|
||||
*/
|
||||
public function getStats($id)
|
||||
{
|
||||
$item = Item::find($id);
|
||||
$graceful = response()->json(['status' => 'inactive', 'html' => '']);
|
||||
|
||||
$item = Item::find($id);
|
||||
if ($item === null) {
|
||||
return $graceful;
|
||||
}
|
||||
|
||||
// Non-enhanced items (or stale records) have no live-stats class.
|
||||
if (empty($item->class)) {
|
||||
return $graceful;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = $item->getconfig();
|
||||
|
||||
// Guard against a stale/renamed class string from the remote apps repo.
|
||||
if (! class_exists($item->class)) {
|
||||
return $graceful;
|
||||
}
|
||||
|
||||
$config = $item->getconfig();
|
||||
if (isset($item->class)) {
|
||||
$application = new $item->class;
|
||||
$application->config = $config;
|
||||
echo $application->livestats();
|
||||
|
||||
// livestats() returns a JSON string; return it verbatim (no re-encoding).
|
||||
return response($application->livestats());
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('getStats failed for item '.$id.' ('.$item->class.'): '.$e->getMessage());
|
||||
|
||||
return $graceful;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -21,6 +22,7 @@ class ItemRestController extends Controller
|
||||
public function index(): Collection
|
||||
{
|
||||
$columns = [
|
||||
'id',
|
||||
'title',
|
||||
'colour',
|
||||
'url',
|
||||
@@ -29,11 +31,27 @@ class ItemRestController extends Controller
|
||||
'appdescription',
|
||||
];
|
||||
|
||||
return Item::select($columns)
|
||||
return Item::with('parents')
|
||||
->select($columns)
|
||||
->where('deleted_at', null)
|
||||
->where('type', '0')
|
||||
->orderBy('order', 'asc')
|
||||
->get();
|
||||
->get()
|
||||
->map(function (Item $item) {
|
||||
return [
|
||||
'title' => $item->title,
|
||||
'colour' => $item->colour,
|
||||
'url' => $item->url,
|
||||
'description' => $item->description,
|
||||
'appid' => $item->appid,
|
||||
'appdescription' => $item->appdescription,
|
||||
'tags' => $item->parents
|
||||
->where('id', '!=', 0)
|
||||
->pluck('title')
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +68,16 @@ class ItemRestController extends Controller
|
||||
*/
|
||||
public function store(Request $request): object
|
||||
{
|
||||
// Imports pass tags as an array of tag titles so they can round-trip
|
||||
// across instances. Resolve those titles into local tag ids (creating
|
||||
// any that don't yet exist) before handing off to the shared store
|
||||
// logic. When no tags are supplied we keep the previous behaviour.
|
||||
if ($request->has('tags')) {
|
||||
$request->merge([
|
||||
'tags' => $this->resolveTags($request->input('tags')),
|
||||
]);
|
||||
}
|
||||
|
||||
$item = ItemController::storelogic($request);
|
||||
|
||||
if ($item) {
|
||||
@@ -59,6 +87,61 @@ class ItemRestController extends Controller
|
||||
return (object) ['status' => 'FAILED'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an incoming list of tags into tag ids.
|
||||
*
|
||||
* Numeric 0 (or "0") maps to the root/default dashboard. Every other entry
|
||||
* is treated as a tag title: an existing tag with that title is reused, and
|
||||
* a missing one is created. The lookup keeps the operation idempotent so
|
||||
* importing many items that share a tag title only ever creates one tag.
|
||||
*
|
||||
* @param mixed $tags
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function resolveTags($tags): array
|
||||
{
|
||||
if (! is_array($tags)) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag === 0 || $tag === '0') {
|
||||
$ids[] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = is_string($tag) ? trim($tag) : $tag;
|
||||
|
||||
if ($title === '' || $title === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = Item::where('type', '1')
|
||||
->where('title', $title)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$ids[] = (int) $existing->id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$created = Item::create([
|
||||
'title' => $title,
|
||||
'type' => '1',
|
||||
'url' => str_slug($title, '-', 'en_US'),
|
||||
'user_id' => User::currentUser()->getId(),
|
||||
]);
|
||||
|
||||
$ids[] = (int) $created->id;
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique($ids));
|
||||
|
||||
return empty($ids) ? [0] : $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
|
||||
@@ -142,7 +142,8 @@ class TagController extends Controller
|
||||
'url' => $slug,
|
||||
]);
|
||||
|
||||
Item::find($id)->update($request->all());
|
||||
// Exclude user_id so an update can never reassign ownership
|
||||
Item::find($id)->update($request->except(['user_id']));
|
||||
|
||||
$route = route('dash', []);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -154,6 +155,11 @@ class UserController extends Controller
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
if ($user->id !== 1) {
|
||||
// Hard-delete this user's items (tiles and tags) so they don't become
|
||||
// orphaned; item_tag pivot rows cascade via the existing FK. Shared
|
||||
// items (user_id = 0) are left untouched.
|
||||
Item::withoutGlobalScopes()->where('user_id', $user->id)->forceDelete();
|
||||
|
||||
$user->delete();
|
||||
$route = route('dash', []);
|
||||
|
||||
|
||||
+3
-1
@@ -86,7 +86,9 @@ class Item extends Model
|
||||
static::addGlobalScope('user_id', function (Builder $builder) {
|
||||
$current_user = User::currentUser();
|
||||
if ($current_user) {
|
||||
$builder->where('user_id', $current_user->getId())->orWhere('user_id', 0);
|
||||
$builder->where(function ($query) use ($current_user) {
|
||||
$query->where('user_id', $current_user->getId())->orWhere('user_id', 0);
|
||||
});
|
||||
} else {
|
||||
$builder->where('user_id', 0);
|
||||
}
|
||||
|
||||
@@ -150,9 +150,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
$db_type = config()->get('database.default');
|
||||
|
||||
if ($db_type == 'sqlite') {
|
||||
$db_file = database_path(env('DB_DATABASE', 'app.sqlite'));
|
||||
$db_file = config()->get('database.connections.sqlite.database');
|
||||
Log::debug('SQLite Database Path: ' . $db_file);
|
||||
if (! is_file($db_file)) {
|
||||
// Do not create a file for the in-memory database identifier.
|
||||
if ($db_file !== ':memory:' && ! is_file($db_file)) {
|
||||
touch($db_file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,12 @@ class Setting extends Model
|
||||
$options = (array) json_decode($this->options);
|
||||
if ($this->key === 'search_provider') {
|
||||
$options = Search::providers()->pluck('name', 'id')->toArray();
|
||||
} elseif ($this->key === 'default_tag') {
|
||||
$options = [];
|
||||
$tags = Item::where('type', 1)->where('id', '>', 0)->pinned()->orderBy('title', 'asc')->get();
|
||||
foreach ($tags as $tag) {
|
||||
$options[$tag->tag_url] = $tag->title;
|
||||
}
|
||||
}
|
||||
$value = (array_key_exists($this->value, $options))
|
||||
? __($options[$this->value])
|
||||
@@ -190,6 +196,12 @@ class Setting extends Model
|
||||
$options = json_decode($this->options);
|
||||
if ($this->key === 'search_provider') {
|
||||
$options = Search::providers()->pluck('name', 'id');
|
||||
} elseif ($this->key === 'default_tag') {
|
||||
$options = ['' => 'app.options.none'];
|
||||
$tags = Item::where('type', 1)->where('id', '>', 0)->pinned()->orderBy('title', 'asc')->get();
|
||||
foreach ($tags as $tag) {
|
||||
$options[$tag->tag_url] = $tag->title;
|
||||
}
|
||||
}
|
||||
$value = '<select name="value" class="form-control">';
|
||||
foreach ($options as $key => $opt) {
|
||||
|
||||
@@ -85,6 +85,11 @@ abstract class SupportedApps
|
||||
'connect_timeout' => 15,
|
||||
] : $overridevars;
|
||||
|
||||
// Check global setting to skip TLS verification (useful for self-signed certificates)
|
||||
if (Setting::fetch('skip_tls_verification')) {
|
||||
$vars['verify'] = false;
|
||||
}
|
||||
|
||||
$client = new Client($vars);
|
||||
|
||||
$method = ($overridemethod === null || $overridemethod === false) ? $this->method : $overridemethod;
|
||||
|
||||
+10
-6
@@ -8,17 +8,18 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php": "^8.4",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"enshrined/svg-sanitize": "^0.21.0",
|
||||
"graham-campbell/github": "^12.5",
|
||||
"enshrined/svg-sanitize": "^0.22.0",
|
||||
"graham-campbell/github": "^13.0",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"laravel/framework": "^11.45",
|
||||
"laravel/tinker": "^2.9",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/ui": "^4.4",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phrity/websocket": "^3.6",
|
||||
"spatie/laravel-html": "^3.11",
|
||||
"spatie/laravel-ignition": "^2.4",
|
||||
"symfony/yaml": "^7.0"
|
||||
@@ -27,7 +28,7 @@
|
||||
"barryvdh/laravel-ide-helper": "^3.0",
|
||||
"filp/whoops": "^2.8",
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"squizlabs/php_codesniffer": "3.*",
|
||||
"symfony/thanks": "^1.2",
|
||||
"fakerphp/faker": "^1.23"
|
||||
@@ -84,6 +85,9 @@
|
||||
"kylekatarnls/update-helper": true,
|
||||
"symfony/thanks": true,
|
||||
"php-http/discovery": true
|
||||
},
|
||||
"platform": {
|
||||
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
|
||||
Generated
+2447
-1409
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
|
||||
'version' => '2.7.7',
|
||||
'version' => '2.8.1',
|
||||
|
||||
'appsource' => env('APP_SOURCE', 'https://appslist.heimdall.site/'),
|
||||
|
||||
|
||||
+5
-1
@@ -7,7 +7,11 @@ return [
|
||||
'connections' => [
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => database_path(env('DB_DATABASE', 'app.sqlite')), // Make sure to use the correct path
|
||||
// Use the correct path, but let the special in-memory identifier
|
||||
// pass through untouched so tests can run against ':memory:'.
|
||||
'database' => env('DB_DATABASE', 'app.sqlite') === ':memory:'
|
||||
? ':memory:'
|
||||
: database_path(env('DB_DATABASE', 'app.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), // Enable foreign key constraints
|
||||
],
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Reassign items whose user_id references a user that no longer exists to a
|
||||
* surviving user, so previously orphaned tiles and tags become visible again.
|
||||
* user_id = 0 means "shared with all users" and is never treated as orphaned.
|
||||
*
|
||||
* The DB query builder is used directly (not the Item model) so the global
|
||||
* scope and soft-delete constraints don't interfere.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Prefer user id 1 if it still exists, otherwise the lowest existing id.
|
||||
$target = DB::table('users')->where('id', 1)->value('id')
|
||||
?? DB::table('users')->min('id');
|
||||
|
||||
// No users left: nothing to reassign to.
|
||||
if ($target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('items')
|
||||
->where('user_id', '!=', 0)
|
||||
->whereNotIn('user_id', function ($query) {
|
||||
$query->select('id')->from('users');
|
||||
})
|
||||
->update(['user_id' => $target]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* This is a data migration; the original ownership cannot be recovered.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -349,5 +349,34 @@ class SettingsSeeder extends Seeder
|
||||
$setting->label = 'app.settings.treat_tags_as';
|
||||
$setting->save();
|
||||
}
|
||||
|
||||
if (! $setting = Setting::find(15)) {
|
||||
$setting = new Setting;
|
||||
$setting->id = 15;
|
||||
$setting->group_id = 4;
|
||||
$setting->key = 'default_tag';
|
||||
$setting->type = 'select';
|
||||
$setting->label = 'app.settings.default_tag';
|
||||
$setting->value = '';
|
||||
$setting->save();
|
||||
} else {
|
||||
$setting->group_id = 4;
|
||||
$setting->label = 'app.settings.default_tag';
|
||||
$setting->save();
|
||||
}
|
||||
|
||||
if (! $setting = Setting::find(16)) {
|
||||
$setting = new Setting;
|
||||
$setting->id = 16;
|
||||
$setting->group_id = 4;
|
||||
$setting->key = 'skip_tls_verification';
|
||||
$setting->type = 'boolean';
|
||||
$setting->label = 'app.settings.skip_tls_verification';
|
||||
$setting->value = '0';
|
||||
$setting->save();
|
||||
} else {
|
||||
$setting->label = 'app.settings.skip_tls_verification';
|
||||
$setting->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ return array (
|
||||
'settings.view' => 'Ansicht',
|
||||
'settings.custom_css' => 'Angepasstes CSS',
|
||||
'settings.custom_js' => 'Angepasstes JavaScript',
|
||||
'settings.treat_tags_as' => 'Tags behandeln als:',
|
||||
'settings.default_tag' => 'Standard Tag',
|
||||
'settings.folders' => 'Ordner',
|
||||
'settings.tags' => 'Tags',
|
||||
'settings.categories' => 'Kategorien',
|
||||
'settings.skip_tls_verification' => 'TLS-Überprüfung überspringen (selbstsignierte Zertifikate)',
|
||||
'options.none' => '- nicht festgelegt -',
|
||||
'options.google' => 'Google',
|
||||
'options.ddg' => 'DuckDuckGo',
|
||||
@@ -74,6 +80,7 @@ return array (
|
||||
'apps.only_admin_account' => 'Nur mit Admin-Konto!',
|
||||
'apps.autologin_url' => 'Auto Login URL',
|
||||
'apps.show_deleted' => 'Gelöschte Anwendung anzeigen',
|
||||
'app.import' => 'Importieren',
|
||||
'dashboard' => 'Home Dashboard',
|
||||
'user.user_list' => 'Nutzer',
|
||||
'user.add_user' => 'Nutzer hinzufügen',
|
||||
@@ -88,6 +95,8 @@ return array (
|
||||
'delete' => 'Löschen',
|
||||
'optional' => 'Optional',
|
||||
'restore' => 'Wiederherstellen',
|
||||
'export' => 'Exportieren',
|
||||
'import' => 'Importieren',
|
||||
'alert.success.item_created' => 'Element erfolgreich erstellt',
|
||||
'alert.success.item_updated' => 'Element erfolgreich aktualisiert',
|
||||
'alert.success.item_deleted' => 'Element erfolgreich gelöscht',
|
||||
@@ -99,6 +108,8 @@ return array (
|
||||
'alert.success.tag_restored' => 'Tag erfolgreich wiederhergestellt',
|
||||
'alert.success.setting_updated' => 'Die Einstellungen wurden übernommen',
|
||||
'alert.error.not_exist' => 'Diese Einstellung existiert nicht.',
|
||||
'alert.error.file_too_big' => 'Datei zu groß',
|
||||
'alert.error.file_not_stored' => 'Datei konnte nicht gespeichert werden',
|
||||
'alert.success.user_created' => 'Nutzer erfolgreich erstellt',
|
||||
'alert.success.user_updated' => 'Nutzer erfolgreich aktualisiert',
|
||||
'alert.success.user_deleted' => 'Nutzer erfolgreich gelöscht',
|
||||
|
||||
@@ -29,9 +29,11 @@ return array (
|
||||
'settings.custom_css' => 'Custom CSS',
|
||||
'settings.custom_js' => 'Custom JavaScript',
|
||||
'settings.treat_tags_as' => 'Treat Tags As:',
|
||||
'settings.default_tag' => 'Default tag',
|
||||
'settings.folders' => 'Folders',
|
||||
'settings.tags' => 'Tags',
|
||||
'settings.categories' => 'Categories',
|
||||
'settings.skip_tls_verification' => 'Skip TLS Verification (for self-signed certificates)',
|
||||
'options.none' => '- not set -',
|
||||
'options.google' => 'Google',
|
||||
'options.ddg' => 'DuckDuckGo',
|
||||
|
||||
Generated
+272
-278
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Heimdall",
|
||||
"name": "Heimdall-LS",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -24,26 +24,14 @@
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -52,30 +40,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.6",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -91,13 +81,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -119,13 +110,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -189,10 +181,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -211,27 +204,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
|
||||
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.3"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -253,10 +248,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
||||
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -309,28 +305,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -350,25 +349,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
|
||||
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -955,15 +956,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
|
||||
"integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz",
|
||||
"integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1491,31 +1493,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1523,13 +1527,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
|
||||
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1658,6 +1663,17 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -2036,15 +2052,6 @@
|
||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -2915,12 +2922,6 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/assert": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
|
||||
@@ -3069,7 +3070,8 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -3119,10 +3121,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"dev": true
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.4.tgz",
|
||||
"integrity": "sha512-QL7sb18rJ1PbdsKsqPA0guxL563vIMwRHgzNrW/uzQuRGN1Cjqd/wonUBAVqHox9KwzHA6vCbM0lXx3k4iQMow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
@@ -3163,21 +3166,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/bonjour-service": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
|
||||
@@ -3201,10 +3189,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -3730,7 +3719,8 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
@@ -3858,12 +3848,6 @@
|
||||
"elliptic": "^6.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
@@ -4354,12 +4338,6 @@
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -4557,12 +4535,6 @@
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/elliptic/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -5218,21 +5190,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5286,9 +5243,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5299,7 +5256,8 @@
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fastest-levenshtein": {
|
||||
"version": "1.0.16",
|
||||
@@ -5492,15 +5450,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5508,6 +5467,7 @@
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -6174,10 +6134,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz",
|
||||
"integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
@@ -6325,10 +6286,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
@@ -6957,10 +6919,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -7265,13 +7238,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
|
||||
"integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"shell-quote": "^1.8.1"
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
@@ -7341,10 +7315,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -7520,12 +7495,6 @@
|
||||
"miller-rabin": "bin/miller-rabin"
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -7620,10 +7589,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -7660,9 +7630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -7670,6 +7640,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -7716,10 +7687,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -7966,10 +7938,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8241,10 +8214,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"dev": true
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@@ -8310,10 +8284,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -8395,9 +8370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8413,8 +8388,9 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -9090,12 +9066,6 @@
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
@@ -9103,12 +9073,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -9685,6 +9657,16 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
|
||||
@@ -9785,12 +9767,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz",
|
||||
"integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
@@ -9998,10 +9981,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
|
||||
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -10016,14 +10000,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
@@ -10035,13 +10020,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -10454,17 +10440,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
|
||||
"integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz",
|
||||
"integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^4.1.3",
|
||||
"css-tree": "^1.1.3",
|
||||
"csso": "^4.2.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"sax": "^1.5.0",
|
||||
"stable": "^0.1.8"
|
||||
},
|
||||
"bin": {
|
||||
@@ -10986,12 +10973,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
@@ -11680,10 +11672,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -11725,10 +11718,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,32 @@
|
||||
"dependencies": {
|
||||
"select2": "~4.0.13",
|
||||
"sortablejs": "^1.15.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.7",
|
||||
"@babel/plugin-transform-modules-systemjs": "^7.29.7",
|
||||
"bn.js": "^5.2.4",
|
||||
"brace-expansion": "^1.1.16",
|
||||
"fast-uri": "^3.1.3",
|
||||
"flatted": "^3.4.2",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"http-proxy-middleware": "^2.0.10",
|
||||
"immutable": "^5.1.9",
|
||||
"js-yaml": "^4.3.0",
|
||||
"launch-editor": "^2.14.1",
|
||||
"lodash": "^4.18.1",
|
||||
"minimatch": "^3.1.5",
|
||||
"node-forge": "^1.4.0",
|
||||
"on-headers": "^1.1.0",
|
||||
"path-to-regexp": "^0.1.13",
|
||||
"picomatch": "^2.3.2",
|
||||
"postcss": "^8.5.16",
|
||||
"qs": "^6.15.3",
|
||||
"serialize-javascript": "^7.0.7",
|
||||
"shell-quote": "^1.9.0",
|
||||
"svgo": "^2.8.2",
|
||||
"uuid": "^11.1.1",
|
||||
"ws": "^8.21.0",
|
||||
"yaml": "^1.10.3"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
@@ -17,12 +12,17 @@
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
||||
Vendored
+3
-2
@@ -927,9 +927,10 @@ div.create .input input, div.create .input select {
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
max-width: 60px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
|
||||
Vendored
+18
-2
@@ -4271,7 +4271,12 @@ $.when($.ready).then(function () {
|
||||
data.url = apiurl;
|
||||
$(".config-item").each(function () {
|
||||
var config = $(this).data("config");
|
||||
data[config] = $(this).val();
|
||||
// For checkboxes, use checked state instead of value attribute
|
||||
if ($(this).is(":checkbox")) {
|
||||
data[config] = $(this).is(":checked") ? "1" : "0";
|
||||
} else {
|
||||
data[config] = $(this).val();
|
||||
}
|
||||
});
|
||||
data.id = $("form[data-item-id]").data("item-id");
|
||||
if (data.password && data.password === fakePassword) {
|
||||
@@ -4287,6 +4292,14 @@ $.when($.ready).then(function () {
|
||||
alert("Something went wrong: ".concat(responseData.responseText.substring(0, 100)));
|
||||
});
|
||||
});
|
||||
// Auto-select the configured default tag on load (tags mode only)
|
||||
var taglist = document.getElementById("taglist");
|
||||
if (taglist !== null) {
|
||||
var defaultTag = taglist.getAttribute("data-default-tag");
|
||||
if (typeof defaultTag === "string" && defaultTag !== "") {
|
||||
$("#taglist .tag[data-tag=\"tag-".concat(defaultTag, "\"]")).trigger("click");
|
||||
}
|
||||
}
|
||||
$("#pinlist").on("click", "a", function (e) {
|
||||
e.preventDefault();
|
||||
var current = $(this);
|
||||
@@ -4446,7 +4459,7 @@ var getCSRFToken = function getCSRFToken() {
|
||||
var mergeItemWithAppDetails = function mergeItemWithAppDetails(item, appDetails) {
|
||||
return {
|
||||
pinned: 1,
|
||||
tags: [0],
|
||||
tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0],
|
||||
appid: item.appid,
|
||||
title: item.title,
|
||||
colour: item.colour,
|
||||
@@ -4476,6 +4489,9 @@ var fetchAppDetails = function fetchAppDetails(appId) {
|
||||
app: appId
|
||||
})
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error("Failed to find app id: ".concat(appId)));
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ Supported applications are recognized by the title of the application as entered
|
||||
[](https://apps.heimdall.site/applications/foundation)
|
||||
|
||||
## Installing
|
||||
Apart from the Laravel 10 dependencies, namely PHP >= 8.1, Ctype PHP Extension, cURL PHP Extension, DOM PHP Extension, Fileinfo PHP Extension, Filter PHP Extension, Hash PHP Extension, Mbstring PHP Extension, OpenSSL PHP Extension, PCRE PHP Extension, PDO PHP Extension, Session PHP Extension, Tokenizer PHP Extension, XML PHP Extension, the only other thing Heimdall needs is sqlite support and zip support (php-zip).
|
||||
Apart from the Laravel 11 dependencies, namely PHP >= 8.4, Ctype PHP Extension, cURL PHP Extension, DOM PHP Extension, Fileinfo PHP Extension, Filter PHP Extension, Hash PHP Extension, Mbstring PHP Extension, OpenSSL PHP Extension, PCRE PHP Extension, PDO PHP Extension, Session PHP Extension, Tokenizer PHP Extension, XML PHP Extension, the only other thing Heimdall needs is sqlite support and zip support (php-zip).
|
||||
|
||||
If you find you can't change the background make sure `php_fileinfo` is enabled in your php.ini. I believe `php_fileinfo` should be enabled by default, but one user came across the issue on a windows system.
|
||||
|
||||
|
||||
@@ -310,7 +310,12 @@ $.when($.ready).then(() => {
|
||||
data.url = apiurl;
|
||||
$(".config-item").each(function () {
|
||||
const config = $(this).data("config");
|
||||
data[config] = $(this).val();
|
||||
// For checkboxes, use checked state instead of value attribute
|
||||
if ($(this).is(":checkbox")) {
|
||||
data[config] = $(this).is(":checked") ? "1" : "0";
|
||||
} else {
|
||||
data[config] = $(this).val();
|
||||
}
|
||||
});
|
||||
|
||||
data.id = $("form[data-item-id]").data("item-id");
|
||||
@@ -334,6 +339,15 @@ $.when($.ready).then(() => {
|
||||
);
|
||||
});
|
||||
});
|
||||
// Auto-select the configured default tag on load (tags mode only)
|
||||
const taglist = document.getElementById("taglist");
|
||||
if (taglist !== null) {
|
||||
const defaultTag = taglist.getAttribute("data-default-tag");
|
||||
if (typeof defaultTag === "string" && defaultTag !== "") {
|
||||
$(`#taglist .tag[data-tag="tag-${defaultTag}"]`).trigger("click");
|
||||
}
|
||||
}
|
||||
|
||||
$("#pinlist").on("click", "a", function (e) {
|
||||
e.preventDefault();
|
||||
const current = $(this);
|
||||
|
||||
Vendored
+11
-2
@@ -60,7 +60,7 @@ const getCSRFToken = () => {
|
||||
*/
|
||||
const mergeItemWithAppDetails = (item, appDetails) => ({
|
||||
pinned: 1,
|
||||
tags: [0],
|
||||
tags: Array.isArray(item.tags) && item.tags.length ? item.tags : [0],
|
||||
|
||||
appid: item.appid,
|
||||
title: item.title,
|
||||
@@ -92,7 +92,16 @@ const fetchAppDetails = (appId) => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ app: appId }),
|
||||
}).then((response) => response.json());
|
||||
}).then((response) => {
|
||||
// A missing app now returns a genuine 404 (see ItemController::appload).
|
||||
// fetch() does not reject on 4xx, so surface it as a rejection here to
|
||||
// keep importItems reporting "Failed to find app id" rather than treating
|
||||
// the {"error":...} body as a successful import.
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error(`Failed to find app id: ${appId}`));
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -722,9 +722,10 @@ div.create {
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
.app-icon {
|
||||
max-width: 60px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
|
||||
@@ -3,7 +3,7 @@ $treat_tags_as = \App\Setting::fetch('treat_tags_as');
|
||||
?>
|
||||
@if( $treat_tags_as == 'tags')
|
||||
@if($taglist->first())
|
||||
<div id="taglist" class="taglist">
|
||||
<div id="taglist" class="taglist" data-default-tag="{{ \App\Setting::fetch('default_tag') }}">
|
||||
<div class="tag white current" data-tag="all">All</div>
|
||||
@foreach($taglist as $tag)
|
||||
<div class="tag link{{ title_color($tag->colour) }}" style="background-color: {{ $tag->colour }}" data-tag="tag-{{ $tag->tag_url }}">{{ $tag->title }}</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* End-to-end coverage for the AJAX POST endpoints that the dashboard relies
|
||||
* on (the routes excluded from CSRF verification: order / appload).
|
||||
*
|
||||
* CSRF itself is disabled while running unit tests, so these tests deliberately
|
||||
* do NOT assert "an un-tokened POST succeeds" (that would be meaningless).
|
||||
* Instead they exercise the controller + routing end-to-end with realistic
|
||||
* input and assert the real, observable behaviour, which is what would break
|
||||
* if the controller or router regressed on a framework upgrade.
|
||||
*/
|
||||
class AjaxPostEndpointsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_order_endpoint_persists_the_new_item_order(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$first = Item::factory()->create(['order' => 5]);
|
||||
$second = Item::factory()->create(['order' => 9]);
|
||||
|
||||
// POST the ids in reverse: index 0 => $second, index 1 => $first.
|
||||
$response = $this->post('/order', [
|
||||
'order' => [$second->id, $first->id],
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$this->assertSame(0, (int) $second->fresh()->order);
|
||||
$this->assertSame(1, (int) $first->fresh()->order);
|
||||
}
|
||||
|
||||
public function test_appload_returns_null_for_the_none_selection(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/appload', ['app' => 'null']);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertSame('', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_appload_surfaces_a_not_found_error_for_an_unknown_app(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->post('/appload', ['app' => 'this-app-does-not-exist']);
|
||||
|
||||
// For an unknown app the controller returns a genuine 404 JSON
|
||||
// response. appload() is declared to return
|
||||
// JsonResponse|string|null, so the JsonResponse is served as-is
|
||||
// (correct status + JSON body) rather than being coerced through
|
||||
// Response::__toString() into a raw HTTP message served as a 200.
|
||||
$response->assertStatus(404);
|
||||
$response->assertExactJson(['error' => 'Application not found.']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Controllers\ItemController;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use ReflectionClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the CSRF configuration wired up in bootstrap/app.php.
|
||||
*
|
||||
* Laravel's request-forgery middleware self-disables while running unit tests,
|
||||
* so we cannot observe CSRF at request time. Instead we assert the *configuration*
|
||||
* directly: the three excluded URIs really are registered on the framework's
|
||||
* PreventRequestForgery middleware, and the routes those exceptions cover still
|
||||
* resolve to the expected controller actions. Either check would fail if a
|
||||
* framework upgrade renamed / deprecated the exception API (validateCsrfTokens()
|
||||
* now proxies preventRequestForgery()) or changed how the except list is stored,
|
||||
* or if the AJAX routes were dropped.
|
||||
*/
|
||||
class CsrfExceptionsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function csrfExceptUris(): array
|
||||
{
|
||||
// The except list is stored in the protected static $neverVerify
|
||||
// property that Middleware::validateCsrfTokens(except: [...]) feeds.
|
||||
$reflection = new ReflectionClass(PreventRequestForgery::class);
|
||||
$property = $reflection->getProperty('neverVerify');
|
||||
$property->setAccessible(true);
|
||||
|
||||
return (array) $property->getValue();
|
||||
}
|
||||
|
||||
public function test_ajax_routes_are_registered_as_csrf_exceptions(): void
|
||||
{
|
||||
$except = $this->csrfExceptUris();
|
||||
|
||||
$this->assertContains('order', $except);
|
||||
$this->assertContains('appload', $except);
|
||||
$this->assertContains('test_config', $except);
|
||||
}
|
||||
|
||||
public function test_csrf_excepted_routes_resolve_to_the_expected_actions(): void
|
||||
{
|
||||
$expected = [
|
||||
'items.order' => ['order', 'setOrder'],
|
||||
'appload' => ['appload', 'appload'],
|
||||
'test_config' => ['test_config', 'testConfig'],
|
||||
];
|
||||
|
||||
foreach ($expected as $name => [$uri, $method]) {
|
||||
$route = Route::getRoutes()->getByName($name);
|
||||
|
||||
$this->assertNotNull($route, "Route [{$name}] is not registered.");
|
||||
$this->assertSame($uri, $route->uri());
|
||||
$this->assertContains('POST', $route->methods());
|
||||
$this->assertSame(ItemController::class . '@' . $method, $route->getActionName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\ItemTag;
|
||||
use App\Setting;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -80,4 +81,25 @@ class DashTest extends TestCase
|
||||
$response->assertSee('Tag 1');
|
||||
$response->assertSee('Tag 2');
|
||||
}
|
||||
|
||||
public function test_dash_exposes_the_configured_default_tag(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
Setting::where('key', 'treat_tags_as')->update(['value' => 'tags']);
|
||||
Setting::where('key', 'default_tag')->update(['value' => 'home-dashboard']);
|
||||
|
||||
Item::factory()->create([
|
||||
'title' => 'Home',
|
||||
'url' => 'home-dashboard',
|
||||
'type' => 1,
|
||||
'pinned' => 1,
|
||||
'user_id' => 0,
|
||||
]);
|
||||
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('data-default-tag="home-dashboard"', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Fixture app whose live stats rendering throws, mirroring the Komga
|
||||
* failure from issue #1558 (broken remote blade / upstream API error).
|
||||
*/
|
||||
class ThrowingStatApp
|
||||
{
|
||||
public $config;
|
||||
|
||||
public function livestats()
|
||||
{
|
||||
throw new \Exception('boom');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixture app whose live stats rendering succeeds and returns the JSON
|
||||
* string the frontend expects.
|
||||
*/
|
||||
class HappyStatApp
|
||||
{
|
||||
public $config;
|
||||
|
||||
public function livestats()
|
||||
{
|
||||
return json_encode(['status' => 'active', 'html' => '<b>ok</b>']);
|
||||
}
|
||||
}
|
||||
|
||||
class GetStatsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_missing_item_id_does_not_500(): void
|
||||
{
|
||||
$response = $this->get('get_stats/999999');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['status' => 'inactive', 'html' => '']);
|
||||
}
|
||||
|
||||
public function test_throwing_app_degrades_gracefully(): void
|
||||
{
|
||||
$item = Item::factory()->create([
|
||||
'class' => ThrowingStatApp::class,
|
||||
]);
|
||||
|
||||
$response = $this->get('get_stats/'.$item->id);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['status' => 'inactive', 'html' => '']);
|
||||
}
|
||||
|
||||
public function test_item_with_no_class_degrades_gracefully(): void
|
||||
{
|
||||
$item = Item::factory()->create([
|
||||
'class' => null,
|
||||
]);
|
||||
|
||||
$response = $this->get('get_stats/'.$item->id);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['status' => 'inactive', 'html' => '']);
|
||||
}
|
||||
|
||||
public function test_happy_path_returns_livestats_output_verbatim(): void
|
||||
{
|
||||
$item = Item::factory()->create([
|
||||
'class' => HappyStatApp::class,
|
||||
]);
|
||||
|
||||
$expected = json_encode(['status' => 'active', 'html' => '<b>ok</b>']);
|
||||
|
||||
$response = $this->get('get_stats/'.$item->id);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertSame($expected, $response->getContent());
|
||||
$response->assertJson(['status' => 'active', 'html' => '<b>ok</b>']);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Feature;
|
||||
use App\Item;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ItemExportTest extends TestCase
|
||||
@@ -34,7 +35,40 @@ class ItemExportTest extends TestCase
|
||||
|
||||
$response = $this->get('api/item');
|
||||
|
||||
$response->assertExactJson([(object)$exampleItem]);
|
||||
$response->assertExactJson([$exampleItem + ["tags" => []]]);
|
||||
}
|
||||
|
||||
public function test_exports_assigned_tag_titles_excluding_the_root_tag(): void
|
||||
{
|
||||
// Mirror the root/default dashboard row that production seeds (id 0),
|
||||
// so the item_tag pivot's foreign key to items.id is satisfied on a
|
||||
// fresh in-memory database.
|
||||
DB::table('items')->insert([
|
||||
'id' => 0,
|
||||
'title' => 'app.dashboard',
|
||||
'url' => '',
|
||||
'type' => 1,
|
||||
'user_id' => 0,
|
||||
'pinned' => 0,
|
||||
]);
|
||||
|
||||
$item = Item::factory()
|
||||
->create([
|
||||
'title' => 'Tagged Item',
|
||||
]);
|
||||
$tag = Item::factory()
|
||||
->create([
|
||||
'type' => 1,
|
||||
'title' => 'Media',
|
||||
]);
|
||||
|
||||
// Assign both the root/default dashboard (id 0) and the Media tag.
|
||||
$item->parents()->sync([0, $tag->id]);
|
||||
|
||||
$response = $this->get('api/item');
|
||||
|
||||
$response->assertJsonCount(1);
|
||||
$response->assertJsonPath('0.tags', ['Media']);
|
||||
}
|
||||
|
||||
public function test_returns_all_items(): void
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\ItemTag;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ItemImportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function importPayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'pinned' => 1,
|
||||
'appid' => 'null',
|
||||
'website' => null,
|
||||
'title' => 'Item A',
|
||||
'colour' => '#00f',
|
||||
'url' => 'http://10.0.1.1',
|
||||
'tags' => [0],
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
public function test_import_creates_and_assigns_a_tag_from_its_title(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => ['Media'],
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson(['status' => 'OK']);
|
||||
|
||||
$tag = Item::where('type', 1)->where('title', 'Media')->first();
|
||||
$this->assertNotNull($tag);
|
||||
$this->assertSame(1, (int) $tag->type);
|
||||
|
||||
$item = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$this->assertNotNull($item);
|
||||
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $item->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_import_reuses_an_existing_tag_for_the_same_title(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => ['Media'],
|
||||
]))->assertStatus(200);
|
||||
|
||||
$this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item B',
|
||||
'tags' => ['Media'],
|
||||
]))->assertStatus(200);
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
Item::where('type', 1)->where('title', 'Media')->count()
|
||||
);
|
||||
|
||||
$tag = Item::where('type', 1)->where('title', 'Media')->first();
|
||||
$itemA = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$itemB = Item::where('type', 0)->where('title', 'Item B')->first();
|
||||
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $itemA->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $itemB->id)->where('tag_id', $tag->id)->exists()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_import_with_root_tag_only_creates_no_tags(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->postJson('api/item', $this->importPayload([
|
||||
'title' => 'Item A',
|
||||
'tags' => [0],
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
// No stray tag items should have been created beyond the seeded
|
||||
// root/default dashboard tag (id 0).
|
||||
$this->assertSame(0, Item::where('type', 1)->where('id', '>', 0)->count());
|
||||
|
||||
// The item should be assigned to the root/default dashboard (tag id 0).
|
||||
$item = Item::where('type', 0)->where('title', 'Item A')->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertTrue(
|
||||
ItemTag::where('item_id', $item->id)->where('tag_id', 0)->exists()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ItemOwnershipTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Create a passwordless user (so the "allowed" middleware lets the
|
||||
* request through) and make it the current session user.
|
||||
*/
|
||||
private function actAsCurrentUser(array $attributes = []): User
|
||||
{
|
||||
$user = User::factory()->create(array_merge([
|
||||
'password' => null,
|
||||
'public_front' => 1,
|
||||
], $attributes));
|
||||
|
||||
$this->withSession(['current_user' => $user]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function test_creating_an_item_assigns_the_creator_as_owner(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$creator = $this->actAsCurrentUser();
|
||||
|
||||
$response = $this->post('/items', [
|
||||
'pinned' => 1,
|
||||
'appid' => 'null',
|
||||
'website' => null,
|
||||
'title' => 'Owned Item',
|
||||
'colour' => '#00f',
|
||||
'url' => 'http://10.0.1.1',
|
||||
'tags' => [0],
|
||||
]);
|
||||
|
||||
$response->assertStatus(302);
|
||||
|
||||
$item = Item::withoutGlobalScopes()->where('title', 'Owned Item')->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertSame($creator->id, (int) $item->user_id);
|
||||
}
|
||||
|
||||
public function test_creating_an_item_ignores_a_crafted_user_id(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$creator = $this->actAsCurrentUser();
|
||||
$other = User::factory()->create();
|
||||
|
||||
$response = $this->post('/items', [
|
||||
'pinned' => 1,
|
||||
'appid' => 'null',
|
||||
'title' => 'Crafted Owner Item',
|
||||
'colour' => '#00f',
|
||||
'url' => 'http://10.0.1.2',
|
||||
'user_id' => $other->id, // attempt to create on behalf of another user
|
||||
'tags' => [0],
|
||||
]);
|
||||
|
||||
$response->assertStatus(302);
|
||||
|
||||
$item = Item::withoutGlobalScopes()->where('title', 'Crafted Owner Item')->first();
|
||||
$this->assertNotNull($item);
|
||||
$this->assertSame($creator->id, (int) $item->user_id);
|
||||
}
|
||||
|
||||
public function test_updating_a_shared_item_does_not_change_its_owner(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// Attacker is a different logged-in user.
|
||||
$attacker = $this->actAsCurrentUser();
|
||||
|
||||
// A shared item (user_id = 0) is visible to every user.
|
||||
$item = Item::factory()->create([
|
||||
'title' => 'Shared Item',
|
||||
'user_id' => 0,
|
||||
]);
|
||||
|
||||
$response = $this->patch('/items/'.$item->id, [
|
||||
'appid' => 'null',
|
||||
'title' => 'Shared Item Edited',
|
||||
'url' => 'http://example.test',
|
||||
'user_id' => $attacker->id, // crafted mass-assignment attempt
|
||||
'tags' => [0],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dash'));
|
||||
|
||||
$fresh = Item::withoutGlobalScopes()->find($item->id);
|
||||
// Ownership is unchanged despite the crafted user_id field...
|
||||
$this->assertSame(0, (int) $fresh->user_id);
|
||||
// ...but the rest of the edit still applied.
|
||||
$this->assertSame('Shared Item Edited', $fresh->title);
|
||||
}
|
||||
|
||||
public function test_updating_an_owned_item_does_not_change_its_owner(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$owner = $this->actAsCurrentUser();
|
||||
|
||||
$item = Item::factory()->create([
|
||||
'title' => 'Owned Item',
|
||||
'user_id' => $owner->id,
|
||||
]);
|
||||
|
||||
$response = $this->patch('/items/'.$item->id, [
|
||||
'appid' => 'null',
|
||||
'title' => 'Owned Item Edited',
|
||||
'url' => 'http://example.test',
|
||||
'user_id' => 999, // crafted mass-assignment attempt
|
||||
'tags' => [0],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dash'));
|
||||
|
||||
$fresh = Item::withoutGlobalScopes()->find($item->id);
|
||||
$this->assertSame($owner->id, (int) $fresh->user_id);
|
||||
$this->assertSame('Owned Item Edited', $fresh->title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OrphanItemRecoveryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* RefreshDatabase already runs every migration up-front, so the orphans
|
||||
* are created afterwards and the data migration is invoked directly.
|
||||
*/
|
||||
public function test_reassigns_orphaned_items_but_leaves_shared_and_valid_items(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// A surviving, valid owner.
|
||||
$validOwner = User::factory()->create();
|
||||
|
||||
// Orphan: user_id references a user that does not exist.
|
||||
$orphan = Item::factory()->create([
|
||||
'title' => 'Orphaned Item',
|
||||
'user_id' => 999,
|
||||
]);
|
||||
|
||||
// Shared items (user_id = 0) must never be reassigned.
|
||||
$shared = Item::factory()->create([
|
||||
'title' => 'Shared Item',
|
||||
'user_id' => 0,
|
||||
]);
|
||||
|
||||
// A validly-owned item must be left untouched.
|
||||
$valid = Item::factory()->create([
|
||||
'title' => 'Valid Item',
|
||||
'user_id' => $validOwner->id,
|
||||
]);
|
||||
|
||||
$migration = include database_path('migrations/2026_07_09_120000_reassign_orphaned_items.php');
|
||||
$migration->up();
|
||||
|
||||
// Orphan reassigned to the surviving admin (id 1).
|
||||
$this->assertSame(1, (int) Item::withoutGlobalScopes()->find($orphan->id)->user_id);
|
||||
// Shared and valid rows unchanged.
|
||||
$this->assertSame(0, (int) Item::withoutGlobalScopes()->find($shared->id)->user_id);
|
||||
$this->assertSame($validOwner->id, (int) Item::withoutGlobalScopes()->find($valid->id)->user_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Broad "the app still boots and renders on this Laravel version" safety net.
|
||||
*
|
||||
* Beyond the focused per-feature tests, this walks the main GET surface of the
|
||||
* app in one place and asserts every route returns its expected status with no
|
||||
* exception. A framework/PHP upgrade that broke view rendering, routing, the
|
||||
* auth scaffolding or the auth middleware would surface here as a 500 / wrong
|
||||
* status even if a more specific test was missing.
|
||||
*/
|
||||
class RoutesRenderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_core_get_routes_boot_without_error(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$routes = [
|
||||
'/' => 200,
|
||||
'/login' => 200,
|
||||
'/userselect' => 200,
|
||||
'/settings' => 200,
|
||||
'/items' => 200,
|
||||
'/items/create' => 200,
|
||||
'/tags' => 200,
|
||||
'/health' => 200,
|
||||
'/up' => 200,
|
||||
];
|
||||
|
||||
foreach ($routes as $uri => $expectedStatus) {
|
||||
$response = $this->get($uri);
|
||||
|
||||
$this->assertSame(
|
||||
$expectedStatus,
|
||||
$response->getStatusCode(),
|
||||
"GET {$uri} returned {$response->getStatusCode()}, expected {$expectedStatus}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_home_redirects_guests_to_login(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// /home is behind the auth middleware; a guest must be redirected to
|
||||
// the login route (redirectGuestsTo in bootstrap/app.php).
|
||||
$response = $this->get('/home');
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_search_redirects_to_the_provider(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$response = $this->get('/search?provider=google&q=heimdall');
|
||||
|
||||
$response->assertStatus(302);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Guards the filesystem configuration the icon / avatar upload paths depend on.
|
||||
*
|
||||
* config/filesystems.php explicitly pins the local disk root to
|
||||
* storage_path('app'). Laravel 12 changed the default local root to
|
||||
* storage_path('app/private'); if that pin were lost (or the framework's
|
||||
* default disks stopped being merged in) every stored icon path would silently
|
||||
* point at the wrong directory. These tests fail loudly if that happens.
|
||||
*/
|
||||
class StorageDiskTest extends TestCase
|
||||
{
|
||||
public function test_local_disk_resolves(): void
|
||||
{
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('local'));
|
||||
}
|
||||
|
||||
public function test_public_disk_resolves(): void
|
||||
{
|
||||
// The public disk comes from the framework defaults merged over the
|
||||
// app's partial config/filesystems.php.
|
||||
$this->assertInstanceOf(Filesystem::class, Storage::disk('public'));
|
||||
}
|
||||
|
||||
public function test_local_disk_root_is_pinned_to_storage_app(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app'), config('filesystems.disks.local.root'));
|
||||
|
||||
// The resolved absolute path must live directly under storage/app,
|
||||
// not the Laravel 12 storage/app/private default.
|
||||
$this->assertSame(storage_path('app/icon.png'), Storage::disk('local')->path('icon.png'));
|
||||
}
|
||||
|
||||
public function test_public_disk_root_is_storage_app_public(): void
|
||||
{
|
||||
$this->assertSame(storage_path('app/public'), config('filesystems.disks.public.root'));
|
||||
}
|
||||
|
||||
public function test_public_disk_supports_a_put_exists_get_round_trip(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$contents = 'icon-bytes';
|
||||
Storage::disk('public')->put('icons/test.png', $contents);
|
||||
|
||||
$this->assertTrue(Storage::disk('public')->exists('icons/test.png'));
|
||||
$this->assertSame($contents, Storage::disk('public')->get('icons/test.png'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Item;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserDeleteItemsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_deleting_a_user_hard_deletes_only_that_users_items(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
// Admin (id 1, passwordless) is the only user allowed to manage users.
|
||||
$this->withSession(['current_user' => User::find(1)]);
|
||||
|
||||
$victim = User::factory()->create();
|
||||
|
||||
// The victim's own tile and tag should both be hard-deleted.
|
||||
$victimTile = Item::factory()->create([
|
||||
'title' => 'Victim Tile',
|
||||
'type' => 0,
|
||||
'user_id' => $victim->id,
|
||||
]);
|
||||
$victimTag = Item::factory()->create([
|
||||
'title' => 'Victim Tag',
|
||||
'type' => 1,
|
||||
'user_id' => $victim->id,
|
||||
]);
|
||||
|
||||
// A shared item and another user's item must be left untouched.
|
||||
$sharedItem = Item::factory()->create([
|
||||
'title' => 'Shared Item',
|
||||
'type' => 0,
|
||||
'user_id' => 0,
|
||||
]);
|
||||
$adminItem = Item::factory()->create([
|
||||
'title' => 'Admin Item',
|
||||
'type' => 0,
|
||||
'user_id' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->delete(route('users.destroy', $victim->id));
|
||||
|
||||
$response->assertRedirect(route('dash'));
|
||||
|
||||
// Victim and their items are gone entirely (force-deleted, not soft-deleted).
|
||||
$this->assertDatabaseMissing('users', ['id' => $victim->id]);
|
||||
$this->assertNull(Item::withoutGlobalScopes()->withTrashed()->find($victimTile->id));
|
||||
$this->assertNull(Item::withoutGlobalScopes()->withTrashed()->find($victimTag->id));
|
||||
|
||||
// Shared and other users' items survive.
|
||||
$this->assertNotNull(Item::withoutGlobalScopes()->find($sharedItem->id));
|
||||
$this->assertNotNull(Item::withoutGlobalScopes()->find($adminItem->id));
|
||||
}
|
||||
|
||||
public function test_user_id_one_cannot_be_deleted(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$this->withSession(['current_user' => User::find(1)]);
|
||||
|
||||
$adminItem = Item::factory()->create([
|
||||
'title' => 'Admin Item',
|
||||
'type' => 0,
|
||||
'user_id' => 1,
|
||||
]);
|
||||
|
||||
$this->delete(route('users.destroy', 1));
|
||||
|
||||
// The admin and their items remain.
|
||||
$this->assertDatabaseHas('users', ['id' => 1]);
|
||||
$this->assertNotNull(Item::withoutGlobalScopes()->find($adminItem->id));
|
||||
}
|
||||
}
|
||||
+38
-1
@@ -6,5 +6,42 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->guardAgainstRealDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse to run the test suite against anything other than an
|
||||
* in-memory SQLite database.
|
||||
*
|
||||
* The suite uses RefreshDatabase (migrate:fresh), which would wipe
|
||||
* whatever database it is pointed at. The only supported test
|
||||
* configuration for this repo is sqlite + ':memory:'. Any other
|
||||
* connection (a real sqlite file, mysql, pgsql, ...) is rejected so
|
||||
* we can never destroy real data.
|
||||
*/
|
||||
private function guardAgainstRealDatabase(): void
|
||||
{
|
||||
$default = config('database.default');
|
||||
$driver = config("database.connections.{$default}.driver");
|
||||
$database = config("database.connections.{$default}.database");
|
||||
|
||||
if ($driver === 'sqlite' && $database === ':memory:') {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Refusing to run tests: the default database connection (%s) is '
|
||||
. 'driver "%s" pointing at "%s". Tests only run against an '
|
||||
. 'in-memory SQLite database (driver "sqlite", database ":memory:") '
|
||||
. 'to avoid wiping real data via RefreshDatabase. Check phpunit.xml '
|
||||
. 'DB_CONNECTION/DB_DATABASE overrides.',
|
||||
$default,
|
||||
$driver,
|
||||
is_scalar($database) ? (string) $database : gettype($database)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
namespace Tests\Unit\database\seeders;
|
||||
|
||||
use App\Item;
|
||||
use App\Setting;
|
||||
use Database\Seeders\SettingsSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SettingsSeederTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* All language keys are defined in all languages based on the en language file.
|
||||
*/
|
||||
@@ -18,4 +23,60 @@ class SettingsSeederTest extends TestCase
|
||||
|
||||
$this->assertTrue(count($languageMap) === count($languageDirectories));
|
||||
}
|
||||
|
||||
public function test_seeds_the_default_tag_setting(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
$setting = Setting::where('key', 'default_tag')->first();
|
||||
|
||||
$this->assertNotNull($setting);
|
||||
$this->assertSame('select', $setting->type);
|
||||
$this->assertSame(4, (int) $setting->group_id);
|
||||
}
|
||||
|
||||
public function test_default_tag_edit_value_lists_all_tags_and_a_none_option(): void
|
||||
{
|
||||
$this->seed();
|
||||
|
||||
Item::factory()->create([
|
||||
'title' => 'Home',
|
||||
'url' => 'home-dashboard',
|
||||
'type' => 1,
|
||||
'pinned' => 1,
|
||||
'user_id' => 0,
|
||||
]);
|
||||
Item::factory()->create([
|
||||
'title' => 'Media',
|
||||
'url' => 'media',
|
||||
'type' => 1,
|
||||
'pinned' => 1,
|
||||
'user_id' => 0,
|
||||
]);
|
||||
// An unpinned tag is not rendered in the dashboard taglist, so it must
|
||||
// not be offered as a default (selecting it would silently do nothing).
|
||||
Item::factory()->create([
|
||||
'title' => 'Archive',
|
||||
'url' => 'archive',
|
||||
'type' => 1,
|
||||
'pinned' => 0,
|
||||
'user_id' => 0,
|
||||
]);
|
||||
|
||||
$setting = Setting::where('key', 'default_tag')->first();
|
||||
$editValue = $setting->edit_value;
|
||||
|
||||
// A "none" option with an empty value, using the shared translation key.
|
||||
$this->assertStringContainsString('<option value="" ', $editValue);
|
||||
$this->assertStringContainsString(__('app.options.none'), $editValue);
|
||||
|
||||
// One option per pinned tag: the slug as the value, the raw title as the label.
|
||||
$this->assertStringContainsString('value="home-dashboard"', $editValue);
|
||||
$this->assertStringContainsString('>Home</option>', $editValue);
|
||||
$this->assertStringContainsString('value="media"', $editValue);
|
||||
$this->assertStringContainsString('>Media</option>', $editValue);
|
||||
|
||||
// The unpinned tag is excluded.
|
||||
$this->assertStringNotContainsString('value="archive"', $editValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for className() in app/Helper.php.
|
||||
*
|
||||
* className() turns a supported-app display name into the PHP class-name
|
||||
* fragment used to resolve enhanced-app classes (see Application::single()).
|
||||
* It relies on a unicode-aware preg_replace, which is exactly the kind of
|
||||
* PCRE behaviour that can change across PHP versions, so the stripping rules
|
||||
* and unicode-safety are pinned here.
|
||||
*/
|
||||
class ClassNameTest extends TestCase
|
||||
{
|
||||
public function test_strips_spaces_and_punctuation(): void
|
||||
{
|
||||
$this->assertSame('HomeAssistant', className('Home Assistant'));
|
||||
$this->assertSame('Pihole', className('Pi-hole'));
|
||||
$this->assertSame('NodeRED', className('Node-RED!'));
|
||||
}
|
||||
|
||||
public function test_keeps_digits(): void
|
||||
{
|
||||
$this->assertSame('App2Go', className('App 2 Go'));
|
||||
}
|
||||
|
||||
public function test_is_unicode_safe(): void
|
||||
{
|
||||
// Letters in other scripts / accented letters must be preserved,
|
||||
// only the separators are removed.
|
||||
$this->assertSame('CaféServer', className('Café Server'));
|
||||
$this->assertSame('中文测试', className('中文 测试'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the colour brightness globals in app/Helper.php.
|
||||
*
|
||||
* get_brightness() and title_color() drive the automatic black/white tile
|
||||
* text colour on the dashboard. They rely on hexdec(), substr() and integer
|
||||
* maths, all of which are sensitive to PHP behavioural changes, so the
|
||||
* expected luminance values and the black/white threshold are pinned here.
|
||||
*/
|
||||
class ColorHelpersTest extends TestCase
|
||||
{
|
||||
public function test_get_brightness_returns_full_luminance_for_white(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#ffffff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_returns_zero_for_black(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_expands_three_char_hex(): void
|
||||
{
|
||||
// #fff / #000 must expand to the six-char form before decoding.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#fff'), 0.0001);
|
||||
$this->assertEqualsWithDelta(0, get_brightness('#000'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_strips_leading_hash_and_other_non_hex(): void
|
||||
{
|
||||
// A value without the leading # must decode identically.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('ffffff'), 0.0001);
|
||||
|
||||
// Interior non-hex separators (the "other non-hex" in the name) must be
|
||||
// stripped before decoding, so these normalise to ffffff. If the
|
||||
// preg_replace were dropped these would decode to a different value.
|
||||
$this->assertEqualsWithDelta(255, get_brightness('#ff:ff:ff'), 0.0001);
|
||||
$this->assertEqualsWithDelta(255, get_brightness('ff-ff-ff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_get_brightness_weights_channels_per_luma_formula(): void
|
||||
{
|
||||
// (R*299 + G*587 + B*114) / 1000
|
||||
$this->assertEqualsWithDelta(76.245, get_brightness('#ff0000'), 0.0001);
|
||||
$this->assertEqualsWithDelta(149.685, get_brightness('#00ff00'), 0.0001);
|
||||
$this->assertEqualsWithDelta(29.07, get_brightness('#0000ff'), 0.0001);
|
||||
}
|
||||
|
||||
public function test_title_color_returns_black_for_bright_colours(): void
|
||||
{
|
||||
// Brightness > 130 => dark text.
|
||||
$this->assertSame(' black', title_color('#ffffff'));
|
||||
$this->assertSame(' black', title_color('#00ff00'));
|
||||
}
|
||||
|
||||
public function test_title_color_returns_white_for_dark_colours(): void
|
||||
{
|
||||
// Brightness <= 130 => light text.
|
||||
$this->assertSame(' white', title_color('#000000'));
|
||||
$this->assertSame(' white', title_color('#0000ff'));
|
||||
$this->assertSame(' white', title_color('#ff0000'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\helpers;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Regression coverage for the byte / size formatting globals in app/Helper.php.
|
||||
*
|
||||
* These are plain global functions (no framework involved) so a PHP upgrade
|
||||
* that changes integer/float division, rounding, or numeric-string casting is
|
||||
* exactly the kind of thing that would silently break them. Every assertion
|
||||
* pins a concrete expected string/integer so the behaviour is locked down.
|
||||
*/
|
||||
class SizeHelpersTest extends TestCase
|
||||
{
|
||||
public function test_format_bytes_uses_drive_size_base_1000_by_default(): void
|
||||
{
|
||||
// Default $is_drive_size = true => divide by 1000 (simulated HD size).
|
||||
$this->assertSame('500B', format_bytes(500));
|
||||
$this->assertSame('1KB', format_bytes(1000));
|
||||
$this->assertSame('2KB', format_bytes(2000));
|
||||
$this->assertSame('1MB', format_bytes(1000000));
|
||||
$this->assertSame('1.5MB', format_bytes(1500000));
|
||||
$this->assertSame('1GB', format_bytes(1000000000));
|
||||
$this->assertSame('2.5GB', format_bytes(2500000000));
|
||||
$this->assertSame('1TB', format_bytes(1000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_base_1024_when_not_drive_size(): void
|
||||
{
|
||||
// $is_drive_size = false => divide by 1024 (real byte size).
|
||||
$this->assertSame('1KB', format_bytes(1024, false));
|
||||
$this->assertSame('1MB', format_bytes(1048576, false));
|
||||
$this->assertSame('1GB', format_bytes(1073741824, false));
|
||||
$this->assertSame('1.43MB', format_bytes(1500000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_drive_size_flag_changes_the_result(): void
|
||||
{
|
||||
// The same byte count must format differently depending on the base.
|
||||
$this->assertSame('1MB', format_bytes(1000000, true));
|
||||
$this->assertSame('977KB', format_bytes(1000000, false));
|
||||
}
|
||||
|
||||
public function test_format_bytes_caps_at_terabytes(): void
|
||||
{
|
||||
// The unit loop stops at TB (index 4) even for very large inputs.
|
||||
$this->assertSame('5TB', format_bytes(5000000000000));
|
||||
}
|
||||
|
||||
public function test_format_bytes_applies_before_and_after_unit_strings(): void
|
||||
{
|
||||
$this->assertSame('1 KBps', format_bytes(1000, true, ' ', 'ps'));
|
||||
$this->assertSame('1.43 MB/s', format_bytes(1500000, false, ' ', '/s'));
|
||||
}
|
||||
|
||||
public function test_parse_size_resolves_gmk_suffixes_to_bytes(): void
|
||||
{
|
||||
$this->assertSame(1073741824, parse_size('1g'));
|
||||
$this->assertSame(2147483648, parse_size('2G'));
|
||||
$this->assertSame(536870912, parse_size('512m'));
|
||||
$this->assertSame(131072, parse_size('128k'));
|
||||
}
|
||||
|
||||
public function test_parse_size_without_suffix_returns_the_integer_value(): void
|
||||
{
|
||||
$this->assertSame(1024, parse_size('1024'));
|
||||
}
|
||||
}
|
||||
Vendored
+12
-2
@@ -3,8 +3,18 @@
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
exit(1);
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
Vendored
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"homepage": "http://aws.amazon.com/sdkforphp",
|
||||
"homepage": "https://aws.amazon.com/sdk-for-php",
|
||||
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||
"keywords": ["aws","amazon","sdk","s3","ec2","dynamodb","cloud","glacier"],
|
||||
"type": "library",
|
||||
@@ -8,7 +8,7 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Amazon Web Services",
|
||||
"homepage": "http://aws.amazon.com"
|
||||
"homepage": "https://aws.amazon.com"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
@@ -20,20 +20,20 @@
|
||||
"guzzlehttp/guzzle": "^7.4.5",
|
||||
"guzzlehttp/psr7": "^2.4.5",
|
||||
"guzzlehttp/promises": "^2.0",
|
||||
"mtdowling/jmespath.php": "^2.8.0",
|
||||
"mtdowling/jmespath.php": "^2.9.1",
|
||||
"ext-pcre": "*",
|
||||
"ext-json": "*",
|
||||
"ext-simplexml": "*",
|
||||
"aws/aws-crt-php": "^1.2.3",
|
||||
"psr/http-message": "^2.0"
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer" : "^2.7.8",
|
||||
"ext-openssl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-pcntl": "*",
|
||||
"ext-sockets": "*",
|
||||
"phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"behat/behat": "~3.0",
|
||||
"doctrine/cache": "~1.4",
|
||||
"aws/aws-php-sns-message-validator": "~1.0",
|
||||
@@ -41,14 +41,14 @@
|
||||
"psr/cache": "^2.0 || ^3.0",
|
||||
"psr/simple-cache": "^2.0 || ^3.0",
|
||||
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
|
||||
"symfony/filesystem": "^v6.4.0 || ^v7.1.0",
|
||||
"yoast/phpunit-polyfills": "^2.0",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.4.0"
|
||||
"dms/phpunit-arraysubset-asserts": "^v0.5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
|
||||
"ext-curl": "To send requests using cURL",
|
||||
"ext-sockets": "To use client-side monitoring",
|
||||
"ext-pcntl": "To use client-side monitoring",
|
||||
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace Aws\ARCRegionSwitch;
|
||||
|
||||
use Aws\AwsClient;
|
||||
|
||||
/**
|
||||
* This client is used to interact with the **ARC - Region switch** service.
|
||||
* @method \Aws\Result approvePlanExecutionStep(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise approvePlanExecutionStepAsync(array $args = [])
|
||||
* @method \Aws\Result cancelPlanExecution(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise cancelPlanExecutionAsync(array $args = [])
|
||||
* @method \Aws\Result createPlan(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createPlanAsync(array $args = [])
|
||||
* @method \Aws\Result deletePlan(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deletePlanAsync(array $args = [])
|
||||
* @method \Aws\Result getPlan(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPlanAsync(array $args = [])
|
||||
* @method \Aws\Result getPlanEvaluationStatus(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPlanEvaluationStatusAsync(array $args = [])
|
||||
* @method \Aws\Result getPlanExecution(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPlanExecutionAsync(array $args = [])
|
||||
* @method \Aws\Result getPlanInRegion(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPlanInRegionAsync(array $args = [])
|
||||
* @method \Aws\Result listPlanExecutionEvents(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPlanExecutionEventsAsync(array $args = [])
|
||||
* @method \Aws\Result listPlanExecutions(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPlanExecutionsAsync(array $args = [])
|
||||
* @method \Aws\Result listPlans(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPlansAsync(array $args = [])
|
||||
* @method \Aws\Result listPlansInRegion(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPlansInRegionAsync(array $args = [])
|
||||
* @method \Aws\Result listRoute53HealthChecks(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listRoute53HealthChecksAsync(array $args = [])
|
||||
* @method \Aws\Result listRoute53HealthChecksInRegion(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listRoute53HealthChecksInRegionAsync(array $args = [])
|
||||
* @method \Aws\Result listTagsForResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listTagsForResourceAsync(array $args = [])
|
||||
* @method \Aws\Result startPlanExecution(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise startPlanExecutionAsync(array $args = [])
|
||||
* @method \Aws\Result tagResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise tagResourceAsync(array $args = [])
|
||||
* @method \Aws\Result untagResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise untagResourceAsync(array $args = [])
|
||||
* @method \Aws\Result updatePlan(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updatePlanAsync(array $args = [])
|
||||
* @method \Aws\Result updatePlanExecution(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updatePlanExecutionAsync(array $args = [])
|
||||
* @method \Aws\Result updatePlanExecutionStep(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updatePlanExecutionStepAsync(array $args = [])
|
||||
*/
|
||||
class ARCRegionSwitchClient extends AwsClient {}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace Aws\ARCRegionSwitch\Exception;
|
||||
|
||||
use Aws\Exception\AwsException;
|
||||
|
||||
/**
|
||||
* Represents an error interacting with the **ARC - Region switch** service.
|
||||
*/
|
||||
class ARCRegionSwitchException extends AwsException {}
|
||||
@@ -21,10 +21,14 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise createAnalyzerAsync(array $args = [])
|
||||
* @method \Aws\Result createArchiveRule(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createArchiveRuleAsync(array $args = [])
|
||||
* @method \Aws\Result createServiceLinkedAnalyzer(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createServiceLinkedAnalyzerAsync(array $args = [])
|
||||
* @method \Aws\Result deleteAnalyzer(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteAnalyzerAsync(array $args = [])
|
||||
* @method \Aws\Result deleteArchiveRule(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteArchiveRuleAsync(array $args = [])
|
||||
* @method \Aws\Result deleteServiceLinkedAnalyzer(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteServiceLinkedAnalyzerAsync(array $args = [])
|
||||
* @method \Aws\Result generateFindingRecommendation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise generateFindingRecommendationAsync(array $args = [])
|
||||
* @method \Aws\Result getAccessPreview(array $args = [])
|
||||
|
||||
@@ -19,6 +19,8 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise getAlternateContactAsync(array $args = [])
|
||||
* @method \Aws\Result getContactInformation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getContactInformationAsync(array $args = [])
|
||||
* @method \Aws\Result getGovCloudAccountInformation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getGovCloudAccountInformationAsync(array $args = [])
|
||||
* @method \Aws\Result getPrimaryEmail(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPrimaryEmailAsync(array $args = [])
|
||||
* @method \Aws\Result getRegionOptStatus(array $args = [])
|
||||
|
||||
+46
@@ -8,22 +8,54 @@ use Aws\AwsClient;
|
||||
*
|
||||
* @method \Aws\Result addTagsToCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise addTagsToCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result createAcmeDomainValidation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createAcmeDomainValidationAsync(array $args = [])
|
||||
* @method \Aws\Result createAcmeEndpoint(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createAcmeEndpointAsync(array $args = [])
|
||||
* @method \Aws\Result createAcmeExternalAccountBinding(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createAcmeExternalAccountBindingAsync(array $args = [])
|
||||
* @method \Aws\Result deleteAcmeDomainValidation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteAcmeDomainValidationAsync(array $args = [])
|
||||
* @method \Aws\Result deleteAcmeEndpoint(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteAcmeEndpointAsync(array $args = [])
|
||||
* @method \Aws\Result deleteAcmeExternalAccountBinding(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteAcmeExternalAccountBindingAsync(array $args = [])
|
||||
* @method \Aws\Result deleteCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result describeAcmeAccount(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise describeAcmeAccountAsync(array $args = [])
|
||||
* @method \Aws\Result describeAcmeDomainValidation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise describeAcmeDomainValidationAsync(array $args = [])
|
||||
* @method \Aws\Result describeAcmeEndpoint(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise describeAcmeEndpointAsync(array $args = [])
|
||||
* @method \Aws\Result describeAcmeExternalAccountBinding(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise describeAcmeExternalAccountBindingAsync(array $args = [])
|
||||
* @method \Aws\Result describeCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise describeCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result exportCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise exportCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result getAccountConfiguration(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getAccountConfigurationAsync(array $args = [])
|
||||
* @method \Aws\Result getAcmeExternalAccountBindingCredentials(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getAcmeExternalAccountBindingCredentialsAsync(array $args = [])
|
||||
* @method \Aws\Result getCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result importCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise importCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result listAcmeAccounts(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listAcmeAccountsAsync(array $args = [])
|
||||
* @method \Aws\Result listAcmeDomainValidations(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listAcmeDomainValidationsAsync(array $args = [])
|
||||
* @method \Aws\Result listAcmeEndpoints(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listAcmeEndpointsAsync(array $args = [])
|
||||
* @method \Aws\Result listAcmeExternalAccountBindings(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listAcmeExternalAccountBindingsAsync(array $args = [])
|
||||
* @method \Aws\Result listCertificates(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listCertificatesAsync(array $args = [])
|
||||
* @method \Aws\Result listTagsForCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listTagsForCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result listTagsForResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listTagsForResourceAsync(array $args = [])
|
||||
* @method \Aws\Result putAccountConfiguration(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise putAccountConfigurationAsync(array $args = [])
|
||||
* @method \Aws\Result removeTagsFromCertificate(array $args = [])
|
||||
@@ -34,8 +66,22 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise requestCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result resendValidationEmail(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise resendValidationEmailAsync(array $args = [])
|
||||
* @method \Aws\Result revokeAcmeAccount(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise revokeAcmeAccountAsync(array $args = [])
|
||||
* @method \Aws\Result revokeAcmeExternalAccountBinding(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise revokeAcmeExternalAccountBindingAsync(array $args = [])
|
||||
* @method \Aws\Result revokeCertificate(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise revokeCertificateAsync(array $args = [])
|
||||
* @method \Aws\Result searchCertificates(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise searchCertificatesAsync(array $args = [])
|
||||
* @method \Aws\Result tagResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise tagResourceAsync(array $args = [])
|
||||
* @method \Aws\Result untagResource(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise untagResourceAsync(array $args = [])
|
||||
* @method \Aws\Result updateAcmeDomainValidation(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateAcmeDomainValidationAsync(array $args = [])
|
||||
* @method \Aws\Result updateAcmeEndpoint(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateAcmeEndpointAsync(array $args = [])
|
||||
* @method \Aws\Result updateCertificateOptions(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateCertificateOptionsAsync(array $args = [])
|
||||
*/
|
||||
|
||||
@@ -43,8 +43,8 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise getBackendJobAsync(array $args = [])
|
||||
* @method \Aws\Result getBackendStorage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getBackendStorageAsync(array $args = [])
|
||||
* @method \Aws\Result getToken(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getTokenAsync(array $args = [])
|
||||
* @method \Aws\Result getChallengeToken(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getChallengeTokenAsync(array $args = [])
|
||||
* @method \Aws\Result importBackendAuth(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise importBackendAuthAsync(array $args = [])
|
||||
* @method \Aws\Result importBackendStorage(array $args = [])
|
||||
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
<?php
|
||||
namespace Aws\Api\Cbor;
|
||||
|
||||
use Aws\Api\Cbor\Exception\CborException;
|
||||
|
||||
/**
|
||||
* Decodes Concise Binary Object Representation encoded strings
|
||||
* into PHP values according to RFC 8949
|
||||
*
|
||||
* https://www.rfc-editor.org/rfc/rfc8949.html
|
||||
*
|
||||
* Supports Major types 0-7 including:
|
||||
* - Type 0: Unsigned integers
|
||||
* - Type 1: Negative integers
|
||||
* - Type 2: Byte strings
|
||||
* - Type 3: Text strings (UTF-8)
|
||||
* - Type 4: Arrays
|
||||
* - Type 5: Maps
|
||||
* - Type 6: Tagged values (timestamps)
|
||||
* - Type 7: Simple values (null, bool, float)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CborDecoder
|
||||
{
|
||||
private int $offset;
|
||||
private int $length;
|
||||
|
||||
/**
|
||||
* Decode CBOR binary data to PHP value
|
||||
*
|
||||
* @param string $data The CBOR-encoded binary data to decode
|
||||
*
|
||||
* @return mixed The decoded PHP value (can be any type: int, string, array, bool, null, float)
|
||||
* @throws CborException If data is empty or malformed CBOR
|
||||
*/
|
||||
public function decode(string $data): mixed
|
||||
{
|
||||
if ($data === '') {
|
||||
throw new CborException("No data to decode");
|
||||
}
|
||||
|
||||
$this->offset = 0;
|
||||
$this->length = strlen($data);
|
||||
|
||||
return $this->decodeValue($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode multiple CBOR values from sequential binary data
|
||||
*
|
||||
* @param string $data The CBOR-encoded binary data containing multiple values
|
||||
*
|
||||
* @return array Array of decoded PHP values in the order they appear in the data
|
||||
* @throws CborException If data is malformed CBOR
|
||||
*/
|
||||
public function decodeAll(string $data): array
|
||||
{
|
||||
$this->length = strlen($data);
|
||||
$this->offset = 0;
|
||||
$values = [];
|
||||
|
||||
while ($this->offset < $this->length) {
|
||||
$values[] = $this->decodeValue($data);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single CBOR value at the current offset
|
||||
*
|
||||
* @param string $data Reference to the CBOR data being decoded
|
||||
*
|
||||
* @return mixed The decoded value
|
||||
* @throws CborException If unexpected end of data or invalid CBOR format
|
||||
*/
|
||||
private function decodeValue(string &$data): mixed
|
||||
{
|
||||
$offset = $this->offset;
|
||||
$length = $this->length;
|
||||
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Unexpected end of data");
|
||||
}
|
||||
|
||||
$byte = ord($data[$offset++]);
|
||||
$majorType = $byte >> 5;
|
||||
$info = $byte & 0x1F;
|
||||
|
||||
switch ($majorType) {
|
||||
case 0: // Unsigned integer
|
||||
if ($info < 24) {
|
||||
$this->offset = $offset;
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 1;
|
||||
|
||||
return ord($data[$offset]);
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 2;
|
||||
|
||||
return (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 4;
|
||||
|
||||
return unpack('N', $data, $offset)[1];
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 8;
|
||||
|
||||
return unpack('J', $data, $offset)[1];
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for integer: $info");
|
||||
}
|
||||
|
||||
case 1: // Negative integer
|
||||
if ($info < 24) {
|
||||
$this->offset = $offset;
|
||||
|
||||
return -1 - $info;
|
||||
}
|
||||
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 1;
|
||||
|
||||
return -1 - ord($data[$offset]);
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 2;
|
||||
|
||||
return -1 - ((ord($data[$offset]) << 8) | ord($data[$offset + 1]));
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 4;
|
||||
|
||||
return -1 - unpack('N', $data, $offset)[1];
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 8;
|
||||
$unsigned = unpack('J', $data, $offset)[1];
|
||||
|
||||
return ($unsigned === 9223372036854775807) ? PHP_INT_MIN : -1 - $unsigned;
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for integer: $info");
|
||||
}
|
||||
|
||||
case 2: // Byte string
|
||||
if ($info < 24) {
|
||||
$len = $info;
|
||||
} else {
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = ord($data[$offset++]);
|
||||
break;
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('N', $data, $offset)[1];
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('J', $data, $offset)[1];
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
case 31:
|
||||
$this->offset = $offset;
|
||||
|
||||
return $this->decodeIndefiniteString($data, 0x40);
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for byte string: $info");
|
||||
}
|
||||
}
|
||||
|
||||
if ($offset + $len > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + $len;
|
||||
|
||||
return substr($data, $offset, $len);
|
||||
|
||||
case 3: // Text string
|
||||
if ($info < 24) {
|
||||
$len = $info;
|
||||
} else {
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = ord($data[$offset++]);
|
||||
break;
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('N', $data, $offset)[1];
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('J', $data, $offset)[1];
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
case 31:
|
||||
$this->offset = $offset;
|
||||
|
||||
return $this->decodeIndefiniteString($data, 0x60);
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for text string: $info");
|
||||
}
|
||||
}
|
||||
|
||||
if ($offset + $len > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + $len;
|
||||
|
||||
return substr($data, $offset, $len);
|
||||
|
||||
case 4: // Array
|
||||
if ($info < 24) {
|
||||
$count = $info;
|
||||
} else {
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = ord($data[$offset++]);
|
||||
break;
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = unpack('N', $data, $offset)[1];
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = unpack('J', $data, $offset)[1];
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
case 31:
|
||||
$this->offset = $offset;
|
||||
|
||||
return $this->decodeIndefiniteArray($data);
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for array: $info");
|
||||
}
|
||||
}
|
||||
|
||||
$this->offset = $offset;
|
||||
$arr = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$arr[] = $this->decodeValue($data);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
|
||||
case 5: // Map
|
||||
if ($info < 24) {
|
||||
$count = $info;
|
||||
} else {
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = ord($data[$offset++]);
|
||||
break;
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = unpack('N', $data, $offset)[1];
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$count = unpack('J', $data, $offset)[1];
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
case 31:
|
||||
$this->offset = $offset;
|
||||
|
||||
return $this->decodeIndefiniteMap($data);
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid additional info for map: $info");
|
||||
}
|
||||
}
|
||||
|
||||
$this->offset = $offset;
|
||||
$map = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$key = $this->decodeValue($data);
|
||||
$map[$key] = $this->decodeValue($data);
|
||||
}
|
||||
|
||||
return $map;
|
||||
|
||||
case 6: // Tag
|
||||
switch ($info) {
|
||||
case 24:
|
||||
$offset++;
|
||||
break;
|
||||
|
||||
case 25:
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
$offset += 8;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->offset = $offset;
|
||||
|
||||
return $this->decodeValue($data);
|
||||
|
||||
case 7: // Simple/float
|
||||
switch ($info) {
|
||||
case 20:
|
||||
$this->offset = $offset;
|
||||
|
||||
return false;
|
||||
|
||||
case 21:
|
||||
$this->offset = $offset;
|
||||
|
||||
return true;
|
||||
|
||||
case 22:
|
||||
case 23:
|
||||
$this->offset = $offset;
|
||||
|
||||
return null;
|
||||
|
||||
case 25: // Half-precision float
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 2;
|
||||
$half = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$sign = ($half >> 15) & 0x01;
|
||||
$exp = ($half >> 10) & 0x1F;
|
||||
$mant = $half & 0x3FF;
|
||||
|
||||
if ($exp === 0) {
|
||||
return $mant === 0
|
||||
? ($sign ? -0.0 : 0.0)
|
||||
: ($sign ? -1 : 1) * pow(2, -14) * ($mant / 1024);
|
||||
}
|
||||
|
||||
if ($exp === 31) {
|
||||
return $mant === 0 ? ($sign ? -INF : INF) : NAN;
|
||||
}
|
||||
|
||||
return (float) (($sign ? -1 : 1) * pow(2, $exp - 15) * (1 + $mant / 1024));
|
||||
|
||||
case 26: // Single-precision float
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 4;
|
||||
|
||||
return unpack('G', $data, $offset)[1];
|
||||
|
||||
case 27: // Double-precision float
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$this->offset = $offset + 8;
|
||||
|
||||
return unpack('E', $data, $offset)[1];
|
||||
|
||||
case 31:
|
||||
throw new CborException("Unexpected break");
|
||||
|
||||
default:
|
||||
throw new CborException("Unknown simple value: $info");
|
||||
}
|
||||
|
||||
default:
|
||||
throw new CborException("Unknown major type: $majorType");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode indefinite-length string (byte or text)
|
||||
*
|
||||
* @param string $data Reference to the CBOR data being decoded
|
||||
* @param int $expectedMajor Expected major type (0x40 for byte string, 0x60 for text string)
|
||||
*
|
||||
* @return string The concatenated string from all chunks
|
||||
* @throws CborException If invalid chunk format or unexpected end of data
|
||||
*/
|
||||
private function decodeIndefiniteString(string &$data, int $expectedMajor): string
|
||||
{
|
||||
$chunks = [];
|
||||
|
||||
while (true) {
|
||||
$offset = $this->offset;
|
||||
$length = $this->length;
|
||||
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Unexpected end of data");
|
||||
}
|
||||
|
||||
$byte = ord($data[$offset++]);
|
||||
|
||||
if ($byte === 0xFF) {
|
||||
$this->offset = $offset;
|
||||
|
||||
return implode('', $chunks);
|
||||
}
|
||||
|
||||
if (($byte & 0xE0) !== $expectedMajor) {
|
||||
throw new CborException("Invalid chunk in indefinite string");
|
||||
}
|
||||
|
||||
$info = $byte & 0x1F;
|
||||
|
||||
if ($info === 31) {
|
||||
throw new CborException("Nested indefinite string");
|
||||
}
|
||||
|
||||
if ($info < 24) {
|
||||
$len = $info;
|
||||
} else {
|
||||
switch ($info) {
|
||||
case 24:
|
||||
if ($offset >= $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = ord($data[$offset++]);
|
||||
break;
|
||||
|
||||
case 25:
|
||||
if ($offset + 2 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = (ord($data[$offset]) << 8) | ord($data[$offset + 1]);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
if ($offset + 4 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('N', $data, $offset)[1];
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
if ($offset + 8 > $length) {
|
||||
throw new CborException("Not enough data");
|
||||
}
|
||||
|
||||
$len = unpack('J', $data, $offset)[1];
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new CborException("Invalid chunk length info: $info");
|
||||
}
|
||||
}
|
||||
|
||||
if ($offset + $len > $length) {
|
||||
throw new CborException("Not enough data for chunk");
|
||||
}
|
||||
|
||||
$chunks[] = substr($data, $offset, $len);
|
||||
$this->offset = $offset + $len;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode indefinite-length array
|
||||
*
|
||||
* @param string $data Reference to the CBOR data being decoded
|
||||
*
|
||||
* @return array The decoded array elements
|
||||
* @throws CborException If unexpected end of data
|
||||
*/
|
||||
private function decodeIndefiniteArray(string &$data): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
while (true) {
|
||||
if ($this->offset >= $this->length) {
|
||||
throw new CborException("Unexpected end of data");
|
||||
}
|
||||
|
||||
if (ord($data[$this->offset]) === 0xFF) {
|
||||
$this->offset++;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result[] = $this->decodeValue($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode indefinite-length map
|
||||
*
|
||||
* @param string $data Reference to the CBOR data being decoded
|
||||
*
|
||||
* @return array The decoded map as associative array
|
||||
* @throws CborException If unexpected end of data or odd number of items
|
||||
*/
|
||||
private function decodeIndefiniteMap(string &$data): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
while (true) {
|
||||
if ($this->offset >= $this->length) {
|
||||
throw new CborException("Unexpected end of data");
|
||||
}
|
||||
|
||||
if (ord($data[$this->offset]) === 0xFF) {
|
||||
$this->offset++;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$key = $this->decodeValue($data);
|
||||
$result[$key] = $this->decodeValue($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
namespace Aws\Api\Cbor;
|
||||
|
||||
use Aws\Api\Cbor\Exception\CborException;
|
||||
|
||||
/**
|
||||
* Encodes PHP values to Concise Binary Object Representation according to RFC 8949
|
||||
* https://www.rfc-editor.org/rfc/rfc8949.html
|
||||
*
|
||||
* Supports Major types 0-7 including:
|
||||
* - Type 0: Unsigned integers
|
||||
* - Type 1: Negative integers
|
||||
* - Type 2: Byte strings (via ['__cbor_bytes' => $data] wrappers)
|
||||
* - Type 3: Text strings (UTF-8)
|
||||
* - Type 4: Arrays
|
||||
* - Type 5: Maps
|
||||
* - Type 6: Tagged values (timestamps)
|
||||
* - Type 7: Simple values (null, bool, float)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CborEncoder
|
||||
{
|
||||
/**
|
||||
* Pre-encoded integers 0-23 (single byte) and common larger values
|
||||
* CBOR major type 0 (unsigned integer)
|
||||
*/
|
||||
private const INT_CACHE = [
|
||||
0 => "\x00", 1 => "\x01", 2 => "\x02", 3 => "\x03",
|
||||
4 => "\x04", 5 => "\x05", 6 => "\x06", 7 => "\x07",
|
||||
8 => "\x08", 9 => "\x09", 10 => "\x0A", 11 => "\x0B",
|
||||
12 => "\x0C", 13 => "\x0D", 14 => "\x0E", 15 => "\x0F",
|
||||
16 => "\x10", 17 => "\x11", 18 => "\x12", 19 => "\x13",
|
||||
20 => "\x14", 21 => "\x15", 22 => "\x16", 23 => "\x17",
|
||||
24 => "\x18\x18", 25 => "\x18\x19", 26 => "\x18\x1A",
|
||||
32 => "\x18\x20", 50 => "\x18\x32", 64 => "\x18\x40",
|
||||
100 => "\x18\x64", 128 => "\x18\x80", 200 => "\x18\xC8",
|
||||
255 => "\x18\xFF", 256 => "\x19\x01\x00", 500 => "\x19\x01\xF4",
|
||||
1000 => "\x19\x03\xE8", 1023 => "\x19\x03\xFF",
|
||||
];
|
||||
|
||||
/**
|
||||
* Pre-encoded negative integers -1 to -24 and common larger values
|
||||
* CBOR major type 1 (negative integer)
|
||||
*/
|
||||
private const NEG_CACHE = [
|
||||
-1 => "\x20", -2 => "\x21", -3 => "\x22", -4 => "\x23",
|
||||
-5 => "\x24", -10 => "\x29", -20 => "\x33", -24 => "\x37",
|
||||
-25 => "\x38\x18", -50 => "\x38\x31", -100 => "\x38\x63",
|
||||
];
|
||||
|
||||
/**
|
||||
* Encode a PHP value to CBOR binary string
|
||||
*
|
||||
* @param mixed $value The value to encode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function encode(mixed $value): string
|
||||
{
|
||||
return $this->encodeValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively encode a value to CBOR
|
||||
*
|
||||
* @param mixed $value Value to encode
|
||||
* @return string Encoded CBOR bytes
|
||||
*/
|
||||
private function encodeValue(mixed $value): string
|
||||
{
|
||||
switch (gettype($value)) {
|
||||
case 'string':
|
||||
$len = strlen($value);
|
||||
if ($len < 24) {
|
||||
return chr(0x60 | $len) . $value;
|
||||
}
|
||||
|
||||
if ($len < 0x100) {
|
||||
return "\x78" . chr($len) . $value;
|
||||
}
|
||||
|
||||
return $this->encodeTextString($value);
|
||||
|
||||
case 'array':
|
||||
if (isset($value['__cbor_timestamp'])) {
|
||||
return "\xC1\xFB" . pack('E', $value['__cbor_timestamp']);
|
||||
}
|
||||
|
||||
// Encode a byte string (major type 2)
|
||||
if (isset($value['__cbor_bytes'])) {
|
||||
$bytes = $value['__cbor_bytes'];
|
||||
$len = strlen($bytes);
|
||||
if ($len < 24) {
|
||||
return chr(0x40 | $len) . $bytes;
|
||||
}
|
||||
|
||||
if ($len < 0x100) {
|
||||
return "\x58" . chr($len) . $bytes;
|
||||
}
|
||||
|
||||
if ($len < 0x10000) {
|
||||
return "\x59" . pack('n', $len) . $bytes;
|
||||
}
|
||||
|
||||
return "\x5A" . pack('N', $len) . $bytes;
|
||||
}
|
||||
|
||||
if (array_is_list($value)) {
|
||||
return $this->encodeArray($value);
|
||||
}
|
||||
|
||||
return $this->encodeMap($value);
|
||||
|
||||
case 'integer':
|
||||
if (isset(self::INT_CACHE[$value])) {
|
||||
return self::INT_CACHE[$value];
|
||||
}
|
||||
|
||||
if (isset(self::NEG_CACHE[$value])) {
|
||||
return self::NEG_CACHE[$value];
|
||||
}
|
||||
|
||||
// Fast path for positive integers
|
||||
// Major type 0: unsigned integer
|
||||
if ($value >= 0) {
|
||||
if ($value < 24) {
|
||||
return chr($value);
|
||||
}
|
||||
|
||||
if ($value < 0x100) {
|
||||
return "\x18" . chr($value);
|
||||
}
|
||||
|
||||
if ($value < 0x10000) {
|
||||
return "\x19" . pack('n', $value);
|
||||
}
|
||||
|
||||
if ($value < 0x100000000) {
|
||||
return "\x1A" . pack('N', $value);
|
||||
}
|
||||
|
||||
return "\x1B" . pack('J', $value);
|
||||
}
|
||||
|
||||
return $this->encodeInteger($value);
|
||||
|
||||
case 'double':
|
||||
// Encode a float (major type 7, float 64)
|
||||
return "\xFB" . pack('E', $value);
|
||||
|
||||
case 'boolean':
|
||||
// Encode a boolean (major type 7, simple)
|
||||
return $value ? "\xF5" : "\xF4";
|
||||
|
||||
case 'NULL':
|
||||
// Encode null (major type 7, simple)
|
||||
return "\xF6";
|
||||
|
||||
case 'object':
|
||||
throw new CborException("Cannot encode object of type: " . get_class($value));
|
||||
|
||||
default:
|
||||
throw new CborException("Cannot encode value of type: " . gettype($value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an integer (major type 0 or 1)
|
||||
*
|
||||
* @param int $value
|
||||
* @return string
|
||||
*/
|
||||
private function encodeInteger(int $value): string
|
||||
{
|
||||
if (isset(self::INT_CACHE[$value])) {
|
||||
return self::INT_CACHE[$value];
|
||||
}
|
||||
|
||||
if (isset(self::NEG_CACHE[$value])) {
|
||||
return self::NEG_CACHE[$value];
|
||||
}
|
||||
|
||||
if ($value >= 0) {
|
||||
// Major type 0: unsigned integer
|
||||
if ($value < 24) {
|
||||
return chr($value);
|
||||
}
|
||||
|
||||
if ($value < 0x100) {
|
||||
return "\x18" . chr($value);
|
||||
}
|
||||
|
||||
if ($value < 0x10000) {
|
||||
return "\x19" . pack('n', $value);
|
||||
}
|
||||
|
||||
if ($value < 0x100000000) {
|
||||
return "\x1A" . pack('N', $value);
|
||||
}
|
||||
|
||||
return "\x1B" . pack('J', $value);
|
||||
}
|
||||
|
||||
// Major type 1: negative integer (-1 - n)
|
||||
$value = -1 - $value;
|
||||
if ($value < 24) {
|
||||
return chr(0x20 | $value);
|
||||
}
|
||||
|
||||
if ($value < 0x100) {
|
||||
return "\x38" . chr($value);
|
||||
}
|
||||
|
||||
if ($value < 0x10000) {
|
||||
return "\x39" . pack('n', $value);
|
||||
}
|
||||
|
||||
if ($value < 0x100000000) {
|
||||
return "\x3A" . pack('N', $value);
|
||||
}
|
||||
|
||||
return "\x3B" . pack('J', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a text string (major type 3)
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
private function encodeTextString(string $value): string
|
||||
{
|
||||
$len = strlen($value);
|
||||
|
||||
if ($len < 24) {
|
||||
return chr(0x60 | $len) . $value;
|
||||
}
|
||||
|
||||
if ($len < 0x100) {
|
||||
return "\x78" . chr($len) . $value;
|
||||
}
|
||||
|
||||
if ($len < 0x10000) {
|
||||
return "\x79" . pack('n', $len) . $value;
|
||||
}
|
||||
|
||||
if ($len < 0x100000000) {
|
||||
return "\x7A" . pack('N', $len) . $value;
|
||||
}
|
||||
|
||||
return "\x7B" . pack('J', $len) . $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an array (major type 4)
|
||||
*
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
private function encodeArray(array $value): string
|
||||
{
|
||||
$count = count($value);
|
||||
|
||||
if ($count < 24) {
|
||||
$result = chr(0x80 | $count);
|
||||
} elseif ($count < 0x100) {
|
||||
$result = "\x98" . chr($count);
|
||||
} elseif ($count < 0x10000) {
|
||||
$result = "\x99" . pack('n', $count);
|
||||
} elseif ($count < 0x100000000) {
|
||||
$result = "\x9A" . pack('N', $count);
|
||||
} else {
|
||||
$result = "\x9B" . pack('J', $count);
|
||||
}
|
||||
|
||||
foreach ($value as $item) {
|
||||
$result .= $this->encodeValue($item);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a map (major type 5)
|
||||
*
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
private function encodeMap(array $value): string
|
||||
{
|
||||
$count = count($value);
|
||||
|
||||
if ($count < 24) {
|
||||
$result = chr(0xA0 | $count);
|
||||
} elseif ($count < 0x100) {
|
||||
$result = "\xB8" . chr($count);
|
||||
} elseif ($count < 0x10000) {
|
||||
$result = "\xB9" . pack('n', $count);
|
||||
} elseif ($count < 0x100000000) {
|
||||
$result = "\xBA" . pack('N', $count);
|
||||
} else {
|
||||
$result = "\xBB" . pack('J', $count);
|
||||
}
|
||||
|
||||
foreach ($value as $k => $v) {
|
||||
if (is_int($k)) {
|
||||
$result .= $this->encodeInteger($k);
|
||||
} else {
|
||||
$len = strlen($k);
|
||||
if ($len < 24) {
|
||||
$result .= chr(0x60 | $len) . $k;
|
||||
} elseif ($len < 0x100) {
|
||||
$result .= "\x78" . chr($len) . $k;
|
||||
} else {
|
||||
$result .= "\x79" . pack('n', $len) . $k;
|
||||
}
|
||||
}
|
||||
|
||||
$result .= $this->encodeValue($v);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty map (major type 5 with 0 elements)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function encodeEmptyMap(): string
|
||||
{
|
||||
return "\xA0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty indefinite map (major type 5 indefinite length)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function encodeEmptyIndefiniteMap(): string
|
||||
{
|
||||
return "\xBF\xFF";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Aws\Api\Cbor\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CborException extends RuntimeException {}
|
||||
@@ -30,11 +30,6 @@ class DateTimeResult extends \DateTime implements \JsonSerializable
|
||||
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromEpoch');
|
||||
}
|
||||
|
||||
// PHP 5.5 does not support sub-second precision
|
||||
if (\PHP_VERSION_ID < 56000) {
|
||||
return new self(gmdate('c', $unixTimestamp));
|
||||
}
|
||||
|
||||
$decimalSeparator = isset(localeconv()['decimal_point']) ? localeconv()['decimal_point'] : ".";
|
||||
$formatString = "U" . $decimalSeparator . "u";
|
||||
$dateTime = DateTime::createFromFormat(
|
||||
|
||||
@@ -31,19 +31,6 @@ abstract class AbstractErrorParser
|
||||
StructureShape $member
|
||||
);
|
||||
|
||||
protected function extractPayload(
|
||||
StructureShape $member,
|
||||
ResponseInterface $response
|
||||
) {
|
||||
if ($member instanceof StructureShape) {
|
||||
// Structure members parse top-level data into a specific key.
|
||||
return $this->payload($response, $member);
|
||||
} else {
|
||||
// Streaming data is just the stream from the response body.
|
||||
return $response->getBody();
|
||||
}
|
||||
}
|
||||
|
||||
protected function populateShape(
|
||||
array &$data,
|
||||
ResponseInterface $response,
|
||||
@@ -57,16 +44,15 @@ abstract class AbstractErrorParser
|
||||
if (!empty($data['code'])) {
|
||||
|
||||
$errors = $this->api->getOperation($command->getName())->getErrors();
|
||||
foreach ($errors as $key => $error) {
|
||||
foreach ($errors as $error) {
|
||||
|
||||
// If error code matches a known error shape, populate the body
|
||||
if ($this->errorCodeMatches($data, $error)) {
|
||||
$modeledError = $error;
|
||||
$data['body'] = $this->extractPayload(
|
||||
$modeledError,
|
||||
$response
|
||||
$data['body'] = $this->payload(
|
||||
$response,
|
||||
$error
|
||||
);
|
||||
$data['error_shape'] = $modeledError;
|
||||
$data['error_shape'] = $error;
|
||||
|
||||
foreach ($error->getMembers() as $name => $member) {
|
||||
switch ($member['location']) {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Parser\AbstractParser;
|
||||
use Aws\Api\StructureShape;
|
||||
use Aws\CommandInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Base implementation for Smithy RPC V2 protocol error parsers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractRpcV2ErrorParser extends AbstractErrorParser
|
||||
{
|
||||
private const HEADER_QUERY_ERROR = 'x-amzn-query-error';
|
||||
private const HEADER_ERROR_TYPE = 'x-amzn-errortype';
|
||||
private const HEADER_REQUEST_ID = 'x-amzn-requestid';
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @param CommandInterface|null $command
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __invoke(
|
||||
ResponseInterface $response,
|
||||
?CommandInterface $command = null
|
||||
) {
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
$data = $this->parseError($response);
|
||||
|
||||
if (isset($data['parsed']['__type'])) {
|
||||
$data['message'] = $data['parsed']['message'] ?? null;
|
||||
}
|
||||
|
||||
$this->populateShape($data, $response, $command);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @param StructureShape $member
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function payload(
|
||||
ResponseInterface $response,
|
||||
StructureShape $member
|
||||
): array;
|
||||
|
||||
/**
|
||||
* @param StreamInterface $body
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function parseBody(
|
||||
StreamInterface $body,
|
||||
ResponseInterface $response
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseError(ResponseInterface $response): array
|
||||
{
|
||||
$statusCode = (string) $response->getStatusCode();
|
||||
$errorCode = null;
|
||||
$errorType = null;
|
||||
|
||||
if ($this->api?->getMetadata('awsQueryCompatible') !== null
|
||||
&& $response->hasHeader(self::HEADER_QUERY_ERROR)
|
||||
&& $awsQueryError = $this->parseQueryCompatibleHeader($response)
|
||||
) {
|
||||
$errorCode = $awsQueryError['code'];
|
||||
$errorType = $awsQueryError['type'];
|
||||
}
|
||||
|
||||
if (!$errorCode && $response->hasHeader(self::HEADER_ERROR_TYPE)) {
|
||||
$errorCode = $this->extractErrorCode(
|
||||
$response->getHeaderLine(self::HEADER_ERROR_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
$parsedBody = null;
|
||||
$body = $response->getBody();
|
||||
if ($body->getSize()) {
|
||||
//TODO handle unseekable streams with CachingStream
|
||||
$parsedBody = array_change_key_case($this->parseBody($body, $response));
|
||||
}
|
||||
|
||||
if (!$errorCode && $parsedBody) {
|
||||
$errorCode = $this->extractErrorCode(
|
||||
$parsedBody['code'] ?? $parsedBody['__type'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'request_id' => $response->getHeaderLine(self::HEADER_REQUEST_ID),
|
||||
'code' => $errorCode ?: null,
|
||||
'message' => null,
|
||||
'type' => $errorType ?? ($statusCode[0] === '4' ? 'client' : 'server'),
|
||||
'parsed' => $parsedBody,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AWS Query Compatible error from header
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return array|null Returns ['code' => string, 'type' => string] or null
|
||||
*/
|
||||
private function parseQueryCompatibleHeader(ResponseInterface $response): ?array
|
||||
{
|
||||
$parts = explode(';', $response->getHeaderLine(self::HEADER_QUERY_ERROR));
|
||||
if (count($parts) === 2 && $parts[0] && $parts[1]) {
|
||||
return [
|
||||
'code' => $parts[0],
|
||||
'type' => $parts[1],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error code from raw error string containing # and/or : delimiters
|
||||
*
|
||||
* @param string $rawErrorCode
|
||||
* @return string
|
||||
*/
|
||||
private function extractErrorCode(string $rawErrorCode): string
|
||||
{
|
||||
// Handle format with both # and uri (e.g., "namespace#ErrorCode:http://foo-bar")
|
||||
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
|
||||
$start = strpos($rawErrorCode, '#') + 1;
|
||||
$end = strpos($rawErrorCode, ':', $start);
|
||||
return substr($rawErrorCode, $start, $end - $start);
|
||||
}
|
||||
|
||||
// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
|
||||
if (str_contains($rawErrorCode, ':')) {
|
||||
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
|
||||
}
|
||||
|
||||
// Handle format with only # (e.g., "namespace#ErrorCode")
|
||||
if (str_contains($rawErrorCode, '#')) {
|
||||
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
|
||||
}
|
||||
|
||||
return $rawErrorCode;
|
||||
}
|
||||
}
|
||||
+108
-13
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Parser\AbstractParser;
|
||||
use Aws\Api\Parser\PayloadParserTrait;
|
||||
use Aws\Api\StructureShape;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -12,41 +13,135 @@ trait JsonParserTrait
|
||||
{
|
||||
use PayloadParserTrait;
|
||||
|
||||
private function genericHandler(ResponseInterface $response)
|
||||
private function genericHandler(ResponseInterface $response): array
|
||||
{
|
||||
$code = (string) $response->getStatusCode();
|
||||
$error_code = null;
|
||||
$error_type = null;
|
||||
|
||||
// Parse error code and type for query compatible services
|
||||
if ($this->api
|
||||
&& !is_null($this->api->getMetadata('awsQueryCompatible'))
|
||||
&& $response->getHeaderLine('x-amzn-query-error')
|
||||
&& $response->hasHeader('x-amzn-query-error')
|
||||
) {
|
||||
$queryError = $response->getHeaderLine('x-amzn-query-error');
|
||||
$parts = explode(';', $queryError);
|
||||
if (isset($parts) && count($parts) == 2 && $parts[0] && $parts[1]) {
|
||||
$error_code = $parts[0];
|
||||
$error_type = $parts[1];
|
||||
$awsQueryError = $this->parseAwsQueryCompatibleHeader($response);
|
||||
if ($awsQueryError) {
|
||||
$error_code = $awsQueryError['code'];
|
||||
$error_type = $awsQueryError['type'];
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error code from X-Amzn-Errortype header
|
||||
if (!$error_code && $response->hasHeader('X-Amzn-Errortype')) {
|
||||
$error_code = $this->extractErrorCode(
|
||||
$response->getHeaderLine('X-Amzn-Errortype')
|
||||
);
|
||||
}
|
||||
|
||||
$parsedBody = null;
|
||||
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
if (!empty($rawBody)) {
|
||||
$parsedBody = $this->parseJson($rawBody, $response);
|
||||
}
|
||||
|
||||
// Parse error code from response body
|
||||
if (!$error_code && $parsedBody) {
|
||||
$error_code = $this->parseErrorFromBody($parsedBody);
|
||||
}
|
||||
|
||||
if (!isset($error_type)) {
|
||||
$error_type = $code[0] == '4' ? 'client' : 'server';
|
||||
}
|
||||
|
||||
return [
|
||||
'request_id' => (string) $response->getHeaderLine('x-amzn-requestid'),
|
||||
'code' => isset($error_code) ? $error_code : null,
|
||||
'request_id' => $response->getHeaderLine('x-amzn-requestid'),
|
||||
'code' => $error_code ?? null,
|
||||
'message' => null,
|
||||
'type' => $error_type,
|
||||
'parsed' => $this->parseJson($response->getBody(), $response)
|
||||
'parsed' => $parsedBody
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AWS Query Compatible error from header
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
* @return array|null Returns ['code' => string, 'type' => string] or null
|
||||
*/
|
||||
private function parseAwsQueryCompatibleHeader(ResponseInterface $response): ?array
|
||||
{
|
||||
$queryError = $response->getHeaderLine('x-amzn-query-error');
|
||||
$parts = explode(';', $queryError);
|
||||
|
||||
if (count($parts) === 2 && $parts[0] && $parts[1]) {
|
||||
return [
|
||||
'code' => $parts[0],
|
||||
'type' => $parts[1]
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse error code from response body
|
||||
*
|
||||
* @param array|null $parsedBody
|
||||
* @return string|null
|
||||
*/
|
||||
private function parseErrorFromBody(?array $parsedBody): ?string
|
||||
{
|
||||
if (!$parsedBody
|
||||
|| (!isset($parsedBody['code']) && !isset($parsedBody['__type']))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$error_code = $parsedBody['code'] ?? $parsedBody['__type'];
|
||||
return $this->extractErrorCode($error_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error code from raw error string containing # and/or : delimiters
|
||||
*
|
||||
* @param string $rawErrorCode
|
||||
* @return string
|
||||
*/
|
||||
private function extractErrorCode(string $rawErrorCode): string
|
||||
{
|
||||
// Handle format with both # and uri (e.g., "namespace#http://foo-bar")
|
||||
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
|
||||
$start = strpos($rawErrorCode, '#') + 1;
|
||||
$end = strpos($rawErrorCode, ':', $start);
|
||||
return substr($rawErrorCode, $start, $end - $start);
|
||||
}
|
||||
|
||||
// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
|
||||
if (str_contains($rawErrorCode, ':')) {
|
||||
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
|
||||
}
|
||||
|
||||
// Handle format with only # (e.g., "namespace#ErrorCode")
|
||||
if (str_contains($rawErrorCode, '#')) {
|
||||
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
|
||||
}
|
||||
|
||||
return $rawErrorCode;
|
||||
}
|
||||
|
||||
protected function payload(
|
||||
ResponseInterface $response,
|
||||
StructureShape $member
|
||||
) {
|
||||
$jsonBody = $this->parseJson($response->getBody(), $response);
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
|
||||
if ($jsonBody) {
|
||||
return $this->parser->parse($member, $jsonBody);
|
||||
if (!empty($rawBody)) {
|
||||
$jsonBody = $this->parseJson($rawBody, $response);
|
||||
} else {
|
||||
$jsonBody = $rawBody;
|
||||
}
|
||||
|
||||
return $this->parser->parse($member, $jsonBody);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Parser\AbstractParser;
|
||||
use Aws\Api\Parser\JsonParser;
|
||||
use Aws\Api\Service;
|
||||
use Aws\CommandInterface;
|
||||
@@ -25,6 +26,7 @@ class JsonRpcErrorParser extends AbstractErrorParser
|
||||
ResponseInterface $response,
|
||||
?CommandInterface $command = null
|
||||
) {
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
$data = $this->genericHandler($response);
|
||||
|
||||
// Make the casing consistent across services.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Parser\AbstractParser;
|
||||
use Aws\Api\Parser\JsonParser;
|
||||
use Aws\Api\Service;
|
||||
use Aws\Api\StructureShape;
|
||||
@@ -26,11 +27,12 @@ class RestJsonErrorParser extends AbstractErrorParser
|
||||
ResponseInterface $response,
|
||||
?CommandInterface $command = null
|
||||
) {
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
$data = $this->genericHandler($response);
|
||||
|
||||
// Merge in error data from the JSON body
|
||||
if ($json = $data['parsed']) {
|
||||
$data = array_replace($data, $json);
|
||||
$data = array_replace($json, $data);
|
||||
}
|
||||
|
||||
// Correct error type from services like Amazon Glacier
|
||||
@@ -38,18 +40,11 @@ class RestJsonErrorParser extends AbstractErrorParser
|
||||
$data['type'] = strtolower($data['type']);
|
||||
}
|
||||
|
||||
// Retrieve the error code from services like Amazon Elastic Transcoder
|
||||
if ($code = $response->getHeaderLine('x-amzn-errortype')) {
|
||||
$colon = strpos($code, ':');
|
||||
$data['code'] = $colon ? substr($code, 0, $colon) : $code;
|
||||
}
|
||||
|
||||
// Retrieve error message directly
|
||||
$data['message'] = isset($data['parsed']['message'])
|
||||
? $data['parsed']['message']
|
||||
: (isset($data['parsed']['Message'])
|
||||
? $data['parsed']['Message']
|
||||
: null);
|
||||
$data['message'] = $data['parsed']['message']
|
||||
?? $data['parsed']['Message']
|
||||
?? $data['parsed']['error_description']
|
||||
?? null;
|
||||
|
||||
$this->populateShape($data, $response, $command);
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Cbor\CborDecoder;
|
||||
use Aws\Api\Parser\RpcV2ParserTrait;
|
||||
use Aws\Api\Service;
|
||||
use Aws\Api\StructureShape;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Parses errors according to Smithy RPC V2 CBOR protocol standards.
|
||||
*
|
||||
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RpcV2CborErrorParser extends AbstractRpcV2ErrorParser
|
||||
{
|
||||
/** @var CborDecoder */
|
||||
private CborDecoder $decoder;
|
||||
|
||||
use RpcV2ParserTrait;
|
||||
|
||||
/**
|
||||
* @param Service|null $api
|
||||
*/
|
||||
public function __construct(?Service $api = null)
|
||||
{
|
||||
$this->decoder = new CborDecoder();
|
||||
parent::__construct($api);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @param StructureShape $member
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function payload(
|
||||
ResponseInterface $response,
|
||||
StructureShape $member
|
||||
): array
|
||||
{
|
||||
$body = $response->getBody();
|
||||
$cborBody = $this->parseCbor($body, $response);
|
||||
|
||||
return $this->resolveOutputShape($member, $cborBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StreamInterface $body
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function parseBody(
|
||||
StreamInterface $body,
|
||||
ResponseInterface $response
|
||||
): mixed
|
||||
{
|
||||
return $this->parseCbor($body, $response);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Aws\Api\ErrorParser;
|
||||
|
||||
use Aws\Api\Parser\AbstractParser;
|
||||
use Aws\Api\Parser\PayloadParserTrait;
|
||||
use Aws\Api\Parser\XmlParser;
|
||||
use Aws\Api\Service;
|
||||
@@ -27,6 +28,7 @@ class XmlErrorParser extends AbstractErrorParser
|
||||
ResponseInterface $response,
|
||||
?CommandInterface $command = null
|
||||
) {
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
$code = (string) $response->getStatusCode();
|
||||
|
||||
$data = [
|
||||
@@ -37,9 +39,9 @@ class XmlErrorParser extends AbstractErrorParser
|
||||
'parsed' => null
|
||||
];
|
||||
|
||||
$body = $response->getBody();
|
||||
if ($body->getSize() > 0) {
|
||||
$this->parseBody($this->parseXml($body, $response), $data);
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
if (!empty($rawBody)) {
|
||||
$this->parseBody($this->parseXml($rawBody, $response), $data);
|
||||
} else {
|
||||
$this->parseHeaders($response, $data);
|
||||
}
|
||||
@@ -100,12 +102,20 @@ class XmlErrorParser extends AbstractErrorParser
|
||||
ResponseInterface $response,
|
||||
StructureShape $member
|
||||
) {
|
||||
$xmlBody = $this->parseXml($response->getBody(), $response);
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
|
||||
if (empty($rawBody)) {
|
||||
return $rawBody;
|
||||
}
|
||||
|
||||
$xmlBody = $this->parseXml($rawBody, $response);
|
||||
$prefix = $this->registerNamespacePrefix($xmlBody);
|
||||
$errorBody = $xmlBody->xpath("//{$prefix}Error");
|
||||
|
||||
if (is_array($errorBody) && !empty($errorBody[0])) {
|
||||
return $this->parser->parse($member, $errorBody[0]);
|
||||
}
|
||||
|
||||
return $rawBody;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Aws\Api\Exception;
|
||||
|
||||
use Aws\HasMonitoringEventsTrait;
|
||||
use Aws\MonitoringEventsInterface;
|
||||
|
||||
class RpcV2CborException extends \RuntimeException implements
|
||||
MonitoringEventsInterface
|
||||
{
|
||||
use HasMonitoringEventsTrait;
|
||||
}
|
||||
+1
-1
@@ -89,7 +89,7 @@ class Operation extends AbstractModel
|
||||
/**
|
||||
* Get an array of operation error shapes.
|
||||
*
|
||||
* @return Shape[]
|
||||
* @return StructureShape[]
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ use Aws\Api\Service;
|
||||
use Aws\Api\StructureShape;
|
||||
use Aws\CommandInterface;
|
||||
use Aws\ResultInterface;
|
||||
use GuzzleHttp\Psr7\CachingStream;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
@@ -43,4 +44,27 @@ abstract class AbstractParser
|
||||
StructureShape $member,
|
||||
$response
|
||||
);
|
||||
|
||||
public static function getBodyContents(ResponseInterface $response): string
|
||||
{
|
||||
$body = $response->getBody();
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
|
||||
return $body->getContents();
|
||||
}
|
||||
|
||||
public static function getResponseWithCachingStream(
|
||||
ResponseInterface $response
|
||||
): ResponseInterface
|
||||
{
|
||||
if (!$response->getBody()->isSeekable()) {
|
||||
return $response->withBody(
|
||||
new CachingStream($response->getBody())
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
+65
-15
@@ -39,6 +39,21 @@ abstract class AbstractRestParser extends AbstractParser
|
||||
|
||||
if ($payload = $output['payload']) {
|
||||
$this->extractPayload($payload, $output, $response, $result);
|
||||
} else {
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
|
||||
if ($response->getBody()->getSize() === null) {
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
$isEmpty = empty($rawBody);
|
||||
} else {
|
||||
$isEmpty = $response->getBody()->getSize() === 0;
|
||||
}
|
||||
|
||||
if (!$isEmpty && count($output->getMembers()) > 0
|
||||
) {
|
||||
// if no payload was found, then parse the contents of the body
|
||||
$this->payload($response, $output, $result);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($output->getMembers() as $name => $member) {
|
||||
@@ -55,14 +70,6 @@ abstract class AbstractRestParser extends AbstractParser
|
||||
}
|
||||
}
|
||||
|
||||
if (!$payload
|
||||
&& $response->getBody()->getSize() > 0
|
||||
&& count($output->getMembers()) > 0
|
||||
) {
|
||||
// if no payload was found, then parse the contents of the body
|
||||
$this->payload($response, $output, $result);
|
||||
}
|
||||
|
||||
return new Result($result);
|
||||
}
|
||||
|
||||
@@ -73,20 +80,38 @@ abstract class AbstractRestParser extends AbstractParser
|
||||
array &$result
|
||||
) {
|
||||
$member = $output->getMember($payload);
|
||||
|
||||
$body = $response->getBody();
|
||||
if (!empty($member['eventstream'])) {
|
||||
$result[$payload] = new EventParsingIterator(
|
||||
$response->getBody(),
|
||||
$body,
|
||||
$member,
|
||||
$this
|
||||
);
|
||||
} else if ($member instanceof StructureShape) {
|
||||
// Structure members parse top-level data into a specific key.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response = AbstractParser::getResponseWithCachingStream($response);
|
||||
|
||||
if ($member instanceof StructureShape) {
|
||||
//Unions must have at least one member set to a non-null value
|
||||
// If the body is empty, we can assume it is unset
|
||||
if ($response->getBody()->getSize() === null) {
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
$isEmpty = empty($rawBody);
|
||||
} else {
|
||||
$isEmpty = $response->getBody()->getSize() === 0;
|
||||
}
|
||||
|
||||
if (!empty($member['union']) && $isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result[$payload] = [];
|
||||
$this->payload($response, $member, $result[$payload]);
|
||||
} else {
|
||||
// Streaming data is just the stream from the response body.
|
||||
$result[$payload] = $response->getBody();
|
||||
// Always set the payload to the body stream, regardless of content
|
||||
$result[$payload] = $body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +125,21 @@ abstract class AbstractRestParser extends AbstractParser
|
||||
&$result
|
||||
) {
|
||||
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
|
||||
// Empty headers should not be deserialized
|
||||
if ($value === null || $value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($shape->getType()) {
|
||||
case 'float':
|
||||
case 'double':
|
||||
$value = (float) $value;
|
||||
$value = match ($value) {
|
||||
'NaN', 'Infinity', '-Infinity' => $value,
|
||||
default => (float) $value
|
||||
};
|
||||
break;
|
||||
case 'long':
|
||||
case 'integer':
|
||||
$value = (int) $value;
|
||||
break;
|
||||
case 'boolean':
|
||||
@@ -143,6 +176,23 @@ abstract class AbstractRestParser extends AbstractParser
|
||||
//output structure.
|
||||
return;
|
||||
}
|
||||
case 'list':
|
||||
$listMember = $shape->getMember();
|
||||
$type = $listMember->getType();
|
||||
|
||||
// Only boolean lists require special handling
|
||||
// other types can be returned as-is
|
||||
if ($type !== 'boolean') {
|
||||
break;
|
||||
}
|
||||
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$value = array_map(
|
||||
static fn($item) => filter_var($item, FILTER_VALIDATE_BOOLEAN),
|
||||
$items
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$result[$name] = $value;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace Aws\Api\Parser;
|
||||
|
||||
use Aws\Api\Operation;
|
||||
use Aws\Api\Parser\Exception\ParserException;
|
||||
use Aws\Result;
|
||||
use Aws\CommandInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Base implementation for Smithy RPC V2 protocol parsers.
|
||||
*
|
||||
* Implementers MUST define the following static property representing
|
||||
* the `Smithy-Protocol` header value:
|
||||
* self::HEADER_SMITHY_PROTOCOL => static::$smithyProtocol
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractRpcV2Parser extends AbstractParser
|
||||
{
|
||||
private const HEADER_SMITHY_PROTOCOL = 'Smithy-Protocol';
|
||||
|
||||
/** @var string */
|
||||
protected static string $smithyProtocol;
|
||||
|
||||
public function __invoke(
|
||||
CommandInterface $command,
|
||||
ResponseInterface $response
|
||||
) {
|
||||
$operation = $this->api->getOperation($command->getName());
|
||||
|
||||
return $this->parseResponse($response, $operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a response according to Smithy RPC V2 protocol standards.
|
||||
*
|
||||
* @param ResponseInterface $response the response to parse.
|
||||
* @param Operation $operation the operation which holds information for
|
||||
* parsing the response.
|
||||
*
|
||||
* @return Result
|
||||
*/
|
||||
private function parseResponse(
|
||||
ResponseInterface $response,
|
||||
Operation $operation
|
||||
): Result
|
||||
{
|
||||
$smithyProtocolHeader = $response->getHeaderLine(self::HEADER_SMITHY_PROTOCOL);
|
||||
if ($smithyProtocolHeader !== static::$smithyProtocol) {
|
||||
$statusCode = $response->getStatusCode();
|
||||
throw new ParserException(
|
||||
"Malformed response: Smithy-Protocol header mismatch (HTTP {$statusCode}). "
|
||||
. 'Expected ' . static::$smithyProtocol
|
||||
);
|
||||
}
|
||||
|
||||
if ($operation['output'] === null) {
|
||||
return new Result([]);
|
||||
}
|
||||
|
||||
$outputShape = $operation->getOutput();
|
||||
foreach ($outputShape->getMembers() as $memberName => $memberProps) {
|
||||
if (!empty($memberProps['eventstream'])) {
|
||||
return new Result([
|
||||
$memberName => new EventParsingIterator(
|
||||
$response->getBody(),
|
||||
$outputShape->getMember($memberName),
|
||||
$this
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->parseMemberFromStream(
|
||||
$response->getBody(),
|
||||
$outputShape,
|
||||
$response
|
||||
);
|
||||
|
||||
return new Result(is_null($result) ? [] : $result);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -50,7 +50,10 @@ class JsonParser
|
||||
$values = $shape->getValue();
|
||||
$target = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$target[$k] = $this->parse($values, $v);
|
||||
// null map values should not be deserialized
|
||||
if (!is_null($v)) {
|
||||
$target[$k] = $this->parse($values, $v);
|
||||
}
|
||||
}
|
||||
return $target;
|
||||
|
||||
|
||||
+9
-4
@@ -63,11 +63,16 @@ class JsonRpcParser extends AbstractParser
|
||||
}
|
||||
}
|
||||
|
||||
$body = $response->getBody();
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
|
||||
$result = $this->parseMemberFromStream(
|
||||
$response->getBody(),
|
||||
$operation->getOutput(),
|
||||
$response
|
||||
);
|
||||
$body,
|
||||
$operation->getOutput(),
|
||||
$response
|
||||
);
|
||||
|
||||
return new Result(is_null($result) ? [] : $result);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ trait MetadataParserTrait
|
||||
&$result
|
||||
) {
|
||||
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
|
||||
// Empty values should not be deserialized
|
||||
if ($value === null || $value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($shape->getType()) {
|
||||
case 'float':
|
||||
@@ -24,6 +28,7 @@ trait MetadataParserTrait
|
||||
$value = (float) $value;
|
||||
break;
|
||||
case 'long':
|
||||
case 'integer':
|
||||
$value = (int) $value;
|
||||
break;
|
||||
case 'boolean':
|
||||
|
||||
+13
-2
@@ -64,10 +64,21 @@ class NonSeekableStreamDecodingEventStreamIterator extends DecodingEventStreamIt
|
||||
while (!empty($this->tempBuffer) && $num > 0) {
|
||||
$byte = array_shift($this->tempBuffer);
|
||||
$bytes .= $byte;
|
||||
$num = $num - 1;
|
||||
$num -= 1;
|
||||
}
|
||||
|
||||
// Loop until we've read the expected number of bytes
|
||||
while ($num > 0 && !$this->stream->eof()) {
|
||||
$chunk = $this->stream->read($num);
|
||||
$chunkLen = strlen($chunk);
|
||||
$bytes .= $chunk;
|
||||
$num -= $chunkLen;
|
||||
|
||||
if ($chunkLen === 0) {
|
||||
break; // Prevent infinite loop on unexpected EOF
|
||||
}
|
||||
}
|
||||
|
||||
$bytes = $bytes . $this->stream->read($num);
|
||||
hash_update($this->hashContext, $bytes);
|
||||
|
||||
return $bytes;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
namespace Aws\Api\Parser;
|
||||
|
||||
use Aws\Api\Parser\Exception\ParserException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
trait PayloadParserTrait
|
||||
{
|
||||
|
||||
+11
-1
@@ -40,7 +40,17 @@ class QueryParser extends AbstractParser
|
||||
ResponseInterface $response
|
||||
) {
|
||||
$output = $this->api->getOperation($command->getName())->getOutput();
|
||||
$xml = $this->parseXml($response->getBody(), $response);
|
||||
// Read the full payload, even in non-seekable streams
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
// Just parse when the body is not empty
|
||||
$xml = !empty($rawBody)
|
||||
? $this->parseXml($rawBody, $response)
|
||||
: null;
|
||||
|
||||
// Empty request bodies should not be deserialized.
|
||||
if (is_null($xml)) {
|
||||
return new Result();
|
||||
}
|
||||
|
||||
if ($this->honorResultWrapper && $output['resultWrapper']) {
|
||||
$xml = $xml->{$output['resultWrapper']};
|
||||
|
||||
+17
-3
@@ -28,10 +28,24 @@ class RestJsonParser extends AbstractRestParser
|
||||
StructureShape $member,
|
||||
array &$result
|
||||
) {
|
||||
$jsonBody = $this->parseJson($response->getBody(), $response);
|
||||
$rawBody = AbstractParser::getBodyContents($response);
|
||||
|
||||
if ($jsonBody) {
|
||||
$result += $this->parser->parse($member, $jsonBody);
|
||||
// Parse JSON if we have content
|
||||
if (!empty($rawBody)) {
|
||||
$parsedJson = $this->parseJson($rawBody, $response);
|
||||
} else {
|
||||
// An empty response body should be deserialized as null
|
||||
$result = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$parsedBody = $this->parser->parse($member, $parsedJson);
|
||||
if (is_string($parsedBody) && $member['document']) {
|
||||
// Document types can be strings: replace entire result
|
||||
$result = $parsedBody;
|
||||
} else {
|
||||
// Merge array/object results into existing result
|
||||
$result = array_merge($result, (array) $parsedBody);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -28,7 +28,12 @@ class RestXmlParser extends AbstractRestParser
|
||||
StructureShape $member,
|
||||
array &$result
|
||||
) {
|
||||
$result += $this->parseMemberFromStream($response->getBody(), $member, $response);
|
||||
$body = $response->getBody();
|
||||
if ($body->isSeekable()) {
|
||||
$body->rewind();
|
||||
}
|
||||
|
||||
$result += $this->parseMemberFromStream($body, $member, $response);
|
||||
}
|
||||
|
||||
public function parseMemberFromStream(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace Aws\Api\Parser;
|
||||
|
||||
use Aws\Api\Cbor\CborDecoder;
|
||||
use Aws\Api\Service;
|
||||
use Aws\Api\StructureShape;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Parses responses according to Smithy RPC V2 CBOR protocol standards.
|
||||
*
|
||||
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RpcV2CborParser extends AbstractRpcV2Parser
|
||||
{
|
||||
/** @var string */
|
||||
protected static string $smithyProtocol = 'rpc-v2-cbor';
|
||||
|
||||
/** @var CborDecoder */
|
||||
private CborDecoder $decoder;
|
||||
|
||||
use RpcV2ParserTrait;
|
||||
|
||||
/**
|
||||
* @param Service $api Service description
|
||||
*/
|
||||
public function __construct(Service $api)
|
||||
{
|
||||
$this->decoder = new CborDecoder();
|
||||
parent::__construct($api);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StreamInterface $stream
|
||||
* @param StructureShape $member
|
||||
* @param $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function parseMemberFromStream(
|
||||
StreamInterface $stream,
|
||||
StructureShape $member,
|
||||
$response
|
||||
): mixed
|
||||
{
|
||||
return $this->resolveOutputShape($member, $this->parseCbor($stream, $response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
namespace Aws\Api\Parser;
|
||||
|
||||
use Aws\Api\Cbor\Exception\CborException;
|
||||
use Aws\Api\DateTimeResult;
|
||||
use Aws\Api\Parser\Exception\ParserException;
|
||||
use Aws\Api\Shape;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Shared parsing logic for RPC V2 Parsers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait RpcV2ParserTrait
|
||||
{
|
||||
/**
|
||||
* Resolves output shape fields that are present in the response
|
||||
*
|
||||
* @param Shape $shape
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveOutputShape(Shape $shape, mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
switch ($shape['type']) {
|
||||
case 'structure':
|
||||
$target = [];
|
||||
foreach ($shape->getMembers() as $name => $member) {
|
||||
$locationName = $member['locationName'] ?: $name;
|
||||
if (isset($value[$locationName])) {
|
||||
$target[$name] = $this->resolveOutputShape($member, $value[$locationName]);
|
||||
}
|
||||
}
|
||||
return $target;
|
||||
|
||||
case 'list':
|
||||
$target = [];
|
||||
foreach ($value as $v) {
|
||||
$target[] = $this->resolveOutputShape($shape->getMember(), $v);
|
||||
}
|
||||
return $target;
|
||||
|
||||
case 'map':
|
||||
$target = [];
|
||||
foreach ($value as $k => $v) {
|
||||
if ($v !== null) {
|
||||
$target[$k] = $this->resolveOutputShape($shape->getValue(), $v);
|
||||
}
|
||||
}
|
||||
return $target;
|
||||
|
||||
case 'timestamp':
|
||||
try {
|
||||
$value = DateTimeResult::fromEpoch($value);
|
||||
} catch (\Exception $e) {
|
||||
trigger_error(
|
||||
'Unable to parse timestamp value for '
|
||||
. $shape->getName()
|
||||
. ': ' . $e->getMessage(),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses CBOR-encoded response data from RPC V2 CBOR services.
|
||||
*
|
||||
* @param StreamInterface $stream
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function parseCbor(
|
||||
StreamInterface $stream,
|
||||
ResponseInterface $response
|
||||
): mixed
|
||||
{
|
||||
try {
|
||||
$cborString = (string) $stream;
|
||||
return empty($cborString)
|
||||
? null
|
||||
: $this->decoder->decode($cborString);
|
||||
} catch (CborException $e) {
|
||||
throw new ParserException(
|
||||
"Malformed Response: error parsing CBOR: {$e->getMessage()}",
|
||||
0,
|
||||
$e,
|
||||
['response' => $response]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -76,15 +76,18 @@ class XmlParser
|
||||
|
||||
private function memberKey(Shape $shape, $name)
|
||||
{
|
||||
if (null !== $shape['locationName']) {
|
||||
return $shape['locationName'];
|
||||
// Check if locationName came from shape definition
|
||||
if ($shape instanceof StructureShape && isset($shape['locationName'])) {
|
||||
$originalDef = $shape->getOriginalDefinition($shape->getName());
|
||||
|
||||
if ($originalDef && isset($originalDef['locationName'])
|
||||
&& $originalDef['locationName'] === $shape['locationName']
|
||||
) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shape instanceof ListShape && $shape['flattened']) {
|
||||
return $shape->getMember()['locationName'] ?: $name;
|
||||
}
|
||||
|
||||
return $name;
|
||||
return $shape['locationName'] ?? $name;
|
||||
}
|
||||
|
||||
private function parse_list(ListShape $shape, \SimpleXMLElement $value)
|
||||
@@ -132,7 +135,12 @@ class XmlParser
|
||||
|
||||
private function parse_float(Shape $shape, $value)
|
||||
{
|
||||
return (float) (string) $value;
|
||||
$value = (string) $value;
|
||||
|
||||
return match ($value) {
|
||||
'NaN', 'Infinity', '-Infinity' => $value,
|
||||
default => (float) $value
|
||||
};
|
||||
}
|
||||
|
||||
private function parse_integer(Shape $shape, $value)
|
||||
@@ -162,12 +170,8 @@ class XmlParser
|
||||
|
||||
private function parse_xml_attribute(Shape $shape, Shape $memberShape, $value)
|
||||
{
|
||||
$namespace = $shape['xmlNamespace']['uri']
|
||||
? $shape['xmlNamespace']['uri']
|
||||
: '';
|
||||
$prefix = $shape['xmlNamespace']['prefix']
|
||||
? $shape['xmlNamespace']['prefix']
|
||||
: '';
|
||||
$namespace = $shape['xmlNamespace']['uri'] ?? '';
|
||||
$prefix = $shape['xmlNamespace']['prefix'] ?? '';
|
||||
if (!empty($prefix)) {
|
||||
$prefix .= ':';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
namespace Aws\Api\Serializer;
|
||||
|
||||
use Aws\Api\Service;
|
||||
|
||||
use Aws\Api\Shape;
|
||||
use Aws\Api\StructureShape;
|
||||
use Aws\CommandInterface;
|
||||
use Aws\EndpointV2\EndpointV2SerializerTrait;
|
||||
use Aws\EndpointV2\Ruleset\RulesetEndpoint;
|
||||
use DateTimeInterface;
|
||||
use GuzzleHttp\Psr7;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Base implementation for Smithy RPC V2 protocol serializers.
|
||||
*
|
||||
* Implementers MUST override the defaultHeader property to represent
|
||||
* protocol-specific default header values:
|
||||
* self::HEADER_SMITHY_PROTOCOL => static::SMITHY_PROTOCOL,
|
||||
* self::HEADER_CONTENT_TYPE => static::DEFAULT_CONTENT_TYPE,
|
||||
* self::HEADER_ACCEPT => static::DEFAULT_ACCEPT
|
||||
*
|
||||
* Implementers must also implement `serialize()`, `resolveBlob()`, and `resolveTimestamp()
|
||||
* according to their respective protocol specifications.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractRpcV2Serializer
|
||||
{
|
||||
protected const HEADER_SMITHY_PROTOCOL = 'Smithy-Protocol';
|
||||
protected const HEADER_CONTENT_TYPE = 'Content-Type';
|
||||
protected const HEADER_ACCEPT = 'Accept';
|
||||
|
||||
/** @var array */
|
||||
protected static array $defaultHeaders;
|
||||
|
||||
/** @var Service */
|
||||
private Service $api;
|
||||
|
||||
/** @var string|Uri */
|
||||
private string|Uri $endpoint;
|
||||
|
||||
/** @var bool */
|
||||
private bool $isUseEndpointV2;
|
||||
|
||||
use EndpointV2SerializerTrait;
|
||||
|
||||
/**
|
||||
* @param Service $api Service API description
|
||||
* @param string $endpoint Endpoint to connect to
|
||||
*/
|
||||
public function __construct(Service $api, string|Uri $endpoint)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->endpoint = Psr7\Utils::uriFor($endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CommandInterface $command Command to serialize into a request.
|
||||
* @param mixed|null $endpoint
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
CommandInterface $command,
|
||||
mixed $endpoint = null
|
||||
)
|
||||
{
|
||||
$commandArgs = $command->toArray();
|
||||
$commandName = $command->getName();
|
||||
$operation = $this->api->getOperation($commandName);
|
||||
$headers = static::$defaultHeaders;
|
||||
|
||||
// Operations with no defined input type must not contain bodies
|
||||
// Content-Type must not be set
|
||||
if ($operation['input'] !== null) {
|
||||
$body = $this->serialize($operation->getInput(), $commandArgs);
|
||||
$headers['Content-Length'] = (string) strlen($body);
|
||||
} else {
|
||||
unset($headers['Content-Type']);
|
||||
}
|
||||
|
||||
if ($endpoint instanceof RulesetEndpoint) {
|
||||
$this->isUseEndpointV2 = true;
|
||||
$this->setEndpointV2RequestOptions($endpoint, $headers);
|
||||
$this->endpoint = $endpoint->getUrl();
|
||||
}
|
||||
|
||||
$requestTarget = $this->buildRequestTarget(
|
||||
$commandName,
|
||||
$operation['http']['requestUri'] ?? ''
|
||||
);
|
||||
$uri = new Uri($this->endpoint . $requestTarget);
|
||||
|
||||
return new Request(
|
||||
$operation['http']['method'],
|
||||
$uri,
|
||||
$headers,
|
||||
$body ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StructureShape $inputShape
|
||||
* @param array $commandArgs
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function serialize(
|
||||
StructureShape $inputShape,
|
||||
array $commandArgs
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Resolves arguments for blob shapes present in the request arguments
|
||||
* into a protocol-specific format.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function resolveBlob(mixed $value): array;
|
||||
|
||||
/**
|
||||
* Resolves arguments for timestamp shapes present in the request arguments
|
||||
* into a protocol-specific format.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function resolveTimestamp(
|
||||
int|float|string|DateTimeInterface $value
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Resolves input shape fields that are present in the request arguments
|
||||
*
|
||||
* @param Shape $shape
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveInputShape(Shape $shape, mixed $value): mixed
|
||||
{
|
||||
switch ($shape->getType()) {
|
||||
case 'structure':
|
||||
$data = [];
|
||||
foreach ($value as $k => $v) {
|
||||
if ($v !== null && $shape->hasMember($k)) {
|
||||
$valueShape = $shape->getMember($k);
|
||||
$data[$valueShape['locationName'] ?: $k]
|
||||
= $this->resolveInputShape($valueShape, $v);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
case 'list':
|
||||
$items = $shape->getMember();
|
||||
foreach ($value as $k => $v) {
|
||||
$value[$k] = $this->resolveInputShape($items, $v);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
case 'map':
|
||||
$values = $shape->getValue();
|
||||
foreach ($value as $k => $v) {
|
||||
$value[$k] = $this->resolveInputShape($values, $v);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
case 'timestamp':
|
||||
return $this->resolveTimestamp($value);
|
||||
|
||||
case 'string':
|
||||
return (string) $value;
|
||||
|
||||
case 'integer':
|
||||
case 'long':
|
||||
return (int) $value;
|
||||
|
||||
case 'double':
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
|
||||
case 'blob':
|
||||
return $this->resolveBlob($value);
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds request URI absolute path
|
||||
*
|
||||
* @param string $commandName
|
||||
* @param string $requestUri
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function buildRequestTarget(
|
||||
string $commandName,
|
||||
string $requestUri
|
||||
): string
|
||||
{
|
||||
$requestUri = str_ends_with($requestUri, '/')
|
||||
? $requestUri
|
||||
: $requestUri . '/';
|
||||
$targetPrefix = $this->api->getMetadata('targetPrefix');
|
||||
|
||||
return "{$requestUri}service/{$targetPrefix}/operation/{$commandName}";
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -45,14 +45,22 @@ class JsonBody
|
||||
* Builds the JSON body based on an array of arguments.
|
||||
*
|
||||
* @param Shape $shape Operation being constructed
|
||||
* @param array $args Associative array of arguments
|
||||
* @param array|string $args Associative array of arguments, or a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function build(Shape $shape, array $args)
|
||||
public function build(Shape $shape, array|string $args)
|
||||
{
|
||||
$result = json_encode($this->format($shape, $args));
|
||||
return $result == '[]' ? '{}' : $result;
|
||||
try {
|
||||
$result = json_encode($this->format($shape, $args), JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new InvalidJsonException(
|
||||
'Unable to encode JSON document ' . $shape->getName() . ': ' .
|
||||
$e->getMessage() . PHP_EOL
|
||||
);
|
||||
}
|
||||
|
||||
return $result === '[]' ? '{}' : $result;
|
||||
}
|
||||
|
||||
private function format(Shape $shape, $value)
|
||||
@@ -60,7 +68,7 @@ class JsonBody
|
||||
switch ($shape['type']) {
|
||||
case 'structure':
|
||||
$data = [];
|
||||
if (isset($shape['document']) && $shape['document']) {
|
||||
if ($shape['document'] ?? false) {
|
||||
return $value;
|
||||
}
|
||||
foreach ($value as $k => $v) {
|
||||
|
||||
@@ -62,23 +62,26 @@ class JsonRpcSerializer
|
||||
$operationName = $command->getName();
|
||||
$operation = $this->api->getOperation($operationName);
|
||||
$commandArgs = $command->toArray();
|
||||
$body = $this->jsonFormatter->build($operation->getInput(), $commandArgs);
|
||||
$headers = [
|
||||
'X-Amz-Target' => $this->api->getMetadata('targetPrefix') . '.' . $operationName,
|
||||
'Content-Type' => $this->contentType
|
||||
];
|
||||
'Content-Type' => $this->contentType,
|
||||
'Content-Length' => (string) strlen($body)
|
||||
];
|
||||
|
||||
if ($endpoint instanceof RulesetEndpoint) {
|
||||
$this->setEndpointV2RequestOptions($endpoint, $headers);
|
||||
}
|
||||
|
||||
$requestUri = $operation['http']['requestUri'] ?? null;
|
||||
$absoluteUri = str_ends_with($this->endpoint, '/')
|
||||
? $this->endpoint : $this->endpoint . $requestUri;
|
||||
|
||||
return new Request(
|
||||
$operation['http']['method'],
|
||||
$this->endpoint,
|
||||
$absoluteUri,
|
||||
$headers,
|
||||
$this->jsonFormatter->build(
|
||||
$operation->getInput(),
|
||||
$commandArgs
|
||||
)
|
||||
$body
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,8 @@ class QueryParamBuilder
|
||||
if (!$this->isFlat($shape)) {
|
||||
$locationName = $shape->getMember()['locationName'] ?: 'member';
|
||||
$prefix .= ".$locationName";
|
||||
} elseif ($name = $this->queryName($items)) {
|
||||
// flattened lists can also model a `locationName`
|
||||
} elseif ($name = $shape['locationName'] ?? $this->queryName($items)) {
|
||||
$parts = explode('.', $prefix);
|
||||
$parts[count($parts) - 1] = $name;
|
||||
$prefix = implode('.', $parts);
|
||||
|
||||
@@ -36,9 +36,7 @@ class QuerySerializer
|
||||
* containing "method", "uri", "headers", and "body" key value pairs.
|
||||
*
|
||||
* @param CommandInterface $command Command to serialize into a request.
|
||||
* @param $endpointProvider Provider used for dynamic endpoint resolution.
|
||||
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
|
||||
*
|
||||
* @param null $endpoint Endpoint resolved using EndpointProviderV2
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
@@ -63,17 +61,20 @@ class QuerySerializer
|
||||
}
|
||||
$body = http_build_query($body, '', '&', PHP_QUERY_RFC3986);
|
||||
$headers = [
|
||||
'Content-Length' => strlen($body),
|
||||
'Content-Length' => (string) strlen($body),
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
];
|
||||
$requestUri = $operation['http']['requestUri'] ?? null;
|
||||
|
||||
if ($endpoint instanceof RulesetEndpoint) {
|
||||
$this->setEndpointV2RequestOptions($endpoint, $headers);
|
||||
}
|
||||
$absoluteUri = str_ends_with($this->endpoint, '/')
|
||||
? $this->endpoint : $this->endpoint . $requestUri;
|
||||
|
||||
return new Request(
|
||||
'POST',
|
||||
$this->endpoint,
|
||||
$absoluteUri,
|
||||
$headers,
|
||||
$body
|
||||
);
|
||||
|
||||
@@ -31,12 +31,11 @@ class RestJsonSerializer extends RestSerializer
|
||||
$this->jsonFormatter = $jsonFormatter ?: new JsonBody($api);
|
||||
}
|
||||
|
||||
protected function payload(StructureShape $member, array $value, array &$opts)
|
||||
protected function payload(StructureShape $member, array|string $value, array &$opts)
|
||||
{
|
||||
$body = isset($value) ?
|
||||
((string) $this->jsonFormatter->build($member, $value))
|
||||
: "{}";
|
||||
$opts['headers']['Content-Type'] = $this->contentType;
|
||||
$body = $this->jsonFormatter->build($member, $value);
|
||||
$opts['headers']['Content-Length'] = (string) strlen($body);
|
||||
$opts['body'] = $body;
|
||||
}
|
||||
}
|
||||
|
||||
+315
-105
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Aws\Api\Serializer;
|
||||
|
||||
use Aws\Api\ListShape;
|
||||
use Aws\Api\MapShape;
|
||||
use Aws\Api\Service;
|
||||
use Aws\Api\Operation;
|
||||
@@ -10,11 +11,13 @@ use Aws\Api\TimestampShape;
|
||||
use Aws\CommandInterface;
|
||||
use Aws\EndpointV2\EndpointV2SerializerTrait;
|
||||
use Aws\EndpointV2\Ruleset\RulesetEndpoint;
|
||||
use DateTimeInterface;
|
||||
use GuzzleHttp\Psr7;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\UriResolver;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Serializes HTTP locations like header, uri, payload, etc...
|
||||
@@ -22,10 +25,15 @@ use Psr\Http\Message\RequestInterface;
|
||||
*/
|
||||
abstract class RestSerializer
|
||||
{
|
||||
use EndpointV2SerializerTrait;
|
||||
private const TEMPLATE_STRING_REGEX = '/\{([^\}]+)\}/';
|
||||
|
||||
private static array $excludeContentType = [
|
||||
's3' => true,
|
||||
'glacier' => true
|
||||
];
|
||||
|
||||
/** @var Service */
|
||||
private $api;
|
||||
private Service $api;
|
||||
|
||||
/** @var Uri */
|
||||
private $endpoint;
|
||||
@@ -33,9 +41,11 @@ abstract class RestSerializer
|
||||
/** @var bool */
|
||||
private $isUseEndpointV2;
|
||||
|
||||
use EndpointV2SerializerTrait;
|
||||
|
||||
/**
|
||||
* @param Service $api Service API description
|
||||
* @param string $endpoint Endpoint to connect to
|
||||
* @param Service $api Service API description
|
||||
* @param string $endpoint Endpoint to connect to
|
||||
*/
|
||||
public function __construct(Service $api, $endpoint)
|
||||
{
|
||||
@@ -45,19 +55,18 @@ abstract class RestSerializer
|
||||
|
||||
/**
|
||||
* @param CommandInterface $command Command to serialize into a request.
|
||||
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
|
||||
*
|
||||
* @param mixed|null $endpoint
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
CommandInterface $command,
|
||||
$endpoint = null
|
||||
mixed $endpoint = null
|
||||
)
|
||||
{
|
||||
$operation = $this->api->getOperation($command->getName());
|
||||
$commandArgs = $command->toArray();
|
||||
$opts = $this->serialize($operation, $commandArgs);
|
||||
$headers = isset($opts['headers']) ? $opts['headers'] : [];
|
||||
$headers = $opts['headers'] ?? [];
|
||||
|
||||
if ($endpoint instanceof RulesetEndpoint) {
|
||||
$this->isUseEndpointV2 = true;
|
||||
@@ -70,16 +79,16 @@ abstract class RestSerializer
|
||||
$operation['http']['method'],
|
||||
$uri,
|
||||
$headers,
|
||||
isset($opts['body']) ? $opts['body'] : null
|
||||
$opts['body'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a hash of request options for a payload body.
|
||||
*
|
||||
* @param StructureShape $member Member to serialize
|
||||
* @param array $value Value to serialize
|
||||
* @param array $opts Request options to modify.
|
||||
* @param StructureShape $member Member to serialize
|
||||
* @param array $value Value to serialize
|
||||
* @param array $opts Request options to modify.
|
||||
*/
|
||||
abstract protected function payload(
|
||||
StructureShape $member,
|
||||
@@ -103,20 +112,20 @@ abstract class RestSerializer
|
||||
$location = $member['location'];
|
||||
if (!$payload && !$location) {
|
||||
$bodyMembers[$name] = $value;
|
||||
} elseif ($location == 'header') {
|
||||
} elseif ($location === 'header') {
|
||||
$this->applyHeader($name, $member, $value, $opts);
|
||||
} elseif ($location == 'querystring') {
|
||||
} elseif ($location === 'querystring') {
|
||||
$this->applyQuery($name, $member, $value, $opts);
|
||||
} elseif ($location == 'headers') {
|
||||
} elseif ($location === 'headers') {
|
||||
$this->applyHeaderMap($name, $member, $value, $opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($bodyMembers)) {
|
||||
$this->payload($operation->getInput(), $bodyMembers, $opts);
|
||||
$this->payload($input, $bodyMembers, $opts);
|
||||
} else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
|
||||
$this->payload($operation->getInput(), [], $opts);
|
||||
$this->payload($input, [], $opts);
|
||||
}
|
||||
|
||||
return $opts;
|
||||
@@ -130,12 +139,38 @@ abstract class RestSerializer
|
||||
|
||||
$m = $input->getMember($name);
|
||||
|
||||
$type = $m->getType();
|
||||
if ($m['streaming'] ||
|
||||
($m['type'] == 'string' || $m['type'] == 'blob')
|
||||
($type === 'string' || $type === 'blob')
|
||||
) {
|
||||
// This path skips setting the content-type header usually done in
|
||||
// RestJsonSerializer and RestXmlSerializer.certain S3 and glacier
|
||||
// operations determine content type in Middleware::ContentType()
|
||||
if (!isset(self::$excludeContentType[$this->api->getServiceName() ?? ''])) {
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
$opts['headers']['Content-Type'] = 'text/plain';
|
||||
break;
|
||||
case 'blob':
|
||||
$opts['headers']['Content-Type'] = 'application/octet-stream';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$body = $args[$name];
|
||||
if (!$m['streaming'] && is_string($body)) {
|
||||
$opts['headers']['Content-Length'] = (string) strlen($body);
|
||||
}
|
||||
|
||||
// Streaming bodies or payloads that are strings are
|
||||
// always just a stream of data.
|
||||
$opts['body'] = Psr7\Utils::streamFor($args[$name]);
|
||||
$stream = Psr7\Utils::streamFor($body);
|
||||
// User-owned resource which should be detached instead of closed
|
||||
// during garbage-collection
|
||||
if (is_resource($body)) {
|
||||
$stream = \Aws\detach_on_close_stream($stream);
|
||||
}
|
||||
$opts['body'] = $stream;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,13 +179,45 @@ abstract class RestSerializer
|
||||
|
||||
private function applyHeader($name, Shape $member, $value, array &$opts)
|
||||
{
|
||||
if ($member->getType() === 'timestamp') {
|
||||
$timestampFormat = !empty($member['timestampFormat'])
|
||||
? $member['timestampFormat']
|
||||
: 'rfc822';
|
||||
$value = TimestampShape::format($value, $timestampFormat);
|
||||
} elseif ($member->getType() === 'boolean') {
|
||||
$value = $value ? 'true' : 'false';
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle lists by applying header logic to each element
|
||||
if ($member instanceof ListShape) {
|
||||
if (!is_array($value)) {
|
||||
throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.');
|
||||
}
|
||||
|
||||
$listMember = $member->getMember();
|
||||
$headerValues = [];
|
||||
|
||||
foreach ($value as $listValue) {
|
||||
if ($listValue === null) {
|
||||
throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.');
|
||||
}
|
||||
|
||||
$tempOpts = ['headers' => []];
|
||||
$this->applyHeader('temp', $listMember, $listValue, $tempOpts);
|
||||
if (!array_key_exists('temp', $tempOpts['headers'])) {
|
||||
throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.');
|
||||
}
|
||||
|
||||
$convertedValue = $tempOpts['headers']['temp'];
|
||||
$headerValues[] = $convertedValue;
|
||||
}
|
||||
|
||||
$value = $headerValues;
|
||||
} else {
|
||||
switch ($member->getType()) {
|
||||
case 'timestamp':
|
||||
$timestampFormat = $member['timestampFormat'] ?? 'rfc822';
|
||||
$value = $this->formatTimestamp($value, $timestampFormat);
|
||||
break;
|
||||
case 'boolean':
|
||||
$value = $this->formatBoolean($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($member['jsonvalue']) {
|
||||
@@ -163,7 +230,7 @@ abstract class RestSerializer
|
||||
$value = base64_encode($value);
|
||||
}
|
||||
|
||||
$opts['headers'][$member['locationName'] ?: $name] = $value;
|
||||
$opts['headers'][$member['locationName'] ?: $name] = self::prepareHeaderValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,158 +240,301 @@ abstract class RestSerializer
|
||||
{
|
||||
$prefix = $member['locationName'];
|
||||
foreach ($value as $k => $v) {
|
||||
$opts['headers'][$prefix . $k] = $v;
|
||||
if ($v === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$opts['headers'][$prefix . $k] = self::prepareHeaderValue($v);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|string[]
|
||||
*/
|
||||
private static function prepareHeaderValue($value)
|
||||
{
|
||||
if (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if ($value === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($value as $key => $item) {
|
||||
if (!is_scalar($item)) {
|
||||
throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.');
|
||||
}
|
||||
|
||||
$value[$key] = (string) $item;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.');
|
||||
}
|
||||
|
||||
private function applyQuery($name, Shape $member, $value, array &$opts)
|
||||
{
|
||||
if ($member instanceof MapShape) {
|
||||
$opts['query'] = isset($opts['query']) && is_array($opts['query'])
|
||||
? $opts['query'] + $value
|
||||
: $value;
|
||||
} elseif ($value !== null) {
|
||||
$type = $member->getType();
|
||||
if ($type === 'boolean') {
|
||||
$value = $value ? 'true' : 'false';
|
||||
} elseif ($type === 'timestamp') {
|
||||
$timestampFormat = !empty($member['timestampFormat'])
|
||||
? $member['timestampFormat']
|
||||
: 'iso8601';
|
||||
$value = TimestampShape::format($value, $timestampFormat);
|
||||
} elseif ($member instanceof ListShape) {
|
||||
$listMember = $member->getMember();
|
||||
$paramName = $member['locationName'] ?: $name;
|
||||
|
||||
foreach ($value as $listValue) {
|
||||
// Recursively call applyQuery for each list element
|
||||
$tempOpts = ['query' => []];
|
||||
$this->applyQuery('temp', $listMember, $listValue, $tempOpts);
|
||||
$opts['query'][$paramName][] = $tempOpts['query']['temp'];
|
||||
}
|
||||
} elseif (!is_null($value)) {
|
||||
switch ($member->getType()) {
|
||||
case 'timestamp':
|
||||
$timestampFormat = $member['timestampFormat'] ?? 'iso8601';
|
||||
$value = $this->formatTimestamp($value, $timestampFormat);
|
||||
break;
|
||||
case 'boolean':
|
||||
$value = $this->formatBoolean($value);
|
||||
break;
|
||||
}
|
||||
|
||||
$opts['query'][$member['locationName'] ?: $name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildEndpoint(Operation $operation, array $args, array $opts)
|
||||
private function buildEndpoint(
|
||||
Operation $operation,
|
||||
array $args,
|
||||
array $opts
|
||||
): UriInterface
|
||||
{
|
||||
// Expand `requestUri` field members
|
||||
$relativeUri = $this->expandUriTemplate($operation, $args);
|
||||
|
||||
// Add query members to relativeUri
|
||||
if (!empty($opts['query'])) {
|
||||
$relativeUri = $this->appendQuery($opts['query'], $relativeUri);
|
||||
}
|
||||
|
||||
// Special case - S3 keys that need path preservation
|
||||
if ($this->api->getServiceName() === 's3'
|
||||
&& isset($args['Key'])
|
||||
&& $this->shouldPreservePath($args['Key'])
|
||||
) {
|
||||
return new Uri($this->endpoint . $relativeUri);
|
||||
}
|
||||
|
||||
return $this->resolveUri($relativeUri, $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands `requestUri` members
|
||||
*
|
||||
* @param Operation $operation
|
||||
* @param array $args
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function expandUriTemplate(Operation $operation, array $args): string
|
||||
{
|
||||
$serviceName = $this->api->getServiceName();
|
||||
// Create an associative array of variable definitions used in expansions
|
||||
$varDefinitions = $this->getVarDefinitions($operation, $args);
|
||||
|
||||
$relative = preg_replace_callback(
|
||||
'/\{([^\}]+)\}/',
|
||||
function (array $matches) use ($varDefinitions) {
|
||||
$isGreedy = substr($matches[1], -1, 1) == '+';
|
||||
$k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
|
||||
if (!isset($varDefinitions[$k])) {
|
||||
return preg_replace_callback(
|
||||
self::TEMPLATE_STRING_REGEX,
|
||||
static function (array $matches) use ($varDefinitions) {
|
||||
$isGreedy = str_ends_with($matches[1], '+');
|
||||
$varName = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
|
||||
|
||||
if (!isset($varDefinitions[$varName])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = $varDefinitions[$varName];
|
||||
|
||||
if ($isGreedy) {
|
||||
return str_replace('%2F', '/', rawurlencode($varDefinitions[$k]));
|
||||
return str_replace('%2F', '/', rawurlencode($value));
|
||||
}
|
||||
|
||||
return rawurlencode($varDefinitions[$k]);
|
||||
return rawurlencode($value);
|
||||
},
|
||||
$operation['http']['requestUri']
|
||||
);
|
||||
}
|
||||
|
||||
// Add the query string variables or appending to one if needed.
|
||||
if (!empty($opts['query'])) {
|
||||
$relative = $this->appendQuery($opts['query'], $relative);
|
||||
}
|
||||
|
||||
$path = $this->endpoint->getPath();
|
||||
|
||||
if ($this->isUseEndpointV2 && $serviceName === 's3') {
|
||||
if (substr($path, -1) === '/' && $relative[0] === '/') {
|
||||
$path = rtrim($path, '/');
|
||||
}
|
||||
$relative = $path . $relative;
|
||||
|
||||
if (strpos($relative, '../') !== false
|
||||
|| substr($relative, -2) === '..'
|
||||
) {
|
||||
if ($relative[0] !== '/') {
|
||||
$relative = '/' . $relative;
|
||||
/**
|
||||
* Checks for path-like key names. If detected, traditional
|
||||
* URI resolution is bypassed.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldPreservePath(string $key): bool
|
||||
{
|
||||
// Keys with dot segments
|
||||
if (str_contains($key, '.')) {
|
||||
$segments = explode('/', $key);
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment === '.' || $segment === '..') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Uri($this->endpoint->withPath('') . $relative);
|
||||
}
|
||||
}
|
||||
|
||||
if (((!empty($relative) && $relative !== '/')
|
||||
&& !$this->isUseEndpointV2)
|
||||
|| (isset($serviceName) && str_starts_with($serviceName, 'geo-'))
|
||||
) {
|
||||
$this->normalizePath($path);
|
||||
// Keys starting with slash
|
||||
if (str_starts_with($key, '/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If endpoint has path, remove leading '/' to preserve URI resolution.
|
||||
if ($path && $relative[0] === '/') {
|
||||
$relative = substr($relative, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $relativeUri
|
||||
* @param array $opts
|
||||
*
|
||||
* @return UriInterface
|
||||
*/
|
||||
private function resolveUri(string $relativeUri, array $opts): UriInterface
|
||||
{
|
||||
$basePath = $this->endpoint->getPath();
|
||||
|
||||
// Only process if we have a non-empty base path
|
||||
if (!empty($basePath) && $basePath !== '/') {
|
||||
// if relative is just '/', we want just the base path without trailing slash
|
||||
if ($relativeUri === '/' || empty($relativeUri)) {
|
||||
// Remove trailing slash if present
|
||||
return $this->endpoint->withPath(rtrim($basePath, '/'));
|
||||
}
|
||||
|
||||
// if relative is '/?query', we want base path without trailing slash + query
|
||||
// for now, this is only seen with S3 GetBucketLocation after processing the model
|
||||
if (empty($opts['query'])
|
||||
&& str_starts_with($relativeUri, '/?')
|
||||
) {
|
||||
$query = substr($relativeUri, 2); // Remove '/?'
|
||||
return $this->endpoint->withQuery($query);
|
||||
}
|
||||
|
||||
// Ensure base path has trailing slash
|
||||
if (!str_ends_with($basePath, '/')) {
|
||||
$this->endpoint = $this->endpoint->withPath($basePath . '/');
|
||||
}
|
||||
|
||||
// Remove leading slash from relative path to make it relative
|
||||
if (str_starts_with($relativeUri, '/')) {
|
||||
$relativeUri = substr($relativeUri, 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Append path to endpoint when leading '//...'
|
||||
// present as uri cannot be properly resolved
|
||||
if ($this->isUseEndpointV2 && strpos($relative, '//') === 0) {
|
||||
return new Uri($this->endpoint . $relative);
|
||||
}
|
||||
|
||||
// Expand path place holders using Amazon's slightly different URI
|
||||
// template syntax.
|
||||
return UriResolver::resolve($this->endpoint, new Uri($relative));
|
||||
return UriResolver::resolve($this->endpoint, new Uri($relativeUri));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StructureShape $input
|
||||
* @param $payload
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function hasPayloadParam(StructureShape $input, $payload)
|
||||
{
|
||||
if ($payload) {
|
||||
$potentiallyEmptyTypes = ['blob','string'];
|
||||
if ($this->api->getMetadata('protocol') == 'rest-xml') {
|
||||
if ($this->api->getProtocol() === 'rest-xml') {
|
||||
$potentiallyEmptyTypes[] = 'structure';
|
||||
}
|
||||
|
||||
$payloadMember = $input->getMember($payload);
|
||||
if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
|
||||
//unions may also be empty/unset
|
||||
if (!empty($payloadMember['union'])
|
||||
|| in_array($payloadMember['type'], $potentiallyEmptyTypes)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($input->getMembers() as $member) {
|
||||
if (!isset($member['location'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function appendQuery($query, $endpoint)
|
||||
/**
|
||||
* @param $query
|
||||
* @param $relativeUri
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function appendQuery($query, $relativeUri): string
|
||||
{
|
||||
$append = Psr7\Query::build($query);
|
||||
return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}";
|
||||
return $relativeUri
|
||||
. (str_contains($relativeUri, '?') ? "&{$append}" : "?{$append}");
|
||||
}
|
||||
|
||||
private function getVarDefinitions($command, $args)
|
||||
/**
|
||||
* @param CommandInterface $command
|
||||
* @param array $args
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getVarDefinitions(
|
||||
Operation $operation,
|
||||
array $args
|
||||
): array
|
||||
{
|
||||
$varDefinitions = [];
|
||||
|
||||
foreach ($command->getInput()->getMembers() as $name => $member) {
|
||||
if ($member['location'] == 'uri') {
|
||||
$varDefinitions[$member['locationName'] ?: $name] =
|
||||
isset($args[$name])
|
||||
? $args[$name]
|
||||
: null;
|
||||
foreach ($operation->getInput()->getMembers() as $name => $member) {
|
||||
if ($member['location'] === 'uri') {
|
||||
$value = $args[$name] ?? null;
|
||||
if (!is_null($value)) {
|
||||
switch ($member->getType()) {
|
||||
case 'timestamp':
|
||||
$timestampFormat = $member['timestampFormat'] ?? 'iso8601';
|
||||
$value = $this->formatTimestamp($value, $timestampFormat);
|
||||
break;
|
||||
case 'boolean':
|
||||
$value = $this->formatBoolean($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$varDefinitions[$member['locationName'] ?: $name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $varDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends trailing slash to non-empty paths with at least one segment
|
||||
* to ensure proper URI resolution
|
||||
* @param DateTimeInterface|string|int $value
|
||||
* @param string $timestampFormat
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return void
|
||||
* @return string
|
||||
*/
|
||||
private function normalizePath(string $path): void
|
||||
private function formatTimestamp(
|
||||
DateTimeInterface|string|int $value,
|
||||
string $timestampFormat
|
||||
): string
|
||||
{
|
||||
if (!empty($path) && $path !== '/' && substr($path, -1) !== '/') {
|
||||
$this->endpoint = $this->endpoint->withPath($path . '/');
|
||||
}
|
||||
return TimestampShape::format($value, $timestampFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function formatBoolean($value): string
|
||||
{
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ class RestXmlSerializer extends RestSerializer
|
||||
protected function payload(StructureShape $member, array $value, array &$opts)
|
||||
{
|
||||
$opts['headers']['Content-Type'] = 'application/xml';
|
||||
$opts['body'] = $this->getXmlBody($member, $value);
|
||||
$body = $this->getXmlBody($member, $value);
|
||||
$opts['headers']['Content-Length'] = (string) strlen($body);
|
||||
$opts['body'] = $body;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class RestXmlSerializer extends RestSerializer
|
||||
*/
|
||||
private function getXmlBody(StructureShape $member, array $value)
|
||||
{
|
||||
$xmlBody = (string)$this->xmlBody->build($member, $value);
|
||||
$xmlBody = $this->xmlBody->build($member, $value);
|
||||
$xmlBody = str_replace("'", "'", $xmlBody);
|
||||
$xmlBody = str_replace('\r', " ", $xmlBody);
|
||||
$xmlBody = str_replace('\n', " ", $xmlBody);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
namespace Aws\Api\Serializer;
|
||||
|
||||
use Aws\Api\Cbor\CborEncoder;
|
||||
use Aws\Api\Cbor\Exception\CborException;
|
||||
use Aws\Api\Exception\RpcV2CborException;
|
||||
use Aws\Api\Service;
|
||||
use Aws\Api\StructureShape;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Serializes requests according to Smithy RPC-V2 CBOR protocol standards.
|
||||
*
|
||||
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RpcV2CborSerializer extends AbstractRpcV2Serializer
|
||||
{
|
||||
/** @var array|string[] */
|
||||
protected static array $defaultHeaders = [
|
||||
self::HEADER_SMITHY_PROTOCOL => 'rpc-v2-cbor',
|
||||
self::HEADER_CONTENT_TYPE => 'application/cbor',
|
||||
self::HEADER_ACCEPT => 'application/cbor',
|
||||
];
|
||||
|
||||
/** @var CborEncoder */
|
||||
private CborEncoder $encoder;
|
||||
|
||||
/**
|
||||
* @param Service $api Service API description
|
||||
* @param string $endpoint Endpoint to connect to
|
||||
*/
|
||||
public function __construct(Service $api, string $endpoint)
|
||||
{
|
||||
$this->encoder = new CborEncoder();
|
||||
parent::__construct($api, $endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StructureShape $inputShape
|
||||
* @param array $commandArgs
|
||||
*
|
||||
* @return string
|
||||
* @throws RpcV2CborException
|
||||
*/
|
||||
public function serialize(
|
||||
StructureShape $inputShape,
|
||||
array $commandArgs
|
||||
): string
|
||||
{
|
||||
try {
|
||||
$resolvedInput = $this->resolveInputShape($inputShape, $commandArgs);
|
||||
return !empty($resolvedInput)
|
||||
? $this->encoder->encode($resolvedInput)
|
||||
: $this->encoder->encodeEmptyIndefiniteMap();
|
||||
} catch (CborException $e) {
|
||||
throw new RpcV2CborException(
|
||||
'Unable to encode CBOR document ' . $inputShape->getName() . ': ' .
|
||||
$e->getMessage() . PHP_EOL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps blob values in order to be encoded properly into
|
||||
* byte strings.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return string[]
|
||||
* @throws RpcV2CborException
|
||||
*/
|
||||
protected function resolveBlob(mixed $value): array
|
||||
{
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
if ($value === false) {
|
||||
throw new RpcV2CborException(
|
||||
'Failed to read resource stream value during serialization',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to differentiate byte string values during encoding
|
||||
return ['__cbor_bytes' => (string) $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps timestamp values in order to be encoded properly into
|
||||
* value tag 1.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return string[]
|
||||
* @throws RpcV2CborException
|
||||
*/
|
||||
protected function resolveTimestamp(
|
||||
int|float|string|DateTimeInterface $value
|
||||
): array
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return ['__cbor_timestamp' => $value];
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
// Preserve milliseconds
|
||||
$micro = (int) $value->format('u');
|
||||
$value = $value->getTimestamp() + $micro / 1e6;
|
||||
} else {
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp === false) {
|
||||
throw new RpcV2CborException(
|
||||
'Request serialization failed: Invalid date/time: ' . $value,
|
||||
);
|
||||
}
|
||||
|
||||
$value = $timestamp;
|
||||
}
|
||||
|
||||
// Wrapper to differentiate timestamp values during encoding
|
||||
return ['__cbor_timestamp' => $value];
|
||||
}
|
||||
}
|
||||
+42
-8
@@ -14,8 +14,8 @@ use XMLWriter;
|
||||
*/
|
||||
class XmlBody
|
||||
{
|
||||
/** @var \Aws\Api\Service */
|
||||
private $api;
|
||||
/** @var Service */
|
||||
private Service $api;
|
||||
|
||||
/**
|
||||
* @param Service $api API being used to create the XML body.
|
||||
@@ -38,7 +38,10 @@ class XmlBody
|
||||
$xml = new XMLWriter();
|
||||
$xml->openMemory();
|
||||
$xml->startDocument('1.0', 'UTF-8');
|
||||
$this->format($shape, $shape['locationName'] ?: $shape['name'], $args, $xml);
|
||||
|
||||
$rootElementName = $this->determineRootElementName($shape);
|
||||
|
||||
$this->format($shape, $rootElementName, $args, $xml);
|
||||
$xml->endDocument();
|
||||
|
||||
return $xml->outputMemory();
|
||||
@@ -51,7 +54,7 @@ class XmlBody
|
||||
if ($ns = $shape['xmlNamespace']) {
|
||||
$xml->writeAttribute(
|
||||
isset($ns['prefix']) ? "xmlns:{$ns['prefix']}" : 'xmlns',
|
||||
$shape['xmlNamespace']['uri']
|
||||
$ns['uri']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,9 +96,17 @@ class XmlBody
|
||||
$this->startElement($shape, $name, $xml);
|
||||
|
||||
foreach ($this->getStructureMembers($shape, $value) as $k => $definition) {
|
||||
// Default to member name
|
||||
$elementName = $k;
|
||||
|
||||
if ($definition['member']['locationName']
|
||||
&& !isset($definition['member']['locationNameAtStructureLevel'])) {
|
||||
$elementName = $definition['member']['locationName'];
|
||||
}
|
||||
|
||||
$this->format(
|
||||
$definition['member'],
|
||||
$definition['member']['locationName'] ?: $k,
|
||||
$elementName,
|
||||
$definition['value'],
|
||||
$xml
|
||||
);
|
||||
@@ -157,11 +168,13 @@ class XmlBody
|
||||
array $value,
|
||||
XMLWriter $xml
|
||||
) {
|
||||
$xmlEntry = $shape['flattened'] ? $shape['locationName'] : 'entry';
|
||||
$xmlEntry = $shape['flattened'] ? $name : 'entry';
|
||||
$xmlKey = $shape->getKey()['locationName'] ?: 'key';
|
||||
$xmlValue = $shape->getValue()['locationName'] ?: 'value';
|
||||
|
||||
$this->startElement($shape, $name, $xml);
|
||||
if (!$shape['flattened']) {
|
||||
$this->startElement($shape, $name, $xml);
|
||||
}
|
||||
|
||||
foreach ($value as $key => $v) {
|
||||
$this->startElement($shape, $xmlEntry, $xml);
|
||||
@@ -170,7 +183,9 @@ class XmlBody
|
||||
$xml->endElement();
|
||||
}
|
||||
|
||||
$xml->endElement();
|
||||
if (!$shape['flattened']) {
|
||||
$xml->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
private function add_blob(Shape $shape, $name, $value, XMLWriter $xml)
|
||||
@@ -217,4 +232,23 @@ class XmlBody
|
||||
$this->defaultShape($shape, $name, $value, $xml);
|
||||
}
|
||||
}
|
||||
|
||||
private function determineRootElementName(Shape $shape): string
|
||||
{
|
||||
$shapeName = $shape->getName();
|
||||
|
||||
// Look up the shape definition first
|
||||
if ($shapeName && $shapeMap = $shape->getShapeMap()) {
|
||||
if (isset($shapeMap[$shapeName]['locationName'])) {
|
||||
return $shapeMap[$shapeName]['locationName'];
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to shape's current locationName
|
||||
if ($shape['locationName']) {
|
||||
return $shape['locationName'];
|
||||
}
|
||||
|
||||
return $shapeName;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -91,7 +91,8 @@ class Service extends AbstractModel
|
||||
'json' => Serializer\JsonRpcSerializer::class,
|
||||
'query' => Serializer\QuerySerializer::class,
|
||||
'rest-json' => Serializer\RestJsonSerializer::class,
|
||||
'rest-xml' => Serializer\RestXmlSerializer::class
|
||||
'rest-xml' => Serializer\RestXmlSerializer::class,
|
||||
'smithy-rpc-v2-cbor' => Serializer\RpcV2CborSerializer::class
|
||||
];
|
||||
|
||||
$proto = $api->getProtocol();
|
||||
@@ -126,7 +127,8 @@ class Service extends AbstractModel
|
||||
'query' => ErrorParser\XmlErrorParser::class,
|
||||
'rest-json' => ErrorParser\RestJsonErrorParser::class,
|
||||
'rest-xml' => ErrorParser\XmlErrorParser::class,
|
||||
'ec2' => ErrorParser\XmlErrorParser::class
|
||||
'ec2' => ErrorParser\XmlErrorParser::class,
|
||||
'smithy-rpc-v2-cbor' => ErrorParser\RpcV2CborErrorParser::class
|
||||
];
|
||||
|
||||
if (isset($mapping[$protocol])) {
|
||||
@@ -149,7 +151,8 @@ class Service extends AbstractModel
|
||||
'json' => Parser\JsonRpcParser::class,
|
||||
'query' => Parser\QueryParser::class,
|
||||
'rest-json' => Parser\RestJsonParser::class,
|
||||
'rest-xml' => Parser\RestXmlParser::class
|
||||
'rest-xml' => Parser\RestXmlParser::class,
|
||||
'smithy-rpc-v2-cbor' => Parser\RpcV2CborParser::class
|
||||
];
|
||||
|
||||
$proto = $api->getProtocol();
|
||||
|
||||
+50
-2
@@ -4,7 +4,7 @@ namespace Aws\Api;
|
||||
/**
|
||||
* Builds shape based on shape references.
|
||||
*/
|
||||
class ShapeMap
|
||||
class ShapeMap implements \ArrayAccess
|
||||
{
|
||||
/** @var array */
|
||||
private $definitions;
|
||||
@@ -51,7 +51,14 @@ class ShapeMap
|
||||
return $this->simple[$shape];
|
||||
}
|
||||
|
||||
$definition = $shapeRef + $this->definitions[$shape];
|
||||
$shapeDefinition = $this->definitions[$shape];
|
||||
$definition = $shapeRef + $shapeDefinition;
|
||||
// Property to know whether the locationName was set at member level
|
||||
// or the structure level.
|
||||
if (isset($shapeDefinition['locationName'])) {
|
||||
$definition['locationNameAtStructureLevel'] = true;
|
||||
}
|
||||
|
||||
$definition['name'] = $definition['shape'];
|
||||
if (isset($definition['shape'])) {
|
||||
unset($definition['shape']);
|
||||
@@ -65,4 +72,45 @@ class ShapeMap
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->definitions[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->definitions[$offset] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
throw new \BadMethodCallException(
|
||||
'ShapeMap is read-only and cannot be modified.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new \BadMethodCallException(
|
||||
'ShapeMap is read-only and cannot be modified.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,32 @@ class StructureShape extends Shape
|
||||
return $members[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to look up the shape's original definition.
|
||||
* ShapeMap::resolve() merges properties from both
|
||||
* member and target shape definitions, causing certain
|
||||
* properties like `locationName` to be overwritten.
|
||||
*
|
||||
* @return ShapeMap
|
||||
* @internal This method is for internal use only and should not be used
|
||||
* by external code. It may be changed or removed without notice.
|
||||
*/
|
||||
public function getShapeMap(): ShapeMap
|
||||
{
|
||||
return $this->shapeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to look up a shape's original definition.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getOriginalDefinition(string $name): ?array
|
||||
{
|
||||
return $this->shapeMap[$name] ?? null;
|
||||
}
|
||||
|
||||
private function generateMembersHash()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Aws\Api;
|
||||
enum SupportedProtocols: string
|
||||
{
|
||||
case JSON = 'json';
|
||||
case CBOR = 'smithy-rpc-v2-cbor';
|
||||
case REST_JSON = 'rest-json';
|
||||
case REST_XML = 'rest-xml';
|
||||
case QUERY = 'query';
|
||||
|
||||
+3
-3
@@ -28,16 +28,16 @@ class TimestampShape extends Shape
|
||||
$value = $value->getTimestamp();
|
||||
} elseif (is_string($value)) {
|
||||
$value = strtotime($value);
|
||||
} elseif (!is_int($value)) {
|
||||
} elseif (!is_int($value) && !is_float($value)) {
|
||||
throw new \InvalidArgumentException('Unable to handle the provided'
|
||||
. ' timestamp type: ' . gettype($value));
|
||||
}
|
||||
|
||||
switch ($format) {
|
||||
case 'iso8601':
|
||||
return gmdate('Y-m-d\TH:i:s\Z', $value);
|
||||
return gmdate('Y-m-d\TH:i:s\Z', (int) $value);
|
||||
case 'rfc822':
|
||||
return gmdate('D, d M Y H:i:s \G\M\T', $value);
|
||||
return gmdate('D, d M Y H:i:s \G\M\T', (int) $value);
|
||||
case 'unixTimestamp':
|
||||
return $value;
|
||||
default:
|
||||
|
||||
+8
-11
@@ -2,6 +2,7 @@
|
||||
namespace Aws\Api;
|
||||
|
||||
use Aws;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Validates a schema against a hash of input.
|
||||
@@ -248,17 +249,7 @@ class Validator
|
||||
|
||||
private function checkArray($arr)
|
||||
{
|
||||
return $this->isIndexed($arr) || $this->isAssociative($arr);
|
||||
}
|
||||
|
||||
private function isAssociative($arr)
|
||||
{
|
||||
return count(array_filter(array_keys($arr), "is_string")) == count($arr);
|
||||
}
|
||||
|
||||
private function isIndexed(array $arr)
|
||||
{
|
||||
return $arr == array_values($arr);
|
||||
return array_is_list($arr) || Aws\is_associative($arr);
|
||||
}
|
||||
|
||||
private function checkCanString($value)
|
||||
@@ -302,6 +293,12 @@ class Validator
|
||||
|
||||
private function checkDocumentType($value)
|
||||
{
|
||||
// To allow objects like value, which
|
||||
// can be used within a member which type is `Document`
|
||||
if ($value instanceof stdClass) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$typeOfFirstKey = gettype(key($value));
|
||||
foreach ($value as $key => $val) {
|
||||
|
||||
@@ -21,6 +21,14 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise createIntegrationResponseAsync(array $args = [])
|
||||
* @method \Aws\Result createModel(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createModelAsync(array $args = [])
|
||||
* @method \Aws\Result createPortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createPortalAsync(array $args = [])
|
||||
* @method \Aws\Result createPortalProduct(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createPortalProductAsync(array $args = [])
|
||||
* @method \Aws\Result createProductPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createProductPageAsync(array $args = [])
|
||||
* @method \Aws\Result createProductRestEndpointPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createProductRestEndpointPageAsync(array $args = [])
|
||||
* @method \Aws\Result createRoute(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise createRouteAsync(array $args = [])
|
||||
* @method \Aws\Result createRouteResponse(array $args = [])
|
||||
@@ -51,6 +59,16 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise deleteIntegrationResponseAsync(array $args = [])
|
||||
* @method \Aws\Result deleteModel(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteModelAsync(array $args = [])
|
||||
* @method \Aws\Result deletePortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deletePortalAsync(array $args = [])
|
||||
* @method \Aws\Result deletePortalProduct(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deletePortalProductAsync(array $args = [])
|
||||
* @method \Aws\Result deletePortalProductSharingPolicy(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deletePortalProductSharingPolicyAsync(array $args = [])
|
||||
* @method \Aws\Result deleteProductPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteProductPageAsync(array $args = [])
|
||||
* @method \Aws\Result deleteProductRestEndpointPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteProductRestEndpointPageAsync(array $args = [])
|
||||
* @method \Aws\Result deleteRoute(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise deleteRouteAsync(array $args = [])
|
||||
* @method \Aws\Result deleteRouteRequestParameter(array $args = [])
|
||||
@@ -67,6 +85,8 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise deleteVpcLinkAsync(array $args = [])
|
||||
* @method \Aws\Result exportApi(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise exportApiAsync(array $args = [])
|
||||
* @method \Aws\Result disablePortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise disablePortalAsync(array $args = [])
|
||||
* @method \Aws\Result resetAuthorizersCache(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise resetAuthorizersCacheAsync(array $args = [])
|
||||
* @method \Aws\Result getApiResource(array $args = [])
|
||||
@@ -103,6 +123,16 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise getModelTemplateAsync(array $args = [])
|
||||
* @method \Aws\Result getModels(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getModelsAsync(array $args = [])
|
||||
* @method \Aws\Result getPortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPortalAsync(array $args = [])
|
||||
* @method \Aws\Result getPortalProduct(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPortalProductAsync(array $args = [])
|
||||
* @method \Aws\Result getPortalProductSharingPolicy(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getPortalProductSharingPolicyAsync(array $args = [])
|
||||
* @method \Aws\Result getProductPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getProductPageAsync(array $args = [])
|
||||
* @method \Aws\Result getProductRestEndpointPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getProductRestEndpointPageAsync(array $args = [])
|
||||
* @method \Aws\Result getRoute(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getRouteAsync(array $args = [])
|
||||
* @method \Aws\Result getRouteResponse(array $args = [])
|
||||
@@ -113,8 +143,6 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise getRoutesAsync(array $args = [])
|
||||
* @method \Aws\Result getRoutingRule(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getRoutingRuleAsync(array $args = [])
|
||||
* @method \Aws\Result listRoutingRules(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listRoutingRulesAsync(array $args = [])
|
||||
* @method \Aws\Result getStage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise getStageAsync(array $args = [])
|
||||
* @method \Aws\Result getStages(array $args = [])
|
||||
@@ -127,6 +155,22 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise getVpcLinksAsync(array $args = [])
|
||||
* @method \Aws\Result importApi(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise importApiAsync(array $args = [])
|
||||
* @method \Aws\Result listPortalProducts(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPortalProductsAsync(array $args = [])
|
||||
* @method \Aws\Result listPortals(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listPortalsAsync(array $args = [])
|
||||
* @method \Aws\Result listProductPages(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listProductPagesAsync(array $args = [])
|
||||
* @method \Aws\Result listProductRestEndpointPages(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listProductRestEndpointPagesAsync(array $args = [])
|
||||
* @method \Aws\Result listRoutingRules(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise listRoutingRulesAsync(array $args = [])
|
||||
* @method \Aws\Result previewPortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise previewPortalAsync(array $args = [])
|
||||
* @method \Aws\Result publishPortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise publishPortalAsync(array $args = [])
|
||||
* @method \Aws\Result putPortalProductSharingPolicy(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise putPortalProductSharingPolicyAsync(array $args = [])
|
||||
* @method \Aws\Result putRoutingRule(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise putRoutingRuleAsync(array $args = [])
|
||||
* @method \Aws\Result reimportApi(array $args = [])
|
||||
@@ -151,6 +195,14 @@ use Aws\AwsClient;
|
||||
* @method \GuzzleHttp\Promise\Promise updateIntegrationResponseAsync(array $args = [])
|
||||
* @method \Aws\Result updateModel(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateModelAsync(array $args = [])
|
||||
* @method \Aws\Result updatePortal(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updatePortalAsync(array $args = [])
|
||||
* @method \Aws\Result updatePortalProduct(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updatePortalProductAsync(array $args = [])
|
||||
* @method \Aws\Result updateProductPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateProductPageAsync(array $args = [])
|
||||
* @method \Aws\Result updateProductRestEndpointPage(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateProductRestEndpointPageAsync(array $args = [])
|
||||
* @method \Aws\Result updateRoute(array $args = [])
|
||||
* @method \GuzzleHttp\Promise\Promise updateRouteAsync(array $args = [])
|
||||
* @method \Aws\Result updateRouteResponse(array $args = [])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user