Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
42 changes: 42 additions & 0 deletions framework/core/js/dist/xslt-polyfill/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions framework/core/js/dist/xslt-polyfill/xslt-polyfill.min.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions framework/core/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"nanoid": "^3.1.30",
"punycode": "^2.1.1",
"textarea-caret": "^3.1.0",
"throttle-debounce": "^3.0.1"
"throttle-debounce": "^3.0.1",
"xslt-polyfill": "^1.0.21"
},
"devDependencies": {
"@flarum/jest-config": "^1.0.0",
Expand All @@ -44,7 +45,8 @@
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"build": "webpack --mode production && yarn run copy-xslt-polyfill",
"copy-xslt-polyfill": "rm -rf dist/xslt-polyfill && mkdir -p dist/xslt-polyfill/dist && SRC=$(node -e \"console.log(require('path').dirname(require.resolve('xslt-polyfill/package.json')))\") && cp $SRC/xslt-polyfill.min.js dist/xslt-polyfill/ && cp $SRC/dist/xslt-wasm.js dist/xslt-polyfill/dist/ && cp $SRC/package.json dist/xslt-polyfill/",
"analyze": "cross-env ANALYZER=true yarn run build",
"format": "prettier --write src",
"format-check": "prettier --check src",
Expand Down
88 changes: 88 additions & 0 deletions framework/core/src/Formatter/XsltPolyfill.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Formatter;

use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use RuntimeException;

class XsltPolyfill
{
/**
* Resolve the public URL of the published xslt-polyfill bundle, if it
* can be served by the configured assets disk.
*
* Returns null when the disk has no public URL (e.g. an in-memory test
* disk), in which case callers should skip the polyfill entirely.
*
* @param FilesystemFactory $filesystemFactory
* @return string|null
*/
public static function publicUrl(FilesystemFactory $filesystemFactory)
{
try {
$url = $filesystemFactory->disk('flarum-assets')->url('xslt-polyfill/xslt-polyfill.min.js');
} catch (RuntimeException $e) {
return null;
}

if (($version = self::version()) !== null) {
$url .= '?v='.$version;
}

return $url;
}

/**
* Locate the vendored xslt-polyfill bundle inside core's js/dist.
*
* The polyfill is copied here from node_modules at `yarn build` time
* (see the copy-xslt-polyfill script in framework/core/js/package.json),
* so it ships as part of the published flarum/core package — operators
* never need to run yarn themselves.
*
* @return string|null
*/
public static function findSource()
{
$sourceDir = __DIR__.'/../../js/dist/xslt-polyfill';

if (file_exists($sourceDir.'/xslt-polyfill.min.js')) {
return $sourceDir;
}

return null;
}

/**
* Read the polyfill version from its package.json, used as a cache-bust
* query string on the published URL so browsers pick up new versions
* without waiting for heuristic revalidation.
*
* @return string|null
*/
public static function version()
{
$sourceDir = self::findSource();
if ($sourceDir === null) {
return null;
}

$packageJson = $sourceDir.'/package.json';
if (! file_exists($packageJson)) {
return null;
}

$data = json_decode(file_get_contents($packageJson), true);

return is_array($data) && isset($data['version']) && is_string($data['version'])
? $data['version']
: null;
}
}
37 changes: 37 additions & 0 deletions framework/core/src/Foundation/Console/AssetsPublishCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

use Flarum\Console\AbstractCommand;
use Flarum\Extension\ExtensionManager;
use Flarum\Formatter\XsltPolyfill;
use Flarum\Foundation\Paths;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Filesystem\Cloud;
use Illuminate\Filesystem\Filesystem;

class AssetsPublishCommand extends AbstractCommand
Expand Down Expand Up @@ -67,6 +69,8 @@ protected function fire()
$target->put("fonts/$relPath", $local->get($fullPath));
}

$this->publishXsltPolyfill($target, $local);

$this->info('Publishing extension assets...');

$extensions = $this->container->make(ExtensionManager::class);
Expand All @@ -79,4 +83,37 @@ protected function fire()
}
}
}

