mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-08-04 15:12:12 +00:00
Paper size was a Gotenberg-only setting stored as a single "210mm 297mm"
string. dompdf had no page settings at all: size was pinned to config/dompdf.php's
fixed 'a4', its top-level `orientation` key was read by nothing (the installed
barryvdh v3 builds options only from `defines`), and margins were whatever
dompdf's own stylesheet said. So the two drivers disagreed about margins by
default -- dompdf 1.2cm, Gotenberg hardcoded to zero -- and selecting dompdf
silently discarded the paper size.
Replaces gotenberg_papersize with pdf_paper_width / pdf_paper_height /
pdf_orientation / pdf_margin_{top,right,bottom,left}, saved and applied for
either driver. Width and height are separate CSS lengths because that is the
only lossless shared notation: Gotenberg has no named sizes, and dompdf's named
table cannot express everything Gotenberg accepts. Named presets (A3/A4/A5/
Letter/Legal) are a convenience in the UI that resolve to a pair of lengths.
PdfPageSetup resolves it once and translates: a points array plus an orientation
argument for dompdf, CSS lengths plus landscape() for Gotenberg. Both are handed
the portrait pair, since each swaps the axes itself. Gotenberg's margins() takes
top, bottom, left, right, which is not the CSS order.
dompdf exposes no margin API, so DompdfDriver injects an @page rule -- at the
top of <head>, so a template declaring its own still wins. Doing it in the driver
rather than a Blade partial means custom templates get it without including
anything.
Margins default to 1.2cm, dompdf's existing default, so Gotenberg starts
matching it rather than rendering edge-to-edge. Verified against a live
gotenberg:8: A4 portrait, A4 landscape and Letter at zero margins all come out
with the same page box and the same ink offsets on both drivers.
A malformed length now throws rather than being ignored. Blank still falls back,
but a value that is set and wrong is an operator mistake, and the drivers would
otherwise fail differently: dompdf throws converting to points, Gotenberg would
forward the string and render at some other size.
Also here:
- Migration splits an existing gotenberg_papersize into the new pair. It earns
its place because that key ships in 2.x, not just a 3.x alpha, so a stable
install that chose Letter would otherwise come back up on A4. Drops
gotenberg_margins, which 2.x also stores and neither driver ever read.
- Removes EnvironmentManager::savePDFVariables/getPDFConfiguration, which had no
caller anywhere, and the unused EnvironmentManager injection in the controller.
- config/dompdf.php: drops the dead `orientation` key and defaults enable_remote
to false, matching .env.example, which sets it explicitly and explains why.
Installs predating that line were falling back to true.
- Retires the settings.pdf.footer_text and pdf_layout strings, which no component
referenced.
Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
315 lines
9.7 KiB
PHP
Executable File
315 lines
9.7 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Support\Setup;
|
|
|
|
use App\Http\Requests\DatabaseEnvironmentRequest;
|
|
use App\Http\Requests\DomainEnvironmentRequest;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class EnvironmentManager
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $envPath;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $delimiter = "\n";
|
|
|
|
/**
|
|
* Set the .env and .env.example paths.
|
|
*/
|
|
public function __construct($path = null)
|
|
{
|
|
$this->envPath = base_path('.env');
|
|
}
|
|
|
|
/**
|
|
* Returns the .env contents
|
|
*
|
|
* @return false|string
|
|
*/
|
|
private function getEnvContents()
|
|
{
|
|
return file_get_contents($this->envPath);
|
|
}
|
|
|
|
/**
|
|
* Updates .env file - inspired by Akaunting
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function updateEnv(array $data)
|
|
{
|
|
if (empty($data) || ! is_array($data) || ! is_file($this->envPath)) {
|
|
return false;
|
|
}
|
|
|
|
$env = $this->getEnvContents();
|
|
|
|
$env = explode($this->delimiter, $env);
|
|
|
|
foreach ($data as $data_key => $data_value) {
|
|
$updated = false;
|
|
|
|
foreach ($env as $env_key => $env_value) {
|
|
$entry = explode('=', $env_value, 2);
|
|
|
|
// Check if new or old key
|
|
if ($entry[0] == $data_key) {
|
|
$env[$env_key] = sprintf('%s=%s', $data_key, $this->encode($data_value));
|
|
$updated = true;
|
|
}
|
|
}
|
|
|
|
// Lets create if not available
|
|
if (! $updated) {
|
|
$env[] = $data_key.'='.$this->encode($data_value);
|
|
}
|
|
}
|
|
|
|
$env = implode($this->delimiter, $env);
|
|
|
|
file_put_contents(base_path('.env'), $env);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Encodes value for .env
|
|
*
|
|
* @return mixed|string
|
|
*/
|
|
private function encode($str)
|
|
{
|
|
// Convert to string if not already
|
|
$str = (string) $str;
|
|
|
|
// If the value is already properly quoted, return as is
|
|
if (strlen($str) >= 2 && $str[0] === '"' && $str[strlen($str) - 1] === '"') {
|
|
return $str;
|
|
}
|
|
|
|
// Check if the value contains characters that need quoting
|
|
// Using a character class regex to properly match special characters
|
|
$specialChars = '\^\'£$%&*()}{@#~?><,|=\-_+¬!';
|
|
$needsQuoting = (
|
|
strpos($str, ' ') !== false ||
|
|
preg_match('/['.preg_quote($specialChars, '/').']/', $str)
|
|
);
|
|
|
|
if ($needsQuoting) {
|
|
// Escape any existing double quotes in the string
|
|
$str = str_replace('"', '\\"', $str);
|
|
$str = '"'.$str.'"';
|
|
}
|
|
|
|
return $str;
|
|
}
|
|
|
|
/**
|
|
* Save the database content to the .env file.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function saveDatabaseVariables(DatabaseEnvironmentRequest $request)
|
|
{
|
|
$appUrl = $request->get('app_url');
|
|
if ($appUrl !== config('app.url')) {
|
|
config(['app.url' => $appUrl]);
|
|
}
|
|
[$sanctumDomain, $sessionDomain] = $this->getDomains(
|
|
$request->getHttpHost()
|
|
);
|
|
$dbEnv = [
|
|
'APP_URL' => $appUrl,
|
|
'APP_LOCALE' => $request->get('app_locale'),
|
|
'DB_CONNECTION' => $request->get('database_connection'),
|
|
'SESSION_DOMAIN' => $sessionDomain,
|
|
];
|
|
if ($sanctumDomain !== null) {
|
|
$dbEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
|
|
}
|
|
if ($dbEnv['DB_CONNECTION'] != 'sqlite') {
|
|
if ($request->has('database_username') && $request->has('database_password')) {
|
|
$dbEnv['DB_HOST'] = $request->get('database_hostname');
|
|
$dbEnv['DB_PORT'] = $request->get('database_port');
|
|
$dbEnv['DB_DATABASE'] = $request->get('database_name');
|
|
$dbEnv['DB_USERNAME'] = $request->get('database_username');
|
|
$dbEnv['DB_PASSWORD'] = $request->get('database_password');
|
|
}
|
|
} else {
|
|
// Laravel 11 requires SQLite at least v3.35.0
|
|
// https://laravel.com/docs/11.x/database#introduction
|
|
if (extension_loaded('sqlite3') && class_exists('\SQLite3') && method_exists('\SQLite3', 'version')) {
|
|
$version = \SQLite3::version();
|
|
if (! empty($version['versionString']) && version_compare($version['versionString'], '3.35.0', '<')) {
|
|
return [
|
|
'error_message' => sprintf('The minimum SQLite version is %s. Your current SQLite version is %s which is not supported. Please upgrade SQLite and retry.', '3.35.0', $version['versionString']),
|
|
];
|
|
}
|
|
} else {
|
|
return [
|
|
'error_message' => sprintf('SQLite3 is not present. Please install SQLite >=%s and retry.', '3.35.0'),
|
|
];
|
|
}
|
|
$dbEnv['DB_DATABASE'] = $request->get('database_name');
|
|
$sqlitePath = $this->resolveSqliteDatabasePath($dbEnv['DB_DATABASE']);
|
|
// Create empty SQLite database if it doesn't exist. Ensure the
|
|
// parent directory exists first so user-supplied absolute paths
|
|
// (e.g. /var/data/foo.sqlite) work even when the directory hasn't
|
|
// been pre-created.
|
|
if (! file_exists($sqlitePath)) {
|
|
$parentDir = dirname($sqlitePath);
|
|
if (! is_dir($parentDir)) {
|
|
mkdir($parentDir, 0755, true);
|
|
}
|
|
copy(database_path('stubs/sqlite.empty.db'), $sqlitePath);
|
|
}
|
|
}
|
|
|
|
try {
|
|
$this->checkDatabaseConnection($request);
|
|
if ($request->get('database_overwrite')) {
|
|
Artisan::call('db:wipe --force');
|
|
}
|
|
if (\Schema::hasTable('users')) {
|
|
return [
|
|
'error' => 'database_should_be_empty',
|
|
];
|
|
}
|
|
} catch (Exception $e) {
|
|
return [
|
|
'error_message' => $e->getMessage(),
|
|
];
|
|
}
|
|
|
|
try {
|
|
$this->updateEnv($dbEnv);
|
|
} catch (Exception $e) {
|
|
return [
|
|
'error' => 'database_variables_save_error',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => 'database_variables_save_successfully',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns PDO object if all ok.
|
|
*
|
|
* @return \Closure|\PDO
|
|
*/
|
|
private function checkDatabaseConnection(DatabaseEnvironmentRequest $request)
|
|
{
|
|
$connection = $request->get('database_connection');
|
|
|
|
$settings = config("database.connections.$connection");
|
|
|
|
$connectionArray = array_merge($settings, [
|
|
'driver' => $connection,
|
|
'database' => $connection === 'sqlite'
|
|
? $this->resolveSqliteDatabasePath($request->get('database_name'))
|
|
: $request->get('database_name'),
|
|
]);
|
|
|
|
if ($connection !== 'sqlite' && $request->has('database_username') && $request->has('database_password')) {
|
|
$connectionArray = array_merge($connectionArray, [
|
|
'username' => $request->get('database_username'),
|
|
'password' => $request->get('database_password'),
|
|
'host' => $request->get('database_hostname'),
|
|
'port' => $request->get('database_port'),
|
|
]);
|
|
}
|
|
|
|
config([
|
|
'database' => [
|
|
'migrations' => 'migrations',
|
|
'default' => $connection,
|
|
'connections' => [$connection => $connectionArray],
|
|
],
|
|
]);
|
|
|
|
DB::purge($connection);
|
|
|
|
return DB::connection($connection)->getPdo();
|
|
}
|
|
|
|
private function resolveSqliteDatabasePath(?string $databasePath): string
|
|
{
|
|
$databasePath = trim((string) $databasePath);
|
|
|
|
if ($databasePath === '') {
|
|
return storage_path('app/database.sqlite');
|
|
}
|
|
|
|
if ($this->isAbsolutePath($databasePath)) {
|
|
return $databasePath;
|
|
}
|
|
|
|
return base_path($databasePath);
|
|
}
|
|
|
|
private function isAbsolutePath(string $path): bool
|
|
{
|
|
return str_starts_with($path, DIRECTORY_SEPARATOR)
|
|
|| preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1;
|
|
}
|
|
|
|
/**
|
|
* Save sanctum stateful domain to the .env file.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function saveDomainVariables(DomainEnvironmentRequest $request)
|
|
{
|
|
try {
|
|
[$sanctumDomain, $sessionDomain] = $this->getDomains(
|
|
$request->get('app_domain')
|
|
);
|
|
$domainEnv = [
|
|
'SESSION_DOMAIN' => $sessionDomain,
|
|
];
|
|
if ($sanctumDomain !== null) {
|
|
$domainEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
|
|
}
|
|
$this->updateEnv($domainEnv);
|
|
} catch (Exception $e) {
|
|
return [
|
|
'error' => 'domain_verification_failed',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => 'domain_variable_save_successfully',
|
|
];
|
|
}
|
|
|
|
private function getDomains(string $requestDomain): array
|
|
{
|
|
$appUrl = config('app.url');
|
|
|
|
$port = parse_url($appUrl, PHP_URL_PORT);
|
|
$currentDomain = parse_url($appUrl, PHP_URL_HOST).(
|
|
$port ? ':'.$port : ''
|
|
);
|
|
|
|
$requestHost = parse_url($requestDomain, PHP_URL_HOST) ?: $requestDomain;
|
|
|
|
$isSame = $currentDomain === $requestDomain;
|
|
|
|
return [
|
|
$isSame && env('SANCTUM_STATEFUL_DOMAINS', false) === false ?
|
|
null : $requestDomain,
|
|
$isSame && env('SESSION_DOMAIN', false) === null ?
|
|
null : $requestHost,
|
|
];
|
|
}
|
|
}
|