Skip to content

Dependency Licensing Improvements #4907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 24, 2024
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
45 changes: 0 additions & 45 deletions app/App/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use BookStack\Entities\Queries\QueryTopFavourites;
use BookStack\Entities\Tools\PageContent;
use BookStack\Http\Controller;
use BookStack\Uploads\FaviconHandler;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;

Expand Down Expand Up @@ -112,48 +111,4 @@ public function index(

return view('home.default', $commonData);
}

/**
* Show the view for /robots.txt.
*/
public function robots()
{
$sitePublic = setting('app-public', false);
$allowRobots = config('app.allow_robots');

if ($allowRobots === null) {
$allowRobots = $sitePublic;
}

return response()
->view('misc.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}

/**
* Show the route for 404 responses.
*/
public function notFound()
{
return response()->view('errors.404', [], 404);
}

/**
* Serve the application favicon.
* Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served
* directly by the webserver in the future.
*/
public function favicon(FaviconHandler $favicons)
{
$exists = $favicons->restoreOriginalIfNotExists();
return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());
}

/**
* Serve a PWA application manifest.
*/
public function pwaManifest(PwaManifestBuilder $manifestBuilder)
{
return response()->json($manifestBuilder->build());
}
}
67 changes: 67 additions & 0 deletions app/App/MetaController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace BookStack\App;

use BookStack\Http\Controller;
use BookStack\Uploads\FaviconHandler;

