first working version
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful

This commit is contained in:
Ferdinand Kuhl 2024-06-21 13:09:34 +02:00
parent 6b8fa964cd
commit 6908aad70c
11 changed files with 387 additions and 0 deletions

View file

@ -0,0 +1,7 @@
steps:
code-style:
image: git.digital-competence.de/woodpecker-ci/plugin-phpcs
settings:
args: Classes/
when:
- event: [push, pull_request, manual]

View file

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowWebManifestUtils\Http;
use GuzzleHttp\Psr7\Response;
use Neos\Cache\Exception as CacheException;
use Neos\Cache\Exception\InvalidDataException;
use Neos\Cache\Frontend\StringFrontend;
use Neos\Flow\Http\Exception;
use Neos\Flow\I18n\Service as I18nService;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\ResourceManagement\ResourceManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class WebManifestMiddleware implements MiddlewareInterface
{
protected int $browserCacheMaxAge;
protected string $defaultName = '';
protected I18nService $i18nService;
protected array $manifestData;
protected string $reactOnPath;
protected ResourceManager $resourceManager;
protected StringFrontend $responseCache;
protected array $uriProperties = ['start_url', 'scope', 'id'];
public function __construct(
int $browserCacheMaxAge,
string $defaultName,
I18nService $i18nService,
array $manifestData,
string $reactOnPath,
ResourceManager $resourceManager,
StringFrontend $responseCache
) {
$this->browserCacheMaxAge = $browserCacheMaxAge;
$this->defaultName = $defaultName;
$this->i18nService = $i18nService;
$this->manifestData = $manifestData;
$this->reactOnPath = $reactOnPath;
$this->resourceManager = $resourceManager;
$this->responseCache = $responseCache;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getUri()->getPath() === $this->reactOnPath) {
return $this->createResponse($request);
}
return $handler->handle($request);
}
/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws CacheException
* @throws InvalidDataException
* @throws \JsonException
* @throws Exception
* @throws MissingActionNameException
*/
protected function createResponse(ServerRequestInterface $request): ResponseInterface
{
$cacheId = (string)$this->i18nService->getConfiguration()->getCurrentLocale();
if ($this->responseCache->has($cacheId)) {
$response = $this->responseCache->get($cacheId);
} else {
$result = $this->manifestData;
if ($result['name'] === null) {
$result['name'] = $this->defaultName;
}
if ($result['short_name'] === null) {
$result['short_name'] = $this->defaultName;
}
if ($result['lang'] === null) {
$result['lang'] = \str_replace(
'_',
'-',
(string)$this->i18nService->getConfiguration()->getCurrentLocale()
);
}
foreach ($this->uriProperties as $uriProperty) {
if (\is_array($result[$uriProperty])) {
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest(ActionRequest::fromHttpRequest($request));
if (isset($result[$uriProperty]['format']) && \is_string($result[$uriProperty]['format'])) {
$uriBuilder->setFormat($result[$uriProperty]['format']);
}
if (!isset($result[$uriProperty]['action'])) {
throw new \InvalidArgumentException(
'if ' . $uriProperty . ' is in array form, action is required',
1718957544
);
}
$result[$uriProperty] = $uriBuilder->uriFor(
$result[$uriProperty]['action'],
$result[$uriProperty]['arguments'] ?? [],
$result[$uriProperty]['controller'] ?? null,
$result[$uriProperty]['package'] ?? null,
$result[$uriProperty]['subPackage'] ?? null,
);
}
}
foreach ($result['icons'] as &$iconData) {
if (!isset($iconData['src']) || !isset($iconData['sizes']) || !isset($iconData['type'])) {
throw new \InvalidArgumentException(
'Icons in manifests require src, sizes and type',
1718961281
);
}
if (\str_starts_with($iconData['src'], 'resource://')) {
$iconData['src'] = $this->resourceManager->getPublicPackageResourceUriByPath($iconData['src']);
}
}
unset($iconData);
if ($result['id'] === null) {
$result['id'] = $result['start_url'];
}
$response = \json_encode($result, \JSON_THROW_ON_ERROR);
$this->responseCache->set($cacheId, $response);
}
return new Response(
200,
[
'Content-Type' => 'application/manifest+json',
'Cache-Control' => 'max-age=' . $this->browserCacheMaxAge . ', must-revalidate',
'Last-Modified' => $this->getCacheDate(),
'Vary' => 'Accept, Accept-Language'
],
$response
);
}
protected function getCacheDate(): string
{
if (false === $lastModified = $this->responseCache->get('lastModified')) {
$lastModified = (new \DateTimeImmutable())->format(\DATE_RFC7231);
$this->responseCache->set('lastModified', $lastModified);
}
return $lastModified;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowWebManifestUtils\ViewHelpers;
use Neos\Flow\Annotations as Flow;
use Neos\FluidAdaptor\Core\ViewHelper\AbstractTagBasedViewHelper;
class WebManifestViewHelper extends AbstractTagBasedViewHelper
{
protected $tagName = 'link';
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowWebManifestUtils", path="reactOnPath")
* @var string
*/
protected string $reactOnPath;
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('useCredentials', 'bool', 'does the manifest requires authentication');
}
public function render(): string
{
if ($this->arguments['useCredentials'] === true) {
$this->tag->addAttribute('crossorigin', 'use-credentials');
}
$this->tag->addAttribute('rel', 'manifest');
$this->tag->addAttribute('href', $this->reactOnPath);
return $this->tag->render();
}
}