/**
* Copies the xslt-polyfill bundle into the public assets disk so the
* Formatter can hand the browser a public URL when native XSLT is
* disabled. Both files are kept in their original relative layout
* (root + ./dist) so the polyfill's currentScript-based wasm loader
* keeps working.
*
* @param Cloud $target
* @param Filesystem $local
*/
private function publishXsltPolyfill(Cloud $target, Filesystem $local)
{
$sourceDir = XsltPolyfill::findSource();

if ($sourceDir === null) {
$this->info('xslt-polyfill not found in node_modules; skipping.');

return;
}

$files = [
'xslt-polyfill.min.js' => 'xslt-polyfill/xslt-polyfill.min.js',
'dist/xslt-wasm.js' => 'xslt-polyfill/dist/xslt-wasm.js',
];

foreach ($files as $relSource => $relTarget) {
$sourcePath = "$sourceDir/$relSource";
if ($local->exists($sourcePath)) {
$target->put($relTarget, $local->get($sourcePath));
}
}
}
}
43 changes: 43 additions & 0 deletions framework/core/src/Frontend/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

namespace Flarum\Frontend;

use Flarum\Formatter\XsltPolyfill;
use Flarum\Frontend\Driver\TitleDriverInterface;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
Expand Down Expand Up @@ -279,9 +281,50 @@ protected function makeHead(): string
return '<meta name="'.e($name).'" content="'.e($content).'">';
}, $this->meta, array_keys($this->meta)));

if ($polyfill = $this->makeXsltPolyfillLoader()) {
$head[] = $polyfill;
}

return implode("\n", array_merge($head, $this->head));
}

/**
* Emit a tiny inline detector that synchronously document.write()s a
* <script src="…xslt-polyfill.min.js"> tag if the browser has no
* working XSLTProcessor. Because document.write of a script tag during
* HTML parsing inserts it inline, the parser blocks until the polyfill
* loads and executes — this guarantees window.XSLTProcessor is in
* place before forum.js runs (s9e calls `new XSLTProcessor` at
* top-level module load).
*
* Browsers with native XSLT pay the cost of the detector only (~200
* bytes); only affected browsers fetch the polyfill itself.
*
* @return string|null
*/
private function makeXsltPolyfillLoader()
{
// @todo v2.0 inject FilesystemFactory as dependency instead
$url = XsltPolyfill::publicUrl(resolve(FilesystemFactory::class));
if ($url === null) {
return null;
}

// JSON-encode the URL with HTML-safe flags so it can't break out of
// the JS string context, even if a hostile asset URL contained
// quotes / angle brackets / ampersands. The JSON-encoded value is
// already a JS string literal (with surrounding quotes), so it can
// be concatenated into the document.write() argument directly.
$jsUrl = json_encode($url, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

// The closing </script> for the written-out tag is split across the
// string literal so the *outer* <script> doesn't close early when
// the HTML parser scans for </script>.
return <<<HTML
<script>(function(){try{if(typeof XSLTProcessor!=="undefined"&&new XSLTProcessor())return;}catch(e){}document.write('<script src='+$jsUrl+'><\/script>');})();</script>
HTML;
}

/**
* @return string
*/
Expand Down
63 changes: 63 additions & 0 deletions framework/core/tests/integration/console/AssetsPublishTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Tests\integration\console;

use Flarum\Testing\integration\ConsoleTestCase;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Contracts\Filesystem\Filesystem;

class AssetsPublishTest extends ConsoleTestCase
{
private function getAssetsDisk(): Filesystem
{
return $this->app()->getContainer()->make(Factory::class)->disk('flarum-assets');
}

/**
* @test
*/
public function publish_command_copies_xslt_polyfill()
{
$disk = $this->getAssetsDisk();
$disk->delete('xslt-polyfill/xslt-polyfill.min.js');
$disk->delete('xslt-polyfill/dist/xslt-wasm.js');

$this->runCommand(['command' => 'assets:publish']);

// The polyfill is vendored in framework/core/js/dist/xslt-polyfill/
// and ships with flarum/core, so publish should always emit both
// files into the assets disk preserving the dist/ layout.
$this->assertTrue(
$disk->exists('xslt-polyfill/xslt-polyfill.min.js'),
'xslt-polyfill.min.js was not published into the flarum-assets disk.'
);
$this->assertTrue(
$disk->exists('xslt-polyfill/dist/xslt-wasm.js'),
'dist/xslt-wasm.js was not published into the flarum-assets disk.'
);
}

/**
* @test
*/
public function published_polyfill_matches_source()
{
$disk = $this->getAssetsDisk();

$this->runCommand(['command' => 'assets:publish']);

$publishedSize = $disk->size('xslt-polyfill/xslt-polyfill.min.js');

$sourcePath = __DIR__.'/../../../js/dist/xslt-polyfill/xslt-polyfill.min.js';
$this->assertFileExists($sourcePath);

$this->assertEquals(filesize($sourcePath), $publishedSize, 'Published polyfill size differs from source.');
}
}
Loading
Loading