diff --git a/CHANGELOG.md b/CHANGELOG.md index 567535826..9c8f89b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ [#512](https://github.com/nextcloud/cookbook/pull/512/) @seyfeb - Speed up index of recipes by using computed properties [#513](https://github.com/nextcloud/cookbook/pull/513) @christianlupus +- Central parsing of parameters for POST/PUT requests to simplify development + [#518](https://github.com/nextcloud/cookbook/pull/518) @christianlupus ### Fixed - Fixed keywords of shared recipes counted multiple times, fixes #491 diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index 081bb48b0..1a4ca9dab 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -10,6 +10,7 @@ use OCA\Cookbook\Service\RecipeService; use OCP\IURLGenerator; use OCA\Cookbook\Service\DbCacheService; +use OCA\Cookbook\Helper\RestParameterParser; class ConfigController extends Controller { /** @@ -25,13 +26,19 @@ class ConfigController extends Controller { * @var DbCacheService */ private $dbCacheService; + + /** + * @var RestParameterParser + */ + private $restParser; - public function __construct($AppName, IRequest $request, IURLGenerator $urlGenerator, RecipeService $recipeService, DbCacheService $dbCacheService) { + public function __construct($AppName, IRequest $request, IURLGenerator $urlGenerator, RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser) { parent::__construct($AppName, $request); $this->service = $recipeService; $this->urlGenerator = $urlGenerator; $this->dbCacheService = $dbCacheService; + $this->restParser = $restParser; } /** @@ -55,17 +62,19 @@ public function list() { public function config() { $this->dbCacheService->triggerCheck(); - if (isset($_POST['folder'])) { - $this->service->setUserFolderPath($_POST['folder']); + $data = $this->restParser->getParameters(); + + if (isset($data['folder'])) { + $this->service->setUserFolderPath($data['folder']); $this->dbCacheService->updateCache(); } - if (isset($_POST['update_interval'])) { - $this->service->setSearchIndexUpdateInterval($_POST['update_interval']); + if (isset($data['update_interval'])) { + $this->service->setSearchIndexUpdateInterval($data['update_interval']); } - if (isset($_POST['print_image'])) { - $this->service->setPrintImage((bool)$_POST['print_image']); + if (isset($data['print_image'])) { + $this->service->setPrintImage((bool)$data['print_image']); } return new DataResponse('OK', Http::STATUS_OK); diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index 3afb45842..f28eab6b6 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -10,6 +10,7 @@ use OCP\AppFramework\Controller; use OCA\Cookbook\Service\RecipeService; use OCA\Cookbook\Service\DbCacheService; +use OCA\Cookbook\Helper\RestParameterParser; class MainController extends Controller { protected $appName; @@ -26,14 +27,20 @@ class MainController extends Controller { * @var IURLGenerator */ private $urlGenerator; + + /** + * @var RestParameterParser + */ + private $restParser; - public function __construct(string $AppName, IRequest $request, RecipeService $recipeService, DbCacheService $dbCacheService, IURLGenerator $urlGenerator) { + public function __construct(string $AppName, IRequest $request, RecipeService $recipeService, DbCacheService $dbCacheService, IURLGenerator $urlGenerator, RestParameterParser $restParser) { parent::__construct($AppName, $request); $this->service = $recipeService; $this->urlGenerator = $urlGenerator; $this->appName = $AppName; $this->dbCacheService = $dbCacheService; + $this->restParser = $restParser; } /** @@ -300,12 +307,14 @@ public function create() { public function import() { $this->dbCacheService->triggerCheck(); - if (!isset($_POST['url'])) { + $data = $this->restParser->getParameters(); + + if (!isset($data['url'])) { return new DataResponse('Field "url" is required', 400); } try { - $recipe_file = $this->service->downloadRecipe($_POST['url']); + $recipe_file = $this->service->downloadRecipe($data['url']); $recipe_json = $this->service->parseRecipeFile($recipe_file); $this->dbCacheService->addRecipe($recipe_file); @@ -323,7 +332,7 @@ public function new() { $this->dbCacheService->triggerCheck(); try { - $recipe_data = $_POST; + $recipe_data = $this->restParser->getParameters(); $file = $this->service->addRecipe($recipe_data); $this->dbCacheService->addRecipe($file); @@ -370,9 +379,7 @@ public function update($id) { $this->dbCacheService->triggerCheck(); try { - $recipe_data = []; - - parse_str(file_get_contents("php://input"), $recipe_data); + $recipe_data = $this->restParser->getParameters(); $recipe_data['id'] = $id; diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index a8eabbdc0..a6ea7aa8e 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -12,6 +12,7 @@ use OCA\Cookbook\Service\RecipeService; use OCP\IURLGenerator; use OCA\Cookbook\Service\DbCacheService; +use OCA\Cookbook\Helper\RestParameterParser; class RecipeController extends Controller { /** @@ -27,13 +28,19 @@ class RecipeController extends Controller { * @var DbCacheService */ private $dbCacheService; + + /** + * @var RestParameterParser + */ + private $restParser; - public function __construct($AppName, IRequest $request, IURLGenerator $urlGenerator, RecipeService $recipeService, DbCacheService $dbCacheService) { + public function __construct($AppName, IRequest $request, IURLGenerator $urlGenerator, RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser) { parent::__construct($AppName, $request); $this->service = $recipeService; $this->urlGenerator = $urlGenerator; $this->dbCacheService = $dbCacheService; + $this->restParser = $restParser; } /** @@ -89,8 +96,7 @@ public function show($id) { public function update($id) { $this->dbCacheService->triggerCheck(); - $recipeData = []; - parse_str(file_get_contents("php://input"), $recipeData); + $recipeData = $this->restParser->getParameters(); $file = $this->service->addRecipe($recipeData); $this->dbCacheService->addRecipe($file); @@ -110,7 +116,7 @@ public function update($id) { public function create() { $this->dbCacheService->triggerCheck(); - $recipeData = $_POST; + $recipeData = $this->restParser->getParameters(); $file = $this->service->addRecipe($recipeData); $this->dbCacheService->addRecipe($file); diff --git a/lib/Helper/RestParameterParser.php b/lib/Helper/RestParameterParser.php new file mode 100644 index 000000000..2b11a6b32 --- /dev/null +++ b/lib/Helper/RestParameterParser.php @@ -0,0 +1,149 @@ +l = $l10n; + } + + /** + * Fetch the parameters from the input accordingly + * + * Depending on the way of transmitting parameters from the browser to the PHP script different ways to recover these have to be done. + * This is a helper that should cover this. + * + * @return array The parameters transmitted + */ + public function getParameters(): array { + if (isset($_SERVER[self::CONTENT_TYPE])) { + $parts = explode(';', $_SERVER[self::CONTENT_TYPE], 2); + + switch (trim($parts[0])) { + case 'application/json': + $enc = $this->getEncoding($_SERVER[self::CONTENT_TYPE]); + return $this->parseApplicationJSON($enc); + break; + + case 'multipart/form-data': + if ($this->isPost()) { + return $_POST; + } else { + throw new \Exception($this->l->t('Cannot parse non-POST multipart encoding. This is a bug.')); + } + break; + + case 'application/x-www-form-urlencoded': + if ($this->isPost()) { + return $_POST; + } else { + $enc = $this->getEncoding($_SERVER[self::CONTENT_TYPE]); + return $this->parseUrlEncoded($enc); + } + break; + } + } else { + throw new \Exception($this->l->t('Cannot detect type of transmitted data. This is a bug, please report it.')); + } + } + + /** + * Parse data transmitted as application/json + * @param $encoding string The encoding to use + * @return array + */ + private function parseApplicationJSON(string $encoding): array { + $rawData = file_get_contents('php://input'); + + if ($encoding !== 'UTF-8') { + $rawData = iconv($encoding, 'UTF-8', $rawData); + } + return json_decode($rawData, true); + } + + /** + * Parse the URL encoded value transmitted + * + * This is by far no ideal solution but just a quick hack in order to cope with this situation. + * Either use the POST method (where PHP does the parsing) or use application/json + * + * @param string $encoding The encoding to use + * @throws \Exception If the requested string is not well-formatted accoring to the minimal implementation + * @return array The values transmitted + */ + private function parseUrlEncoded(string $encoding): array { + $rawData = file_get_contents('php://input'); + if ($encoding !== 'UTF-8') { + $rawData = iconv($encoding, 'UTF-8', $rawData); + } + + $ret = []; + foreach (explode('&', $rawData) as $assignment) { + $parts = explode('=', $assignment, 2); + + if (count($parts) < 2) { + throw new \Exception($this->l->t('Invlaid URL-encoded string found. Please report a bug.')); + } + + $key = $parts[0]; + $value = urldecode($parts[1]); + + if (str_ends_with($key, '[]')) { + // Drop '[]' at the end + $key = substr($key, 0, -2); + + if (!array_key_exists($key, $ret)) { + $ret[$key] = []; + } + + $ret[$key][] = $value; + } else { + $ret[$key] = $value; + } + } + + return $ret; + } + + /** + * Get the encoding from the header + * @param string $header The header to parse + * @return string The encoding as string + */ + private function getEncoding(string $header): string { + $parts = explode(';', $header); + + // Fallback encoding + $enc = 'UTF-8'; + + for ($i = 1; $i < count($parts); $i++) { + if (str_starts_with(trim($parts[$i]), self::CHARSET)) { + // Drop string 'charset=' + $enc = substr(trim($parts[1]), strlen(self::CHARSET) + 1); + break; + } + } + + return $enc; + } + + /** + * Check if the request is a POST request + * @return bool true, if the request is a POST request. + */ + private function isPost(): bool { + return $_SERVER[self::REQUEST_METHOD] === 'POST'; + } +}