[], ]; $contentRoot = $_ENV['app']['contentFolderPath']; $method = $request->getMethod(); $path = $request->getPathInfo(); try { $config = $this->buildConfig($contentRoot . $path); // check api key $apiKey = $_GET['key'] ?? $_POST['key'] ?? null; if (empty($apiKey)) { $response->setStatusCode(Response::HTTP_BAD_REQUEST); throw new \Exception('API key missing'); } if (! in_array($apiKey, $config['api']['keys'])) { $response->setStatusCode(Response::HTTP_UNAUTHORIZED); throw new \Exception('API key does not match'); } // GET if ($method == 'GET') { if (str_ends_with($path, '/fields')) { $this->formPath = $formPath = $contentRoot . str_replace('/fields', '', $path); $fields = $this->buildFields($formPath, $_GET['page'] ?? null); // flatten paged form if ($this->isPagedFieldSet($fields) && isset($_GET['flat'])) { $fields = array_merge(...array_values($fields)); } $content['data'] = $fields; } else if (str_ends_with($path, '/entries')) { if (! isset($_GET['dateFrom'])) { $response->setStatusCode(Response::HTTP_BAD_REQUEST); throw new \Exception('dateFrom parameter missing'); } $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; } } } } } $content['data'] = $entries; } else { $content['data'] = Toml::parseFile($contentRoot . $path . '.toml'); } } // POST else if ($method == 'POST') { if (str_ends_with($path, '/validate')) { $this->formPath = $formPath = $contentRoot . str_replace('/validate', '', $path); $fields = $this->buildFields($formPath, $_GET['page'] ?? null); $content = $this->validateRequest($fields); } 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() ); } } } } catch (\Exception $exception) { $content['error'] = basename(get_class($exception)) . ': ' . $exception->getMessage(); } $response->headers->set('Content-Type', 'application/json'); $response->headers->set('Access-Control-Allow-Origin', implode(',', $config['api']['cors']['origins'])); $response->setContent(json_encode($content)); $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) { $config = []; $currentDirectory = $formPath; while (true) { $configFile = $currentDirectory . '/config/config.toml'; if (file_exists($configFile)) { $parsedConfig = Toml::parseFile($configFile); $apiKeys = array_merge($parsedConfig['api']['keys'] ?? [], $config['api']['keys'] ?? []); $config = array_replace_recursive($parsedConfig, $config); $config['api']['keys'] = $apiKeys; } // include custom functions $functionsFile = $currentDirectory . '/config/functions.php'; if (file_exists($functionsFile)) { include_once $functionsFile; } if (str_ends_with($currentDirectory, '/' . basename($_ENV['app']['contentFolderPath'])) || $currentDirectory == '/') { break; } $currentDirectory = dirname($currentDirectory); } 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; } }