class MetaController extends Controller
{
/**
* Show the view for /robots.txt.
*/
public function robots()
{
$sitePublic = setting('app-public', false);
$allowRobots = config('app.allow_robots');

if ($allowRobots === null) {
$allowRobots = $sitePublic;
}

return response()
->view('misc.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}

/**
* Show the route for 404 responses.
*/
public function notFound()
{
return response()->view('errors.404', [], 404);
}

/**
* Serve the application favicon.
* Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served
* directly by the webserver in the future.
*/
public function favicon(FaviconHandler $favicons)
{
$exists = $favicons->restoreOriginalIfNotExists();
return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());
}

/**
* Serve a PWA application manifest.
*/
public function pwaManifest(PwaManifestBuilder $manifestBuilder)
{
return response()->json($manifestBuilder->build());
}

/**
* Show license information for the application.
*/
public function licenses()
{
$this->setPageTitle(trans('settings.licenses'));

return view('help.licenses', [
'license' => file_get_contents(base_path('LICENSE')),
'phpLibData' => file_get_contents(base_path('dev/licensing/php-library-licenses.txt')),
'jsLibData' => file_get_contents(base_path('dev/licensing/js-library-licenses.txt')),
]);
}
}
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"lint": "phpcs",
"test": "phpunit",
"t-reset": "@php artisan test --recreate-databases",
"build-licenses": [
"@php ./dev/licensing/gen-js-licenses",
"@php ./dev/licensing/gen-php-licenses"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
Expand Down
4 changes: 4 additions & 0 deletions dev/build/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ esbuild.build({
format: 'esm',
minify: isProd,
logLevel: 'info',
banner: {
js: '// See the "/licenses" URI for full package license details',
css: '/* See the "/licenses" URI for full package license details */',
},
}).then(result => {
fs.writeFileSync('esbuild-meta.json', JSON.stringify(result.metafile));
}).catch(() => process.exit(1));
63 changes: 63 additions & 0 deletions dev/licensing/gen-js-licenses
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env php
<?php

// This script reads the project composer.lock file to generate
// clear license details for our PHP dependencies.

declare(strict_types=1);
require "gen-licenses-shared.php";

$rootPath = dirname(__DIR__, 2);
$outputPath = "{$rootPath}/dev/licensing/js-library-licenses.txt";
$outputSeparator = "\n-----------\n";

$packages = [
...glob("{$rootPath}/node_modules/*/package.json"),
...glob("{$rootPath}/node_modules/@*/*/package.json"),
];

$packageOutput = array_map(packageToOutput(...), $packages);

$licenseInfo = implode($outputSeparator, $packageOutput) . "\n";
file_put_contents($outputPath, $licenseInfo);

echo "License information written to {$outputPath}\n";
echo implode("\n", getWarnings()) . "\n";

function packageToOutput(string $packagePath): string
{
global $rootPath;
$package = json_decode(file_get_contents($packagePath));
$output = ["{$package->name}"];

$license = $package->license ?? '';
if ($license) {
$output[] = "License: {$license}";
} else {
warn("Package {$package->name}: No license found");
}

$licenseFile = findLicenseFile($package->name, $packagePath);
if ($licenseFile) {
$relLicenseFile = str_replace("{$rootPath}/", '', $licenseFile);
$output[] = "License File: {$relLicenseFile}";
$copyright = findCopyright($licenseFile);
if ($copyright) {
$output[] = "Copyright: {$copyright}";
} else {
warn("Package {$package->name}: no copyright found in its license");
}
}

$source = $package->repository->url ?? $package->repository ?? '';
if ($source) {
$output[] = "Source: {$source}";
}

$link = $package->homepage ?? $source;
if ($link) {
$output[] = "Link: {$link}";
}

return implode("\n", $output);
}
66 changes: 66 additions & 0 deletions dev/licensing/gen-licenses-shared.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

$warnings = [];

function findLicenseFile(string $packageName, string $packagePath): string
{
$licenseNameOptions = [
'license', 'LICENSE', 'License',
'license.*', 'LICENSE.*', 'License.*',
'license-*.*', 'LICENSE-*.*', 'License-*.*',
];
$packageDir = dirname($packagePath);

$foundLicenses = [];
foreach ($licenseNameOptions as $option) {
$search = glob("{$packageDir}/$option");
array_push($foundLicenses, ...$search);
}

if (count($foundLicenses) > 1) {
warn("Package {$packageName}: more than one license file found");
}

if (count($foundLicenses) > 0) {
$fileName = basename($foundLicenses[0]);
return "{$packageDir}/{$fileName}";
}

warn("Package {$packageName}: no license files found");
return '';
}

function findCopyright(string $licenseFile): string
{
$fileContents = file_get_contents($licenseFile);
$pattern = '/^.*?copyright (\(c\)|\d{4})[\s\S]*?(\n\n|\.\n)/mi';
$matches = [];
preg_match($pattern, $fileContents, $matches);
$copyright = trim($matches[0] ?? '');

if (str_contains($copyright, 'i.e.')) {
return '';
}

$emailPattern = '/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i';
return preg_replace_callback($emailPattern, obfuscateEmail(...), $copyright);
}

function obfuscateEmail(array $matches): string
{
return preg_replace('/[^@.]/', '*', $matches[1]);
}

function warn(string $text): void
{
global $warnings;
$warnings[] = "WARN:" . $text;
}

function getWarnings(): array
{
global $warnings;
return $warnings;
}
55 changes: 55 additions & 0 deletions dev/licensing/gen-php-licenses
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env php
<?php

// This script reads the project composer.lock file to generate
// clear license details for our PHP dependencies.

declare(strict_types=1);
require "gen-licenses-shared.php";

$rootPath = dirname(__DIR__, 2);
$outputPath = "{$rootPath}/dev/licensing/php-library-licenses.txt";
$composerLock = json_decode(file_get_contents($rootPath . "/composer.lock"));
$outputSeparator = "\n-----------\n";

$packages = $composerLock->packages;
$packageOutput = array_map(packageToOutput(...), $packages);

$licenseInfo = implode($outputSeparator, $packageOutput) . "\n";
file_put_contents($outputPath, $licenseInfo);

echo "License information written to {$outputPath}\n";
echo implode("\n", getWarnings()) . "\n";

function packageToOutput(stdClass $package) : string {
global $rootPath;
$output = ["{$package->name}"];

$licenses = is_array($package->license) ? $package->license : [$package->license];
$output[] = "License: " . implode(' ', $licenses);

$packagePath = "{$rootPath}/vendor/{$package->name}/package.json";
$licenseFile = findLicenseFile($package->name, $packagePath);
if ($licenseFile) {
$relLicenseFile = str_replace("{$rootPath}/", '', $licenseFile);
$output[] = "License File: {$relLicenseFile}";
$copyright = findCopyright($licenseFile);
if ($copyright) {
$output[] = "Copyright: {$copyright}";
} else {
warn("Package {$package->name}: no copyright found in its license");
}
}

$source = $package->source->url;
if ($source) {
$output[] = "Source: {$source}";
}

$link = $package->homepage ?? $package->source->url ?? '';
if ($link) {
$output[] = "Link: {$link}";
}

return implode("\n", $output);
}
Loading