feat: add RichTextEditor component for rich text editing in articles
feat: create SanitizeBlogBodiesCommand to clean existing blog bodies according to current HTML sanitization policies test: add AppointmentTreatmentSessionLinkTest to ensure appointment booking functionality works correctly with treatment session links
This commit is contained in:
@@ -38,6 +38,8 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class ResourceBlockController extends BaseController
|
||||
{
|
||||
use \App\Shared\Controller\PermissionGateTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly ClinicResourceRepository $resources,
|
||||
private readonly ResourceOccupancyRepository $occupancy,
|
||||
@@ -63,6 +65,11 @@ class ResourceBlockController extends BaseController
|
||||
#[Route('/api/v1/resource/{uuid}/blocks', name: 'resource_block_create', methods: ['POST'])]
|
||||
public function create(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'resources', 'create');
|
||||
|
||||
$resource = $this->requireResource($user, $uuid);
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
@@ -105,6 +112,11 @@ class ResourceBlockController extends BaseController
|
||||
#[Route('/api/v1/resource-block/{uuid}', name: 'resource_block_delete', methods: ['DELETE'])]
|
||||
public function delete(#[CurrentUser] User $user, string $uuid): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'resources', 'delete');
|
||||
|
||||
$block = $this->occupancy->findOneBy(['uuid' => $uuid]);
|
||||
[$entityType, $entityId] = $this->branches->pair($user);
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[OA\Tag(name: 'Appointments')]
|
||||
class AppointmentController extends BaseController
|
||||
{
|
||||
use \App\Shared\Controller\PermissionGateTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly AppointmentRepository $appointmentRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
@@ -1042,6 +1044,11 @@ class AppointmentController extends BaseController
|
||||
#[Route('/api/v1/appointment/{uuid}/status', methods: ['PATCH'])]
|
||||
public function updateStatus(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'appointments', 'update_status');
|
||||
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
@@ -1131,6 +1138,11 @@ class AppointmentController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function confirm(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'appointments', 'update_status');
|
||||
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
@@ -1214,6 +1226,11 @@ class AppointmentController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'appointments', 'update_status');
|
||||
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
@@ -1396,6 +1413,11 @@ class AppointmentController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function serviceReschedule(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->denySecretaryWithout($user, 'appointments', 'update_status');
|
||||
|
||||
$appointment = $this->appointmentRepo->findByUuid($uuid);
|
||||
if ($appointment === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
|
||||
|
||||
@@ -187,6 +187,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/weekly-schedule', methods: ['POST'])]
|
||||
public function createSchedule(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
||||
|
||||
@@ -243,6 +249,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/weekly-schedule/{uuid}', methods: ['PATCH'])]
|
||||
public function updateSchedule(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
// uuid may be doctor uuid or schedule uuid
|
||||
@@ -325,6 +337,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/booking-setting/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteSchedule(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$schedule = $this->scheduleRepo->findByUuid($uuid);
|
||||
if ($schedule === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'برنامه یافت نشد', 404);
|
||||
@@ -366,6 +384,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/date-override', methods: ['POST'])]
|
||||
public function createOverride(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
||||
$dateStr = trim($data['date'] ?? '');
|
||||
@@ -399,6 +423,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/date-override/{uuid}', methods: ['PATCH'])]
|
||||
public function updateOverride(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$override = $this->overrideRepo->findByUuid($uuid);
|
||||
if ($override === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'Override یافت نشد', 404);
|
||||
@@ -425,6 +455,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/date-override/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteOverride(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$override = $this->overrideRepo->findByUuid($uuid);
|
||||
if ($override === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'Override یافت نشد', 404);
|
||||
@@ -484,6 +520,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/holidays/{uuid}', methods: ['DELETE'])]
|
||||
public function deleteHoliday(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$holiday = $this->holidayRepo->findByUuid($uuid);
|
||||
if ($holiday === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'تعطیلات یافت نشد', 404);
|
||||
@@ -501,6 +543,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/holidays', methods: ['POST'])]
|
||||
public function createHoliday(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
||||
$startStr = trim($data['start_date'] ?? '');
|
||||
@@ -541,6 +589,12 @@ class AppointmentSettingsController extends BaseController
|
||||
#[Route('/api/v1/appointment-settings/holidays/{uuid}', methods: ['PATCH'])]
|
||||
public function updateHoliday(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از هر واکشی. چکِ اصلی (denyDoctorAccess) به پزشکِ
|
||||
// همان رکورد نیاز دارد و بالا نمیرود؛ ولی سهمِ منشی از آن همیشه همین
|
||||
// توگل است، پس این خط هیچ مسیرِ مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳
|
||||
// تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'appointment_settings', 'update');
|
||||
|
||||
$holiday = $this->holidayRepo->findByUuid($uuid);
|
||||
if ($holiday === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'تعطیلات یافت نشد', 404);
|
||||
|
||||
@@ -62,6 +62,10 @@ class MyAppointmentsController extends BaseController
|
||||
private readonly \App\Resource\Service\ResourceBookingSlotService $resourceSlots,
|
||||
private readonly \App\Appointment\Availability\Service\ResourceOccupier $occupier,
|
||||
private readonly \App\Treatment\Service\SessionBookingLink $sessionLink,
|
||||
// اتصال نوبت به جلسهٔ درمان جفتِ [entityType, entityId] میخواهد. تا پیش از
|
||||
// ۲۰۲۶-۰۸-۰۸ همینجا صدا زده میشد ولی هرگز تزریق نشده بود، پس هر درخواستِ
|
||||
// دارای `treatment_session_uuid` روی «Undefined property» ۵۰۰ میگرفت.
|
||||
private readonly \App\Doctor\Service\AddressResolver $branches,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -760,7 +760,7 @@ class AuthController extends BaseController
|
||||
if ($rel->getOwnerType() === \App\Secretary\Entity\DoctorSecretary::OWNER_CLINIC && $rel->getClinic() !== null) {
|
||||
// scope کلینیک — یک context به ازای هر کلینیک (نه هر دکتر)
|
||||
$clinicUuid = $rel->getClinic()->getUuid();
|
||||
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinicUuid && ($c['role'] ?? '') === 'secretary');
|
||||
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinicUuid && $c['role'] === 'secretary');
|
||||
if (empty($alreadyAdded)) {
|
||||
$contexts[] = [
|
||||
'type' => 'clinic',
|
||||
|
||||
@@ -225,15 +225,20 @@ class BillingController extends BaseController
|
||||
/**
|
||||
* فیلترهای مشترک لیست پرداختها و خلاصهی آن؛ یک منبع تا دو نما واگرا نشوند.
|
||||
*
|
||||
* @return array{national_code:?string,status:?string,from:?string,to:?string}
|
||||
* @return array{national_code:?string,status:?string,from:?int,to:?int}
|
||||
*/
|
||||
private function paymentFilters(Request $request): array
|
||||
{
|
||||
// `from`/`to` مهرِ زمانیاند و قرارداد سرویس `int` میخواهد. تبدیل همینجا
|
||||
// انجام میشود، در مرزِ ورودی، نه در repository — وگرنه هر مصرفکنندهٔ تازه
|
||||
// باید همان cast را تکرار کند.
|
||||
$timestamp = static fn (?string $raw): ?int => ($raw ?: null) === null ? null : (int) $raw;
|
||||
|
||||
return [
|
||||
'national_code' => $request->query->get('national_code') ?: null,
|
||||
'status' => $request->query->get('status') ?: null,
|
||||
'from' => $request->query->get('from') ?: null,
|
||||
'to' => $request->query->get('to') ?: null,
|
||||
'from' => $timestamp($request->query->get('from')),
|
||||
'to' => $timestamp($request->query->get('to')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Command;
|
||||
|
||||
use App\Blog\Service\BlogBodySanitizer;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* پاکسازی بدنهٔ مقالههای موجود با همان سیاستی که مسیر ذخیره اعمال میکند.
|
||||
*
|
||||
* یافتهٔ ۵ آدیت ۲۰۲۶-۰۸-۰۷ دفاع را در **لحظهٔ ذخیره** گذاشت، پس مقالههایی که پیش
|
||||
* از آن ذخیره شدهاند هنوز HTML خام دارند و `BlogReviewPage` خامشان را رندر میکند.
|
||||
* این دستور همان بدهی را تسویه میکند.
|
||||
*
|
||||
* دستور است نه migration: سیاستِ `html_sanitizer.yaml` ممکن است دوباره سفت شود و
|
||||
* آنوقت باید همین گذر دوباره اجرا شود. migration یکبارمصرف است.
|
||||
*
|
||||
* بدنهای که پس از پاکسازی کاملاً خالی میشود دستنخورده میماند و فقط گزارش
|
||||
* میشود: مقالهٔ منتشرشده را نباید بیصدا تهی کرد — تصمیمش با آدم است.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'app:blog:sanitize-bodies',
|
||||
description: 'Re-runs the blog body sanitizer over rows saved before it existed',
|
||||
)]
|
||||
class SanitizeBlogBodiesCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly BlogBodySanitizer $sanitizer,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'فقط گزارش بده، چیزی ننویس');
|
||||
$this->addOption('show', null, InputOption::VALUE_REQUIRED, 'قبل/بعدِ یک مقاله را چاپ کن (id)');
|
||||
}
|
||||
|
||||
/**
|
||||
* جنسِ تغییرِ یک بدنه: `stripped` یا `hardened`.
|
||||
*
|
||||
* تفکیک لازم است چون پاکسازی سه کارِ متفاوت میکند و فقط یکیشان امنیتی است:
|
||||
*
|
||||
* - decode شدن entity (` ` → U+00A0) — بیاثر.
|
||||
* - افزودن `rel="noopener noreferrer"` به `<a>` — سختسازی، سیاستِ
|
||||
* `html_sanitizer.yaml`. جلوی reverse tabnabbing را میگیرد.
|
||||
* - **حذفِ** تگ یا attribute — تنها حالتی که یعنی آن مقاله markupِ غیرمجاز دارد.
|
||||
*
|
||||
* بدونِ این تفکیک، عددِ «۴۲۶ مقاله تغییر میکند» گمراهکننده بود و یک `UPDATE`
|
||||
* انبوه را بهجای یک بررسی هدفمند توجیه میکرد.
|
||||
*/
|
||||
private static function classify(string $before, string $after): string
|
||||
{
|
||||
// `<br>` → `<br />` فقط سریالسازیِ خروجی است. بدون یکسانسازی، هر مقالهٔ
|
||||
// دارای خطشکن بهغلط «markup غیرمجاز» گزارش میشد.
|
||||
$tags = static function (string $html): array {
|
||||
preg_match_all('/<[^>]+>/', $html, $m);
|
||||
|
||||
return array_map(
|
||||
static fn (string $t): string => preg_replace('/\s*\/>$/', '>', $t) ?? $t,
|
||||
$m[0],
|
||||
);
|
||||
};
|
||||
|
||||
// سیاست، `rel` را روی هر `<a>` **تحمیل** میکند. پس هم نبودنش و هم مقدارِ
|
||||
// ضعیفترِ قبلی (`rel="noopener"`) با مقدار کامل جایگزین میشود. هر دو سمت
|
||||
// نرمال میشوند تا این سختسازی بهغلط «حذف» شمرده نشود.
|
||||
$withoutRel = static fn (string $tag): string => preg_replace(
|
||||
'/\s+rel="[^"]*"/',
|
||||
'',
|
||||
$tag,
|
||||
) ?? $tag;
|
||||
|
||||
$beforeTags = array_map($withoutRel, $tags($before));
|
||||
$afterTags = array_map($withoutRel, $tags($after));
|
||||
|
||||
if ($beforeTags !== $afterTags) {
|
||||
return 'stripped';
|
||||
}
|
||||
|
||||
$text = static function (string $html): string {
|
||||
$decoded = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return preg_replace('/\s+/u', ' ', str_replace("\u{a0}", ' ', $decoded)) ?? '';
|
||||
};
|
||||
|
||||
return $text($before) === $text($after) ? 'hardened' : 'stripped';
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = (bool) $input->getOption('dry-run');
|
||||
|
||||
if (($showId = $input->getOption('show')) !== null) {
|
||||
$before = (string) $this->connection->fetchOne('SELECT body FROM blogs WHERE id = ?', [(int) $showId]);
|
||||
file_put_contents(sys_get_temp_dir() . '/blog-before.html', $before);
|
||||
file_put_contents(sys_get_temp_dir() . '/blog-after.html', $this->sanitizer->clean($before));
|
||||
$io->success(sys_get_temp_dir() . '/blog-{before,after}.html نوشته شد');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$rows = $this->connection->fetchAllAssociative('SELECT id, title, body FROM blogs');
|
||||
$changed = [];
|
||||
$emptied = [];
|
||||
$hardened = 0;
|
||||
$stripped = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$before = (string) $row['body'];
|
||||
$after = $this->sanitizer->clean($before);
|
||||
|
||||
if ($after === $before) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim(strip_tags($after)) === '') {
|
||||
$emptied[] = $row;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::classify($before, $after) === 'hardened') {
|
||||
$hardened++;
|
||||
} else {
|
||||
$stripped[] = sprintf('#%d — %s', $row['id'], $row['title']);
|
||||
}
|
||||
|
||||
$changed[] = ['id' => (int) $row['id'], 'title' => (string) $row['title'], 'body' => $after];
|
||||
}
|
||||
|
||||
$io->section(sprintf('%d مقاله بررسی شد', count($rows)));
|
||||
$io->definitionList(
|
||||
['بدون تغییر' => count($rows) - count($changed) - count($emptied)],
|
||||
['سختسازی (افزودن rel / decode شدن entity)' => $hardened],
|
||||
['حذفِ تگ یا attribute غیرمجاز' => count($stripped)],
|
||||
['خالی میشد و دستنخورده ماند' => count($emptied)],
|
||||
);
|
||||
|
||||
if ($stripped !== []) {
|
||||
$io->warning('این مقالهها markupِ غیرمجاز دارند:');
|
||||
$io->listing(array_slice($stripped, 0, 30));
|
||||
}
|
||||
|
||||
if ($emptied !== []) {
|
||||
$io->warning(sprintf(
|
||||
'%d مقاله پس از پاکسازی خالی میشد و دستنخورده ماند. دستی بررسی کن:',
|
||||
count($emptied),
|
||||
));
|
||||
$io->listing(array_map(
|
||||
static fn (array $r): string => sprintf('#%d — %s', $r['id'], $r['title']),
|
||||
$emptied,
|
||||
));
|
||||
}
|
||||
|
||||
if ($changed === []) {
|
||||
$io->success('هیچ بدنهای تغییر نکرد؛ همه از قبل با سیاست فعلی همخواناند.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->note(sprintf('dry-run: %d بدنه تغییر میکرد. چیزی نوشته نشد.', count($changed)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->connection->transactional(function (Connection $conn) use ($changed): void {
|
||||
foreach ($changed as $row) {
|
||||
$conn->executeStatement(
|
||||
'UPDATE blogs SET body = :body WHERE id = :id',
|
||||
['body' => $row['body'], 'id' => $row['id']],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$io->success(sprintf('%d بدنه پاکسازی و ذخیره شد.', count($changed)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -224,6 +224,11 @@ class ClinicController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_info', 'update');
|
||||
|
||||
$clinic = $this->clinicRepo->findByUuid($uuid);
|
||||
if ($clinic === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404);
|
||||
@@ -385,6 +390,11 @@ class ClinicController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function detachDoctor(string $clinicUuid, string $doctorUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'delete');
|
||||
|
||||
$clinic = $this->clinicRepo->findByUuid($clinicUuid);
|
||||
if ($clinic === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404);
|
||||
|
||||
@@ -58,6 +58,11 @@ class ClinicDoctorPermissionController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function updatePermissions(string $clinicUuid, string $doctorUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'update');
|
||||
|
||||
$clinic = $this->resolveClinic($clinicUuid, $user, 'update');
|
||||
$doctor = $this->resolveMember($clinic, $doctorUuid);
|
||||
$perm = $this->permRepo->getOrCreate($clinic, $doctor);
|
||||
|
||||
@@ -35,6 +35,11 @@ class ClinicInvitationController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function inviteDoctor(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'create');
|
||||
|
||||
$clinic = $this->clinicRepo->findByUuid($uuid);
|
||||
if (!$clinic) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404);
|
||||
@@ -92,6 +97,11 @@ class ClinicInvitationController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function resendInvitation(string $invUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'create');
|
||||
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
@@ -107,6 +117,11 @@ class ClinicInvitationController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function changeInvitationStatus(string $invUuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'update');
|
||||
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
@@ -126,6 +141,11 @@ class ClinicInvitationController extends BaseController
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function deleteInvitation(string $invUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
// پیشچکِ منشی، پیش از واکشیِ رکورد. چکِ اصلی به خودِ شیء نیاز دارد و بالا
|
||||
// نمیرود؛ سهمِ منشی از آن اما همیشه همین توگل است، پس این خط هیچ مسیرِ
|
||||
// مجازی نمیبندد و فقط ۴۰۴ را به ۴۰۳ تبدیل میکند. یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷.
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'clinic_doctors', 'delete');
|
||||
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Insurance\Entity\TenantServiceCoverage;
|
||||
use App\Insurance\Enum\ServiceCategory;
|
||||
use App\Clinic\Security\ClinicDoctorAccessChecker;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use App\ClinicService\Repository\CatalogCategoryRepository;
|
||||
use App\ClinicService\Repository\ServiceItemAuditLogRepository;
|
||||
use App\ClinicService\Repository\ServiceItemRepository;
|
||||
@@ -46,7 +45,6 @@ class ClinicServiceController extends BaseController
|
||||
private readonly InventoryItemRepository $inventoryItemRepo,
|
||||
private readonly ServiceItemAuditService $auditService,
|
||||
private readonly ServiceItemAuditLogRepository $auditLogRepo,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly EntityContextResolver $contextResolver,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||
|
||||
@@ -147,6 +147,10 @@ class ServiceItem
|
||||
public function getConsumables(): Collection
|
||||
{
|
||||
// Doctrine بدون constructor هیدریت میکند؛ از property تایپشده محافظت کن.
|
||||
// phpstan فقط constructor را میبیند و میگوید این property همیشه مقدار
|
||||
// دارد. Doctrine اما بدون constructor هیدریت میکند، پس روی نمونهٔ
|
||||
// نیمهساخته میتواند initialize نشده باشد. گارد عمدی است.
|
||||
/** @phpstan-ignore-next-line */
|
||||
return $this->consumables ??= new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -209,6 +213,10 @@ class ServiceItem
|
||||
public function getStaffMembers(): Collection
|
||||
{
|
||||
// Doctrine hydrates without the constructor; guard the typed property.
|
||||
// phpstan فقط constructor را میبیند و میگوید این property همیشه مقدار
|
||||
// دارد. Doctrine اما بدون constructor هیدریت میکند، پس روی نمونهٔ
|
||||
// نیمهساخته میتواند initialize نشده باشد. گارد عمدی است.
|
||||
/** @phpstan-ignore-next-line */
|
||||
return $this->staffMembers ??= new ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ class DoctorClaimService
|
||||
|
||||
private function finalize(Doctor $doctor, User $target, DoctorClaimRequest $claim, ?string $nationalCode): void
|
||||
{
|
||||
$surrogate = $this->em->wrapInTransaction(function () use ($doctor, $target, $claim, $nationalCode): ?User {
|
||||
$surrogate = $this->em->wrapInTransaction(function () use ($doctor, $target, $claim, $nationalCode): User {
|
||||
$locked = $this->em->find(Doctor::class, $doctor->getId(), LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
if ($locked->getOwnerStatus() !== 'pending_transfer') {
|
||||
@@ -170,8 +170,7 @@ class DoctorClaimService
|
||||
});
|
||||
|
||||
// حذف امن جانشین — پس از flush انتقال، تا شمارش پزشکانِ متصل قطعی باشد
|
||||
if ($surrogate !== null
|
||||
&& $surrogate->getId() !== $target->getId()
|
||||
if ($surrogate->getId() !== $target->getId()
|
||||
&& $surrogate->hasRole(DoctorImportService::ROLE_UNCLAIMED_DOCTOR)
|
||||
&& $this->em->getRepository(Doctor::class)->count(['user' => $surrogate]) === 0) {
|
||||
$this->em->remove($surrogate);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Inventory\Entity\InventoryItem;
|
||||
use App\Inventory\Entity\InventoryPackage;
|
||||
use App\Inventory\Entity\InventoryPackageItem;
|
||||
use App\Inventory\Repository\InventoryItemRepository;
|
||||
use App\Inventory\Repository\InventoryPackageRepository;
|
||||
|
||||
/**
|
||||
* Inventory domain logic: derived aggregates (stat counters, package totals and
|
||||
@@ -17,7 +16,6 @@ class InventoryService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InventoryItemRepository $itemRepo,
|
||||
private readonly InventoryPackageRepository $packageRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,17 +4,14 @@ namespace App\Patient\Service;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Appointment\Service\AppointmentInsuranceService;
|
||||
use App\Auth\Repository\UserRepository;
|
||||
use App\Billing\Service\BillingCalculator;
|
||||
use App\Billing\ValueObject\Money;
|
||||
use App\ClinicService\Repository\ServiceItemRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Insurance\Enum\ServiceCategory;
|
||||
use App\Insurance\Repository\EntityInsurancePricingRepository;
|
||||
use App\Insurance\Service\TenantInsuranceService;
|
||||
use App\Inventory\Repository\InventoryItemRepository;
|
||||
use App\Inventory\Repository\InventoryPackageRepository;
|
||||
use App\Doctor\Repository\DoctorAddressRepository;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
@@ -46,10 +43,7 @@ class PatientService
|
||||
private readonly InventoryItemRepository $inventoryItemRepo,
|
||||
private readonly InventoryPackageRepository $inventoryPackageRepo,
|
||||
private readonly ClinicStaffRepository $staffRepo,
|
||||
private readonly UserRepository $userRepo,
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly DoctorAddressRepository $addressRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly TenantInsuranceService $tenantInsuranceService,
|
||||
private readonly AppointmentInsuranceService $appointmentInsurance,
|
||||
private readonly BillingCalculator $billingCalculator,
|
||||
|
||||
@@ -73,7 +73,7 @@ class RecordNumberGenerator
|
||||
if ($token === 'MM') return str_pad((string) $jm, 2, '0', STR_PAD_LEFT);
|
||||
|
||||
// {SEQ} یا {SEQ:n} — پدینگ سقف نیست: شمارندهٔ بلندتر از n بریده نمیشود.
|
||||
$width = isset($m[2]) && $m[2] !== '' ? (int) $m[2] : 1;
|
||||
$width = isset($m[2]) ? (int) $m[2] : 1;
|
||||
|
||||
return str_pad((string) $counter, $width, '0', STR_PAD_LEFT);
|
||||
},
|
||||
|
||||
@@ -315,7 +315,7 @@ class ResourceController extends BaseController
|
||||
private function dayStart(?string $date): ?int
|
||||
{
|
||||
if ($date === null || $date === '') {
|
||||
return strtotime('today midnight') ?: null;
|
||||
return strtotime('today midnight');
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Sms\Entity\SmsLog;
|
||||
use App\Sms\Service\SmsService;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,7 @@ class SecretaryService
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly SmsService $smsService,
|
||||
private readonly string $appUrl,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
/** Find the secretary User by mobile or create it; ensure ROLE_SECRETARY, apply name/password. */
|
||||
@@ -110,7 +112,7 @@ class SecretaryService
|
||||
$created[] = $row;
|
||||
}
|
||||
|
||||
$this->secretaryRepo->getEntityManager()->flush(); // flush the batch of new rows
|
||||
$this->em->flush(); // flush the batch of new rows
|
||||
|
||||
if (!empty($created)) {
|
||||
$this->sendWelcomeSms($mobile, $clinic->getName() ?? 'کلینیک');
|
||||
@@ -181,7 +183,7 @@ class SecretaryService
|
||||
}
|
||||
}
|
||||
|
||||
$this->secretaryRepo->getEntityManager()->flush();
|
||||
$this->em->flush();
|
||||
|
||||
return [
|
||||
'added' => $added,
|
||||
|
||||
@@ -37,7 +37,14 @@ trait PermissionGateTrait
|
||||
* گِیت میکند این را بازنویسی میکند و بعد `denyUnlessGranted($user, $action)`
|
||||
* صدا میزند؛ کنترلری که چند منبع دارد `denyUnlessGrantedOn()` را مستقیم میزند.
|
||||
*/
|
||||
abstract private function permissionResource(): string;
|
||||
private function permissionResource(): string
|
||||
{
|
||||
throw new \LogicException(sprintf(
|
||||
'%s باید permissionResource() را بازنویسی کند، یا بهجای denyUnlessGranted() '
|
||||
. 'از denyUnlessGrantedOn() با منبعِ صریح استفاده کند.',
|
||||
static::class,
|
||||
));
|
||||
}
|
||||
|
||||
/** @param 'view'|'create'|'update'|'delete'|'cancel'|'update_status' $action */
|
||||
private function denyUnlessGranted(User $user, string $action): void
|
||||
@@ -55,4 +62,27 @@ trait PermissionGateTrait
|
||||
$this->secretaryAccess->denyUnlessGranted($user, $resource, $action);
|
||||
$this->clinicDoctorAccess->denyUnlessGranted($user, $resource, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* پیشچکِ منشی — برای کنترلری که چکِ اصلیاش به **خودِ رکورد** نیاز دارد.
|
||||
*
|
||||
* یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷: در دهها روت، گِیت بعد از `findByUuid()` مینشیند،
|
||||
* پس uuidِ ناموجود اول `404` میگیرد و همان تفاوتِ ۴۰۳/۴۰۴ به کاربرِ بیمجوز
|
||||
* میگوید کدام رکورد در این محیط وجود دارد.
|
||||
*
|
||||
* چکِ اصلی را نمیشود بالا برد: به `$clinic` یا `$doctor`ِ همان رکورد نیاز دارد.
|
||||
* ولی سهمِ منشیِ آن چک **همیشه** همین توگل است، پس این پیشچک اکیداً ضعیفتر
|
||||
* است — هر کسی را که رد کند، چکِ پایینتر هم رد میکرد. یعنی هیچ مسیرِ مجازی
|
||||
* بسته نمیشود و فقط ۴۰۴ به ۴۰۳ تبدیل میشود.
|
||||
*
|
||||
* عمداً `ClinicDoctorAccessChecker` را صدا نمیزند: پزشکِ عضو ممکن است روی
|
||||
* رکوردِ **کلینیکِ دیگری** که مالکش است اقدام کند، و آنجا محیطِ فعال با محیطِ
|
||||
* رکورد یکی نیست. آن حالت را فقط چکِ شیءمحورِ پایین میتواند درست بسنجد.
|
||||
*
|
||||
* @param 'view'|'create'|'update'|'delete'|'cancel'|'update_status' $action
|
||||
*/
|
||||
private function denySecretaryWithout(User $user, string $resource, string $action): void
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, $resource, $action);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user