diff --git a/composer.json b/composer.json index b89b853..3804ad0 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,14 @@ }, "require": { "php": "^8.2", - "statamic/cms": "^4.0 || ^5.0" + "statamic/cms": "^6.0" }, "require-dev": { "nunomaduro/collision": "^8.1", - "orchestra/testbench": "^9.0", - "pestphp/pest": "^2.4", - "pestphp/pest-plugin-watch": "^2.0", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", "laravel/pint": "^1.13", - "larastan/larastan": "^2.9" + "larastan/larastan": "^3.0" }, "extra": { "statamic": { diff --git a/src/Http/Controllers/ApiController.php b/src/Http/Controllers/ApiController.php new file mode 100644 index 0000000..95967df --- /dev/null +++ b/src/Http/Controllers/ApiController.php @@ -0,0 +1,15 @@ +getExtensions($mimetypes->guessMimeType($tmpPath)))->first(); // Create filename if not set through request diff --git a/src/Http/Controllers/CollectionEntriesController.php b/src/Http/Controllers/CollectionEntriesController.php index 25a777f..ba6a405 100644 --- a/src/Http/Controllers/CollectionEntriesController.php +++ b/src/Http/Controllers/CollectionEntriesController.php @@ -3,11 +3,11 @@ namespace Tv2regionerne\StatamicPrivateApi\Http\Controllers; use Facades\Statamic\API\FilterAuthorizer; +use Illuminate\Support\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Collections\EntriesController as CpController; use Statamic\Http\Resources\API\EntryResource; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; @@ -48,6 +48,11 @@ public function store(Request $request, $collection) { $collection = $this->collectionFromHandle($collection); + $request->replace($this->normalizeDateFieldtypeValuesByHandles( + $this->dateFieldHandlesForCollection($collection), + $request->all() + )); + try { $response = (new CpController($request))->store($request, $collection, Facades\Site::current()); } catch (ValidationException $e) { @@ -75,7 +80,7 @@ public function update(Request $request, $collection, $entry) $originalData = collect((new CpController($request))->edit($request, $collection, $entry)->get('values'))->filter(); $originalData = $originalData->merge($request->all()); - $request->merge($originalData->all()); + $request->replace($this->normalizeDateFieldtypeValues($entry, $originalData->all())); try { $response = (new CpController($request))->update($request, $collection, $entry); @@ -137,6 +142,76 @@ private function entryFromId($entry) return $entry; } + private function normalizeDateFieldtypeValues($entry, array $payload): array + { + $dateFieldHandles = $entry->blueprint()->fields()->all() + ->filter(fn ($field) => $field->type() === 'date') + ->keys(); + + return $this->normalizeDateFieldtypeValuesByHandles($dateFieldHandles, $payload); + } + + private function dateFieldHandlesForCollection($collection) + { + return $collection->entryBlueprints() + ->flatMap(fn ($blueprint) => $blueprint->fields()->all()) + ->filter(fn ($field) => $field->type() === 'date') + ->keys() + ->unique() + ->values(); + } + + private function normalizeDateFieldtypeValuesByHandles($dateFieldHandles, array $payload): array + { + foreach ($dateFieldHandles as $handle) { + if (! Arr::has($payload, $handle)) { + continue; + } + + Arr::set($payload, $handle, $this->normalizeDateValue(Arr::get($payload, $handle))); + } + + return $payload; + } + + private function normalizeDateValue($value) + { + if (! is_array($value)) { + return $value; + } + + if (Arr::has($value, 'start') || Arr::has($value, 'end')) { + return [ + 'start' => $this->normalizeDateValue(Arr::get($value, 'start')), + 'end' => $this->normalizeDateValue(Arr::get($value, 'end')), + ]; + } + + if (! Arr::has($value, 'date') && ! Arr::has($value, 'time')) { + return $value; + } + + $date = Arr::get($value, 'date'); + + if (! $date) { + return null; + } + + $time = Arr::get($value, 'time') ?: '00:00:00'; + + if (preg_match('/^\d{2}:\d{2}$/', $time)) { + $time .= ':00'; + } + + try { + return Carbon::parse($date.' '.$time) + ->utc() + ->format('Y-m-d\\TH:i:s.v\\Z'); + } catch (\Throwable $e) { + return (string) $date; + } + } + protected function allowedFilters() { return FilterAuthorizer::allowedForSubResources('api', 'collections', $this->collectionHandle); diff --git a/src/Http/Controllers/CollectionTreesController.php b/src/Http/Controllers/CollectionTreesController.php index 380828e..3688090 100644 --- a/src/Http/Controllers/CollectionTreesController.php +++ b/src/Http/Controllers/CollectionTreesController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Collections\CollectionTreeController as CpController; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; diff --git a/src/Http/Controllers/CollectionsController.php b/src/Http/Controllers/CollectionsController.php index 96e5555..66ac32f 100644 --- a/src/Http/Controllers/CollectionsController.php +++ b/src/Http/Controllers/CollectionsController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Collections\CollectionsController as CpController; use Statamic\Query\ItemQueryBuilder; use Tv2regionerne\StatamicPrivateApi\Http\Resources\CollectionResource; diff --git a/src/Http/Controllers/FormSubmissionsController.php b/src/Http/Controllers/FormSubmissionsController.php index 1559b39..73c060d 100644 --- a/src/Http/Controllers/FormSubmissionsController.php +++ b/src/Http/Controllers/FormSubmissionsController.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Forms\FormSubmissionsController as CpController; use Statamic\Query\ItemQueryBuilder; use Tv2regionerne\StatamicPrivateApi\Http\Resources\FormSubmissionResource; diff --git a/src/Http/Controllers/FormsController.php b/src/Http/Controllers/FormsController.php index f7d6297..747f9d2 100644 --- a/src/Http/Controllers/FormsController.php +++ b/src/Http/Controllers/FormsController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Forms\FormsController as CpController; use Statamic\Http\Resources\API\FormResource; use Statamic\Query\ItemQueryBuilder; diff --git a/src/Http/Controllers/GlobalVariablesController.php b/src/Http/Controllers/GlobalVariablesController.php index af6aef0..91722a5 100644 --- a/src/Http/Controllers/GlobalVariablesController.php +++ b/src/Http/Controllers/GlobalVariablesController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Tv2regionerne\StatamicPrivateApi\Http\Resources\GlobalVariablesResource; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; @@ -45,8 +44,7 @@ public function update(Request $request, $handle, ?string $site = null) } $set->data($values); - - $set->globalSet()->addLocalization($set)->save(); + $set->save(); $global = $this->globalFromHandle($handle); diff --git a/src/Http/Controllers/GlobalsController.php b/src/Http/Controllers/GlobalsController.php index e5219da..094592f 100644 --- a/src/Http/Controllers/GlobalsController.php +++ b/src/Http/Controllers/GlobalsController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Globals\GlobalsController as CpController; use Statamic\Query\ItemQueryBuilder; use Tv2regionerne\StatamicPrivateApi\Http\Resources\GlobalResource; diff --git a/src/Http/Controllers/NavTreesController.php b/src/Http/Controllers/NavTreesController.php index 0f81ee1..7f60764 100644 --- a/src/Http/Controllers/NavTreesController.php +++ b/src/Http/Controllers/NavTreesController.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Navigation\NavigationTreeController as CpController; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; diff --git a/src/Http/Controllers/NavsController.php b/src/Http/Controllers/NavsController.php index 286b04b..837d8e0 100644 --- a/src/Http/Controllers/NavsController.php +++ b/src/Http/Controllers/NavsController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Navigation\NavigationController as CpController; use Statamic\Query\ItemQueryBuilder; use Tv2regionerne\StatamicPrivateApi\Http\Resources\NavResource; @@ -73,7 +72,11 @@ public function destroy(Request $request, $nav) { $nav = $this->navFromHandle($nav); - return (new CpController($request))->destroy($nav->handle()); + $this->authorize('delete', $nav); + + $nav->delete(); + + return response()->json(); } private function navFromHandle($nav) diff --git a/src/Http/Controllers/TaxonomiesController.php b/src/Http/Controllers/TaxonomiesController.php index 38253f1..49c374e 100644 --- a/src/Http/Controllers/TaxonomiesController.php +++ b/src/Http/Controllers/TaxonomiesController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Taxonomies\TaxonomiesController as CpController; use Statamic\Query\ItemQueryBuilder; use Tv2regionerne\StatamicPrivateApi\Http\Resources\TaxonomyResource; diff --git a/src/Http/Controllers/TaxonomyTermsController.php b/src/Http/Controllers/TaxonomyTermsController.php index da67b04..b55bc2d 100644 --- a/src/Http/Controllers/TaxonomyTermsController.php +++ b/src/Http/Controllers/TaxonomyTermsController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; use Statamic\Http\Controllers\CP\Taxonomies\TermsController as CpController; use Statamic\Http\Resources\API\TermResource; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; diff --git a/src/Http/Controllers/UsersController.php b/src/Http/Controllers/UsersController.php index 41ba30b..d107260 100644 --- a/src/Http/Controllers/UsersController.php +++ b/src/Http/Controllers/UsersController.php @@ -3,12 +3,12 @@ namespace Tv2regionerne\StatamicPrivateApi\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; use Statamic\Contracts\Auth\User; use Statamic\Facades; -use Statamic\Http\Controllers\API\ApiController; -use Statamic\Http\Controllers\CP\Users\UsersController as CpController; use Statamic\Http\Resources\API\UserResource; +use Statamic\Rules\UniqueUserValue; use Tv2regionerne\StatamicPrivateApi\Traits\VerifiesPrivateAPI; class UsersController extends ApiController @@ -41,14 +41,35 @@ public function show($id) public function store(Request $request) { + abort_if(! $this->resourcesAllowed('users', ''), 404); + + $this->authorize('create', [User::class]); + try { - if (! $request->input('invitation')) { - $request = $request->merge(['invitation' => ['send' => false]]); + $validator = Validator::make($request->all(), [ + 'email' => ['required', 'email', new UniqueUserValue], + ]); + + $validator->setAttributeNames([ + 'email' => 'Email Address', + ]); + + $validator->validate(); + + $user = Facades\User::make() + ->email($request->string('email')->toString()); + + foreach ($request->except(['email', 'super', 'groups', 'roles', 'invitation']) as $key => $value) { + $user->set($key, $value); + } + + if ($request->boolean('super') && Facades\User::current()?->isSuper()) { + $user->makeSuper(); } - (new CpController($request))->store($request); + $user->save(); - $user = Facades\User::findByEmail($request->input('email')); + $user = Facades\User::findByEmail($request->string('email')->toString()); return app(UserResource::class)::make($user); } catch (ValidationException $e) { @@ -64,14 +85,34 @@ public function update(Request $request, $id) abort(404); } + $this->authorize('edit', $user); + try { - $data = $this->show($id)->toArray($request); + $payload = array_merge($request->all(), [ + 'email' => $request->input('email', $user->email()), + ]); + + $validator = Validator::make($payload, [ + 'email' => ['required', 'email', new UniqueUserValue(except: $user->id())], + ]); + + $validator->setAttributeNames([ + 'email' => 'Email Address', + ]); - $mergedData = collect($data)->merge($request->all()); + $validator->validate(); - $request->merge($mergedData->all()); + foreach ($request->except(['email', 'super', 'groups', 'roles', 'invitation']) as $key => $value) { + $user->set($key, $value); + } + + $user->email($payload['email']); + + if ($request->has('super') && Facades\User::current()?->isSuper() && Facades\User::current()?->id() !== $user->id()) { + $user->super = $request->boolean('super'); + } - (new CpController($request))->update($request, $id); + $user->save(); return app(UserResource::class)::make($user); } catch (ValidationException $e) { diff --git a/tests/Http/Controllers/CollectionEntriesControllerTest.php b/tests/Http/Controllers/CollectionEntriesControllerTest.php index b0692b5..40368da 100644 --- a/tests/Http/Controllers/CollectionEntriesControllerTest.php +++ b/tests/Http/Controllers/CollectionEntriesControllerTest.php @@ -91,6 +91,47 @@ $this->assertSame('test', $entry1->fresh()->get('title')); }); +it('updates dated entries without failing date validation on partial updates', function () { + $collection = tap(Facades\Collection::make('test')->dated(true))->save(); + + $entry = tap(Facades\Entry::make()->id('dated-entry')->collection($collection)->date('2026-01-15')->set('title', 'before'))->save(); + + $this->actingAs(makeUser()); + + $response = $this->patch(route('private.collections.entries.update', ['collection' => $collection->handle(), 'entry' => $entry->id()]), [ + 'title' => 'after', + ]); + + $response->assertOk(); + $response->assertJsonPath('data.title', 'after'); + + $updated = $entry->fresh(); + + $this->assertSame('after', $updated->get('title')); + $this->assertSame('2026-01-15', $updated->date()->format('Y-m-d')); +}); + +it('updates dated entries when date is sent as date and time array', function () { + $collection = tap(Facades\Collection::make('test')->dated(true))->save(); + + $entry = tap(Facades\Entry::make()->id('dated-entry-array')->collection($collection)->date('2026-01-15')->set('title', 'before'))->save(); + + $this->actingAs(makeUser()); + + $response = $this->patch(route('private.collections.entries.update', ['collection' => $collection->handle(), 'entry' => $entry->id()]), [ + 'date' => [ + 'date' => '2025-06-10', + 'time' => '09:30', + ], + ]); + + $response->assertOk(); + + $updated = $entry->fresh(); + + $this->assertSame('2025-06-10', $updated->date()->format('Y-m-d')); +}); + it('gets deletes an entry', function () { $collection = tap(Facades\Collection::make('test'))->save(); @@ -128,6 +169,29 @@ $this->assertSame('test', Facades\Entry::all()->first()->get('title')); }); +it('creates dated entries when date is sent as date and time array', function () { + Event::fake(); + + $collection = tap(Facades\Collection::make('test')->dated(true))->save(); + + $this->actingAs(makeUser()); + + $response = $this->post(route('private.collections.entries.store', ['collection' => $collection->handle()]), [ + 'title' => 'test', + 'date' => [ + 'date' => '2025-06-10', + 'time' => '09:30', + ], + ]); + + $response->assertOk(); + + $entry = Facades\Entry::all()->first(); + + $this->assertSame('test', $entry->get('title')); + $this->assertSame('2025-06-10', $entry->date()->format('Y-m-d')); +}); + it('returns validation errors when creating an entry', function () { Event::fake(); diff --git a/tests/Http/Controllers/CollectionTreesControllerTest.php b/tests/Http/Controllers/CollectionTreesControllerTest.php index 464d51e..887b0d4 100644 --- a/tests/Http/Controllers/CollectionTreesControllerTest.php +++ b/tests/Http/Controllers/CollectionTreesControllerTest.php @@ -39,9 +39,9 @@ }); it('updates a tree', function () { - Facades\Site::setConfig(['sites' => [ + Facades\Site::setSites([ 'en' => ['url' => 'http://domain.com/', 'locale' => 'en'], - ]]); + ]); $collection = tap(Facades\Collection::make('test')->structureContents(['root' => true, 'max_depth' => 3]))->save(); diff --git a/tests/Http/Controllers/GlobalsVariablesControllerTest.php b/tests/Http/Controllers/GlobalsVariablesControllerTest.php index fdc6c0d..e753a01 100644 --- a/tests/Http/Controllers/GlobalsVariablesControllerTest.php +++ b/tests/Http/Controllers/GlobalsVariablesControllerTest.php @@ -3,14 +3,13 @@ use Statamic\Facades; it('gets variables', function () { - Facades\Site::setConfig(['sites' => [ + Facades\Site::setSites([ 'en' => ['url' => 'http://domain.com/', 'locale' => 'en'], - ]]); + ]); $global1 = tap(Facades\GlobalSet::make('test'))->save(); $vars = $global1->makeLocalization('en'); - $global1->addLocalization($vars); - $global1->save(); + $vars->save(); $this->actingAs(makeUser()); @@ -23,15 +22,14 @@ }); it('updates a variable', function () { - Facades\Site::setConfig(['sites' => [ + Facades\Site::setSites([ 'en' => ['url' => 'http://domain.com/', 'locale' => 'en'], - ]]); + ]); $global1 = tap(Facades\GlobalSet::make('test'))->save(); $vars = $global1->makeLocalization('en'); $vars->data(['test' => 'yes']); - $global1->addLocalization($vars); - $global1->save(); + $vars->save(); $response = $this->patch(route('private.globals.variables.update', ['globalset' => $global1->handle(), 'site' => 'en']), [ 'test' => 'no', diff --git a/tests/TestCase.php b/tests/TestCase.php index 38110b6..ada68b7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,7 +4,7 @@ use Illuminate\Encryption\Encrypter; use Illuminate\Foundation\Testing\RefreshDatabase; -use Statamic\Extend\Manifest; +use Statamic\Addons\Manifest; use Statamic\Providers\StatamicServiceProvider; use Statamic\Stache\Stores\UsersStore; use Statamic\Statamic; @@ -25,8 +25,7 @@ protected function setUp(): void $this->runLaravelMigrations(); - \Facades\Statamic\Version::shouldReceive('get')->andReturn('4.0.0-testing'); - $this->addToAssertionCount(-1); // Dont want to assert this + \Facades\Statamic\Version::shouldReceive('get')->andReturn('6.0.0-testing'); $this->preventSavingStacheItemsToDisk(); } @@ -75,7 +74,7 @@ protected function getEnvironmentSetUp($app) ]); // Opret 'test' disken midlertidigt for testens varighed - //Storage::fake('test'); + // Storage::fake('test'); } protected function resolveApplicationConfiguration($app) @@ -91,7 +90,7 @@ protected function resolveApplicationConfiguration($app) 'cp', 'forms', 'static_caching', - //'sites', + // 'sites', 'stache', 'system', 'users', @@ -100,7 +99,7 @@ protected function resolveApplicationConfiguration($app) foreach ($configs as $config) { $app['config']->set( "statamic.$config", - require(__DIR__."/../vendor/statamic/cms/config/{$config}.php") + require (__DIR__."/../vendor/statamic/cms/config/{$config}.php") ); } @@ -112,7 +111,7 @@ protected function resolveApplicationConfiguration($app) 'directory' => __DIR__.'/__fixtures__/users', ]); - $app['config']->set('private-api', require(__DIR__.'/../config/private-api.php')); + $app['config']->set('private-api', require (__DIR__.'/../config/private-api.php')); $app['config']->set('private-api.enabled', true); $app['config']->set('private-api.middleware', 'web'); $app['config']->set('private-api.resources', [