Files
projectsend/app/Support/Rules.php
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

108 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support;
use App\Modules\Platform\Captcha\Captcha;
use App\Modules\Platform\Captcha\CaptchaForm;
use App\Modules\Platform\Captcha\CaptchaRule;
use App\Modules\Platform\Localization\TimezoneRegistry;
use Illuminate\Validation\Rule;
/**
* Validation rules shared across modules, where having one definition
* matters more than having it next to its caller.
*/
class Rules
{
/**
* The rule for a user-supplied public URL slug.
*
* The pattern is deliberately strict — lowercase alphanumerics in
* hyphen-separated runs, with no leading, trailing or doubled hyphen —
* because these land directly in a public URL path segment. Loosening
* it in one place and not the others is exactly what this being shared
* is meant to prevent.
*
* A slug only matters (and is only shown) once the file, folder or
* group is public; otherwise one derived from the name stands in, so
* the field is required only when `public` is true. On an update,
* omitting it leaves the current slug alone — it must not silently
* change just because the name did.
*
* @param string $table the table whose slugs must stay distinct
* @param int|null $ignoreId the row being updated, which must not
* collide with the slug it already holds
* @return array<int, mixed>
*/
public static function slug(string $table, ?int $ignoreId = null): array
{
$unique = Rule::unique($table, 'slug');
return [
'required_if:public,true',
'nullable',
'string',
'max:255',
'regex:/^[a-z0-9]+(-[a-z0-9]+)*$/',
$ignoreId === null ? $unique : $unique->ignore($ignoreId),
];
}
/**
* The rule for an IANA timezone identifier.
*
* Shared because two things write `users.timezone` — the picker on the
* profile form and the silent browser detection behind PUT /timezone —
* and a zone the registry would refuse must not be storable through
* either. The framework's own `timezone` rule is not enough on its own:
* it accepts abbreviations and offsets that `DateTimeZone` tolerates
* but that never appear in the picker, so membership is checked too.
*
* Presence is the caller's business: both callers currently require it,
* but the field is always sent by a control that has a value, so
* whether an omission clears or keeps is a decision for the form, not
* for the format.
*
* @return array<int, mixed>
*/
public static function timezone(): array
{
return ['string', 'timezone', Rule::in(app(TimezoneRegistry::class)->all())];
}
/**
* The rules protecting one form with a CAPTCHA, or none at all.
*
* Shared because four unrelated forms — login, client registration,
* the password-reset request and a visitor's comment — must each
* enforce this identically, and because "this installation does not
* protect this form" needs to read as an empty rule set at every one
* of them rather than as four slightly different conditionals.
*
* Spread into the caller's rules:
*
* $request->validate([
* 'email' => ['required', 'email'],
* ...Rules::captcha(CaptchaForm::Login),
* ]);
*
* `bail` so a missing token is refused before any network call, and
* `required` even on the forms that let an unreachable provider
* through: failing open is about *our server* being unable to ask, and
* treating an absent field as an answer is precisely the bug that left
* v1's registration form unprotected.
*
* @return array<string, array<int, mixed>>
*/
public static function captcha(CaptchaForm $form): array
{
if (! app(Captcha::class)->protects($form)) {
return [];
}
return ['captcha_token' => ['bail', 'required', 'string', 'max:5000', new CaptchaRule($form)]];
}
}