From b2a86c7df7d5a473e80034832a01b21444fa50e6 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Sun, 6 Mar 2022 00:48:14 +0100 Subject: Refactor --- src/App.php | 350 ++++--------------------------- src/Builder.php | 80 +++++++ src/Controllers/EntriesController.php | 84 ++++++++ src/Controllers/FieldsController.php | 25 +++ src/Controllers/SubmissionController.php | 49 +++++ src/Controllers/ValidationController.php | 26 +++ src/HookManager.php | 42 ++++ src/HttpException.php | 6 + src/PluginLoader.php | 29 +++ src/PreLoader.php | 15 ++ src/Utilities.php | 40 ++++ src/Validator.php | 94 +++++++++ 12 files changed, 528 insertions(+), 312 deletions(-) create mode 100644 src/Builder.php create mode 100644 src/Controllers/EntriesController.php create mode 100644 src/Controllers/FieldsController.php create mode 100644 src/Controllers/SubmissionController.php create mode 100644 src/Controllers/ValidationController.php create mode 100644 src/HookManager.php create mode 100644 src/HttpException.php create mode 100644 src/PluginLoader.php create mode 100644 src/PreLoader.php create mode 100644 src/Utilities.php create mode 100644 src/Validator.php (limited to 'src') diff --git a/src/App.php b/src/App.php index 92ceb4a..15be58b 100644 --- a/src/App.php +++ b/src/App.php @@ -2,6 +2,10 @@ namespace FlatFileForms; +use FlatFileForms\Controllers\EntriesController; +use FlatFileForms\Controllers\FieldsController; +use FlatFileForms\Controllers\SubmissionController; +use FlatFileForms\Controllers\ValidationController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Yosymfony\Toml\Toml; @@ -33,104 +37,34 @@ class App // check api key $apiKey = $_GET['key'] ?? $_POST['key'] ?? null; if (empty($apiKey)) { - $response->setStatusCode(Response::HTTP_BAD_REQUEST); - throw new \Exception('API key missing'); + throw new HttpException('API key missing', Response::HTTP_BAD_REQUEST); } if (! in_array($apiKey, $config['api']['keys'])) { - $response->setStatusCode(Response::HTTP_UNAUTHORIZED); - throw new \Exception('API key does not match'); + throw new HttpException('API key does not match', Response::HTTP_UNAUTHORIZED); } // GET if ($method == 'GET') { if (str_ends_with($path, '/fields')) { - $this->formPath = $formPath = $contentRoot . str_replace('/fields', '', $path); + $formPath = $contentRoot . str_replace('/fields', '', $path); - $fields = $this->buildFields($formPath, $_GET['page'] ?? null); + $builder = new Builder($formPath); - // flatten paged form - if ($this->isPagedFieldSet($fields) && isset($_GET['flat'])) { - $fields = array_merge(...array_values($fields)); - } + $fieldsController = new FieldsController(); - $content['data'] = $fields; + $content = $fieldsController->getFields($builder); } else if (str_ends_with($path, '/entries')) { if (! isset($_GET['dateFrom'])) { - $response->setStatusCode(Response::HTTP_BAD_REQUEST); - throw new \Exception('dateFrom parameter missing'); + throw new HttpException('dateFrom parameter missing', Response::HTTP_BAD_REQUEST); } - $this->formPath = $formPath = $contentRoot . str_replace('/entries', '', $path); - - $entries = []; - - $dateFrom = new \DateTime($_GET['dateFrom']); - $dateTo = new \DateTime($_GET['dateTo'] ?? 'now'); - - $dateRangeYears = range($dateFrom->format('Y'), $dateTo->format('Y')); - $dateRangeYearsCount = count($dateRangeYears); - foreach ($dateRangeYears as $dateRangeYearIdx => $dateRangeYear) { - $yearPath = "$formPath/entries/$dateRangeYear"; - if (! is_dir($yearPath)) { - continue; - } - - if ($dateRangeYearsCount === 1) { - $dateRangeMonths = range($dateFrom->format('m'), $dateTo->format('m')); - } - else if ($dateRangeYearIdx === 0) { - $dateRangeMonths = range($dateFrom->format('m'), 12); - } - else if ($dateRangeYearIdx === $dateRangeYearsCount - 1) { - $dateRangeMonths = range(1, $dateTo->format('m')); - } - else { - $dateRangeMonths = range(1, 12); - } - - $dateRangeMonthsCount = count($dateRangeMonths); - foreach ($dateRangeMonths as $dateRangeMonthIdx => $dateRangeMonth) { - $monthPath = "$yearPath/" . sprintf('%02d', $dateRangeMonth); - if (! is_dir($monthPath)) { - continue; - } - - if ($dateRangeMonthsCount === 1) { - $dateRangeDays = range($dateFrom->format('d'), $dateTo->format('d')); - } - else if ($dateRangeYearIdx === 0 && $dateRangeMonthIdx === 0) { - $dateRangeDays = range($dateFrom->format('d'), 31); - } - else if ($dateRangeYearIdx === $dateRangeYearsCount - 1 && $dateRangeMonthIdx === $dateRangeMonthsCount - 1) { - $dateRangeDays = range(1, $dateTo->format('d')); - } - else { - $dateRangeDays = range(1, 31); - } - - foreach ($dateRangeDays as $dateRangeDay) { - $dayPath = "$monthPath/" . sprintf('%02d', $dateRangeDay); - if (! is_dir($dayPath)) { - continue; - } - - $entriesForDay = $this->scandir($dayPath); - foreach ($entriesForDay as $entryForDay) { - $entry = Toml::parseFile("$dayPath/$entryForDay"); - if (isset($_GET['flat'])) { - $entries[] = $entry; - } else { - $entries[$dateRangeYear][$dateRangeMonth][$dateRangeDay][] = $entry; - } - } - } - - } - } + $formPath = $contentRoot . str_replace('/entries', '', $path); + + $entriesController = new EntriesController(); - $content['data'] = $entries; + $content = $entriesController->getEntries(); } else { @@ -141,46 +75,36 @@ class App // POST else if ($method == 'POST') { if (str_ends_with($path, '/validate')) { - $this->formPath = $formPath = $contentRoot . str_replace('/validate', '', $path); + $formPath = $contentRoot . str_replace('/validate', '', $path); - $fields = $this->buildFields($formPath, $_GET['page'] ?? null); + $builder = new Builder($formPath); + $validator = new Validator($formPath); - $content = $this->validateRequest($fields); + $validationController = new ValidationController(); + + $content = $validationController->validateRequest($builder, $validator); } else if (str_ends_with($path, '/submit')) { - $this->formPath = $formPath = $contentRoot . str_replace('/submit', '', $path); - - $fields = $this->buildFields($formPath); - - $content = $this->validateRequest($fields); - - // if there were no validation errors then add entry - if (empty($content['error'])) { - $date = new \Datetime(); - $entry = [ - 'fields' => $_POST, - 'date' => $date->format('c'), - ]; - - $builder = new TomlBuilder(); - $builder->addValue('date', $entry['date']); - $builder->addTable('fields'); - foreach ($entry['fields'] as $entryKey => $entryValue) { - $builder->addValue($entryKey, $entryValue); - } - - $entryDirectory = $formPath . '/entries/' . $date->format('Y/m/d'); - @mkdir($entryDirectory, 0774, true); - $entryFilename = $date->format('Ymd_Hi_') . hash('adler32', serialize($entry)) . '.toml'; - file_put_contents( - $entryDirectory . '/' . $entryFilename, - $builder->getTomlString() - ); + $formPath = $contentRoot . str_replace('/submit', '', $path); + + $builder = new Builder($formPath); + $validator = new Validator($formPath); + + $submissionController = new SubmissionController(); + + $content = $submissionController->submit($builder, $validator, $formPath); + + if (! empty($content['error'])) { + throw new HttpException($content['error'], Response::HTTP_UNPROCESSABLE_ENTITY); } } } } catch (\Exception $exception) { + if ($exception instanceof HttpException) { + $response->setStatusCode($exception->getCode()); + } + $content['error'] = basename(get_class($exception)) . ': ' . $exception->getMessage(); } @@ -190,178 +114,10 @@ class App $response->send(); } - /** - * @param array $fields - */ - public function validateRequest($fields) { - $content = []; - $hasInvalidFields = false; - - $fields = $this->validateFields($fields); - - if ($this->isPagedFieldSet($fields)) { - // remove surplus field values from response - $fields = array_map(function ($page) { - return array_map(function ($field) { - return array_intersect_key($field, array_flip([ - 'is_valid', - ])); - }, $page); - }, $fields); - - $flattened = array_merge(...array_values($fields)); - $hasInvalidFields = in_array(false, array_column($flattened, 'is_valid')); - } else { - // remove surplus field values from response - $fields = array_map(function ($field) { - return array_intersect_key($field, array_flip([ - 'is_valid', - ])); - }, $fields); - - $hasInvalidFields = in_array(false, array_column($fields, 'is_valid')); - } - - $content['data'] = $fields; - - if ($hasInvalidFields) { - $content['error'] = 'Invalid fields'; - } - - return $content; - } - - /** - * @param string $formPath - * @param mixed $page - */ - public function buildFields($formPath, $page = null) - { - $parsed = Toml::parseFile($formPath . '/fields/_fields.toml'); - $fields = []; - - // if a page is requested - if ($page) { - if (! isset($parsed['page'])) { - throw new \Exception('Form has no pages'); - } - - if (! isset($parsed['page'][$page])) { - throw new \Exception('Form has no page ' . $page); - } - - $fields = $this->buildSinglePageFields($parsed['page'][$page], $formPath); - } - - // else get all fields - else { - // if form is paged - if (isset($parsed['page'])) { - $pages = $parsed['page']; - foreach ($pages as $pageKey => $pageFields) { - $fields[$pageKey] = $this->buildSinglePageFields($pageFields, $formPath); - } - } - - // if form is not paged - else { - foreach ($parsed['field'] as $key => $field) { - $fields[$key] = $this->buildSingleField($formPath, $key, $field); - } - } - } - - return $fields; - } - - /** - * @param array $pageFields - * @param string $formPath - */ - public function buildSinglePageFields($pageFields, $formPath) - { - $fields = []; - - if (! empty($pageFields['file'])) { - $pageFields = array_replace_recursive($pageFields, Toml::parseFile($formPath . '/fields/' . $pageFields['file'])); - } - - foreach ($pageFields['field'] as $key => $field) { - $fields[$key] = $this->buildSingleField($formPath, $key, $field); - } - - return $fields; - } - - /** - * @param string $formPath - * @param string $key - * @param string $field - */ - public function buildSingleField($formPath, $key, $field) - { - if (! empty($field['file'])) { - $field = array_replace_recursive($field, Toml::parseFile($formPath . '/fields/' . $field['file'])); - } - - if (empty($field['name'])) { - $field['name'] = $key; - } - - return $field; - } - - /** - * @param array $fields - */ - public function validateFields($fields) - { - if ($this->isPagedFieldSet($fields)) { - foreach ($fields as $pageKey => &$pageFields) { - foreach ($pageFields as $key => &$field) { - $field = $this->validateSingleField($field); - } - } - } else { - foreach ($fields as $key => &$field) { - $field = $this->validateSingleField($field); - } - } - - return $fields; - } - - /** - * @param array $field - */ - public function validateSingleField($field) - { - $value = $_POST[$field['name']] ?? ''; - $field['is_valid'] = true; - - if (isset($field['required']) && empty($value)) { - $field['is_valid'] = false; - } - - if (isset($field['validation']['pattern']) && preg_match_all('/' . $field['validation']['pattern'] . '/', $value) === 0) { - $field['is_valid'] = false; - } - - $validationFunctionName = 'validate_' . basename($this->formPath) . '_' . $field['name']; - if (function_exists($validationFunctionName)) { - $field = call_user_func($validationFunctionName, $field, $value); - } - - return $field; - } - - /** - * @param string $formPath - */ - public function buildConfig($formPath) + public function buildConfig(string $requestPath): array { $config = []; - $currentDirectory = $formPath; + $currentDirectory = $requestPath; while (true) { $configFile = $currentDirectory . '/config/config.toml'; if (file_exists($configFile)) { @@ -387,34 +143,4 @@ class App return $config; } - - /** - * @param array $fields - */ - public function isPagedFieldSet($fields) - { - $firstItem = reset($fields); - - return ! (isset($firstItem['name']) && ! is_array($firstItem['name'])); - } - - /** - * @param string|array $paths - */ - public function scandir($paths) - { - $paths = (array)$paths; - - $scanned = []; - foreach ($paths as $path) { - $filtered = array_values( - array_filter( - scandir($path), fn ($item) => ! in_array($item, ['.', '..']) - ) - ); - array_push($scanned, ...$filtered); - } - - return $scanned; - } } diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..308fa82 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,80 @@ +formPath . '/fields/_fields.toml'); + $fields = []; + + // if a page is requested + if ($page) { + if (! isset($parsed['page'])) { + throw new \Exception('Form has no pages'); + } + + if (! isset($parsed['page'][$page])) { + throw new \Exception('Form has no page ' . $page); + } + + $fields = $this->buildSinglePageFields($parsed['page'][$page]); + } + + // else get all fields + else { + // if form is paged + if (isset($parsed['page'])) { + $pages = $parsed['page']; + foreach ($pages as $pageKey => $pageFields) { + $fields[$pageKey] = $this->buildSinglePageFields($pageFields); + } + } + + // if form is not paged + else { + foreach ($parsed['field'] as $key => $field) { + $fields[$key] = $this->buildSingleField($key, $field); + } + } + } + + return $fields; + } + + public function buildSinglePageFields(array $pageFields): array + { + $fields = []; + + if (! empty($pageFields['file'])) { + $pageFields = array_replace_recursive($pageFields, Toml::parseFile($this->formPath . '/fields/' . $pageFields['file'])); + } + + foreach ($pageFields['field'] as $key => $field) { + $fields[$key] = $this->buildSingleField($key, $field); + } + + return $fields; + } + + public function buildSingleField(string $key, array $field): array + { + if (! empty($field['file'])) { + $field = array_replace_recursive($field, Toml::parseFile($this->formPath . '/fields/' . $field['file'])); + } + + if (empty($field['name'])) { + $field['name'] = $key; + } + + return $field; + } +} diff --git a/src/Controllers/EntriesController.php b/src/Controllers/EntriesController.php new file mode 100644 index 0000000..13aa8e0 --- /dev/null +++ b/src/Controllers/EntriesController.php @@ -0,0 +1,84 @@ +format('Y'), $dateTo->format('Y')); + $dateRangeYearsCount = count($dateRangeYears); + foreach ($dateRangeYears as $dateRangeYearIdx => $dateRangeYear) { + $yearPath = "$formPath/entries/$dateRangeYear"; + if (! is_dir($yearPath)) { + continue; + } + + if ($dateRangeYearsCount === 1) { + $dateRangeMonths = range($dateFrom->format('m'), $dateTo->format('m')); + } + else if ($dateRangeYearIdx === 0) { + $dateRangeMonths = range($dateFrom->format('m'), 12); + } + else if ($dateRangeYearIdx === $dateRangeYearsCount - 1) { + $dateRangeMonths = range(1, $dateTo->format('m')); + } + else { + $dateRangeMonths = range(1, 12); + } + + $dateRangeMonthsCount = count($dateRangeMonths); + foreach ($dateRangeMonths as $dateRangeMonthIdx => $dateRangeMonth) { + $monthPath = "$yearPath/" . sprintf('%02d', $dateRangeMonth); + if (! is_dir($monthPath)) { + continue; + } + + if ($dateRangeMonthsCount === 1) { + $dateRangeDays = range($dateFrom->format('d'), $dateTo->format('d')); + } + else if ($dateRangeYearIdx === 0 && $dateRangeMonthIdx === 0) { + $dateRangeDays = range($dateFrom->format('d'), 31); + } + else if ($dateRangeYearIdx === $dateRangeYearsCount - 1 && $dateRangeMonthIdx === $dateRangeMonthsCount - 1) { + $dateRangeDays = range(1, $dateTo->format('d')); + } + else { + $dateRangeDays = range(1, 31); + } + + foreach ($dateRangeDays as $dateRangeDay) { + $dayPath = "$monthPath/" . sprintf('%02d', $dateRangeDay); + if (! is_dir($dayPath)) { + continue; + } + + $entriesForDay = $utilities->scandir($dayPath); + foreach ($entriesForDay as $entryForDay) { + $entry = Toml::parseFile("$dayPath/$entryForDay"); + if (isset($_GET['flat'])) { + $entries[] = $entry; + } else { + $entries[$dateRangeYear][$dateRangeMonth][$dateRangeDay][] = $entry; + } + } + } + + } + } + + $content['data'] = $entries; + + return $content; + } +} diff --git a/src/Controllers/FieldsController.php b/src/Controllers/FieldsController.php new file mode 100644 index 0000000..c407604 --- /dev/null +++ b/src/Controllers/FieldsController.php @@ -0,0 +1,25 @@ +buildFields($_GET['page'] ?? null); + + // flatten paged form + if ($utilities->isPagedFieldSet($fields) && isset($_GET['flat'])) { + $fields = array_merge(...array_values($fields)); + } + + $content['data'] = $fields; + + return $content; + } +} diff --git a/src/Controllers/SubmissionController.php b/src/Controllers/SubmissionController.php new file mode 100644 index 0000000..f2c4c80 --- /dev/null +++ b/src/Controllers/SubmissionController.php @@ -0,0 +1,49 @@ +buildFields(); + + // run through validation + $result = $validator->validateRequest($fields); + + $content['data'] = $result['fields']; + + // if there were no validation errors then add entry + if (empty($result['error'])) { + $date = new \Datetime(); + $entry = [ + 'fields' => $_POST, + 'date' => $date->format('c'), + ]; + + $entryBuilder = new TomlBuilder(); + $entryBuilder->addValue('date', $entry['date']); + $entryBuilder->addTable('fields'); + foreach ($entry['fields'] as $entryKey => $entryValue) { + $entryBuilder->addValue($entryKey, $entryValue); + } + + $entryDirectory = $formPath . '/entries/' . $date->format('Y/m/d'); + @mkdir($entryDirectory, 0774, true); + $entryFilename = $date->format('Ymd_Hi_') . hash('adler32', serialize($entry)) . '.toml'; + file_put_contents( + $entryDirectory . '/' . $entryFilename, + $entryBuilder->getTomlString() + ); + } + else { + $content['error'] = $result['error']; + } + + return $content; + } +} diff --git a/src/Controllers/ValidationController.php b/src/Controllers/ValidationController.php new file mode 100644 index 0000000..f90359a --- /dev/null +++ b/src/Controllers/ValidationController.php @@ -0,0 +1,26 @@ +buildFields($_GET['page'] ?? null); + + $result = $validator->validateRequest($fields); + + $content['data'] = $result['fields']; + if (! empty($result['error'])) { + $content['error'] = $result['error']; + } + + return $content; + } +} diff --git a/src/HookManager.php b/src/HookManager.php new file mode 100644 index 0000000..96ca8d3 --- /dev/null +++ b/src/HookManager.php @@ -0,0 +1,42 @@ +action[$name][$priority][] = compact('name', 'function', 'priority'); + } + + public function addFilter(string $name, callable $function, int $priority = 10): void + { + $this->filters[$name][$priority][] = compact('name', 'callable', 'priority'); + } + + public function doAction(string $name, mixed ...$arguments): void + { + foreach ($this->actions[$name] as $actions) { + foreach ($actions as $action) { + call_user_func_array($action['function'], $arguments); + } + } + } + + public function applyFilter(string $name, mixed $value, mixed ...$arguments): mixed + { + // set $value as first argument + array_unshift($arguments, $value); + + foreach ($this->filters[$name] as $filters) { + foreach ($filters as $filter) { + $value = call_user_func_array($filter['function'], $arguments); + } + } + + return $value; + } +} diff --git a/src/HttpException.php b/src/HttpException.php new file mode 100644 index 0000000..85d2854 --- /dev/null +++ b/src/HttpException.php @@ -0,0 +1,6 @@ +scandir($pluginsDirectoryPath); + foreach ($pluginDirectories as $directory) { + $pluginClass = 'FlatFileForms\\Plugins\\' . $directory . '\\Plugin'; + $plugin = new $pluginClass(); + } + } +} diff --git a/src/PreLoader.php b/src/PreLoader.php new file mode 100644 index 0000000..57a0f52 --- /dev/null +++ b/src/PreLoader.php @@ -0,0 +1,15 @@ + $path . '/' . $item, + array_filter( + scandir($path), fn ($item) => ! in_array($item, ['.', '..']) + ) + ) + ); + } + + public function scandirMultiple(string|array $paths): array + { + $paths = (array)$paths; + + $merged = []; + foreach ($paths as $path) { + $scanned = $this->scandir($path); + array_push($merged, ...$scanned); + } + + return $merged; + } +} diff --git a/src/Validator.php b/src/Validator.php new file mode 100644 index 0000000..33cdf34 --- /dev/null +++ b/src/Validator.php @@ -0,0 +1,94 @@ +validateFields($fields); + + if ($utilities->isPagedFieldSet($fields)) { + // remove surplus field values from response + $fields = array_map(function ($page) { + return array_map(function ($field) { + return array_intersect_key($field, array_flip([ + 'is_valid', + ])); + }, $page); + }, $fields); + + $flattened = array_merge(...array_values($fields)); + $hasInvalidFields = in_array(false, array_column($flattened, 'is_valid')); + } else { + // remove surplus field values from response + $fields = array_map(function ($field) { + return array_intersect_key($field, array_flip([ + 'is_valid', + ])); + }, $fields); + + $hasInvalidFields = in_array(false, array_column($fields, 'is_valid')); + } + + $result['fields'] = $fields; + + if ($hasInvalidFields) { + $result['error'] = 'invalid fields'; + } + + return $result; + } + + public function validateFields(array $fields): array + { + /**@var Utilities $utilities*/ + global $utilities; + + if ($utilities->isPagedFieldSet($fields)) { + foreach ($fields as $pageKey => &$pageFields) { + foreach ($pageFields as $key => &$field) { + $field = $this->validateSingleField($field); + } + } + } else { + foreach ($fields as $key => &$field) { + $field = $this->validateSingleField($field); + } + } + + return $fields; + } + + public function validateSingleField(array $field): array + { + $value = $_POST[$field['name']] ?? ''; + $field['is_valid'] = true; + + if (isset($field['required']) && empty($value)) { + $field['is_valid'] = false; + } + + if (isset($field['validation']['pattern']) && preg_match_all('/' . $field['validation']['pattern'] . '/', $value) === 0) { + $field['is_valid'] = false; + } + + $validationFunctionName = 'validate_' . basename($this->formPath) . '_' . $field['name']; + if (function_exists($validationFunctionName)) { + $field = call_user_func($validationFunctionName, $field, $value); + } + + return $field; + } +} -- cgit v1.2.3