first working version
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
This commit is contained in:
parent
6b8fa964cd
commit
6908aad70c
11 changed files with 387 additions and 0 deletions
7
.woodpecker/code-style.yml
Normal file
7
.woodpecker/code-style.yml
Normal 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]
|
151
Classes/Http/WebManifestMiddleware.php
Normal file
151
Classes/Http/WebManifestMiddleware.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
35
Classes/ViewHelpers/WebManifestViewHelper.php
Normal file
35
Classes/ViewHelpers/WebManifestViewHelper.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
4
Configuration/Caches.yaml
Normal file
4
Configuration/Caches.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
DigiComp_FlowWebManifestUtils_Responses:
|
||||||
|
frontend: Neos\Cache\Frontend\StringFrontend
|
||||||
|
#backend: Neos\Cache\Backend\SimpleFileBackend
|
||||||
|
backend: Neos\Cache\Backend\TransientMemoryBackend
|
26
Configuration/Objects.yaml
Normal file
26
Configuration/Objects.yaml
Normal 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"
|
39
Configuration/Settings.Manifest.yaml
Normal file
39
Configuration/Settings.Manifest.yaml
Normal 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'
|
12
Configuration/Settings.yaml
Normal file
12
Configuration/Settings.yaml
Normal 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
19
LICENSE
Normal 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
22
README.md
Normal 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
|
|
@ -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
23
composer.json
Normal 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": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue