From 77a95be2a01fdd2aa9a95e4853387c35f2df55a6 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Wed, 7 Aug 2019 20:13:48 -0400 Subject: [PATCH] Release 2.4.0 (#1182) * Parent + Nested validation changes (#1138) * Issue Fix #1109 (#1146) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Issue Fix #1109 * Add Special characters in the radom string generator * Issue Fix #1109 * Remove other option * Imagick changes * Update logo without page refresh (#1162) * Resolve the error of validation of primary key (#1166) * Don't use special characters in creation of project auth tokens (#1169) * Comment PHP variables in htaccess. (#1145) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Remove warning of undefined index size (#1140) * Add thumbnails for PDF. (#1141) * Remove PHP vaeiable from htaccess. * Update .htaccess * Update public/.htaccess Co-Authored-By: Rijk van Zanten * Issue Fix #1161 (#1165) * Issue fix #1161 * Check for O2M * Add option to prevent upsizing contained thumbnails. (#1168) * Issue Fix #1170 (#1175) * Release 2.3.1 (#1158) * Parent + Nested validation changes (#1138) * #1121 (#1126) * Public Role : UserId should be set 0 instead of null * Change : invalid token * Issue Fix #1109 (#1146) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Issue Fix #1109 * Add Special characters in the radom string generator * Issue Fix #1109 * Remove other option * Imagick changes * Issue Fix #1148 (#1152) * Fix 1149 (#1156) * Process relation & non relatinal fields sequentially to solve logical operator issue * Process relation & non relatinal fields sequentially to solve logical operator issue * Fixed namespace of InvalidLoggerConfigurationException (#1153) * Bump version to v2.3.1 * Merge conflict resolve * Issue Fix #1170 * Rebase files * Issue fix/1176 (#1177) * added array type to schema (#1163) * Parent + Nested validation changes (#1138) * #1121 (#1126) * Public Role : UserId should be set 0 instead of null * Change : invalid token * Issue Fix #1109 (#1146) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Issue Fix #1109 * Add Special characters in the radom string generator * Issue Fix #1109 * Remove other option * Imagick changes * Issue Fix #1148 (#1152) * Fix 1149 (#1156) * Process relation & non relatinal fields sequentially to solve logical operator issue * Process relation & non relatinal fields sequentially to solve logical operator issue * Fixed namespace of InvalidLoggerConfigurationException (#1153) * added array type to schema * Update logo without page refresh (#1162) * skipped schema validation for config from api.php * Resolve the error of validation of primary key (#1166) * Don't use special characters in creation of project auth tokens (#1169) * Comment PHP variables in htaccess. (#1145) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Remove warning of undefined index size (#1140) * Add thumbnails for PDF. (#1141) * Remove PHP vaeiable from htaccess. * Update .htaccess * Update public/.htaccess Co-Authored-By: Rijk van Zanten * Issue Fix #1161 (#1165) * Issue fix #1161 * Check for O2M * Add option to prevent upsizing contained thumbnails. (#1168) * Issue Fix #1170 (#1175) * Release 2.3.1 (#1158) * Parent + Nested validation changes (#1138) * #1121 (#1126) * Public Role : UserId should be set 0 instead of null * Change : invalid token * Issue Fix #1109 (#1146) * Added file support for 7.0 (Explained) (#1124) * Bump version to 2.3.0 (#1120) * Added file support for 7.0 (Explained) Having `public` in front of `const` completely breaks the application for `PHP 7.0` usage, which broke everything when I pulled origin. Though I understand `PHP 7.0` isn't officially supported, and that `PHP 7.1+` is, there is no reason to use public alongside const as the default visibility of class constants are public. We might as well provide support where possible if it doesn't hurt. Explained here: https://stackoverflow.com/a/51568547 * Issue Fix #1114 (#1128) * Issue Fix #1114 * Change exception message * Update .gitignore (#1129) * Bump version to 2.3.0 (#1120) * Update .gitignore * Update .gitignore * Update .gitignore * Update .gitignore * Issue Fix #1125 (#1134) * Issue Fix #1131 (#1135) * create thumb for pdf if imagick is available (#1123) * Bump version to 2.3.0 (#1120) * create thumb for pdf if imagick is available * Issue Fix #1109 * Add Special characters in the radom string generator * Issue Fix #1109 * Remove other option * Imagick changes * Issue Fix #1148 (#1152) * Fix 1149 (#1156) * Process relation & non relatinal fields sequentially to solve logical operator issue * Process relation & non relatinal fields sequentially to solve logical operator issue * Fixed namespace of InvalidLoggerConfigurationException (#1153) * Bump version to v2.3.1 * Merge conflict resolve * Issue Fix #1170 * Rebase files * added new config key "ext" for all external config with type ARRAY. added tests. * made "ext" key optional * Issue fix/1176 (#1177) * Issue fix #1179 (#1181) * Release v2.4.0 --- package.json | 2 +- public/.htaccess | 9 +- src/core/Directus/Application/Application.php | 2 +- src/core/Directus/Config/Schema/Base.php | 13 ++ src/core/Directus/Config/Schema/Group.php | 4 +- src/core/Directus/Config/Schema/Schema.php | 15 +- src/core/Directus/Config/Schema/Types.php | 1 + src/core/Directus/Config/Schema/Value.php | 31 ++-- src/core/Directus/Filesystem/Files.php | 5 +- src/core/Directus/Filesystem/Thumbnailer.php | 5 +- src/core/Directus/Services/ItemsService.php | 145 +++++++----------- src/core/Directus/Services/ProjectService.php | 1 + src/core/Directus/Services/TablesService.php | 17 +- .../Util/Installation/InstallerUtils.php | 5 +- .../Util/Installation/stubs/config.stub | 2 +- src/core/Directus/Util/StringUtils.php | 10 +- src/endpoints/Settings.php | 41 ++++- src/helpers/app.php | 4 +- src/helpers/file.php | 51 +++--- tests/api/Config/ContextTest.php | 3 + tests/api/Config/SchemaTest.php | 1 + tests/api/Config/sources/source.json | 7 +- tests/api/Config/sources/source.php | 3 + 23 files changed, 206 insertions(+), 171 deletions(-) diff --git a/package.json b/package.json index be08d776b6..8cb52febdf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@directus/api", "private": true, - "version": "2.3.1", + "version": "2.4.0", "description": "Directus API", "main": "index.js", "repository": "directus/api", diff --git a/public/.htaccess b/public/.htaccess index e02f624c38..939244f897 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -20,7 +20,8 @@ Options +SymLinksIfOwnerMatch RewriteRule !^admin index.php?%{QUERY_STRING} [L] - - php_value upload_max_filesize 50M - php_value post_max_size 100M - \ No newline at end of file +# Uncomment the following lines to modify PHP settings. The lines below can be used to increase the max upload size of files in Directus +# + #php_value upload_max_filesize 50M + #php_value post_max_size 100M +# diff --git a/src/core/Directus/Application/Application.php b/src/core/Directus/Application/Application.php index cd26de8a0c..0b9d1c7149 100644 --- a/src/core/Directus/Application/Application.php +++ b/src/core/Directus/Application/Application.php @@ -13,7 +13,7 @@ class Application extends App * * @var string */ - const DIRECTUS_VERSION = '2.3.1'; + const DIRECTUS_VERSION = '2.4.0'; /** * NOT USED diff --git a/src/core/Directus/Config/Schema/Base.php b/src/core/Directus/Config/Schema/Base.php index 64fd0a7bdc..5fe3dc252f 100644 --- a/src/core/Directus/Config/Schema/Base.php +++ b/src/core/Directus/Config/Schema/Base.php @@ -113,4 +113,17 @@ public function optional($value = null) } return $this->_optional; } + + /** + * Returns the $context with normalized array keys. + * @param $context + * @return mixed + */ + protected function normalize($context) { + foreach ($context as $context_key => $context_value) { + $context[strtolower(str_replace("-", "", str_replace("_", "", $context_key)))] = $context_value; + } + + return $context; + } } diff --git a/src/core/Directus/Config/Schema/Group.php b/src/core/Directus/Config/Schema/Group.php index 463c8b85ce..a7112c1f37 100644 --- a/src/core/Directus/Config/Schema/Group.php +++ b/src/core/Directus/Config/Schema/Group.php @@ -25,9 +25,7 @@ public function value($context) $value = []; $current = []; - foreach ($context as $context_key => $context_value) { - $context[strtolower(str_replace("-", "", str_replace("_", "", $context_key)))] = $context_value; - } + $context = $this->normalize($context); if (!isset($context[$this->key()])) { if ($this->optional()) { diff --git a/src/core/Directus/Config/Schema/Schema.php b/src/core/Directus/Config/Schema/Schema.php index a49c977b57..f26566f75e 100644 --- a/src/core/Directus/Config/Schema/Schema.php +++ b/src/core/Directus/Config/Schema/Schema.php @@ -77,8 +77,8 @@ public static function get() { ]), new Group('cors', [ new Value('enabled', Types::BOOLEAN, true), - new Value('origin', 'array', ['*']), - new Value('methods', 'array', [ + new Value('origin', Types::ARRAY, ['*']), + new Value('methods', Types::ARRAY, [ 'GET', 'POST', 'PUT', @@ -86,8 +86,8 @@ public static function get() { 'DELETE', 'HEAD' ]), - new Value('headers', 'array', []), - new Value('exposed_headers', 'array', []), + new Value('headers', Types::ARRAY, []), + new Value('exposed_headers', Types::ARRAY, []), new Value('max_age', Types::INTEGER, null), new Value('credentials', Types::BOOLEAN, false), ]), @@ -101,14 +101,14 @@ public static function get() { new Value('timeout', Types::INTEGER, 10), ]), new Group('hooks', [ - new Value('actions', 'array', []), - new Value('filters', 'array', []), + new Value('actions', Types::ARRAY, []), + new Value('filters', Types::ARRAY, []), ]), new Group('feedback', [ new Value('token', Types::STRING, 'a-kind-of-unique-token'), new Value('login', Types::STRING, true), ]), - new Value('tableBlacklist', 'array', []), + new Value('tableBlacklist', Types::ARRAY, []), new Group('auth', [ new Value('secret_key', Types::STRING, ''), new Value('public_key', Types::STRING, ''), @@ -139,6 +139,7 @@ public static function get() { ]), ]), ]), + new Value('ext?', Types::ARRAY, []), ]); } } diff --git a/src/core/Directus/Config/Schema/Types.php b/src/core/Directus/Config/Schema/Types.php index 7e21ac34d1..982a6f16fc 100644 --- a/src/core/Directus/Config/Schema/Types.php +++ b/src/core/Directus/Config/Schema/Types.php @@ -11,4 +11,5 @@ interface Types const FLOAT = 'float'; const STRING = 'string'; const BOOLEAN = 'boolean'; + const ARRAY = 'array'; } diff --git a/src/core/Directus/Config/Schema/Value.php b/src/core/Directus/Config/Schema/Value.php index e15c09a1f8..e00eef8ca0 100644 --- a/src/core/Directus/Config/Schema/Value.php +++ b/src/core/Directus/Config/Schema/Value.php @@ -35,9 +35,7 @@ public function __construct($name, $type, $default = null) */ public function value($context) { - foreach ($context as $context_key => $context_value) { - $context[strtolower(str_replace("-", "", str_replace("_", "", $context_key)))] = $context_value; - } + $context = $this->normalize($context); if (!isset($context) || !isset($context[$this->key()])) { if ($this->optional()) { @@ -50,19 +48,20 @@ public function value($context) $value = $context[$this->key()]; switch ($this->_type) { - case Types::INTEGER: - return intval($value); - case Types::BOOLEAN: - $value = strtolower($value); - return $value === "true" || $value === "1" || $value === "on" || $value === "yes" || boolval($value); - case Types::FLOAT: - return floatval($value); - // TODO: add support to arrays - case 'array': - return $this->_default; - case Types::STRING: - default: - return $value; + case Types::INTEGER: + return intval($value); + case Types::BOOLEAN: + $value = strtolower($value); + return $value === "true" || $value === "1" || $value === "on" || $value === "yes" || boolval($value); + case Types::FLOAT: + return floatval($value); + case Types::ARRAY: + if (!is_array($value)) { + return $this->_default; + } + case Types::STRING: + default: + return $value; } } } diff --git a/src/core/Directus/Filesystem/Files.php b/src/core/Directus/Filesystem/Files.php index 5778d066da..f7a5fdc20b 100644 --- a/src/core/Directus/Filesystem/Files.php +++ b/src/core/Directus/Filesystem/Files.php @@ -259,6 +259,9 @@ public function saveData($fileData, $fileName, $replace = false) // When file is uploaded via multipart form data then We will get object of Slim\Http\UploadFile // When file is uploaded via URL (Youtube, Vimeo, or image link) then we will get base64 encode string. $size = null; + + $title = $fileName; + if (is_object($fileData)) { $size = $fileData->getSize(); $checksum = hash_file('md5', $fileData->file); @@ -281,7 +284,7 @@ public function saveData($fileData, $fileName, $replace = false) unset($fileData); $fileData = $this->getFileInfo($fileName); - $fileData['title'] = Formatting::fileNameToFileTitle($fileName); + $fileData['title'] = Formatting::fileNameToFileTitle($title); $fileData['filename'] = basename($filePath); $fileData['storage'] = $this->config['adapter']; diff --git a/src/core/Directus/Filesystem/Thumbnailer.php b/src/core/Directus/Filesystem/Thumbnailer.php index 94ee68d7d0..e30f187429 100644 --- a/src/core/Directus/Filesystem/Thumbnailer.php +++ b/src/core/Directus/Filesystem/Thumbnailer.php @@ -179,9 +179,12 @@ public function contain() // crop image $img->resize($this->width, $this->height, function ($constraint) { $constraint->aspectRatio(); + if (ArrayUtils::get($options, 'preventUpsize')) { + $constraint->upsize(); + } }); - if( ArrayUtils::get($options, 'resizeCanvas')) { + if (ArrayUtils::get($options, 'resizeCanvas')) { $img->resizeCanvas($this->width, $this->height, ArrayUtils::get($options, 'position', 'center'), ArrayUtils::get($options, 'resizeRelative', false), ArrayUtils::get($options, 'canvasBackground', [255, 255, 255, 0])); } diff --git a/src/core/Directus/Services/ItemsService.php b/src/core/Directus/Services/ItemsService.php index fcc4396b89..d85535fa1e 100644 --- a/src/core/Directus/Services/ItemsService.php +++ b/src/core/Directus/Services/ItemsService.php @@ -30,7 +30,7 @@ public function createItem($collection, $payload, $params = []) { $this->enforceCreatePermissions($collection, $payload, $params); $this->validatePayload($collection, null, $payload, $params); - + // Validate Password if password policy settled in the system settings. if($collection == SchemaManager::COLLECTION_USERS){ $passwordValidation = get_directus_setting('password_policy'); @@ -42,39 +42,15 @@ public function createItem($collection, $payload, $params = []) //Validate nested payload $tableSchema = SchemaService::getCollection($collection); $collectionAliasColumns = $tableSchema->getAliasFields(); - + foreach ($collectionAliasColumns as $aliasColumnDetails) { - $colName = $aliasColumnDetails->getName(); - - $relationalCollectionName = ""; - if($this->isManyToManyField($aliasColumnDetails)){ - $relationalCollectionName = $aliasColumnDetails->getRelationship()->getCollectionManyToMany(); - - if($relationalCollectionName && isset($payload[$colName])){ - foreach($payload[$colName] as $individual){ - if(!isset($individual['$delete'])){ - $validatePayload = $individual[$aliasColumnDetails->getRelationship()->getJunctionOtherRelatedField()]; - $this->validatePayload($relationalCollectionName, null, $validatePayload,$params); - } - } - } + + $this->validateManyToManyCollection($payload, $params, $aliasColumnDetails); }else{ - if($aliasColumnDetails->isOneToMany()){ - $relationalCollectionName = $aliasColumnDetails->getRelationship()->getCollectionMany(); - }else if($aliasColumnDetails->isManyToOne()){ - $relationalCollectionName = $aliasColumnDetails->getRelationship()->getCollectionOne(); - } - if($relationalCollectionName && isset($payload[$colName])){ - foreach($payload[$colName] as $individual){ - if(!isset($individual['$delete'])){ - $this->validatePayload($relationalCollectionName, null, $individual,$params,$collection); - } - } - } - } + $this->validateAliasCollection($payload, $params, $aliasColumnDetails, []); + } } - $tableGateway = $this->createTableGateway($collection); $newRecord = $tableGateway->createRecord($payload, $this->getCRUDParams($params)); @@ -185,63 +161,59 @@ public function findOne($collection, array $params = []) 'single' => true ])); } - + /** * Validate Parent Collection Fields */ public function validateParentCollectionFields($collection, $payload, $params, $recordData){ $tableColumns = SchemaService::getAllCollectionFields($collection); $collectionFields = $payload; - + foreach($tableColumns as $key => $column){ - if(!empty($recordData) && !$column->hasPrimaryKey()){ - $columnName = $column->getName(); + if(!empty($recordData)){ + + $columnName = $column->getName(); + $collectionFields[$columnName] = array_key_exists($column->getName(), $collectionFields) ? $collectionFields[$column->getName()]: (DataTypes::isJson($column->getType()) ? (array) $recordData[$columnName] : $recordData[$columnName]); } } - + $this->validatePayload($collection, null, $collectionFields, $params); } - + /** * Validate Many To Many Collection Fields */ - public function validateManyToManyCollection($payload, $params, $aliasColumnDetails, $recordData){ + public function validateManyToManyCollection($payload, $params, $aliasColumnDetails){ $colName = $aliasColumnDetails->getName(); $relationalCollectionName = $aliasColumnDetails->getRelationship()->getCollectionManyToMany(); if($relationalCollectionName && isset($payload[$colName])){ $relationalCollectionPrimaryKey = SchemaService::getCollectionPrimaryKey($relationalCollectionName); $relationalCollectionColumns = SchemaService::getAllCollectionFields($relationalCollectionName); - foreach($payload[$colName] as $individual){ - if(!isset($individual['$delete'])){ + foreach($payload[$colName] as $individual){ + if(!isset($individual['$delete'])){ $aliasField = $aliasColumnDetails->getRelationship()->getJunctionOtherRelatedField(); $validatePayload = $individual[$aliasField]; - $storedData = (!empty($recordData) && isset($recordData[$colName])) ? $recordData[$colName] : [] ; - + + foreach($relationalCollectionColumns as $column){ - if(!empty($recordData) && !$column->isAlias() && !$column->hasPrimaryKey() && isset($recordData[$colName])){ - $search = array_search($individual[$relationalCollectionPrimaryKey], array_column($storedData, $relationalCollectionPrimaryKey)); + if(!$column->isAlias() && !$column->hasPrimaryKey() && !empty($validatePayload[$relationalCollectionPrimaryKey])){ $columnName = $column->getName(); - if($search !== false){ - $dbObj = isset($storedData[$search][$aliasField]) ? $storedData[$search][$aliasField] : []; - $validatePayload[$columnName] = array_key_exists($columnName, $validatePayload) ? $validatePayload[$columnName]: (isset($dbObj[$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $dbObj[$columnName] : $dbObj[$columnName])) : null); - }else{ - $relationalCollectionData = $this->findByIds( - $relationalCollectionName, - $validatePayload[$relationalCollectionPrimaryKey], - $params - ); - $validatePayload[$columnName] = array_key_exists($columnName, $validatePayload) ? $validatePayload[$columnName]: (isset($relationalCollectionData['data'][$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $relationalCollectionData['data'][$columnName] : $relationalCollectionData['data'][$columnName])) : null); - } + $relationalCollectionData = $this->findByIds( + $relationalCollectionName, + $validatePayload[$relationalCollectionPrimaryKey], + $params + ); + $validatePayload[$columnName] = array_key_exists($columnName, $validatePayload) ? $validatePayload[$columnName]: (isset($relationalCollectionData['data'][$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $relationalCollectionData['data'][$columnName] : $relationalCollectionData['data'][$columnName])) : null); } } $this->validatePayload($relationalCollectionName, null, $validatePayload,$params); } } - } + } } - + /** * Validate Alias Collection Fields (O2M and M2O - Including Translations and Files) */ @@ -256,33 +228,30 @@ public function validateAliasCollection($payload, $params, $aliasColumnDetails, $parentCollectionName = $aliasColumnDetails->getRelationship()->getCollectionMany(); } if($relationalCollectionName && isset($payload[$colName])){ - + $relationalCollectionPrimaryKey = SchemaService::getCollectionPrimaryKey($relationalCollectionName); $parentCollectionPrimaryKey = SchemaService::getCollectionPrimaryKey($parentCollectionName); $relationalCollectionColumns = SchemaService::getAllCollectionFields($relationalCollectionName); $foreignJoinColumn = $aliasColumnDetails->getRelationship()->getFieldMany(); - - foreach($payload[$colName] as $individual){ - if(!isset($individual['$delete'])){ + + foreach($payload[$colName] as $individual){ + + if(!isset($individual['$delete'])){ foreach($relationalCollectionColumns as $key => $column){ - if(!empty($recordData) && !$column->isAlias() && !$column->hasPrimaryKey() && isset($recordData[$colName])){ - $search = array_search($individual[$relationalCollectionPrimaryKey], array_column($recordData[$colName], $relationalCollectionPrimaryKey)); + + if(!$column->isAlias() && !$column->hasPrimaryKey() && !empty($individual[$relationalCollectionPrimaryKey])){ $columnName = $column->getName(); - if($search !== false){ - $individual[$columnName] = array_key_exists($columnName, $individual) ? $individual[$columnName]: (isset($recordData[$colName][$search][$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $recordData[$colName][$search][$columnName] : $recordData[$colName][$search][$columnName])) : null); - }else{ - $relationalCollectionData = $this->findByIds( - $relationalCollectionName, - $individual[$relationalCollectionPrimaryKey], - $params - ); - $individual[$columnName] = array_key_exists($columnName, $individual) ? $individual[$columnName]: (isset($relationalCollectionData['data'][$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $relationalCollectionData['data'][$columnName] : $relationalCollectionData['data'][$columnName])) : null); - } + $relationalCollectionData = $this->findByIds( + $relationalCollectionName, + $individual[$relationalCollectionPrimaryKey], + $params + ); + $individual[$columnName] = array_key_exists($columnName, $individual) ? $individual[$columnName]: (isset($relationalCollectionData['data'][$columnName]) ? ((DataTypes::isJson($column->getType()) ? (array) $relationalCollectionData['data'][$columnName] : $relationalCollectionData['data'][$columnName])) : null); } } // only add parent id's to items that are lacking the parent column - if (empty($individual[$foreignJoinColumn])) { + if (empty($individual[$foreignJoinColumn]) && !empty($recordData[$parentCollectionPrimaryKey])) { $individual[$foreignJoinColumn] = $recordData[$parentCollectionPrimaryKey]; } $this->validatePayload($relationalCollectionName, null, $individual,$params); @@ -303,10 +272,10 @@ public function checkRelationalItemDeletable($collection, $payload, $recordData) // Check if field is O2M and required if($column->hasRelationship() && $column->isOneToMany() && ($column->isRequired() || (!$column->isNullable() && $column->getDefaultValue() == null))){ if(!empty($recordData)){ - $columnName = $column->getName(); - + $columnName = $column->getName(); + if(isset($recordData[$columnName]) && isset($payload[$columnName]) && count($recordData[$columnName]) == count($payload[$columnName]) ){ - $fieldMany = $column->getRelationship()->getFieldMany(); + $fieldMany = $column->getRelationship()->getFieldMany(); $collectionMany = SchemaService::getCollection($column->getRelationship()->getCollectionMany()); $primaryKeyCollectionMany = $collectionMany->getPrimaryKeyName(); $alreadyStoredEntries = array_column($recordData[$columnName],$primaryKeyCollectionMany); @@ -325,7 +294,7 @@ public function checkRelationalItemDeletable($collection, $payload, $recordData) } } } - + /** * Updates a single item in the given collection and id * @@ -344,22 +313,22 @@ public function update($collection, $id, $payload, array $params = []) $dbData = $this->findByIds($collection,$id,['fields' => '*.*.*']); $recordData = !empty($dbData['data']) ? $dbData['data'] : []; $this->validateParentCollectionFields($collection, $payload, $params, $recordData); - + //Validate alias field payload $tableSchema = SchemaService::getCollection($collection); $collectionAliasColumns = $tableSchema->getAliasFields(); - + foreach ($collectionAliasColumns as $aliasColumnDetails) { - if($this->isManyToManyField($aliasColumnDetails)){ - $this->validateManyToManyCollection($payload, $params, $aliasColumnDetails, $recordData); + if($this->isManyToManyField($aliasColumnDetails)){ + $this->validateManyToManyCollection($payload, $params, $aliasColumnDetails); }else{ - $this->validateAliasCollection($payload, $params, $aliasColumnDetails, $recordData); - } + $this->validateAliasCollection($payload, $params, $aliasColumnDetails, $recordData); + } } // There is a scenario in which user tries to delete all the relational data although it is required. This can be possible for o2M only and API have to restrict that. - $this->checkRelationalItemDeletable($collection, $payload, $recordData); - + $this->checkRelationalItemDeletable($collection, $payload, $recordData); + $this->checkItemExists($collection, $id); $tableGateway = $this->createTableGateway($collection); @@ -561,10 +530,10 @@ protected function getStatusValue($collection, $id) return $row[$collectionObject->getStatusField()->getName()]; } - + /** * Checks whether the relationship is MANY TO MANY - * + * * @param $fieldMany * @param $collectionMany * @@ -578,7 +547,7 @@ protected function isManyToManyField($field){ 'collection_many' => $relationship->getCollectionMany(), ]; $tableGateway = $this->createTableGateway(SchemaManager::COLLECTION_RELATIONS); - $junctionEntries = $tableGateway->getItems(['filter' => $junctionConditions]); + $junctionEntries = $tableGateway->getItems(['filter' => $junctionConditions]); return !empty($junctionEntries['data']) ? true : false; } return false; diff --git a/src/core/Directus/Services/ProjectService.php b/src/core/Directus/Services/ProjectService.php index 6bbac8f3ba..2097804a57 100644 --- a/src/core/Directus/Services/ProjectService.php +++ b/src/core/Directus/Services/ProjectService.php @@ -46,6 +46,7 @@ public function create(array $data) 'timezone' => 'string', 'locale' => 'string', + 'logs_path' => 'string', 'project_name' => 'string', 'app_url' => 'string', diff --git a/src/core/Directus/Services/TablesService.php b/src/core/Directus/Services/TablesService.php index d42f792e43..c6580334c8 100644 --- a/src/core/Directus/Services/TablesService.php +++ b/src/core/Directus/Services/TablesService.php @@ -294,7 +294,7 @@ public function deleteField($collection, $field, array $params = []) $hookEmitter->run('field.delete:before', [$collection, $field]); $hookEmitter->run('field.delete.' . $collection . ':before', [$field]); - $tableService->dropColumn($collection, $field); + $tableService->dropColumn($collection, $field, $params); $hookEmitter->run('field.delete', [$collection, $field]); $hookEmitter->run('field.delete.' . $collection, [$field]); @@ -730,7 +730,7 @@ public function batchUpdateFieldWithIds($collectionName, array $fieldNames, arra return $allItems; } - public function dropColumn($collectionName, $fieldName) + public function dropColumn($collectionName, $fieldName, array $params = []) { $tableObject = $this->getSchemaManager()->getCollection($collectionName); if (!$tableObject) { @@ -753,7 +753,7 @@ public function dropColumn($collectionName, $fieldName) } if ($columnObject->hasRelationship()) { - $this->removeColumnRelationship($columnObject); + $this->removeColumnRelationship($columnObject,$params); } if ($columnObject->isManaged()) { @@ -900,7 +900,7 @@ public function removeColumnInfo($collectionName, $fieldName) * * @return bool|int */ - public function removeColumnRelationship(Field $field) + public function removeColumnRelationship(Field $field, array $params = []) { if (!$field->hasRelationship()) { return false; @@ -909,7 +909,7 @@ public function removeColumnRelationship(Field $field) if ($this->shouldRemoveRelationshipRecord($field)) { $result = $this->removeRelationshipRecord($field); } else { - $result = $this->removeRelationshipFromRecord($field); + $result = $this->removeRelationshipFromRecord($field, $params); } return $result; @@ -950,7 +950,7 @@ protected function removeRelationshipRecord(Field $field) * * @return int */ - protected function removeRelationshipFromRecord(Field $field) + protected function removeRelationshipFromRecord(Field $field, array $params = []) { $tableGateway = $this->getRelationsTableGateway(); @@ -975,7 +975,10 @@ protected function removeRelationshipFromRecord(Field $field) */ if (!empty($junctionEntries['data'])) { $tableGateway->delete($junctionConditions); - $this->dropTable($relationship->getCollectionMany()); + + if(isset($params['delete_junction'])){ + $this->dropTable($relationship->getCollectionMany()); + } return $tableGateway->delete($conditions['values']); } else { diff --git a/src/core/Directus/Util/Installation/InstallerUtils.php b/src/core/Directus/Util/Installation/InstallerUtils.php index 5556eb467a..d80be3e47f 100644 --- a/src/core/Directus/Util/Installation/InstallerUtils.php +++ b/src/core/Directus/Util/Installation/InstallerUtils.php @@ -880,7 +880,7 @@ private static function dropTables($basePath, $projectName) private static function createConfigData(array $data) { $corsEnabled = ArrayUtils::get($data, 'cors_enabled', true); - $authSecret = ArrayUtils::get($data, 'auth_secret', StringUtils::randomString(32)); + $authSecret = ArrayUtils::get($data, 'auth_secret', StringUtils::randomString(32, false)); $authPublic = ArrayUtils::get($data, 'auth_public', generate_uuid4()); return ArrayUtils::defaults([ @@ -891,9 +891,10 @@ private static function createConfigData(array $data) 'db_password' => null, 'db_socket' => '', 'mail_from' => 'admin@example.com', - 'feedback_token' => sha1(gmdate('U') . StringUtils::randomString(32)), + 'feedback_token' => sha1(gmdate('U') . StringUtils::randomString(32, false)), 'feedback_login' => true, 'timezone' => get_default_timezone(), + 'logs_path' => __DIR__ . '/../../../../../logs', 'cache' => [ 'enabled' => false, 'response_ttl' => 3600, diff --git a/src/core/Directus/Util/Installation/stubs/config.stub b/src/core/Directus/Util/Installation/stubs/config.stub index 117cfc087c..6613eca69e 100644 --- a/src/core/Directus/Util/Installation/stubs/config.stub +++ b/src/core/Directus/Util/Installation/stubs/config.stub @@ -8,7 +8,7 @@ return [ 'settings' => [ 'logger' => [ - 'path' => __DIR__ . '/../logs', + 'path' => '{{logs_path}}', ], ], diff --git a/src/core/Directus/Util/StringUtils.php b/src/core/Directus/Util/StringUtils.php index bf80fe2c88..dac277322c 100644 --- a/src/core/Directus/Util/StringUtils.php +++ b/src/core/Directus/Util/StringUtils.php @@ -123,10 +123,14 @@ public static function random($length = 16) * * @return string */ - public static function randomString($length = 16) + public static function randomString($length = 16, $special_chars = true) { // TODO: Add options to allow symbols or user provided characters to extend the list - $pool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+}{';'?>.<,"; + $pool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + if ($special_chars) { + $pool .= "!@#$%^&*()_+}{;?>.<,"; + } return substr(str_shuffle(str_repeat($pool, $length)), 0, $length); } @@ -234,7 +238,7 @@ public static function replacePlaceholder($string, $data = [], $placeHolderForma } /** - * If any variable of the given string have null value as a replacement then the + * If any variable of the given string have null value as a replacement then the * result will be 'null'(string). So we need to replace it with blank string. */ $string = str_replace("'null'", "''", $string); diff --git a/src/endpoints/Settings.php b/src/endpoints/Settings.php index 6ce625c789..b2c3bc41f9 100644 --- a/src/endpoints/Settings.php +++ b/src/endpoints/Settings.php @@ -7,6 +7,7 @@ use Directus\Application\Http\Response; use Directus\Application\Route; use Directus\Services\SettingsService; +use Directus\Services\FilesServices; use function Directus\regex_numeric_ids; class Settings extends Route @@ -149,13 +150,8 @@ public function read(Request $request, Response $response) * * @return Response */ - public function getInterfaceBasedInput($request, $setting) + public function getInterfaceBasedInput($request, $setting, $fieldData) { - $service = new SettingsService($this->container); - $fieldData = $service->findAllFields( - $request->getQueryParams() - ); - $inputData = $request->getParsedBody(); foreach ($fieldData['data'] as $key => $value) { if ($value['field'] == $setting) { @@ -180,6 +176,33 @@ public function getInterfaceBasedInput($request, $setting) return $inputData; } + /** + * @param Request $request + * @param Response $response + * + * @return Response + */ + public function getInterfaceBasedOutput($setting, $fieldData) + { + $fileService = new FilesServices($this->container); + $response = $setting['value']; + foreach ($fieldData['data'] as $value) { + if ($value['field'] == $setting['key']) { + if ($setting['value'] != null) { + switch ($value['type']) { + case 'file': + $responseData = $fileService->findByIds($setting['value'],[]); + if( !empty($responseData['data']) ){ + $response = $responseData['data']; + } + break; + } + } + } + } + return $response; + } + /** * @param Request $request * @param Response $response @@ -212,7 +235,10 @@ public function update(Request $request, Response $response) * */ - $inputData = $this->getInterfaceBasedInput($request, $serviceData['data']['key']); + $fieldData = $service->findAllFields( + $request->getQueryParams() + ); + $inputData = $this->getInterfaceBasedInput($request, $serviceData['data']['key'], $fieldData); $responseData = $service->update( $request->getAttribute('id'), $inputData, @@ -220,6 +246,7 @@ public function update(Request $request, Response $response) ); $responseData['data']['value'] = $payload['value']; + $responseData['data']['value'] = $this->getInterfaceBasedOutput($responseData['data'], $fieldData); return $this->responseWithData($request, $response, $responseData); } diff --git a/src/helpers/app.php b/src/helpers/app.php index 7c54b220b9..dea3dc3de2 100644 --- a/src/helpers/app.php +++ b/src/helpers/app.php @@ -100,9 +100,7 @@ function get_project_config($name = null, $basePath = null) if (!file_exists($configFilePath)) { throw new UnknownProjectException($name); } - $configData = $schema->value([ - "directus" => Context::from_file($configFilePath) - ]); + $configData = $schema->value(['directus' => Context::from_file($configFilePath)]); } $config = new Config($configData); diff --git a/src/helpers/file.php b/src/helpers/file.php index d36069b559..e102056f87 100644 --- a/src/helpers/file.php +++ b/src/helpers/file.php @@ -191,6 +191,7 @@ function add_default_thumbnail_dimensions(array &$list) */ function get_thumbnails(array $row) { + $filename = $row['filename']; $type = array_get($row, 'type'); $thumbnailFilenameParts = explode('.', $filename); @@ -218,6 +219,7 @@ function get_thumbnails(array $row) } $size = explode('x', $dimension); + if (count($size) == 2) { $thumbnailUrl = get_thumbnail_url($filename, $size[0], $size[1]); $thumbnailRelativeUrl = get_thumbnail_path($filename, $size[0], $size[1]); @@ -297,7 +299,8 @@ function get_proxy_path($path) // env/width/height/mode/quality/name return sprintf( '/downloads/%s/%s', - $projectName, $path + $projectName, + $path ); } } @@ -365,17 +368,16 @@ function is_a_url($value) * * @return bool */ - function validate_file($value,$constraint,$options = null) + function validate_file($value, $constraint, $options = null) { - switch ($constraint) { + switch ($constraint) { case 'mimeTypes': - validate_file_mime_type($value,$options); - break; + validate_file_mime_type($value, $options); + break; case 'maxSize': - validate_file_size($value,$options); - break; - - } + validate_file_size($value, $options); + break; + } } } @@ -387,14 +389,14 @@ function validate_file($value,$constraint,$options = null) * * @return bool */ - function validate_file_mime_type($value,$options) + function validate_file_mime_type($value, $options) { $mimeTypes = $options; $mime = $value; - - if($options == null) { - $options=get_directus_setting('file_mimetype_whitelist'); - $mimeTypes = explode(",",$options); + + if ($options == null) { + $options = get_directus_setting('file_mimetype_whitelist'); + $mimeTypes = explode(",", $options); } foreach ($mimeTypes as $mimeType) { if ($mimeType === $mime) { @@ -406,7 +408,7 @@ function validate_file_mime_type($value,$options) } } } - $message='The mime type of the file is invalid.Allowed mime types are '.$options.'.'; + $message = 'The mime type of the file is invalid.Allowed mime types are ' . $options . '.'; throw new InvalidRequestException($message); } } @@ -418,13 +420,13 @@ function validate_file_mime_type($value,$options) * * @return bool */ - function validate_file_size($value,$options) + function validate_file_size($value, $options) { - $maxSize=$options; - if($options == null) { - $maxSize=get_directus_setting('file_max_size'); + $maxSize = $options; + if ($options == null) { + $maxSize = get_directus_setting('file_max_size'); } - $size=$maxSize; + $size = $maxSize; $factors = [ 'KB' => 1000, 'MB' => 1000000, @@ -433,21 +435,20 @@ function validate_file_size($value,$options) ]; if (ctype_digit((string) $maxSize)) { $maxSize = (int) $maxSize; - } elseif (preg_match('/^(\d++)('.implode('|', array_keys($factors)).')$/', $maxSize, $matches)) { + } elseif (preg_match('/^(\d++)(' . implode('|', array_keys($factors)) . ')$/', $maxSize, $matches)) { $maxSize = $matches[1] * $factors[$unit = $matches[2]]; } else { throw new InvalidRequestException(sprintf('"%s" is not a valid maximum size.', $size)); } if (0 === $value) { - $message='An empty file is not allowed.'; + $message = 'An empty file is not allowed.'; throw new InvalidRequestException($message); } - if($value > $maxSize){ - $message='The file is too large. Allowed maximum size is '.$size.'.'; + if ($value > $maxSize) { + $message = 'The file is too large. Allowed maximum size is ' . $size . '.'; throw new InvalidRequestException($message); } } } - diff --git a/tests/api/Config/ContextTest.php b/tests/api/Config/ContextTest.php index 9c99ca2c21..28343298e8 100644 --- a/tests/api/Config/ContextTest.php +++ b/tests/api/Config/ContextTest.php @@ -120,6 +120,9 @@ public function testContextFile() "b" => "2" ], ], + 'arrayval' => [ + 'some' => 'array' + ] ]; // Should load values from php source file diff --git a/tests/api/Config/SchemaTest.php b/tests/api/Config/SchemaTest.php index cc5d8d40c2..97e28c8e65 100644 --- a/tests/api/Config/SchemaTest.php +++ b/tests/api/Config/SchemaTest.php @@ -115,6 +115,7 @@ public function testDefaults() 'social_providers' => [], ], + 'ext' => [] ], $data); } diff --git a/tests/api/Config/sources/source.json b/tests/api/Config/sources/source.json index 1a0c533ded..1ab116fa24 100644 --- a/tests/api/Config/sources/source.json +++ b/tests/api/Config/sources/source.json @@ -4,5 +4,10 @@ "a": 1, "b": 2 } - } + }, + "arrayval": [ + { + "some": "object", + } + ] } diff --git a/tests/api/Config/sources/source.php b/tests/api/Config/sources/source.php index 6fcd56e723..6de17fe03b 100644 --- a/tests/api/Config/sources/source.php +++ b/tests/api/Config/sources/source.php @@ -7,4 +7,7 @@ "b" => 2, ], ], + 'arrayval' => [ + 'some' => 'array' + ] ];