mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
6e47d76ba6
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.
108 lines
4.0 KiB
PHP
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)]];
|
|
}
|
|
}
|