From b12d72bef6d8c6e610a24d6dc756e57bb6efd472 Mon Sep 17 00:00:00 2001 From: Sander Dorigo Date: Thu, 23 Jun 2022 09:33:43 +0200 Subject: [PATCH] New code for API 2 --- app/Api/V1/Controllers/Controller.php | 6 ++- .../V1/Controllers/System/AboutController.php | 4 +- .../Requests/Models/Account/StoreRequest.php | 2 +- .../Controllers/Model/Bill/SumController.php | 4 +- app/Api/V2/Request/Generic/DateRequest.php | 4 +- app/Exceptions/BadHttpHeaderException.php | 13 ++++++ app/Exceptions/Handler.php | 18 +++++++- app/Http/Kernel.php | 2 + app/Http/Middleware/AcceptHeaders.php | 45 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 15 +++++++ app/Support/Request/ConvertsDataTypes.php | 3 ++ app/Validation/FireflyValidator.php | 2 +- routes/api.php | 1 + 13 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 app/Exceptions/BadHttpHeaderException.php create mode 100644 app/Http/Middleware/AcceptHeaders.php diff --git a/app/Api/V1/Controllers/Controller.php b/app/Api/V1/Controllers/Controller.php index 9250a7cc2a..40d01b83f9 100644 --- a/app/Api/V1/Controllers/Controller.php +++ b/app/Api/V1/Controllers/Controller.php @@ -83,9 +83,13 @@ abstract class Controller extends BaseController { $bag = new ParameterBag; $page = (int)request()->get('page'); - if (0 === $page) { + + if ($page < 1) { $page = 1; } + if ($page > (2^16)) { + $page = (2^16); + } $bag->set('page', $page); // some date fields: diff --git a/app/Api/V1/Controllers/System/AboutController.php b/app/Api/V1/Controllers/System/AboutController.php index 8c2cf0995a..b78a995d3f 100644 --- a/app/Api/V1/Controllers/System/AboutController.php +++ b/app/Api/V1/Controllers/System/AboutController.php @@ -62,7 +62,7 @@ class AboutController extends Controller 'driver' => $currentDriver, ]; - return response()->json(['data' => $data])->header('Content-Type', self::CONTENT_TYPE); + return response()->api(['data' => $data])->header('Content-Type', self::CONTENT_TYPE); } /** @@ -83,6 +83,6 @@ class AboutController extends Controller $resource = new Item(auth()->user(), $transformer, 'users'); - return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); + return response()->api($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V1/Requests/Models/Account/StoreRequest.php b/app/Api/V1/Requests/Models/Account/StoreRequest.php index 2da7798692..c8f8faa27b 100644 --- a/app/Api/V1/Requests/Models/Account/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Account/StoreRequest.php @@ -104,7 +104,7 @@ class StoreRequest extends FormRequest $type = $this->convertString('type'); $rules = [ 'name' => 'required|min:1|uniqueAccountForUser', - 'type' => 'required|' . sprintf('in:%s', $types), + 'type' => 'required|min:1|' . sprintf('in:%s', $types), 'iban' => ['iban', 'nullable', new UniqueIban(null, $type)], 'bic' => 'bic|nullable', 'account_number' => ['between:1,255', 'nullable', new UniqueAccountNumber(null, $type)], diff --git a/app/Api/V2/Controllers/Model/Bill/SumController.php b/app/Api/V2/Controllers/Model/Bill/SumController.php index 34378f6064..312fdaaa6b 100644 --- a/app/Api/V2/Controllers/Model/Bill/SumController.php +++ b/app/Api/V2/Controllers/Model/Bill/SumController.php @@ -59,7 +59,7 @@ class SumController extends Controller $converted = $this->cerSum($result); // convert to JSON response: - return response()->json($converted); + return response()->api($converted); } /** @@ -73,6 +73,6 @@ class SumController extends Controller $converted = $this->cerSum($result); // convert to JSON response: - return response()->json($converted); + return response()->api($converted); } } diff --git a/app/Api/V2/Request/Generic/DateRequest.php b/app/Api/V2/Request/Generic/DateRequest.php index 9071159578..f983b00cec 100644 --- a/app/Api/V2/Request/Generic/DateRequest.php +++ b/app/Api/V2/Request/Generic/DateRequest.php @@ -58,8 +58,8 @@ class DateRequest extends FormRequest public function rules(): array { return [ - 'start' => 'required|date', - 'end' => 'required|date|after:start', + 'start' => 'required|date|after:1900-01-01|before:2099-12-31', + 'end' => 'required|date|after_or_equal:start|before:2099-12-31|after:1900-01-01', ]; } } diff --git a/app/Exceptions/BadHttpHeaderException.php b/app/Exceptions/BadHttpHeaderException.php new file mode 100644 index 0000000000..c3173d1550 --- /dev/null +++ b/app/Exceptions/BadHttpHeaderException.php @@ -0,0 +1,13 @@ +json(['message' => $e->getMessage(), 'exception' => 'OAuthServerException'], 401); } + if($e instanceof BadRequestHttpException) { + return response()->json(['message' => $e->getMessage(), 'exception' => 'BadRequestHttpException'], 400); + } + + if ($e instanceof BadHttpHeaderException) { + // is always API exception. + return response()->json(['message' => $e->getMessage(), 'exception' => 'BadHttpHeaderException'], $e->statusCode); + } if ($request->expectsJson()) { + $errorCode = 500; + $errorCode = $e instanceof MethodNotAllowedHttpException ? 405: $errorCode; + $isDebug = config('app.debug', false); if ($isDebug) { return response()->json( @@ -106,12 +119,13 @@ class Handler extends ExceptionHandler 'file' => $e->getFile(), 'trace' => $e->getTrace(), ], - 500 + $errorCode ); } return response()->json( - ['message' => sprintf('Internal Firefly III Exception: %s', $e->getMessage()), 'exception' => get_class($e)], 500 + ['message' => sprintf('Internal Firefly III Exception: %s', $e->getMessage()), 'exception' => get_class($e)], + $errorCode ); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 90f7fb65bb..78a4ea778d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Http; +use FireflyIII\Http\Middleware\AcceptHeaders; use FireflyIII\Http\Middleware\Authenticate; use FireflyIII\Http\Middleware\Binder; use FireflyIII\Http\Middleware\EncryptCookies; @@ -177,6 +178,7 @@ class Kernel extends HttpKernel ], 'api' => [ + AcceptHeaders::class, EnsureFrontendRequestsAreStateful::class, 'auth:api,sanctum', 'bindings', diff --git a/app/Http/Middleware/AcceptHeaders.php b/app/Http/Middleware/AcceptHeaders.php new file mode 100644 index 0000000000..16d6aa68b6 --- /dev/null +++ b/app/Http/Middleware/AcceptHeaders.php @@ -0,0 +1,45 @@ +getMethod(); + + if ('GET' === $method && !$request->accepts(['application/json', 'application/vdn.api+json'])) { + throw new BadHttpHeaderException('Your request must accept either application/json or application/vdn.api+json.'); + } + if (('POST' === $method || 'PUT' === $method) && 'application/json' !== (string)$request->header('Content-Type')) { + $error = new BadHttpHeaderException('B'); + $error->statusCode = 415; + throw $error; + } + + // throw bad request if trace id is not a UUID + $uuid = $request->header('X-Trace-Id', null); + if (is_string($uuid) && '' !== trim($uuid) && (preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', trim($uuid)) !== 1)) { + throw new BadRequestHttpException('Bad X-Trace-Id header.'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0f5a22b381..39cdfa5924 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -27,6 +27,7 @@ use Illuminate\Support\ServiceProvider; use Laravel\Passport\Passport; use Laravel\Sanctum\Sanctum; use URL; +use Illuminate\Support\Facades\Response; /** * @codeCoverageIgnore @@ -45,6 +46,20 @@ class AppServiceProvider extends ServiceProvider if ('heroku' === config('app.env')) { URL::forceScheme('https'); } + + Response::macro('api', function ($value) { + $headers = [ + 'Cache-Control' => 'no-store' + ]; + $uuid = (string) request()->header('X-Trace-Id'); + if ('' !== trim($uuid) && (preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', trim($uuid)) === 1)) { + $headers['X-Trace-Id'] = $uuid; + } + return response() + ->json($value) + ->withHeaders($headers); + + }); } /** diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php index 8cd445b9f5..e958c2ab0c 100644 --- a/app/Support/Request/ConvertsDataTypes.php +++ b/app/Support/Request/ConvertsDataTypes.php @@ -119,6 +119,9 @@ trait ConvertsDataTypes $secondSearch = $keepNewlines ? ["\r"] : ["\r", "\n", "\t", "\036", "\025"]; $string = str_replace($secondSearch, '', $string); + // clear zalgo text (TODO also in API v2) + $string = preg_replace('/\pM/u', '', $string); + return trim($string); } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 6538d0684e..014f77a5ab 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -427,7 +427,7 @@ class FireflyValidator extends Validator return $this->validateByAccountTypeString($value, $parameters, $this->data['objectType']); } if (array_key_exists('type', $this->data)) { - return $this->validateByAccountTypeString($value, $parameters, $this->data['type']); + return $this->validateByAccountTypeString($value, $parameters, (string) $this->data['type']); } if (array_key_exists('account_type_id', $this->data)) { return $this->validateByAccountTypeId($value, $parameters); diff --git a/routes/api.php b/routes/api.php index d60ac769c7..26a132eb78 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,6 +24,7 @@ declare(strict_types=1); /** * V2 API route for TransactionSum API endpoints + * TODO what to do with these routes */ Route::group( ['namespace' => 'FireflyIII\Api\V2\Controllers\Transaction\Sum', 'prefix' => 'v2/transaction/sum',