Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Das Manifest, mit dem ein Browser OpenNIT als App führt.
*
* Es kommt aus der Anwendung statt als Datei im Webroot: So trägt es den Namen,
* den der Betreiber vergeben hat, und es liegt unter `.webmanifest` statt unter
* `.json`. Serverhärtungen sperren im Webroot häufig pauschal `*.json`, um
* `composer.json` und dergleichen zu verbergen – ein Manifest unter dieser
* Endung wäre davon mitbetroffen und der Browser bekäme es nie zu sehen.
*/
use OpenNIT\Controllers\PwaController;
return static function (): void {
$wurzel = dirname(__DIR__);
$quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);
/**
* Ruft das Manifest ab und gibt Inhalt samt gesetzter Kopfzeilen zurück.
*
* @return array{0:array<string,mixed>, 1:array<string,string>}
*/
$abrufen = static function (string $appName): array {
$controller = new class ($appName) extends PwaController {
/** @var array<string,string> */
public array $kopf = [];
public function __construct(private string $appName)
{
}
protected function db()
{
return null;
}
};
// Die Anwendung liefert nur den Namen; alles Weitere steht im Manifest.
$appKlasse = new ReflectionClass(OpenNIT\Core\Application::class);
$app = $appKlasse->newInstanceWithoutConstructor();
$cfg = $appKlasse->getProperty('config');
$cfg->setAccessible(true);
$cfg->setValue($app, ['app' => ['name' => $appName]]);
$feld = new ReflectionProperty(PwaController::class, 'app');
$feld->setAccessible(true);
$feld->setValue($controller, $app);
$response = new class extends OpenNIT\Core\Response {
/** @var array<string,string> */
public array $gesetzt = [];
public function setHeader(string $name, string $value): self
{
$this->gesetzt[$name] = $value;
return $this;
}
};
ob_start();
$controller->manifest(new OpenNIT\Core\Request(), $response);
$roh = (string)ob_get_clean();
return [(array)json_decode($roh, true), $response->gesetzt];
};
// ----------------------------------------------------------------
test('Das Manifest ist gültiges JSON mit den Pflichtangaben', function () use ($abrufen) {
[$m, $kopf] = $abrufen('OpenNIT');
foreach (['name', 'short_name', 'start_url', 'display', 'icons'] as $feld) {
assertTrue(isset($m[$feld]), 'Dem Manifest fehlt ' . $feld);
}
assertSame('standalone', $m['display']);
assertTrue(count($m['icons']) >= 2, 'Es fehlen Symbole');
foreach ($m['icons'] as $icon) {
assertTrue(isset($icon['src'], $icon['sizes'], $icon['type']), 'Ein Symbol ist unvollständig');
assertTrue(str_starts_with((string)$icon['src'], '/'), 'Ein Symbol zeigt nicht auf diese Installation');
}
});
test('Es trägt den Namen, den der Betreiber vergeben hat', function () use ($abrufen) {
[$m] = $abrufen('IT-Dokumentation Musterfirma');
assertSame('IT-Dokumentation Musterfirma', $m['name']);
// Die Beschreibung sagt, worum es geht – den Namen trägt das Manifest
// bereits; zweimal hintereinander liest es sich wie ein Versehen.
assertFalse(
str_contains((string)$m['description'], (string)$m['name']),
'Die Beschreibung wiederholt den Namen: ' . $m['description']
);
assertTrue(trim((string)$m['description']) !== '', 'Die Beschreibung fehlt');
});
test('Ein leerer Name fällt auf den Produktnamen zurück', function () use ($abrufen) {
[$m] = $abrufen(' ');
assertSame('OpenNIT', $m['name']);
});
test('Der Kurzname bleibt kurz genug für den Startbildschirm', function () use ($abrufen) {
[$m] = $abrufen('Eine ausgesprochen lange Bezeichnung der Installation');
assertTrue(mb_strlen((string)$m['short_name']) <= 12, 'Kurzname: ' . $m['short_name']);
});
test('Der Kurzname endet an einer Wortgrenze', function () use ($abrufen) {
// Mitten im Wort abgeschnitten sieht der Name auf dem Startbildschirm
// nach einem Fehler aus.
[$m] = $abrufen('IT-Dokumentation Musterfirma');
assertSame('IT-Dokumentation', $m['short_name'], 'Der Kurzname endet mitten im Wort');
[$kurz] = $abrufen('Doku Haus GmbH');
assertSame('Doku Haus', $kurz['short_name'], 'Es wurde nicht an der Wortgrenze gekürzt');
// Ein kurzer Name bleibt unangetastet.
[$knapp] = $abrufen('Doku');
assertSame('Doku', $knapp['short_name']);
// Reicht schon das erste Wort über die Grenze, bleibt es stehen – das
// Betriebssystem kürzt dann selbst, mit Auslassungszeichen.
[$lang] = $abrufen('Unternehmensdokumentation');
assertSame('Unternehmensdokumentation', $lang['short_name']);
});
test('Der Medientyp ist der des Standards', function () use ($abrufen) {
// Mit application/json weisen Browser das Manifest zurück.
[, $kopf] = $abrufen('OpenNIT');
assertTrue(
str_starts_with($kopf['Content-Type'] ?? '', 'application/manifest+json'),
'Content-Type: ' . ($kopf['Content-Type'] ?? '—')
);
});
// ----------------------------------------------------------------
// Wie es erreichbar ist
// ----------------------------------------------------------------
test('Der Pfad endet nicht auf .json', function () use ($quelle) {
// Eine Härtung, die `*.json` im Webroot sperrt, nähme dem Browser sonst
// das Manifest – ohne dass an OpenNIT etwas falsch wäre.
$layout = $quelle('templates/layouts/main.php');
assertTrue(
str_contains($layout, '<link rel="manifest" href="/manifest.webmanifest">'),
'Das Layout verweist nicht auf das Manifest unter seiner eigenen Endung'
);
assertFalse(
str_contains($layout, 'href="/manifest.json"'),
'Das Layout verweist weiterhin auf die .json-Fassung'
);
});
test('Das Manifest ist ohne Anmeldung erreichbar', function () use ($quelle) {
// Ein Browser holt es ohne Sitzungsdaten; hinter einer Anmeldung bliebe
// es unerreichbar.
$app = $quelle('src/Core/Application.php');
assertTrue(
(bool)preg_match(
"/router->get\('\/manifest\.webmanifest',\s*'PwaController@manifest'\);/",
$app
),
'Die Route fehlt oder verlangt eine Anmeldung'
);
});
test('Der veraltete Hinweis steht nicht mehr allein', function () use ($quelle) {
// Browser melden `apple-mobile-web-app-capable` als überholt; die
// allgemeine Schreibweise gehört daneben, die eigene bleibt für ältere
// iOS-Fassungen stehen.
$layout = $quelle('templates/layouts/main.php');
assertTrue(
str_contains($layout, '<meta name="mobile-web-app-capable" content="yes">'),
'Die allgemeine Schreibweise fehlt'
);
assertTrue(
str_contains($layout, '<meta name="apple-mobile-web-app-capable" content="yes">'),
'Die Schreibweise für ältere iOS-Fassungen wurde entfernt'
);
});
};