View file

@ -0,0 +1,4 @@
DigiComp_FlowWebManifestUtils_Responses:
frontend: Neos\Cache\Frontend\StringFrontend
#backend: Neos\Cache\Backend\SimpleFileBackend
backend: Neos\Cache\Backend\TransientMemoryBackend

View file

@ -0,0 +1,26 @@
DigiComp.FlowWebManifestUtils:ManifestResponseCache:
className: "Neos\\Cache\\Frontend\\StringFrontend"
factoryObjectName: "Neos\\Flow\\Cache\\CacheManager"
factoryMethodName: "getCache"
arguments:
1:
value: "DigiComp_FlowWebManifestUtils_Responses"
DigiComp.FlowWebManifestUtils:WebManifestMiddleware:
className: "DigiComp\\FlowWebManifestUtils\\Http\\WebManifestMiddleware"
autowiring: true
arguments:
1:
setting: "DigiComp.FlowWebManifestUtils.browserCacheMaxAge"
2:
setting: "Neos.Flow.core.applicationName"
3:
object: "Neos\\Flow\\I18n\\Service"
4:
setting: "DigiComp.FlowWebManifestUtils.manifest"
5:
setting: "DigiComp.FlowWebManifestUtils.reactOnPath"
6:
object: "Neos\\Flow\\ResourceManagement\\ResourceManager"
7:
object: "DigiComp.FlowWebManifestUtils:ManifestResponseCache"

View file

@ -0,0 +1,39 @@
DigiComp:
FlowWebManifestUtils:
manifest:
# Splash screen background color (hex value)
background_color: ''
description: ''
# valid values: 'ltr', 'rtl', 'auto'
dir: 'auto'
# Valid values: 'browser', 'standalone', 'minimal-ui', 'fullscreen'
display: 'standalone'
# Array/List of icons with relative rendered path
# example:
# {
# "src": "resource://Vendor.Site/Public/Icons/icon-72x72.png",
# "sizes": "72x72",
# "type": "image/png"
# }
icons: [ ]
# if null, the start_url will forge its identity
id: ~
# Language of your page, e.g. 'en-EN' or 'de-DE', null for auto inferring from current locale
lang: ~
# App name, null for reuse of Neos.Flow.core.applicationName
name: ~
# Valid values: 'landscape', 'portrait'
orientation: 'any'
# See https://w3c.github.io/manifest/#related_applications-member, currently out of scope
#prefer_related_applications: []
#related_applications: []
# Application scope
scope: '/'
# Name below your app icon, null for reuse of Neos.Flow.core.applicationName
short_name: ~
shortcuts: []
# Uri or array in the shape: {action: string, arguments: string, controller: string, packageKey: string, format: string}
start_url: '/'
# Hex value
theme_color: '#fff'

View file

@ -0,0 +1,12 @@
DigiComp:
FlowWebManifestUtils:
reactOnPath: '/app.webmanifest'
browserCacheMaxAge: 3600
Neos:
Flow:
http:
middlewares:
webManifest:
position: "start 100"
middleware: "DigiComp.FlowWebManifestUtils:WebManifestMiddleware"

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Ferdinand Kuhl <f.kuhl@digital-competence.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

22
README.md Normal file
View file

@ -0,0 +1,22 @@
# DigiComp.FlowWebManifestUtils
This package provides small helpers to provide a PWA webmanifest.
It consists of two parts: a middleware, which will return the manifest to the browser and a view helper, which makes
it easy to integrate your manifest to your (fluid template).
All you need to do is to integrate the Manifest-VieHelper to the `head` section of your template and adjust the
`DigiComp.FlowWebmanifestUtils.manifest` values in Settings.yaml to match your needs.
Let me provide you an example:
```html
<html>
<head>
<DigiComp.FlowWebmanifestUtils:WebManifest />
</head>
</html>
```
That is the minimum you need to do, to have working manifest for your application.
The start_url - Parameter in the

View file

@ -0,0 +1,49 @@
type: dictionary
additionalProperties: false
properties:
name:
type: ["string", "null"]
short_name:
type: ["string", "null"]
lang:
type: ["string", "null"]
theme_color:
type: ["string", "null"]
background_color:
type: ["string", "null"]
display:
enum: ["browser", "standalone", "minimal-ui", "fullscreen"]
orientation:
enum: ["landscape", "portrait", "any"]
scope:
type: ["string"]
start_url:
type: ["string", "dictionary"]
properties:
action:
type: ["string"]
controller:
type: ["string"]
package:
type: ["string"]
subPackage:
type: ["string"]
arguments:
type: ["string"]
format:
type: ["string"]
icons:
type: ["array"]
required: true
items:
type: "dictionary"
additionalProperties: false
required: true
minItems: 1
properties:
src:
type: ["string"]
sizes:
type: ["string"]
type:
type: ["string"]

23
composer.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "digicomp/flow-webmanifest-utils",
"type": "neos-package",
"description": "",
"require": {
"ext-json": "*",
"neos/flow": "^6.3 | ^7.3 | ^8.3",
"php": ">= 7.4",
"symfony/polyfill-php80": "^1.16"
},
"autoload": {
"psr-4": {
"DigiComp\\FlowWebManifestUtils\\": "Classes"
}
},
"extra": {
"branch-alias": {
"dev-develop": "1.0.x-dev"
},
"applied-flow-migrations": [
]
}
}