Files
ignacionelson 479dc61d2d Move branding into core, and leave white-labelling behind
Logo and watermark belonged in the private package for one reason: that
is where they were written. Nothing about them needs a hosted platform,
and an installation wanting its own mark on the pages it serves is the
ordinary case rather than the exotic one. They are core's now, and every
installation has them.

Hiding "Powered by ProjectSend" did not come. That is what a hosted
customer pays for, and its gate is not a capability key but the absence
of the code: cloud-modules keeps the listener, so an installation without
that package holds the column and has nothing able to read it. Flipping
an edition variable buys nothing, which was true before and stays true.
Core renders the switch where Capability::AttributionHide is held and has
no route that can save it -- there is a test asserting exactly that, which
fails the day white-labelling quietly becomes free.

The migrations move with their original filenames on purpose. A Cloud
tenant already ran them under those names, so Laravel skips them there
and the table and its data are untouched; a fresh install or a community
one runs them from here for the first time.

What got better on the way rather than merely moving:

The watermark listeners take core's real RenderingImage and
ResolvingImageRendering instead of duck-typed `object` payloads, and the
tests construct the genuine events rather than anonymous stand-ins that
imitated their shape. The package had to do it that way -- it builds with
no host present -- so three PHPStan ignore entries existed to describe
what the type system could not see. They are gone.

ModuleBoundaryTest asserted "branding is cloud-only, and the suite runs as
community", which was never what it was testing. It now reads the
capability off the route and subtracts it, so the invariant holds for
whichever module is installed.

The 43 branding strings arrived in all sixteen locales from the package's
own catalogues rather than being retranslated, and the package's are
pruned to the one string it still uses.

A hosted plan without branding subtracts branding.customize and
attribution.hide from the instance's environment. The row is never
deleted by that: a downgrade is usually an expired card rather than a
decision, and wiping somebody's artwork over a billing event is a loss
they would find weeks later with no way to know what it used to be.
Hiding reverses; deleting does not.
2026-08-28 13:27:10 -03:00

90 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Modules\Platform\Branding\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use App\Modules\Platform\Branding\Watermark\WatermarkPosition;
/**
* A single-row settings table — see the migration's comment for why no
* tenant/owner column is needed.
*
* @property int $id
* @property string|null $logo_path
* @property bool $watermark_enabled
* @property string|null $watermark_path
* @property WatermarkPosition $watermark_position
* @property int $watermark_size
* @property int $watermark_opacity
* @property bool $hide_attribution
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
*/
class BrandingSetting extends Model
{
protected $table = 'branding_settings';
protected $guarded = [];
protected $casts = [
'watermark_enabled' => 'boolean',
'watermark_position' => WatermarkPosition::class,
'watermark_size' => 'integer',
'watermark_opacity' => 'integer',
'hide_attribution' => 'boolean',
];
/**
* Mirrors the migration's column defaults, so an unsaved instance
* answers the same as a freshly created row would. That is what lets
* the settings screen render `new BrandingSetting` on an install that
* has never touched branding, instead of either creating a row on a
* GET or restating these numbers a second time in the controller.
*/
protected $attributes = [
'watermark_enabled' => false,
'watermark_position' => 'bottom-right',
'watermark_size' => 30,
'watermark_opacity' => 60,
'hide_attribution' => false,
];
public static function current(): self
{
return static::query()->firstOrCreate([]);
}
public function logoUrl(): ?string
{
return $this->logo_path === null ? null : Storage::disk('public')->url($this->logo_path);
}
public function watermarkUrl(): ?string
{
return $this->watermark_path === null ? null : Storage::disk('public')->url($this->watermark_path);
}
/**
* The artwork to stamp on a thumbnail being rendered right now, or
* null when this installation is not watermarking.
*
* Phrased as "which image, if any" rather than as a boolean because
* the toggle alone is not enough to act on: removing the image
* leaves the toggle standing, and a row restored from a backup can
* carry an `enabled` that its file no longer backs. Answering both
* halves at once means a caller cannot check one and use the other.
*/
public function activeWatermarkPath(): ?string
{
return $this->watermark_enabled ? $this->watermark_path : null;
}
public function watermarksThumbnails(): bool
{
return $this->activeWatermarkPath() !== null;
}
}