diff --git a/app/Docs/ExternalDocs.php b/app/Docs/ExternalDocs.php index c54f37f43..da4196557 100644 --- a/app/Docs/ExternalDocs.php +++ b/app/Docs/ExternalDocs.php @@ -11,6 +11,6 @@ public static function create(string $objectId = null): BaseObject { return parent::create($objectId) ->description('GitHub Wiki') - ->url('https://github.com/LondonBoroughSutton/helpyourselfsutton-api/wiki'); + ->url('https://github.com/Connected-Places/api/wiki'); } } diff --git a/app/Docs/Info.php b/app/Docs/Info.php index 040e42851..7a47b587f 100644 --- a/app/Docs/Info.php +++ b/app/Docs/Info.php @@ -16,7 +16,7 @@ public static function create(string $objectId = null): BaseObject ->version('v1') ->contact( Contact::create() - ->name('Ayup Digital') + ->name('Ayup Connect') ->url('https://ayup.agency') ); } diff --git a/app/Docs/OpenApi.php b/app/Docs/OpenApi.php index 7cd995bf0..75602d42a 100644 --- a/app/Docs/OpenApi.php +++ b/app/Docs/OpenApi.php @@ -148,8 +148,7 @@ public static function create(string $objectId = null): BaseObject Tags\TaxonomyOrganisationsTag::create(), Tags\UpdateRequestsTag::create(), Tags\UsersTag::create() - ) - ->externalDocs(ExternalDocs::create()); + ); } /** diff --git a/app/Docs/Operations/OrganisationEvents/IndexOrganisationEventOperation.php b/app/Docs/Operations/OrganisationEvents/IndexOrganisationEventOperation.php index 077232e1a..47c8fb42a 100644 --- a/app/Docs/Operations/OrganisationEvents/IndexOrganisationEventOperation.php +++ b/app/Docs/Operations/OrganisationEvents/IndexOrganisationEventOperation.php @@ -49,16 +49,21 @@ public static function create(string $objectId = null): BaseObject StartsAfterParameter::create(), EndsBeforeParameter::create(), EndsAfterParameter::create(), - FilterParameter::create(null, 'wheelchair') + FilterParameter::create(null, 'has_wheelchair_access') ->description('Has a wheelchair accessible location') ->schema( Schema::boolean() ), - FilterParameter::create(null, 'induction-loop') + FilterParameter::create(null, 'has_induction_loop') ->description('Has a location with an induction loop') ->schema( Schema::boolean() ), + FilterParameter::create(null, 'has_accessible_toilet') + ->description('Has a location with an accessible toilet') + ->schema( + Schema::boolean() + ), FilterParameter::create(null, 'organisation_id') ->description('Comma separated list of organisation IDs to filter by') ->schema( diff --git a/app/Docs/Schemas/Organisation/UpdateOrganisationSchema.php b/app/Docs/Schemas/Organisation/UpdateOrganisationSchema.php index 1590140a1..f9789c403 100644 --- a/app/Docs/Schemas/Organisation/UpdateOrganisationSchema.php +++ b/app/Docs/Schemas/Organisation/UpdateOrganisationSchema.php @@ -19,7 +19,6 @@ public static function create(string $objectId = null): BaseObject 'name', 'slug', 'description', - 'url' ) ->properties( Schema::string('name'), diff --git a/app/Docs/Schemas/Page/PageListItemSchema.php b/app/Docs/Schemas/Page/PageListItemSchema.php index daf5f6de3..977655d49 100644 --- a/app/Docs/Schemas/Page/PageListItemSchema.php +++ b/app/Docs/Schemas/Page/PageListItemSchema.php @@ -19,7 +19,6 @@ public static function create(string $objectId = null): BaseObject Schema::string('slug'), Schema::string('title'), Schema::string('excerpt'), - Schema::string('content'), Schema::integer('order'), Schema::boolean('enabled'), Schema::string('page_type') diff --git a/app/Docs/Schemas/Page/PageSchema.php b/app/Docs/Schemas/Page/PageSchema.php index 3fcda1866..b621f6224 100644 --- a/app/Docs/Schemas/Page/PageSchema.php +++ b/app/Docs/Schemas/Page/PageSchema.php @@ -34,6 +34,8 @@ public static function create(string $objectId = null): BaseObject PageListItemSchema::create('parent'), Schema::array('children') ->items(PageListItemSchema::create()), + Schema::array('ancestors') + ->items(PageListItemSchema::create()), Schema::array('collection_categories') ->items(CollectionCategorySchema::create()), Schema::array('collection_personas') diff --git a/app/Docs/Schemas/Service/UpdateServiceSchema.php b/app/Docs/Schemas/Service/UpdateServiceSchema.php index d1885b5b2..722359354 100644 --- a/app/Docs/Schemas/Service/UpdateServiceSchema.php +++ b/app/Docs/Schemas/Service/UpdateServiceSchema.php @@ -29,10 +29,7 @@ public static function create(string $objectId = null): BaseObject 'fees_url', 'testimonial', 'video_embed', - 'url', 'contact_name', - 'contact_phone', - 'contact_email', 'show_referral_disclaimer', 'referral_method', 'referral_button_text', diff --git a/app/Http/Controllers/Core/V1/OrganisationEventController.php b/app/Http/Controllers/Core/V1/OrganisationEventController.php index beba49307..f653adee8 100644 --- a/app/Http/Controllers/Core/V1/OrganisationEventController.php +++ b/app/Http/Controllers/Core/V1/OrganisationEventController.php @@ -55,6 +55,7 @@ public function index(IndexRequest $request): AnonymousResourceCollection AllowedFilter::scope('ends_after'), AllowedFilter::scope('has_wheelchair_access', 'hasWheelchairAccess'), AllowedFilter::scope('has_induction_loop', 'hasInductionLoop'), + AllowedFilter::scope('has_accessible_toilet', 'hasAccessibleToilet'), AllowedFilter::scope('collections', 'inCollections'), AllowedFilter::custom('has_permission', new HasPermissionFilter()), ]) diff --git a/app/Http/Controllers/Core/V1/PageController.php b/app/Http/Controllers/Core/V1/PageController.php index 6a846ff20..f57157e90 100644 --- a/app/Http/Controllers/Core/V1/PageController.php +++ b/app/Http/Controllers/Core/V1/PageController.php @@ -49,6 +49,7 @@ public function index(IndexRequest $request): AnonymousResourceCollection ->allowedIncludes([ 'parent', 'children', + 'ancestors', AllowedInclude::relationship('landingPageAncestors', 'landingPageAncestors'), ]) ->allowedFilters([ @@ -82,7 +83,7 @@ public function store(StoreRequest $request, PagePersistenceService $persistence event(EndpointHit::onCreate($request, "Created page [{$entity->id}]", $entity)); - $entity->load('landingPageAncestors', 'parent', 'children', 'collectionCategories', 'collectionPersonas'); + $entity->load('landingPageAncestors', 'parent', 'children', 'ancestors', 'collectionCategories', 'collectionPersonas'); return new PageResource($entity); } @@ -93,7 +94,7 @@ public function store(StoreRequest $request, PagePersistenceService $persistence public function show(ShowRequest $request, Page $page): PageResource { $baseQuery = Page::query() - ->with(['landingPageAncestors', 'parent', 'children', 'collectionCategories', 'collectionPersonas']) + ->with(['landingPageAncestors', 'parent', 'children', 'ancestors', 'collectionCategories', 'collectionPersonas']) ->where('id', $page->id); $page = QueryBuilder::for($baseQuery) diff --git a/app/Http/Controllers/Core/V1/Service/ImportController.php b/app/Http/Controllers/Core/V1/Service/ImportController.php index a5e06b1ac..e15e8ceec 100644 --- a/app/Http/Controllers/Core/V1/Service/ImportController.php +++ b/app/Http/Controllers/Core/V1/Service/ImportController.php @@ -140,7 +140,7 @@ public function validateSpreadsheet(string $filePath) ), ], 'intro' => ['required', 'string', 'min:1', 'max:300'], - 'description' => ['required', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(1600)], + 'description' => ['required', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(config('local.service_description_max_chars'))], 'wait_time' => ['present', 'nullable', Rule::in([ Service::WAIT_TIME_ONE_WEEK, Service::WAIT_TIME_TWO_WEEKS, diff --git a/app/Http/Filters/Organisation/HasPermissionFilter.php b/app/Http/Filters/Organisation/HasPermissionFilter.php index c7085c39f..9dffa16bc 100644 --- a/app/Http/Filters/Organisation/HasPermissionFilter.php +++ b/app/Http/Filters/Organisation/HasPermissionFilter.php @@ -11,9 +11,15 @@ class HasPermissionFilter implements Filter { public function __invoke(Builder $query, $value, string $property): Builder { - $organisationIds = []; $user = request()->user('api'); + // If Global or Super Admin, apply no filter + if ($user && $user->isGlobalAdmin()) { + return $query; + } + + $organisationIds = []; + if ($user) { $userOrganisationIds = $user->organisations() ->pluck(table(Organisation::class, 'id')) diff --git a/app/Http/Filters/OrganisationEvent/HasPermissionFilter.php b/app/Http/Filters/OrganisationEvent/HasPermissionFilter.php index 6bbc76bca..6e6fd215f 100644 --- a/app/Http/Filters/OrganisationEvent/HasPermissionFilter.php +++ b/app/Http/Filters/OrganisationEvent/HasPermissionFilter.php @@ -11,9 +11,15 @@ class HasPermissionFilter implements Filter { public function __invoke(Builder $query, $value, string $property): Builder { - $organisationIds = []; $user = request()->user('api'); + // If Global or Super Admin, apply no filter + if ($user && $user->isGlobalAdmin()) { + return $query; + } + + $organisationIds = []; + if ($user && $user->isOrganisationAdmin()) { $organisationIds = $user->organisations() ->pluck(table(Organisation::class, 'id')) diff --git a/app/Http/Filters/Service/HasPermissionFilter.php b/app/Http/Filters/Service/HasPermissionFilter.php index 64b083c4a..e98967ae9 100644 --- a/app/Http/Filters/Service/HasPermissionFilter.php +++ b/app/Http/Filters/Service/HasPermissionFilter.php @@ -10,9 +10,16 @@ class HasPermissionFilter implements Filter { public function __invoke(Builder $query, $value, string $property): Builder { - $serviceIds = request()->user('api') - ? request()->user('api')->services()->pluck(table(Service::class, 'id'))->toArray() - : []; + $user = request()->user('api'); + + // If Global or Super Admin, apply no filter + if ($user && $user->isGlobalAdmin()) { + return $query; + } + + $serviceIds = $user + ? $user->services()->pluck(table(Service::class, 'id'))->toArray() + : []; return $query->whereIn(table(Service::class, 'id'), $serviceIds); } diff --git a/app/Http/Requests/Organisation/StoreRequest.php b/app/Http/Requests/Organisation/StoreRequest.php index f71d2bcc1..67cf302f4 100644 --- a/app/Http/Requests/Organisation/StoreRequest.php +++ b/app/Http/Requests/Organisation/StoreRequest.php @@ -9,6 +9,8 @@ use App\Models\Taxonomy; use App\Rules\FileIsMimeType; use App\Rules\FileIsPendingAssignment; +use App\Rules\MarkdownMaxLength; +use App\Rules\MarkdownMinLength; use App\Rules\RootTaxonomyIs; use App\Rules\Slug; use App\Rules\UkPhoneNumber; @@ -46,7 +48,12 @@ public function rules(): array new Slug(), ], 'name' => ['required', 'string', 'min:1', 'max:255'], - 'description' => ['required', 'string', 'min:1', 'max:10000'], + 'description' => [ + 'required', + 'string', + new MarkdownMinLength(1), + new MarkdownMaxLength(config('local.organisation_description_max_chars'), 'Description tab - The long description must be ' . config('local.organisation_description_max_chars') . ' characters or fewer.'), + ], 'url' => ['present', 'nullable', 'url', 'max:255'], 'email' => ['present', 'nullable', 'email', 'max:255'], 'phone' => [ diff --git a/app/Http/Requests/Organisation/UpdateRequest.php b/app/Http/Requests/Organisation/UpdateRequest.php index 2f2225328..ecae04adc 100644 --- a/app/Http/Requests/Organisation/UpdateRequest.php +++ b/app/Http/Requests/Organisation/UpdateRequest.php @@ -10,7 +10,8 @@ use App\Rules\CanUpdateCategoryTaxonomyRelationships; use App\Rules\FileIsMimeType; use App\Rules\FileIsPendingAssignment; -use App\Rules\NullableIf; +use App\Rules\MarkdownMaxLength; +use App\Rules\MarkdownMinLength; use App\Rules\RootTaxonomyIs; use App\Rules\Slug; use App\Rules\UkPhoneNumber; @@ -48,24 +49,22 @@ public function rules(): array new Slug(), ], 'name' => ['string', 'min:1', 'max:255'], - 'description' => ['string', 'min:1', 'max:10000'], + 'description' => [ + 'string', + new MarkdownMinLength(1), + new MarkdownMaxLength(config('local.organisation_description_max_chars'), 'Description tab - The long description must be ' . config('local.organisation_description_max_chars') . ' characters or fewer.'), + ], 'url' => [ - new NullableIf(function () { - return $this->user()->isGlobalAdmin(); - }), + 'nullable', 'url', 'max:255'], 'email' => [ - new NullableIf(function () { - return $this->user()->isGlobalAdmin() || $this->input('phone', $this->organisation->phone) !== null; - }), + 'nullable', 'email', 'max:255', ], 'phone' => [ - new NullableIf(function () { - return $this->user()->isGlobalAdmin() || $this->input('email', $this->organisation->email) !== null; - }), + 'nullable', 'string', 'min:1', 'max:255', diff --git a/app/Http/Requests/OrganisationEvent/StoreRequest.php b/app/Http/Requests/OrganisationEvent/StoreRequest.php index eb0e37350..f0e4039f7 100644 --- a/app/Http/Requests/OrganisationEvent/StoreRequest.php +++ b/app/Http/Requests/OrganisationEvent/StoreRequest.php @@ -62,7 +62,7 @@ function ($attribute, $value, $fail) { 'required', 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(3000, 'Description tab - The long description must be 3000 characters or fewer.'), + new MarkdownMaxLength(config('local.event_description_max_chars'), 'Description tab - The long description must be ' . config('local.event_description_max_chars') . ' characters or fewer.'), ], 'is_free' => ['required', 'boolean'], 'fees_text' => [ diff --git a/app/Http/Requests/OrganisationEvent/UpdateRequest.php b/app/Http/Requests/OrganisationEvent/UpdateRequest.php index 0516f7932..9e6f7c9e4 100644 --- a/app/Http/Requests/OrganisationEvent/UpdateRequest.php +++ b/app/Http/Requests/OrganisationEvent/UpdateRequest.php @@ -68,7 +68,7 @@ public function rules(): array 'description' => [ 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(3000, 'Description tab - The long description must be 3000 characters or fewer.'), + new MarkdownMaxLength(config('local.event_description_max_chars'), 'Description tab - The long description must be ' . config('local.event_description_max_chars') . ' characters or fewer.'), ], 'is_free' => ['boolean'], 'fees_text' => ['nullable', 'string', 'min:1', 'max:255', 'required_if:is_free,false'], diff --git a/app/Http/Requests/OrganisationSignUpForm/StoreRequest.php b/app/Http/Requests/OrganisationSignUpForm/StoreRequest.php index 74adbb263..ab2446269 100644 --- a/app/Http/Requests/OrganisationSignUpForm/StoreRequest.php +++ b/app/Http/Requests/OrganisationSignUpForm/StoreRequest.php @@ -77,17 +77,23 @@ public function rules(): array new Slug(), ], 'organisation.name' => ['required_without:organisation.id', 'nullable', 'string', 'min:1', 'max:255'], - 'organisation.description' => ['required_without:organisation.id', 'nullable', 'string', 'min:1', 'max:10000'], - 'organisation.url' => ['required_without:organisation.id', 'nullable', 'url', 'max:255'], + 'organisation.description' => [ + 'required_without:organisation.id', + 'nullable', + 'string', + new MarkdownMinLength(1), + new MarkdownMaxLength(config('local.organisation_description_max_chars'), 'Description tab - The long description must be ' . config('local.organisation_description_max_chars') . ' characters or fewer.'), + ], + 'organisation.url' => ['sometimes', 'nullable', 'url', 'max:255'], 'organisation.email' => [ + 'sometimes', 'nullable', - 'required_without_all:organisation.id,organisation.phone', 'email', 'max:255', ], 'organisation.phone' => [ + 'sometimes', 'nullable', - 'required_without_all:organisation.id,organisation.email', 'string', 'min:1', 'max:255', @@ -121,7 +127,7 @@ public function rules(): array 'required', 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(3000), + new MarkdownMaxLength(config('local.service_description_max_chars'), 'Description tab - The long description must be ' . config('local.service_description_max_chars') . ' characters or fewer.'), ], 'service.wait_time' => ['sometimes', 'present', 'nullable', Rule::in([ Service::WAIT_TIME_ONE_WEEK, @@ -135,7 +141,7 @@ public function rules(): array 'service.fees_url' => ['sometimes', 'present', 'nullable', 'url', 'max:255'], 'service.testimonial' => ['sometimes', 'present', 'nullable', 'string', 'min:1', 'max:255'], 'service.video_embed' => ['sometimes', 'present', 'nullable', 'url', 'max:255', new VideoEmbed()], - 'service.url' => ['sometimes', 'required', 'url', 'max:255'], + 'service.url' => ['sometimes', 'present', 'nullable', 'url', 'max:255'], 'service.contact_name' => ['sometimes', 'present', 'nullable', 'string', 'min:1', 'max:255'], 'service.contact_phone' => ['sometimes', 'present', 'nullable', 'string', 'min:1', 'max:255'], 'service.contact_email' => ['sometimes', 'present', 'nullable', 'email', 'max:255'], @@ -151,7 +157,7 @@ public function rules(): array 'required_with:service.useful_infos.*', 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(10000), + new MarkdownMaxLength(config('local.useful_info_description_max_chars')), ], 'service.useful_infos.*.order' => [ 'required_with:service.useful_infos.*', @@ -210,10 +216,7 @@ public function messages(): array 'organisation.slug.unique' => '3. Organisation - The organisation is already listed. Please contact us for help logging in ' . config('local.global_admin.email') . '.', 'organisation.name.required' => '3. Organisation - Please enter the organisation name.', 'organisation.description.required' => '3. Organisation - Please enter a one-line summary of the organisation.', - 'organisation.url.required' => '3. Organisation - Please enter a valid web address in the correct format (starting with https:// or http://).', 'organisation.url.url' => '3. Organisation - Please enter a valid web address in the correct format (starting with https:// or http://).', - 'organisation.email.required_without' => '3. Organisation - Please enter a public email address and/or a public phone number.', - 'organisation.phone.required_without' => '3. Organisation - Please enter a public phone number and/or public email address.', 'organisation.email.email' => '3. Organisation - Please enter the email for your organisation (eg. name@example.com).', 'service.slug.required' => "4. Service, Details tab - Please enter the name of your {$type}.", @@ -221,7 +224,6 @@ public function messages(): array 'service.video_embed.url' => '4. Service, Additional info tab - Please enter a valid video link (eg. https://www.youtube.com/watch?v=JyHR_qQLsLM).', 'service.intro.required' => "4. Service, Description tab - Please enter a brief description of the {$type}.", 'service.description.required' => "4. Service, Description tab - Please enter all the information someone should know about your {$type}.", - 'service.url.required' => "4. Service, Details tab - Please provide the web address for your {$type}.", 'service.url.url' => '4. Service, Details tab - Please enter a valid web address in the correct format (starting with https:// or http://).', 'service.contact_email.email' => "4. Service, Additional Info tab - Please enter an email address users can use to contact your {$type} (eg. name@example.com).", ]; diff --git a/app/Http/Requests/Service/StoreRequest.php b/app/Http/Requests/Service/StoreRequest.php index eff9ccf07..a2d276f01 100644 --- a/app/Http/Requests/Service/StoreRequest.php +++ b/app/Http/Requests/Service/StoreRequest.php @@ -85,7 +85,7 @@ function ($attribute, $value, $fail) { 'required', 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(3000, 'Description tab - The long description must be 3000 characters or fewer.'), + new MarkdownMaxLength(config('local.service_description_max_chars'), 'Description tab - The long description must be ' . config('local.service_description_max_chars') . ' characters or fewer.'), ], 'wait_time' => ['present', 'nullable', Rule::in([ Service::WAIT_TIME_ONE_WEEK, @@ -99,7 +99,7 @@ function ($attribute, $value, $fail) { 'fees_url' => ['present', 'nullable', 'url', 'max:255'], 'testimonial' => ['present', 'nullable', 'string', 'min:1', 'max:255'], 'video_embed' => ['present', 'nullable', 'url', 'max:255', new VideoEmbed()], - 'url' => ['required', 'url', 'max:255'], + 'url' => ['present', 'nullable', 'url', 'max:255'], 'contact_name' => ['present', 'nullable', 'string', 'min:1', 'max:255'], 'contact_phone' => [ 'present', @@ -188,7 +188,7 @@ function ($attribute, $value, $fail) { 'useful_infos' => ['present', 'array'], 'useful_infos.*' => ['array'], 'useful_infos.*.title' => ['required_with:useful_infos.*', 'string', 'min:1', 'max:255'], - 'useful_infos.*.description' => ['required_with:useful_infos.*', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(10000)], + 'useful_infos.*.description' => ['required_with:useful_infos.*', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(config('local.useful_info_description_max_chars'))], 'useful_infos.*.order' => ['required_with:useful_infos.*', 'integer', 'min:1', new InOrder(array_pluck_multi($this->useful_infos, 'order'))], 'offerings' => ['present', 'array'], diff --git a/app/Http/Requests/Service/UpdateRequest.php b/app/Http/Requests/Service/UpdateRequest.php index d29ed008c..60ada616f 100644 --- a/app/Http/Requests/Service/UpdateRequest.php +++ b/app/Http/Requests/Service/UpdateRequest.php @@ -100,7 +100,7 @@ public function rules(): array 'description' => [ 'string', new MarkdownMinLength(1), - new MarkdownMaxLength(3000, 'Description tab - The long description must be 3000 characters or fewer.'), + new MarkdownMaxLength(config('local.service_description_max_chars'), 'Description tab - The long description must be ' . config('local.service_description_max_chars') . ' characters or fewer.'), ], 'wait_time' => [ 'nullable', @@ -117,7 +117,7 @@ public function rules(): array 'fees_url' => ['nullable', 'url', 'max:255'], 'testimonial' => ['nullable', 'string', 'min:1', 'max:255'], 'video_embed' => ['nullable', 'string', 'url', 'max:255', new VideoEmbed()], - 'url' => ['url', 'max:255'], + 'url' => ['nullable', 'url', 'max:255'], 'contact_name' => ['nullable', 'string', 'min:1', 'max:255'], 'contact_phone' => [ 'nullable', @@ -218,7 +218,7 @@ public function rules(): array 'useful_infos' => ['array'], 'useful_infos.*' => ['array'], 'useful_infos.*.title' => ['required_with:useful_infos.*', 'string', 'min:1', 'max:255'], - 'useful_infos.*.description' => ['required_with:useful_infos.*', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(10000)], + 'useful_infos.*.description' => ['required_with:useful_infos.*', 'string', new MarkdownMinLength(1), new MarkdownMaxLength(config('local.useful_info_description_max_chars'))], 'useful_infos.*.order' => [ 'required_with:useful_infos.*', 'integer', diff --git a/app/Http/Resources/PageResource.php b/app/Http/Resources/PageResource.php index a6cda15cc..e14e582da 100644 --- a/app/Http/Resources/PageResource.php +++ b/app/Http/Resources/PageResource.php @@ -18,16 +18,67 @@ public function toArray(Request $request): array 'title' => $this->title, 'slug' => $this->slug, 'excerpt' => $this->excerpt, - 'content' => $this->content, + 'content' => $this->when(request()->routeIs('core.v1.pages.store', 'core.v1.pages.show', 'core.v1.pages.update'), $this->content), 'order' => $this->order, 'enabled' => $this->enabled, 'page_type' => $this->page_type, 'image' => new FileResource($this->image), - 'landing_page' => new static($this->whenLoaded('landingPageAncestors', function () { - return $this->landingPage; - })), - 'parent' => new static($this->whenLoaded('parent')), - 'children' => static::collection($this->whenLoaded('children')), + 'parent' => $this->whenLoaded('parent', function () { + return $this->parent ? [ + 'id' => $this->parent->id, + 'title' => $this->parent->title, + 'slug' => $this->parent->slug, + 'excerpt' => $this->parent->excerpt, + 'order' => $this->parent->order, + 'enabled' => $this->parent->enabled, + 'page_type' => $this->parent->page_type, + 'created_at' => $this->parent->created_at->format(CarbonImmutable::ISO8601), + 'updated_at' => $this->parent->updated_at->format(CarbonImmutable::ISO8601), + ] : null; + }), + 'children' => $this->whenLoaded('children', function () { + return $this->children->map(function ($child) { + return [ + 'id' => $child->id, + 'title' => $child->title, + 'slug' => $child->slug, + 'excerpt' => $child->excerpt, + 'order' => $child->order, + 'enabled' => $child->enabled, + 'page_type' => $child->page_type, + 'created_at' => $child->created_at->format(CarbonImmutable::ISO8601), + 'updated_at' => $child->updated_at->format(CarbonImmutable::ISO8601), + ]; + }); + }), + 'ancestors' => $this->whenLoaded('ancestors', function () { + return static::defaultOrder()->ancestorsOf($this->id)->map(function ($ancestor) { + return [ + 'id' => $ancestor->id, + 'title' => $ancestor->title, + 'slug' => $ancestor->slug, + 'excerpt' => $ancestor->excerpt, + 'order' => $ancestor->order, + 'enabled' => $ancestor->enabled, + 'page_type' => $ancestor->page_type, + 'created_at' => $ancestor->created_at->format(CarbonImmutable::ISO8601), + 'updated_at' => $ancestor->updated_at->format(CarbonImmutable::ISO8601), + ]; + }); + }), + 'landing_page' => $this->whenLoaded('landingPageAncestors', function () { + return $this->landingPage ? [ + 'id' => $this->landingPage->id, + 'title' => $this->landingPage->title, + 'slug' => $this->landingPage->slug, + 'excerpt' => $this->landingPage->excerpt, + 'order' => $this->landingPage->order, + 'enabled' => $this->landingPage->enabled, + 'page_type' => $this->landingPage->page_type, + 'created_at' => $this->landingPage->created_at->format(CarbonImmutable::ISO8601), + 'updated_at' => $this->landingPage->updated_at->format(CarbonImmutable::ISO8601), + ] : null; + }), 'collection_categories' => CollectionCategoryResource::collection($this->whenLoaded('collectionCategories')), 'collection_personas' => CollectionPersonaResource::collection($this->whenLoaded('collectionPersonas')), 'created_at' => $this->created_at->format(CarbonImmutable::ISO8601), diff --git a/app/Models/Scopes/OrganisationEventScopes.php b/app/Models/Scopes/OrganisationEventScopes.php index fe80db189..7574e792d 100644 --- a/app/Models/Scopes/OrganisationEventScopes.php +++ b/app/Models/Scopes/OrganisationEventScopes.php @@ -53,6 +53,17 @@ public function scopeHasInductionLoop(Builder $query, bool $required): Builder }); } + public function scopeHasAccessibleToilet(Builder $query, bool $required): Builder + { + return $query->whereExists(function ($query) use ($required) { + $locationsTable = (new Location())->getTable(); + $query->select(DB::raw(1)) + ->from($locationsTable) + ->whereRaw("$locationsTable.id = {$this->getTable()}.location_id") + ->where("$locationsTable.has_accessible_toilet", (bool)$required); + }); + } + /** * @param bool $required */ diff --git a/app/Observers/ServiceObserver.php b/app/Observers/ServiceObserver.php index a372a1dde..b406f3ef1 100644 --- a/app/Observers/ServiceObserver.php +++ b/app/Observers/ServiceObserver.php @@ -12,10 +12,11 @@ class ServiceObserver { /** - * Handle the organisation "created" event. + * Handle the service "created" event. */ public function created(Service $service): void { + // Add service admin roles to all service->organisation->admins UserRole::query() ->with('user') ->where('role_id', Role::organisationAdmin()->id) @@ -27,7 +28,7 @@ public function created(Service $service): void } /** - * Handle the organisation "updated" event. + * Handle the service "updated" event. */ public function updated(Service $service): void { @@ -81,7 +82,7 @@ public function updated(Service $service): void } /** - * Handle the organisation "deleting" event. + * Handle the service "deleting" event. */ public function deleting(Service $service) { diff --git a/app/Rules/PageContent.php b/app/Rules/PageContent.php index 5bfe2551c..ffc7d0396 100644 --- a/app/Rules/PageContent.php +++ b/app/Rules/PageContent.php @@ -40,13 +40,21 @@ public function validate(string $attribute, mixed $value, Closure $fail): void // Immediately fail if the value is not an array. if (!is_array($value)) { $fail(__('validation.array')); + + return; } if ($value['type'] === 'copy') { if (!array_key_exists('value', $value)) { $fail('Invalid format for content'); + + return; } + $validateMarkdown = new MarkdownMaxLength(config('local.page_copy_max_chars'), 'Description tab - The page content must be ' . config('local.page_copy_max_chars') . ' characters or fewer.'); + + $validateMarkdown->validate($attribute, $value['value'], fn ($msg) => $fail($msg)); + if (($this->pageType === 'landing' && mb_strpos($attribute, 'introduction') && empty($value['value']))) { $fail('Page content is required for introduction'); } diff --git a/app/UpdateRequest/NewServiceCreatedByGlobalAdmin.php b/app/UpdateRequest/NewServiceCreatedByGlobalAdmin.php index 644978578..895034300 100644 --- a/app/UpdateRequest/NewServiceCreatedByGlobalAdmin.php +++ b/app/UpdateRequest/NewServiceCreatedByGlobalAdmin.php @@ -69,7 +69,7 @@ public function applyUpdateRequest(UpdateRequest $updateRequest): UpdateRequest 'last_modified_at' => Carbon::now(), ]; - // Add the elegibility types + // Add the custom elegibility types if ($data->has('eligibility_types') && $data['eligibility_types']['custom'] ?? null) { // Create the custom fields foreach ($data['eligibility_types']['custom'] as $customEligibilityType => $value) { @@ -80,8 +80,14 @@ public function applyUpdateRequest(UpdateRequest $updateRequest): UpdateRequest $service = Service::create($insert); + // Create the service eligibility taxonomy records + if ($data->has('eligibility_types') && $data['eligibility_types']['taxonomies'] ?? null) { + $eligibilityTypes = Taxonomy::whereIn('id', $data['eligibility_types']['taxonomies'])->get(); + $service->syncEligibilityRelationships($eligibilityTypes); + } + // Update the logo file - if ($data->has('logo_file_id')) { + if ($data->get('logo_file_id')) { /** @var \App\Models\File $file */ $file = File::findOrFail($data['logo_file_id'])->assigned(); @@ -147,12 +153,6 @@ public function applyUpdateRequest(UpdateRequest $updateRequest): UpdateRequest $service->syncTaxonomyRelationships($taxonomies); } - // Create the service eligibility taxonomy records - if ($data->has('eligibility_types') && $data['eligibility_types']['taxonomies'] ?? null) { - $eligibilityTypes = Taxonomy::whereIn('id', $data['eligibility_types']['taxonomies'])->get(); - $service->syncEligibilityRelationships($eligibilityTypes); - } - // Ensure conditional fields are reset if needed. $service->resetConditionalFields(); diff --git a/app/UpdateRequest/NewServiceCreatedByOrgAdmin.php b/app/UpdateRequest/NewServiceCreatedByOrgAdmin.php index c1afba08f..b87d764ad 100644 --- a/app/UpdateRequest/NewServiceCreatedByOrgAdmin.php +++ b/app/UpdateRequest/NewServiceCreatedByOrgAdmin.php @@ -4,6 +4,7 @@ use App\Contracts\AppliesUpdateRequests; use App\Http\Requests\Service\StoreRequest; +use App\Models\File; use App\Models\Service; use App\Models\Taxonomy; use App\Models\UpdateRequest; @@ -68,8 +69,34 @@ public function applyUpdateRequest(UpdateRequest $updateRequest): UpdateRequest 'last_modified_at' => Carbon::now(), ]; + // Add the custom elegibility types + if ($data->has('eligibility_types') && $data['eligibility_types']['custom'] ?? null) { + // Create the custom fields + foreach ($data['eligibility_types']['custom'] as $customEligibilityType => $value) { + $fieldName = 'eligibility_' . $customEligibilityType . '_custom'; + $insert[$fieldName] = $value; + } + } + $service = Service::create($insert); + // Create the service eligibility taxonomy records + if ($data->has('eligibility_types') && $data['eligibility_types']['taxonomies'] ?? null) { + $eligibilityTypes = Taxonomy::whereIn('id', $data['eligibility_types']['taxonomies'])->get(); + $service->syncEligibilityRelationships($eligibilityTypes); + } + + // Update the logo file + if ($data->get('logo_file_id')) { + /** @var \App\Models\File $file */ + $file = File::findOrFail($data['logo_file_id'])->assigned(); + + // Create resized version for common dimensions. + foreach (config('local.cached_image_dimensions') as $maxDimension) { + $file->resizedVersion($maxDimension); + } + } + if ($data->has('useful_infos')) { $service->usefulInfos()->delete(); foreach ($data->get('useful_infos') as $usefulInfo) { diff --git a/aws/resources.py b/aws/resources.py index 4c616b293..cda46301b 100644 --- a/aws/resources.py +++ b/aws/resources.py @@ -1086,7 +1086,97 @@ def create_aws_web_acl_resource(template, aws_metric_name_variable, aws_managed MetricName=Join('-',[aws_metric_name_variable, 'CommonRuleSet']), SampledRequestsEnabled=True ) - ) + ), + wafv2.WebACLRule( + Name='AWS-AWSManagedRulesKnownBadInputsRuleSet', + Statement=wafv2.StatementOne( + ManagedRuleGroupStatement=wafv2.ManagedRuleGroupStatement( + Name='AWSManagedRulesKnownBadInputsRuleSet', + VendorName='AWS' + ) + ), + OverrideAction=wafv2.OverrideAction( + **{"None":wafv2.NoneAction()} + ), + Priority=2, + VisibilityConfig=wafv2.VisibilityConfig( + CloudWatchMetricsEnabled=True, + MetricName=Join('-',[aws_metric_name_variable, 'KnownBadInputs']), + SampledRequestsEnabled=True + ) + ), + wafv2.WebACLRule( + Name='AWS-AWSManagedRulesSQLiRuleSet', + Statement=wafv2.StatementOne( + ManagedRuleGroupStatement=wafv2.ManagedRuleGroupStatement( + Name='AWSManagedRulesSQLiRuleSet', + VendorName='AWS' + ) + ), + OverrideAction=wafv2.OverrideAction( + **{"None":wafv2.NoneAction()} + ), + Priority=2, + VisibilityConfig=wafv2.VisibilityConfig( + CloudWatchMetricsEnabled=True, + MetricName=Join('-',[aws_metric_name_variable, 'SQLi']), + SampledRequestsEnabled=True + ) + ), + wafv2.WebACLRule( + Name='AWS-AWSManagedRulesPHPRuleSet', + Statement=wafv2.StatementOne( + ManagedRuleGroupStatement=wafv2.ManagedRuleGroupStatement( + Name='AWSManagedRulesPHPRuleSet', + VendorName='AWS' + ) + ), + OverrideAction=wafv2.OverrideAction( + **{"None":wafv2.NoneAction()} + ), + Priority=2, + VisibilityConfig=wafv2.VisibilityConfig( + CloudWatchMetricsEnabled=True, + MetricName=Join('-',[aws_metric_name_variable, 'PHP']), + SampledRequestsEnabled=True + ) + ), + wafv2.WebACLRule( + Name='AWS-AWSManagedRulesAmazonIpReputationList', + Statement=wafv2.StatementOne( + ManagedRuleGroupStatement=wafv2.ManagedRuleGroupStatement( + Name='AWSManagedRulesAmazonIpReputationList', + VendorName='AWS' + ) + ), + OverrideAction=wafv2.OverrideAction( + **{"None":wafv2.NoneAction()} + ), + Priority=2, + VisibilityConfig=wafv2.VisibilityConfig( + CloudWatchMetricsEnabled=True, + MetricName=Join('-',[aws_metric_name_variable, 'AmazonIpReputationList']), + SampledRequestsEnabled=True + ) + ), + wafv2.WebACLRule( + Name='AWS-AWSManagedRulesAnonymousIpList', + Statement=wafv2.StatementOne( + ManagedRuleGroupStatement=wafv2.ManagedRuleGroupStatement( + Name='AWSManagedRulesAnonymousIpList', + VendorName='AWS' + ) + ), + OverrideAction=wafv2.OverrideAction( + **{"None":wafv2.NoneAction()} + ), + Priority=2, + VisibilityConfig=wafv2.VisibilityConfig( + CloudWatchMetricsEnabled=True, + MetricName=Join('-',[aws_metric_name_variable, 'AnonymousIpList']), + SampledRequestsEnabled=True + ) + ), ], Scope='REGIONAL', VisibilityConfig=wafv2.VisibilityConfig( diff --git a/composer.json b/composer.json index 79fb1de94..e98e46cb5 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "brianium/paratest": "^6.8", "fakerphp/faker": "^1.16", "friendsofphp/php-cs-fixer": "^3.2", - "laravel/telescope": "^4.16", + "laravel/telescope": "^5.0", "mockery/mockery": "^1.4.4", "nunomaduro/collision": "^6.3", "pda/pheanstalk": "~4.0", diff --git a/composer.lock b/composer.lock index 4159764af..2f06636ca 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6996305118478b8f10e7b353c2234993", + "content-hash": "a886ea2d6acd9cd3d1bed300346fa893", "packages": [ { "name": "alphagov/notifications-php-client", @@ -10854,37 +10854,35 @@ }, { "name": "laravel/telescope", - "version": "v4.17.2", + "version": "v5.0.4", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "64da53ee46b99ef328458eaed32202b51e325a11" + "reference": "b5f9783c8e1ec3ec387b289d3ca8a8f85e76b4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/64da53ee46b99ef328458eaed32202b51e325a11", - "reference": "64da53ee46b99ef328458eaed32202b51e325a11", + "url": "https://api.github.com/repos/laravel/telescope/zipball/b5f9783c8e1ec3ec387b289d3ca8a8f85e76b4fb", + "reference": "b5f9783c8e1ec3ec387b289d3ca8a8f85e76b4fb", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^8.37|^9.0|^10.0", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0", "php": "^8.0", - "symfony/var-dumper": "^5.0|^6.0" + "symfony/console": "^5.3|^6.0|^7.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" }, "require-dev": { "ext-gd": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4", - "orchestra/testbench": "^6.0|^7.0|^8.0", + "laravel/octane": "^1.4|^2.0|dev-develop", + "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.0|^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - }, "laravel": { "providers": [ "Laravel\\Telescope\\TelescopeServiceProvider" @@ -10919,9 +10917,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v4.17.2" + "source": "https://github.com/laravel/telescope/tree/v5.0.4" }, - "time": "2023-11-01T14:01:06+00:00" + "time": "2024-04-22T09:19:03+00:00" }, { "name": "mockery/mockery", diff --git a/config/local.php b/config/local.php index c0f320ce8..7b301bfc6 100644 --- a/config/local.php +++ b/config/local.php @@ -68,8 +68,34 @@ 150, 350, ], + /** * The request api rate limit per minute per user / IP. */ 'api_rate_limit' => env('API_RATE_LIMIT', 300), + + /** + * Organisation description character limit. + */ + 'organisation_description_max_chars' => env('ORG_DESCRIPTION_MAX_CHARS', 3000), + + /** + * Service description character limit. + */ + 'service_description_max_chars' => env('SERVICE_DESCRIPTION_MAX_CHARS', 10000), + + /** + * Useful Info description character limit. + */ + 'useful_info_description_max_chars' => env('USEFUL_INFO_DESCRIPTION_MAX_CHARS', 1000), + + /** + * Organisation Event description character limit. + */ + 'event_description_max_chars' => env('EVENT_DESCRIPTION_MAX_CHARS', 10000), + + /** + * Page copy character limit. + */ + 'page_copy_max_chars' => env('PAGE_COPY_MAX_CHARS', 60000), ]; diff --git a/database/diagrams/ERD.mwb b/database/diagrams/ERD.mwb index 04150ed00..6450898a3 100644 Binary files a/database/diagrams/ERD.mwb and b/database/diagrams/ERD.mwb differ diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php index c93a6762e..fb3a81128 100644 --- a/database/factories/PageFactory.php +++ b/database/factories/PageFactory.php @@ -20,6 +20,7 @@ public function definition(): array return [ 'title' => $title, 'slug' => Str::slug($title), + 'excerpt' => $this->faker->sentence, 'content' => [ 'introduction' => [ 'content' => [ diff --git a/database/migrations/2024_05_02_105406_update_services_make_url_nullable.php b/database/migrations/2024_05_02_105406_update_services_make_url_nullable.php new file mode 100644 index 000000000..e2c4f505f --- /dev/null +++ b/database/migrations/2024_05_02_105406_update_services_make_url_nullable.php @@ -0,0 +1,27 @@ +string('url')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->string('url')->nullable(false)->change(); + }); + } +}; diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 79960a5b1..c9d3621a4 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -49,7 +49,7 @@ COPY packaged /var/www/html RUN chown -R www-data: /var/www/html # Increase the soft ulimit nofiles for users -RUN sed -i '/End of file/ i * soft nofile 40960\n' /etc/security/limits.conf && \ +RUN sed -i '/End of file/ i * soft nofile 81920\n' /etc/security/limits.conf && \ sed -i '/end of pam-auth-update/ i session required pam_limits.so' /etc/pam.d/common-session && \ sed -i '/end of pam-auth-update/ i session required pam_limits.so' /etc/pam.d/common-session-noninteractive diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 80f036703..2c1450b5c 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -1,5 +1,5 @@ - +
@@ -18,7 +18,7 @@ @yield('css') - +