From d5812f11a0b4e983f79d0fb41bfaba89cdc11a25 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Tue, 9 Jul 2019 18:04:48 -0400 Subject: [PATCH] v2.2.2 --- README.md | 2 +- composer.json | 3 +- ...323_add_login_attempts_allowed_setting.php | 27 +++ ...0190704063752_update_istanbul_timezone.php | 168 ++++++++++++++++++ public/.htaccess | 2 +- public/thumbnail/index.php | 15 +- public/uploads/_/originals/.htaccess | 8 + src/core/Directus/Application/Application.php | 4 +- .../Application/CoreServicesProvider.php | 28 ++- .../Exception/UserSuspendedException.php | 15 ++ src/core/Directus/Authentication/Provider.php | 65 +++++++ .../User/Provider/UserProviderInterface.php | 1 + .../InvalidCacheAdapterException.php | 21 +++ .../InvalidCacheConfigurationException.php | 21 +++ src/core/Directus/Config/Context.php | 108 +++++++++++ .../Exception/InvalidProjectException.php | 22 +++ .../Exception/UnknownProjectException.php | 17 ++ src/core/Directus/Config/Schema/Base.php | 116 ++++++++++++ .../Config/Schema/Exception/OmitException.php | 10 ++ src/core/Directus/Config/Schema/Group.php | 51 ++++++ src/core/Directus/Config/Schema/Node.php | 44 +++++ src/core/Directus/Config/Schema/Schema.php | 144 +++++++++++++++ src/core/Directus/Config/Schema/Types.php | 14 ++ src/core/Directus/Config/Schema/Value.php | 64 +++++++ src/core/Directus/Console/Common/User.php | 9 +- .../Console/Modules/InstallModule.php | 4 +- .../TableGateway/BaseTableGateway.php | 25 ++- .../DirectusActivityTableGateway.php | 45 +++++ .../TableGateway/RelationalTableGateway.php | 65 ++++--- src/core/Directus/Filesystem/Files.php | 5 +- src/core/Directus/GraphQL/FieldsConfig.php | 13 ++ src/core/Directus/Services/MailService.php | 3 +- src/core/Directus/Services/UsersService.php | 14 ++ src/helpers/app.php | 24 ++- src/web.php | 57 ++++-- vendor/autoload.php | 2 +- vendor/composer/autoload_classmap.php | 13 ++ vendor/composer/autoload_real.php | 14 +- vendor/composer/autoload_static.php | 23 ++- vendor/composer/installed.json | 38 ++-- vendor/guzzlehttp/psr7/.editorconfig | 9 - vendor/guzzlehttp/psr7/CHANGELOG.md | 23 ++- vendor/guzzlehttp/psr7/composer.json | 10 +- vendor/guzzlehttp/psr7/src/LimitStream.php | 2 +- vendor/guzzlehttp/psr7/src/MessageTrait.php | 60 +++++-- vendor/guzzlehttp/psr7/src/Request.php | 9 + vendor/guzzlehttp/psr7/src/Response.php | 28 ++- vendor/guzzlehttp/psr7/src/ServerRequest.php | 2 +- vendor/guzzlehttp/psr7/src/Stream.php | 33 ++-- vendor/guzzlehttp/psr7/src/Uri.php | 36 +++- vendor/guzzlehttp/psr7/src/functions.php | 1 + vendor/ralouphie/getallheaders/.travis.yml | 18 -- vendor/ralouphie/getallheaders/README.md | 8 + vendor/ralouphie/getallheaders/composer.json | 13 +- vendor/ralouphie/getallheaders/phpunit.xml | 22 --- .../getallheaders/tests/GetAllHeadersTest.php | 121 ------------- 56 files changed, 1387 insertions(+), 332 deletions(-) create mode 100644 migrations/upgrades/schemas/20190702092323_add_login_attempts_allowed_setting.php create mode 100644 migrations/upgrades/schemas/20190704063752_update_istanbul_timezone.php create mode 100644 src/core/Directus/Authentication/Exception/UserSuspendedException.php create mode 100644 src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php create mode 100644 src/core/Directus/Cache/Exception/InvalidCacheConfigurationException.php create mode 100644 src/core/Directus/Config/Context.php create mode 100644 src/core/Directus/Config/Exception/InvalidProjectException.php create mode 100644 src/core/Directus/Config/Exception/UnknownProjectException.php create mode 100644 src/core/Directus/Config/Schema/Base.php create mode 100644 src/core/Directus/Config/Schema/Exception/OmitException.php create mode 100644 src/core/Directus/Config/Schema/Group.php create mode 100644 src/core/Directus/Config/Schema/Node.php create mode 100644 src/core/Directus/Config/Schema/Schema.php create mode 100644 src/core/Directus/Config/Schema/Types.php create mode 100644 src/core/Directus/Config/Schema/Value.php delete mode 100644 vendor/guzzlehttp/psr7/.editorconfig delete mode 100644 vendor/ralouphie/getallheaders/.travis.yml delete mode 100644 vendor/ralouphie/getallheaders/phpunit.xml delete mode 100644 vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php diff --git a/README.md b/README.md index 19592a39cf..d85e274c09 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Directus allows you to manage multilingual content in as many languages as your ### Community -* **[Slack](https://slack.directus.io)** — Come join almost a thousand members discussing the future of Directus. Our helpful members are also quick to offer advice for simple questions you may have while getting started. +* **[Slack](https://directus.chat)** — Come join over a thousand members discussing the future of Directus. Our helpful members are also quick to offer advice for simple questions you may have while getting started. * **[Twitter](https://twitter.com/directus)** — Follow us on Twitter to be the first to hear about product updates, see sneak peeks of new features, and vote on polls regarding the future of our platform. ### GitHub Tickets diff --git a/composer.json b/composer.json index 91f9c7a90f..ddd39b3e65 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "webonyx/graphql-php": "^0.13.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.25" + "phpunit/phpunit": "^5.7.25", + "phpbench/phpbench": "@dev" }, "suggest": { "paragonie/random_compat": "Generates cryptographically more secure pseudo-random bytes", diff --git a/migrations/upgrades/schemas/20190702092323_add_login_attempts_allowed_setting.php b/migrations/upgrades/schemas/20190702092323_add_login_attempts_allowed_setting.php new file mode 100644 index 0000000000..a1307e0092 --- /dev/null +++ b/migrations/upgrades/schemas/20190702092323_add_login_attempts_allowed_setting.php @@ -0,0 +1,27 @@ + 'login_attempts_allowed', + 'type' => 'integer', + 'interface' => 'numeric', + ]; + $collection = 'directus_settings'; + + $checkSql = sprintf('SELECT 1 FROM `directus_fields` WHERE `collection` = "%s" AND `field` = "%s";', $collection, $fieldObject['field']); + $result = $this->query($checkSql)->fetch(); + + if (!$result) { + $insertSqlFormat = 'INSERT INTO `directus_fields` (`collection`, `field`, `type`, `interface`) VALUES ("%s", "%s", "%s", "%s");'; + $insertSql = sprintf($insertSqlFormat, $collection, $fieldObject['field'], $fieldObject['type'], $fieldObject['interface']); + $this->execute($insertSql); + } + + } +} diff --git a/migrations/upgrades/schemas/20190704063752_update_istanbul_timezone.php b/migrations/upgrades/schemas/20190704063752_update_istanbul_timezone.php new file mode 100644 index 0000000000..61cf4431aa --- /dev/null +++ b/migrations/upgrades/schemas/20190704063752_update_istanbul_timezone.php @@ -0,0 +1,168 @@ + [ + 'Pacific/Midway' => '(UTC-11:00) Midway Island', + 'Pacific/Samoa' => '(UTC-11:00) Samoa', + 'Pacific/Honolulu' => '(UTC-10:00) Hawaii', + 'US/Alaska' => '(UTC-09:00) Alaska', + 'America/Los_Angeles' => '(UTC-08:00) Pacific Time (US & Canada)', + 'America/Tijuana' => '(UTC-08:00) Tijuana', + 'US/Arizona' => '(UTC-07:00) Arizona', + 'America/Chihuahua' => '(UTC-07:00) Chihuahua', + 'America/Mexico/La_Paz' => '(UTC-07:00) La Paz', + 'America/Mazatlan' => '(UTC-07:00) Mazatlan', + 'US/Mountain' => '(UTC-07:00) Mountain Time (US & Canada)', + 'America/Managua' => '(UTC-06:00) Central America', + 'US/Central' => '(UTC-06:00) Central Time (US & Canada)', + 'America/Guadalajara' => '(UTC-06:00) Guadalajara', + 'America/Mexico_City' => '(UTC-06:00) Mexico City', + 'America/Monterrey' => '(UTC-06:00) Monterrey', + 'Canada/Saskatchewan' => '(UTC-06:00) Saskatchewan', + 'America/Bogota' => '(UTC-05:00) Bogota', + 'US/Eastern' => '(UTC-05:00) Eastern Time (US & Canada)', + 'US/East-Indiana' => '(UTC-05:00) Indiana (East)', + 'America/Lima' => '(UTC-05:00) Lima', + 'America/Quito' => '(UTC-05:00) Quito', + 'Canada/Atlantic' => '(UTC-04:00) Atlantic Time (Canada)', + 'America/New_York' => '(UTC-04:00) New York', + 'America/Caracas' => '(UTC-04:30) Caracas', + 'America/La_Paz' => '(UTC-04:00) La Paz', + 'America/Santiago' => '(UTC-04:00) Santiago', + 'America/Santo_Domingo' => '(UTC-04:00) Santo Domingo', + 'Canada/Newfoundland' => '(UTC-03:30) Newfoundland', + 'America/Sao_Paulo' => '(UTC-03:00) Brasilia', + 'America/Argentina/Buenos_Aires' => '(UTC-03:00) Buenos Aires', + 'America/Argentina/GeorgeTown' => '(UTC-03:00) Georgetown', + 'America/Godthab' => '(UTC-03:00) Greenland', + 'America/Noronha' => '(UTC-02:00) Mid-Atlantic', + 'Atlantic/Azores' => '(UTC-01:00) Azores', + 'Atlantic/Cape_Verde' => '(UTC-01:00) Cape Verde Is.', + 'Africa/Casablanca' => '(UTC+00:00) Casablanca', + 'Europe/Edinburgh' => '(UTC+00:00) Edinburgh', + 'Etc/Greenwich' => '(UTC+00:00) Greenwich Mean Time : Dublin', + 'Europe/Lisbon' => '(UTC+00:00) Lisbon', + 'Europe/London' => '(UTC+00:00) London', + 'Africa/Monrovia' => '(UTC+00:00) Monrovia', + 'UTC' => '(UTC+00:00) UTC', + 'Europe/Amsterdam' => '(UTC+01:00) Amsterdam', + 'Europe/Belgrade' => '(UTC+01:00) Belgrade', + 'Europe/Berlin' => '(UTC+01:00) Berlin', + 'Europe/Bern' => '(UTC+01:00) Bern', + 'Europe/Bratislava' => '(UTC+01:00) Bratislava', + 'Europe/Brussels' => '(UTC+01:00) Brussels', + 'Europe/Budapest' => '(UTC+01:00) Budapest', + 'Europe/Copenhagen' => '(UTC+01:00) Copenhagen', + 'Europe/Ljubljana' => '(UTC+01:00) Ljubljana', + 'Europe/Madrid' => '(UTC+01:00) Madrid', + 'Europe/Paris' => '(UTC+01:00) Paris', + 'Europe/Prague' => '(UTC+01:00) Prague', + 'Europe/Rome' => '(UTC+01:00) Rome', + 'Europe/Sarajevo' => '(UTC+01:00) Sarajevo', + 'Europe/Skopje' => '(UTC+01:00) Skopje', + 'Europe/Stockholm' => '(UTC+01:00) Stockholm', + 'Europe/Vienna' => '(UTC+01:00) Vienna', + 'Europe/Warsaw' => '(UTC+01:00) Warsaw', + 'Africa/Lagos' => '(UTC+01:00) West Central Africa', + 'Europe/Zagreb' => '(UTC+01:00) Zagreb', + 'Europe/Athens' => '(UTC+02:00) Athens', + 'Europe/Bucharest' => '(UTC+02:00) Bucharest', + 'Africa/Cairo' => '(UTC+02:00) Cairo', + 'Africa/Harare' => '(UTC+02:00) Harare', + 'Europe/Helsinki' => '(UTC+02:00) Helsinki', + 'Asia/Jerusalem' => '(UTC+02:00) Jerusalem', + 'Europe/Kyiv' => '(UTC+02:00) Kyiv', + 'Africa/Johannesburg' => '(UTC+02:00) Pretoria', + 'Europe/Riga' => '(UTC+02:00) Riga', + 'Europe/Sofia' => '(UTC+02:00) Sofia', + 'Europe/Tallinn' => '(UTC+02:00) Tallinn', + 'Europe/Vilnius' => '(UTC+02:00) Vilnius', + 'Europe/Istanbul' => '(UTC+03:00) Istanbul', + 'Asia/Baghdad' => '(UTC+03:00) Baghdad', + 'Asia/Kuwait' => '(UTC+03:00) Kuwait', + 'Europe/Minsk' => '(UTC+03:00) Minsk', + 'Africa/Nairobi' => '(UTC+03:00) Nairobi', + 'Asia/Riyadh' => '(UTC+03:00) Riyadh', + 'Europe/Volgograd' => '(UTC+03:00) Volgograd', + 'Asia/Tehran' => '(UTC+03:30) Tehran', + 'Asia/Abu_Dhabi' => '(UTC+04:00) Abu Dhabi', + 'Asia/Baku' => '(UTC+04:00) Baku', + 'Europe/Moscow' => '(UTC+04:00) Moscow', + 'Asia/Muscat' => '(UTC+04:00) Muscat', + 'Europe/St_Petersburg' => '(UTC+04:00) St. Petersburg', + 'Asia/Tbilisi' => '(UTC+04:00) Tbilisi', + 'Asia/Yerevan' => '(UTC+04:00) Yerevan', + 'Asia/Kabul' => '(UTC+04:30) Kabul', + 'Asia/Islamabad' => '(UTC+05:00) Islamabad', + 'Asia/Karachi' => '(UTC+05:00) Karachi', + 'Asia/Tashkent' => '(UTC+05:00) Tashkent', + 'Asia/Calcutta' => '(UTC+05:30) Chennai', + 'Asia/Kolkata' => '(UTC+05:30) Kolkata', + 'Asia/Mumbai' => '(UTC+05:30) Mumbai', + 'Asia/New_Delhi' => '(UTC+05:30) New Delhi', + 'Asia/Sri_Jayawardenepura' => '(UTC+05:30) Sri Jayawardenepura', + 'Asia/Katmandu' => '(UTC+05:45) Kathmandu', + 'Asia/Almaty' => '(UTC+06:00) Almaty', + 'Asia/Astana' => '(UTC+06:00) Astana', + 'Asia/Dhaka' => '(UTC+06:00) Dhaka', + 'Asia/Yekaterinburg' => '(UTC+06:00) Ekaterinburg', + 'Asia/Rangoon' => '(UTC+06:30) Rangoon', + 'Asia/Bangkok' => '(UTC+07:00) Bangkok', + 'Asia/Hanoi' => '(UTC+07:00) Hanoi', + 'Asia/Jakarta' => '(UTC+07:00) Jakarta', + 'Asia/Novosibirsk' => '(UTC+07:00) Novosibirsk', + 'Asia/Beijing' => '(UTC+08:00) Beijing', + 'Asia/Chongqing' => '(UTC+08:00) Chongqing', + 'Asia/Hong_Kong' => '(UTC+08:00) Hong Kong', + 'Asia/Krasnoyarsk' => '(UTC+08:00) Krasnoyarsk', + 'Asia/Kuala_Lumpur' => '(UTC+08:00) Kuala Lumpur', + 'Australia/Perth' => '(UTC+08:00) Perth', + 'Asia/Singapore' => '(UTC+08:00) Singapore', + 'Asia/Taipei' => '(UTC+08:00) Taipei', + 'Asia/Ulan_Bator' => '(UTC+08:00) Ulaan Bataar', + 'Asia/Urumqi' => '(UTC+08:00) Urumqi', + 'Asia/Irkutsk' => '(UTC+09:00) Irkutsk', + 'Asia/Osaka' => '(UTC+09:00) Osaka', + 'Asia/Sapporo' => '(UTC+09:00) Sapporo', + 'Asia/Seoul' => '(UTC+09:00) Seoul', + 'Asia/Tokyo' => '(UTC+09:00) Tokyo', + 'Australia/Adelaide' => '(UTC+09:30) Adelaide', + 'Australia/Darwin' => '(UTC+09:30) Darwin', + 'Australia/Brisbane' => '(UTC+10:00) Brisbane', + 'Australia/Canberra' => '(UTC+10:00) Canberra', + 'Pacific/Guam' => '(UTC+10:00) Guam', + 'Australia/Hobart' => '(UTC+10:00) Hobart', + 'Australia/Melbourne' => '(UTC+10:00) Melbourne', + 'Pacific/Port_Moresby' => '(UTC+10:00) Port Moresby', + 'Australia/Sydney' => '(UTC+10:00) Sydney', + 'Asia/Yakutsk' => '(UTC+10:00) Yakutsk', + 'Asia/Vladivostok' => '(UTC+11:00) Vladivostok', + 'Pacific/Auckland' => '(UTC+12:00) Auckland', + 'Pacific/Fiji' => '(UTC+12:00) Fiji', + 'Pacific/Kwajalein' => '(UTC+12:00) International Date Line West', + 'Asia/Kamchatka' => '(UTC+12:00) Kamchatka', + 'Asia/Magadan' => '(UTC+12:00) Magadan', + 'Pacific/Marshall_Is' => '(UTC+12:00) Marshall Is.', + 'Asia/New_Caledonia' => '(UTC+12:00) New Caledonia', + 'Asia/Solomon_Is' => '(UTC+12:00) Solomon Is.', + 'Pacific/Wellington' => '(UTC+12:00) Wellington', + 'Pacific/Tongatapu' => '(UTC+13:00) Nuku\'alofa' + ], + 'placeholder' => 'Choose a timezone...' + ]); + + $this->execute(\Directus\phinx_update( + $this->getAdapter(), + 'directus_fields', + ['options' => $options], + ['collection' => 'directus_users', 'field' => 'timezone'] + )); + } +} diff --git a/public/.htaccess b/public/.htaccess index c13c18f45e..e02f624c38 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -23,4 +23,4 @@ Options +SymLinksIfOwnerMatch php_value upload_max_filesize 50M php_value post_max_size 100M - + \ No newline at end of file diff --git a/public/thumbnail/index.php b/public/thumbnail/index.php index a83e5e716d..0dafda9cd9 100644 --- a/public/thumbnail/index.php +++ b/public/thumbnail/index.php @@ -43,11 +43,11 @@ if (!$image) { // now we can create the thumb switch ($thumbnailer->action) { - // http://image.intervention.io/api/resize + // http://image.intervention.io/api/resize case 'contain': $image = $thumbnailer->contain(); break; - // http://image.intervention.io/api/fit + // http://image.intervention.io/api/fit case 'crop': default: $image = $thumbnailer->crop(); @@ -58,13 +58,14 @@ header('Content-type: ' . $thumbnailer->getThumbnailMimeType()); header("Pragma: cache"); header('Cache-Control: max-age=' . $timeToLive); - header('Last-Modified: '. gmdate('D, d M Y H:i:s \G\M\T', time())); - header('Expires: '. gmdate('D, d M Y H:i:s \G\M\T', time() + $timeToLive)); + header("Access-Control-Allow-Origin: *"); + header("Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS"); + header("Access-Control-Allow-Headers: Access-Control-Allow-Headers,Content-Type"); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', time())); + header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + $timeToLive)); echo $image; exit(0); -} - -catch (Exception $e) { +} catch (Exception $e) { $filePath = ArrayUtils::get($settings, 'thumbnail_not_found_location'); if (is_string($filePath) && !empty($filePath) && $filePath[0] !== '/') { $filePath = $basePath . '/' . $filePath; diff --git a/public/uploads/_/originals/.htaccess b/public/uploads/_/originals/.htaccess index 1c9c8e88b3..df5cb35238 100644 --- a/public/uploads/_/originals/.htaccess +++ b/public/uploads/_/originals/.htaccess @@ -8,6 +8,14 @@ deny from all + + + Header set Access-Control-Allow-Origin "*" + Header set Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS" + Header set Access-Control-Allow-Headers "Access-Control-Allow-Headers,Content-Type" + + + # Respond with 404 if the file doesn't exists # Before the API mod_rewrite catches the request diff --git a/src/core/Directus/Application/Application.php b/src/core/Directus/Application/Application.php index 9a8185d8ab..08b25be9e2 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.2.1'; + const DIRECTUS_VERSION = '2.2.2'; /** * NOT USED @@ -142,7 +142,7 @@ public function setCheckRequirementsFunction(\Closure $function) protected function createConfig(array $appConfig) { return [ - 'settings' => ArrayUtils::pull($appConfig, 'settings', []), + 'settings' => $appConfig['settings'], 'config' => function () use ($appConfig) { return new Config($appConfig); } diff --git a/src/core/Directus/Application/CoreServicesProvider.php b/src/core/Directus/Application/CoreServicesProvider.php index dd69eb223f..3e0ee366e8 100644 --- a/src/core/Directus/Application/CoreServicesProvider.php +++ b/src/core/Directus/Application/CoreServicesProvider.php @@ -18,6 +18,8 @@ use Directus\Authentication\Sso\Social; use Directus\Authentication\User\Provider\UserTableGatewayProvider; use Directus\Cache\Response; +use Directus\Cache\Exception\InvalidCacheAdapterException; +use Directus\Cache\Exception\InvalidCacheConfigurationException; use Directus\Config\StatusMapping; use Directus\Database\Connection; use Directus\Database\Exception\ConnectionFailedException; @@ -127,12 +129,14 @@ protected function getLogger() $filenameFormat = '%s.%s.log'; foreach (Logger::getLevels() as $name => $level) { + if ($path !== "php://stdout" && $path !== "php://stderr") { + $path . '/' . sprintf($filenameFormat, strtolower($name), date('Y-m-d')); + } $handler = new StreamHandler( - $path . '/' . sprintf($filenameFormat, strtolower($name), date('Y-m-d')), + $path, $level, false ); - $handler->setFormatter($formatter); $logger->pushHandler($handler); } @@ -542,7 +546,7 @@ protected function getEmitter() ]; // Authenticated user can see their private info // Admin can see all users private info - if (!$acl->isAdmin() && $userId !== (int)$row['id']) { + if (!$acl->isAdmin() && $userId !== (int) $row['id']) { $omit = array_merge($omit, [ 'token', 'email_notifications', @@ -894,7 +898,7 @@ protected function getCache() $pool = $poolConfig; } else { if (!in_array($poolConfig['adapter'], ['apc', 'apcu', 'array', 'filesystem', 'memcached', 'memcache', 'redis', 'void'])) { - throw new \Exception("Valid cache adapters are 'apc', 'apcu', 'filesystem', 'memcached', 'memcache', 'redis'"); + throw new InvalidCacheAdapterException(); } $pool = new VoidCachePool(); @@ -915,7 +919,7 @@ protected function getCache() if ($adapter == 'filesystem') { if (empty($poolConfig['path']) || !is_string($poolConfig['path'])) { - throw new \Exception('"cache.pool.path parameter is required for "filesystem" adapter and must be a string'); + throw new InvalidCacheConfigurationException($adapter); } $cachePath = $poolConfig['path']; @@ -931,6 +935,15 @@ protected function getCache() } if ($adapter == 'memcached' || $adapter == 'memcache') { + + if ($adapter == 'memcached' && !extension_loaded('memcached')) { + throw new InvalidCacheConfigurationException($adapter); + } + + if ($adapter == 'memcache' && !extension_loaded('memcache')) { + throw new InvalidCacheConfigurationException($adapter); + } + $client = $adapter == 'memcached' ? new \Memcached() : new \Memcache(); if (isset($poolConfig['url'])) { $urls = explode(';', $poolConfig['url']); @@ -954,6 +967,11 @@ protected function getCache() } if ($adapter == 'redis') { + + if (!extension_loaded('redis')) { + throw new InvalidCacheConfigurationException($adapter); + } + $host = (isset($poolConfig['host'])) ? $poolConfig['host'] : 'localhost'; $port = (isset($poolConfig['port'])) ? $poolConfig['port'] : 6379; $socket = (isset($poolConfig['socket'])) ? $poolConfig['socket'] : null; diff --git a/src/core/Directus/Authentication/Exception/UserSuspendedException.php b/src/core/Directus/Authentication/Exception/UserSuspendedException.php new file mode 100644 index 0000000000..c10da5b4d8 --- /dev/null +++ b/src/core/Directus/Authentication/Exception/UserSuspendedException.php @@ -0,0 +1,15 @@ +get('password'))) { + + $this->recordActivityAndCheckLoginAttempt($user); throw new InvalidUserCredentialsException(); } @@ -166,6 +174,44 @@ public function findUserWithCredentials($email, $password) return $user; } + /** + * Record invalid credentials action and throw exception if the maximum invalid login attempts reached. + */ + public function recordActivityAndCheckLoginAttempt($user) + { + $userId = $user->get('id'); + $activityTableGateway = TableGatewayFactory::create(SchemaManager::COLLECTION_ACTIVITY, ['acl' => false]); + + // Added this before calculation as system may throw the exception here. + $activityTableGateway->recordAction($userId, SchemaManager::COLLECTION_USERS, DirectusActivityTableGateway::ACTION_INVALID_CREDENTIALS); + + $loginAttemptsAllowed = get_directus_setting('login_attempts_allowed'); + + if(!empty($loginAttemptsAllowed)){ + + // We added 'Invalid credentials' entry before this condition so need to increase this counter with 1 + $totalLoginAttemptsAllowed = $loginAttemptsAllowed + 1; + + $invalidLoginAttempts = $activityTableGateway->getInvalidLoginAttempts($userId, $totalLoginAttemptsAllowed); + if(!empty($invalidLoginAttempts)){ + $lastInvalidCredentialsEntry = current($invalidLoginAttempts); + $firstInvalidCredentialsEntry = end($invalidLoginAttempts); + + $lastLoginAttempt = $activityTableGateway->getLastLoginOrStatusUpdateAttempt($userId); + + if(!empty($lastLoginAttempt) && !in_array($lastLoginAttempt['id'], range($firstInvalidCredentialsEntry['id'], $lastInvalidCredentialsEntry['id'])) && count($invalidLoginAttempts) > $loginAttemptsAllowed){ + + $tableGateway = TableGatewayFactory::create(SchemaManager::COLLECTION_USERS, ['acl' => false]); + $update = [ + 'status' => DirectusUsersTableGateway::STATUS_SUSPENDED + ]; + $tableGateway->update($update, ['id' => $userId]); + throw new UserSuspendedException(); + } + } + } + } + /** * Returns a user with the given email if exists * Otherwise throw an UserNotFoundException @@ -204,6 +250,21 @@ public function isActive(UserInterface $user) return $user->get('status') == $userProvider::STATUS_ACTIVE; } + /** + * Checks if the user is suspended + * + * @param UserInterface $user + * + * @return bool + */ + public function isSuspended(UserInterface $user) + { + $userProvider = $this->userProvider; + + // TODO: Cast attributes values + return $user->get('status') == $userProvider::STATUS_SUSPENDED; + } + /** * Authenticate an user using a JWT Token * @@ -611,6 +672,10 @@ protected function validateUser($user) throw new UserNotFoundException(); } + if ($this->isSuspended($user)) { + throw new UserSuspendedException(); + } + if (!$this->isActive($user)) { throw new UserInactiveException(); } diff --git a/src/core/Directus/Authentication/User/Provider/UserProviderInterface.php b/src/core/Directus/Authentication/User/Provider/UserProviderInterface.php index 7f96c152ca..6c1b450676 100644 --- a/src/core/Directus/Authentication/User/Provider/UserProviderInterface.php +++ b/src/core/Directus/Authentication/User/Provider/UserProviderInterface.php @@ -14,6 +14,7 @@ interface UserProviderInterface * @var int */ const STATUS_ACTIVE = DirectusUsersTableGateway::STATUS_ACTIVE; + const STATUS_SUSPENDED = DirectusUsersTableGateway::STATUS_SUSPENDED; /** diff --git a/src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php b/src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php new file mode 100644 index 0000000000..d9e450d5e1 --- /dev/null +++ b/src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php @@ -0,0 +1,21 @@ + [ + * 'b' => [ + * 'c' => 12345 + * ] + * ] + * ]; + */ + private static function expand(&$target, $path, $value) + { + $segment = array_shift($path); + if (sizeof($path) === 0) { // leaf + if (!is_array($target)) { + // TODO: raise warning - overwriting value + $target = []; + } + if (array_key_exists($segment, $target)) { + // TODO: raise warning - overwriting group + } + $target[$segment] = $value; + return; + } + if (!isset($target[$segment])) { + $target[$segment] = []; + } + Context::expand($target[$segment], $path, $value); + } + + /** + * Normalize the array indexes + */ + private static function normalize(&$target) { + if (!is_array($target)) { + return; + } + + $sort = false; + foreach ($target as $key => $value) { + Context::normalize($target[$key]); + $sort |= is_numeric($key); + } + + if ($sort) { + // TODO: which one? + sort($target, SORT_NUMERIC); + // vs. + //$target = array_values($target); + } + } + + /** + * Source + */ + public static function from_map($source) { + $target = []; + ksort($source); + foreach ($source as $key => $value) { + Context::expand($target, explode('_', strtolower($key)), $value); + } + Context::normalize($target); + return $target; + } + + /** + * Create + */ + public static function from_env() + { + if (empty($_ENV)) { + throw new \Error('No environment variables available. Check php_ini "variables_order" value.'); + } + return Context::from_map($_ENV); + } + + /** + * Loads variables from PHP file + */ + public static function from_file($file) { + return require $file; + } + + /** + * Loads variables from PHP file + */ + public static function from_array($array) { + return $array; + } + + /** + * Loads variables from JSON file + */ + public static function from_json($file) { + return json_decode(file_get_contents($file)); + } +} diff --git a/src/core/Directus/Config/Exception/InvalidProjectException.php b/src/core/Directus/Config/Exception/InvalidProjectException.php new file mode 100644 index 0000000000..c670bbfb3a --- /dev/null +++ b/src/core/Directus/Config/Exception/InvalidProjectException.php @@ -0,0 +1,22 @@ +_optional = substr($name, -1) == '?'; + if ($this->_optional) { + $name = substr($name, 0, -1); + } + $this->_key = str_replace('_', '', strtolower($name)); + $this->_name = strtolower($name); + $this->_children = $children; + $this->_parent = null; + foreach ($children as &$child) { + $child->parent($this); + } + } + + /** + * Returns the node key + * @return string + */ + public function key() + { + return $this->_key; + } + + /** + * Returns the node name + * @return string + */ + public function name() + { + return $this->_name; + } + + /** + * Returns the parent node + * @return Node + */ + public function parent($value = false) + { + if ($value !== false) { + $this->_parent = $value; + if ($this->_parent !== null) { + if ($this->_parent->optional() === true) { + $this->_optional = true; + } + } + } + return $this->_parent; + } + + /** + * Returns the children nodes + * @return Node[] + */ + public function children($value = false) + { + if ($value !== false) { + $this->_children = $value; + } + return $this->_children; + } + + /** + * Returns wether the node is optional or not + * @return boolean + */ + public function optional($value = null) + { + if ($value !== null) { + $this->_optional = $value; + } + return $this->_optional; + } +} diff --git a/src/core/Directus/Config/Schema/Exception/OmitException.php b/src/core/Directus/Config/Schema/Exception/OmitException.php new file mode 100644 index 0000000000..f24e014fff --- /dev/null +++ b/src/core/Directus/Config/Schema/Exception/OmitException.php @@ -0,0 +1,10 @@ +key()])) { + if ($this->optional()) { + throw new OmitException(); + } + } else { + $current = $context[$this->key()]; + } + + foreach ($this->children() as $children) { + try { + $value[$children->name()] = $children->value($current); + } catch (OmitException $ex) { + continue; + } + } + + if (empty($value)) { + if ($this->parent() !== null && $this->optional()) { + throw new OmitException(); + } + } + + return $value; + } +} diff --git a/src/core/Directus/Config/Schema/Node.php b/src/core/Directus/Config/Schema/Node.php new file mode 100644 index 0000000000..65a491df72 --- /dev/null +++ b/src/core/Directus/Config/Schema/Node.php @@ -0,0 +1,44 @@ +:[ + new Value('origin', 'array', ['*']), + new Value('methods', 'array', [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD' + ]), + new Value('headers', 'array', []), + new Value('exposed_headers', 'array', []), + new Value('max_age', Types::INTEGER, null), + new Value('credentials', Types::BOOLEAN, false), + ]), + new Group('rate_limit', [ + new Value('enabled', Types::BOOLEAN, false), + new Value('limit', Types::INTEGER, 100), + new Value('interval', Types::INTEGER, 60), + new Value('adapter', Types::STRING, 'redis'), + new Value('host', Types::STRING, '127.0.0.1'), + new Value('port', Types::INTEGER, 6379), + new Value('timeout', Types::INTEGER, 10), + ]), + new Group('hooks', [ + new Value('actions', 'array', []), + new Value('filters', '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 Group('auth', [ + new Value('secret_key', Types::STRING, ''), + new Value('public_key', Types::STRING, ''), + new Group('social_providers', [ + new Group('okta?', [ + new Value('client_id', Types::STRING, ''), + new Value('client_secret', Types::STRING, ''), + new Value('base_url', Types::STRING, 'https://dev-000000.oktapreview.com/oauth2/default') + ]), + new Group('github?', [ + new Value('client_id', Types::STRING, ''), + new Value('client_secret', Types::STRING, ''), + ]), + new Group('facebook?', [ + new Value('client_id', Types::STRING, ''), + new Value('client_secret', Types::STRING, ''), + new Value('graph_api_version', Types::STRING, 'v2.8'), + ]), + new Group('google?', [ + new Value('client_id', Types::STRING, ''), + new Value('client_secret', Types::STRING, ''), + new Value('hosted_domain', Types::STRING, '*'), + new Value('use_oidc_mode', Types::BOOLEAN, true), + ]), + new Group('twitter?', [ + new Value('identifier', Types::STRING, ''), + new Value('secret', Types::STRING, ''), + ]), + ]), + ]), + ]); + } +} diff --git a/src/core/Directus/Config/Schema/Types.php b/src/core/Directus/Config/Schema/Types.php new file mode 100644 index 0000000000..0628f2e974 --- /dev/null +++ b/src/core/Directus/Config/Schema/Types.php @@ -0,0 +1,14 @@ +_type = $type; + $this->_default = $default; + } + + /** + * Gets a value from leaf node + */ + public function value($values) + { + if (!isset($values) || !isset($values[$this->key()])) { + if ($this->optional()) { + throw new OmitException(); + } else { + return $this->_default; + } + } + + $value = $values[$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; + } + } +} diff --git a/src/core/Directus/Console/Common/User.php b/src/core/Directus/Console/Common/User.php index 46d533d0f1..84f4eb247e 100644 --- a/src/core/Directus/Console/Common/User.php +++ b/src/core/Directus/Console/Common/User.php @@ -6,6 +6,7 @@ use Directus\Console\Common\Exception\PasswordChangeException; use Directus\Console\Common\Exception\UserUpdateException; use Zend\Db\TableGateway\TableGateway; +use Directus\Util\Installation\InstallerUtils; class User { @@ -14,14 +15,16 @@ class User private $db; private $usersTableGateway; - public function __construct($base_path) + public function __construct($base_path, $projectName) { if ($base_path == null) { $base_path = \Directus\base_path(); } + $config = require InstallerUtils::createConfigPath($base_path, $projectName); + $this->directus_path = $base_path; - $this->app = new Application($base_path, require $base_path. '/config/api.php'); + $this->app = new Application($base_path, $config); $this->db = $this->app->getContainer()->get('database'); $this->usersTableGateway = new TableGateway('directus_users', $this->db); @@ -90,7 +93,6 @@ public function changePassword($email, $password) } catch (\PDOException $ex) { throw new PasswordChangeException('Failed to change password' . ': ' . str($ex)); } - } /** @@ -122,5 +124,4 @@ public function changeEmail($id, $email) throw new PasswordChangeException('Could not change email for ID ' . $id . ': ' . str($ex)); } } - } diff --git a/src/core/Directus/Console/Modules/InstallModule.php b/src/core/Directus/Console/Modules/InstallModule.php index bcb00e8b38..f5890b6f9f 100644 --- a/src/core/Directus/Console/Modules/InstallModule.php +++ b/src/core/Directus/Console/Modules/InstallModule.php @@ -198,12 +198,12 @@ public function cmdInstall($args, $extra) } // NOTE: Do we really want to change the email when re-run install command? - $user = new User($directus_path); + $user = new User($directus_path, $projectName); try { $hasEmail = ArrayUtils::has($data, 'user_email'); if ($hasEmail && !$user->userExists($data['user_email'])) { InstallerUtils::addDefaultUser($directus_path, $data, $projectName); - }else{ + } else { //TODO: Verify this method is required or not! if ($hasEmail) { $user->changeEmail(1, $data['user_email']); diff --git a/src/core/Directus/Database/TableGateway/BaseTableGateway.php b/src/core/Directus/Database/TableGateway/BaseTableGateway.php index 4c261bf215..e7a82680bb 100644 --- a/src/core/Directus/Database/TableGateway/BaseTableGateway.php +++ b/src/core/Directus/Database/TableGateway/BaseTableGateway.php @@ -1238,14 +1238,33 @@ protected function enforceReadPermission(Builder $builder) * @throws \Exception */ public function enforceUpdatePermission(Update $update) - { + { + $collectionObject = $this->getTableSchema(); + $statusField = $collectionObject->getStatusField(); + $updateState = $update->getRawState(); + $updateData = $updateState['set']; + + //If a collection has status field then records are not actually deleting, they are soft deleting + //Check delete permission for soft delete + if ( + $statusField + && ArrayUtils::has($updateData, $statusField->getName()) + && in_array( + ArrayUtils::get($updateData, $collectionObject->getStatusField()->getName()), + $this->getStatusMapping()->getSoftDeleteStatusesValue() + ) + ) { + $delete = $this->sql->delete(); + $delete->where($updateState['where']); + $this->enforceDeletePermission($delete); + return; + } + if ($this->acl->canUpdateAll($this->table) && $this->acl->isAdmin()) { return; } - $collectionObject = $this->getTableSchema(); $currentUserId = $this->acl->getUserId(); - $updateState = $update->getRawState(); $updateTable = $this->getRawTableNameFromQueryStateTable($updateState['table']); $select = $this->sql->select(); $select->where($updateState['where']); diff --git a/src/core/Directus/Database/TableGateway/DirectusActivityTableGateway.php b/src/core/Directus/Database/TableGateway/DirectusActivityTableGateway.php index 68ccdaf31f..d4e9ade743 100644 --- a/src/core/Directus/Database/TableGateway/DirectusActivityTableGateway.php +++ b/src/core/Directus/Database/TableGateway/DirectusActivityTableGateway.php @@ -6,6 +6,7 @@ use Directus\Util\DateTimeUtils; use Zend\Db\Adapter\AdapterInterface; use Zend\Db\Sql\Insert; +use Zend\Db\Sql\Select; class DirectusActivityTableGateway extends RelationalTableGateway { @@ -18,6 +19,8 @@ class DirectusActivityTableGateway extends RelationalTableGateway const ACTION_COMMENT = 'comment'; const ACTION_UPLOAD = 'upload'; const ACTION_AUTHENTICATE = 'authenticate'; + const ACTION_INVALID_CREDENTIALS = 'invalid-credentials'; + const ACTION_UPDATE_USER_STATUS = 'status-update'; public static $_tableName = 'directus_activity'; @@ -65,4 +68,46 @@ public function recordLogin($userId) $this->insertWith($insert); } + + public function recordAction($userId, $collection, $action, $comment = null) + { + $logData = [ + 'collection' => $collection, + 'action' => $action, + 'action_by' => $userId, + 'item' => $userId, + 'action_on' => DateTimeUtils::now()->toString(), + 'ip' => \Directus\get_request_ip(), + 'comment' => $comment, + 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '' + ]; + + $insert = new Insert($this->getTable()); + $insert + ->values($logData); + + $this->insertWith($insert); + } + + public function getInvalidLoginAttempts($userId, $loginAttemptsAllowed) + { + $select = new Select($this->getTable()); + $select->where->equalTo('action_by', $userId); + $select->where->equalTo('action', self::ACTION_INVALID_CREDENTIALS); + $select->order('id DESC'); + $select->limit($loginAttemptsAllowed); + $invalidLoginAttempts = $this->selectWith($select)->toArray(); + return $this->parseRecord($invalidLoginAttempts); + } + + public function getLastLoginOrStatusUpdateAttempt($userId) + { + $select = new Select($this->getTable()); + $select->where->equalTo('action_by', $userId); + $select->where('action = "'.self::ACTION_AUTHENTICATE.'" OR action = "'.self::ACTION_UPDATE_USER_STATUS.'"'); + $select->order('id DESC'); + $select->limit(1); + $lastLoginAttempt = $this->selectWith($select)->toArray(); + return $this->parseRecord(current($lastLoginAttempt)); + } } diff --git a/src/core/Directus/Database/TableGateway/RelationalTableGateway.php b/src/core/Directus/Database/TableGateway/RelationalTableGateway.php index 0a61800e04..2bc0a0c7f4 100644 --- a/src/core/Directus/Database/TableGateway/RelationalTableGateway.php +++ b/src/core/Directus/Database/TableGateway/RelationalTableGateway.php @@ -965,22 +965,25 @@ public function createEntriesMetadata(array $entries, array $list = []) $tableSchema = $this->getTableSchema($this->table); $metadata = []; + $countedData = []; if (empty($list) || in_array('*', $list)) { $list = $allKeys; } + $countedData['result_count'] = count($entries); if (in_array('result_count', $list)) { $metadata['result_count'] = count($entries); } + $condition = null; + if ($this->getTableSchema()->hasStatusField()) { + $fieldName = $this->getTableSchema()->getStatusField()->getName(); + $condition = new In($fieldName, $this->getNonSoftDeleteStatuses()); + } + $countedData['total_count'] = $this->countTotal($condition); + if (in_array('total_count', $list)) { - $condition = null; - if ($this->getTableSchema()->hasStatusField()) { - $fieldName = $this->getTableSchema()->getStatusField()->getName(); - $condition = new In($fieldName, $this->getNonSoftDeleteStatuses()); - } - $metadata['total_count'] = $this->countTotal($condition); } @@ -990,7 +993,7 @@ public function createEntriesMetadata(array $entries, array $list = []) } if (in_array('filter_count', $list) || in_array('page', $list)) { - $metadata = $this->createMetadataPagination($metadata, $_GET); + $metadata = $this->createMetadataPagination($metadata, $_GET,$countedData); } return $metadata; @@ -1004,7 +1007,7 @@ public function createEntriesMetadata(array $entries, array $list = []) * * @return array */ - public function createMetadataPagination(array $metadata = [], array $params = []) + public function createMetadataPagination(array $metadata = [], array $params = [], array $countedData = []) { if (empty($params)) $params = $_GET; @@ -1014,12 +1017,15 @@ public function createMetadataPagination(array $metadata = [], array $params = [ $page = intval(ArrayUtils::get($params, 'page', 1)); $offset = intval(ArrayUtils::get($params, 'offset', -1)); - $total = intval(ArrayUtils::get($metadata, 'Published') ?: ArrayUtils::get($metadata, 'total_count')); - $rows = intval(ArrayUtils::get($metadata, 'result_count')); + $total = intval(ArrayUtils::get($metadata, 'Published') ?: ArrayUtils::get($countedData, 'total_count')); + $rows = intval(ArrayUtils::get($countedData, 'result_count')); $pathname = explode('?', ArrayUtils::get($_SERVER, 'REQUEST_URI')); $url = trim(\Directus\get_url(), '/') . reset($pathname); - if (!$rows || !$total) return $metadata; + $meta_param=explode(',',$params['meta']); + if((in_array('filter_count',$meta_param) || in_array('*',$meta_param))) { + $metadata['filter_count'] = $countedData['total_count']; + } if ($filtered) { $filteredparams = array_merge($params, [ @@ -1030,9 +1036,11 @@ public function createMetadataPagination(array $metadata = [], array $params = [ $entries = $this->fetchItems($filteredparams); $total = count($entries); - $metadata['filter_count'] = $total; + if(in_array('filter_count',$meta_param) || in_array('*',$meta_param)){ + $metadata['filter_count'] = $total; + } } - + $limit = $limit < 1 ? $rows : $limit; $pages = $total ? ceil($total / $limit) : 1; $page = $page > $pages ? $pages : ($page && $offset >= 0 ? (floor($offset / $limit) + 1) : $page); @@ -1046,20 +1054,23 @@ public function createMetadataPagination(array $metadata = [], array $params = [ $last = ($pages < 2) ? null : (($pages - 1) * $limit); } - $metadata = array_merge($metadata, [ - "limit" => $limit, - "offset" => $offset, - "page" => $page, - "page_count" => $pages, - "links" => [ - "self" => $url, - "current" => "{$url}?" . urldecode(http_build_query(array_merge($params, ["page" => $page]))), - "next" => $next > 0 && $page < $pages ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $next, "page" => $page + 1])))) : null, - "previous" => $previous >= 0 && $page > 1 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $previous, "page" => $page - 1])))) : null, - "first" => $first >= 0 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $first, "page" => 1])))) : null, - "last" => $last > 0 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $last, "page" => $pages])))) : null - ] - ]); + + if(in_array('page',$meta_param) || in_array('*',$meta_param)) { + $metadata = array_merge($metadata, [ + "limit" => $limit, + "offset" => $offset, + "page" => $page, + "page_count" => $pages, + "links" => [ + "self" => $url, + "current" => "{$url}?" . urldecode(http_build_query(array_merge($params, ["page" => $page]))), + "next" => $next > 0 && $page < $pages ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $next, "page" => $page + 1])))) : null, + "previous" => $previous >= 0 && $page > 1 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $previous, "page" => $page - 1])))) : null, + "first" => $first >= 0 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $first, "page" => 1])))) : null, + "last" => $last > 0 ? ("{$url}?" . urldecode(http_build_query(array_merge($params, ["offset" => $last, "page" => $pages])))) : null + ] + ]); + } return $metadata; } diff --git a/src/core/Directus/Filesystem/Files.php b/src/core/Directus/Filesystem/Files.php index 70855b61e3..62936d6a43 100644 --- a/src/core/Directus/Filesystem/Files.php +++ b/src/core/Directus/Filesystem/Files.php @@ -33,7 +33,8 @@ class Files private $defaults = [ 'description' => '', 'tags' => '', - 'location' => '' + 'location' => '', + 'charset' => '' ]; /** @@ -591,7 +592,7 @@ public function uniqueName($fileName, $targetPath = null, $attempt = 0) if (preg_match($trailingDigit, $fileName, $matches)) { // Convert "fname-1.jpg" to "fname-2.jpg" - $attempt = 1 + (int)$matches[1]; + $attempt = 1 + (int) $matches[1]; $newName = preg_replace( $trailingDigit, filename_put_ext("-{$attempt}", $ext), diff --git a/src/core/Directus/GraphQL/FieldsConfig.php b/src/core/Directus/GraphQL/FieldsConfig.php index ccee314af1..72b475ca9c 100644 --- a/src/core/Directus/GraphQL/FieldsConfig.php +++ b/src/core/Directus/GraphQL/FieldsConfig.php @@ -1,4 +1,5 @@ getRelation('translation', $v['collection'], $v['field']); + $fields[$v['field']] = Types::listOf(Types::userCollection($relation['collection_many'])); + break; case 'user_created': case 'user_updated': $fields[$v['field']] = Types::directusUser(); @@ -232,6 +237,14 @@ private function getRelation($type, $collection, $field) $relation = $firstRelation; } + break; + case 'translation': + foreach ($relationsData['data'] as $k => $v) { + if ($v['collection_one'] == $collection && $v['field_one'] == $field) { + $relation = $v; + break; + } + } break; } diff --git a/src/core/Directus/Services/MailService.php b/src/core/Directus/Services/MailService.php index 067871bb0a..22f9eb8560 100644 --- a/src/core/Directus/Services/MailService.php +++ b/src/core/Directus/Services/MailService.php @@ -11,7 +11,6 @@ use Directus\Util\ArrayUtils; use Directus\Validator\Validator; use Zend\Db\Sql\Predicate\In; -use function Directus\get_project_config; class MailService extends AbstractService { @@ -163,7 +162,7 @@ protected function getFrom($defaultEmail = false) /** @var Acl $acl */ $acl = $this->container->get('acl'); if ($defaultEmail) { - $config = get_project_config(); + $config = $this->container->get('config'); return [ $config->get('mail.default.from') => $acl->getUserFullName() ]; diff --git a/src/core/Directus/Services/UsersService.php b/src/core/Directus/Services/UsersService.php index 92a6a5605c..a314367b91 100644 --- a/src/core/Directus/Services/UsersService.php +++ b/src/core/Directus/Services/UsersService.php @@ -10,6 +10,7 @@ use Directus\Database\Exception\ItemNotFoundException; use Directus\Database\Schema\SchemaManager; use Directus\Database\TableGateway\DirectusUsersTableGateway; +use Directus\Database\TableGateway\DirectusActivityTableGateway; use Directus\Database\TableGateway\RelationalTableGateway; use Directus\Exception\ForbiddenException; use Directus\Exception\ForbiddenLastAdminException; @@ -65,8 +66,21 @@ public function update($id, array $payload, array $params = []) // Fetch the entry even if it's not "published" $params['status'] = '*'; + $oldRecord = $this->find( + $id, + ArrayUtils::omit($params, $this->itemsService::SINGLE_ITEM_PARAMS_BLACKLIST) + ); $newRecord = $tableGateway->updateRecord($id, $payload, $this->getCRUDParams($params)); + if(!is_null(ArrayUtils::get($payload, $status->getName()))){ + $activityTableGateway = $this->createTableGateway(SchemaManager::COLLECTION_ACTIVITY); + $activityTableGateway->recordAction( + $id, + SchemaManager::COLLECTION_USERS, + DirectusActivityTableGateway::ACTION_UPDATE_USER_STATUS + ); + } + try { $item = $this->find( $newRecord->getId(), diff --git a/src/helpers/app.php b/src/helpers/app.php index 38a5e9b407..7c54b220b9 100644 --- a/src/helpers/app.php +++ b/src/helpers/app.php @@ -10,6 +10,10 @@ use Directus\Application\Http\Response; use Directus\Collection\Collection; use Directus\Config\Config; +use Directus\Config\Context; +use Directus\Config\Schema\Schema; +use Directus\Config\Exception\UnknownProjectException; +use Directus\Config\Exception\InvalidProjectException; use Directus\Exception\Exception; use Directus\Exception\UnauthorizedException; use Slim\Http\Body; @@ -83,11 +87,25 @@ function get_project_config($name = null, $basePath = null) return $configs[$configFilePath]; } - if (!file_exists($configFilePath)) { - throw new Exception('Unknown environment: ' . $name); + $config = []; + $schema = Schema::get(); + + if (getenv("DIRECTUS_USE_ENV") === "1") { + if ($name !== "_") { + throw new InvalidProjectException(); + } + $configFilePath = "__env__"; + $configData = $schema->value(Context::from_env()); + } else { + if (!file_exists($configFilePath)) { + throw new UnknownProjectException($name); + } + $configData = $schema->value([ + "directus" => Context::from_file($configFilePath) + ]); } - $config = new Config(require $configFilePath); + $config = new Config($configData); $configs[$configFilePath] = $config; return $config; diff --git a/src/web.php b/src/web.php index b23d17493d..745ca58fa2 100644 --- a/src/web.php +++ b/src/web.php @@ -1,5 +1,9 @@ value(Context::from_env()); + } else { + $configData = $schema->value([]); + } + return \Directus\create_unknown_project_app($basePath, $configData); } -$configFilePath = \Directus\create_config_path($basePath, $projectName); -if (!file_exists($configFilePath)) { - http_response_code(404); + +$maintenanceFlagPath = \Directus\create_maintenanceflag_path($basePath); +if (file_exists($maintenanceFlagPath)) { + http_response_code(503); header('Content-Type: application/json'); echo json_encode([ 'error' => [ - 'error' => 8, - 'message' => 'API Environment Configuration Not Found: ' . $projectName + 'code' => 21, + 'message' => 'This API instance is currently down for maintenance. Please try again later.' ] ]); exit; } -$maintenanceFlagPath = \Directus\create_maintenanceflag_path($basePath); -if (file_exists($maintenanceFlagPath)) { - http_response_code(503); +try { + $app = \Directus\create_app_with_project_name($basePath, $projectName); +} catch (ErrorException $e) { + http_response_code($e->getStatusCode()); header('Content-Type: application/json'); echo json_encode([ 'error' => [ - 'error' => 21, - 'message' => 'This API instance is currently down for maintenance. Please try again later.' + 'code' => $e->getCode(), + 'message' => $e->getMessage() ] ]); exit; } -$app = \Directus\create_app($basePath, require $configFilePath); - // ---------------------------------------------------------------------------- // @@ -80,8 +90,21 @@ $container = $app->getContainer(); -\Directus\register_global_hooks($app); -\Directus\register_extensions_hooks($app); +try { + \Directus\register_global_hooks($app); + \Directus\register_extensions_hooks($app); +} catch (ErrorException $e) { + http_response_code($e->getStatusCode()); + header('Content-Type: application/json'); + echo json_encode([ + 'error' => [ + 'code' => $e->getCode(), + 'message' => $e->getMessage() + ] + ]); + exit; +} + $app->getContainer()->get('hook_emitter')->run('application.boot', $app); @@ -210,8 +233,8 @@ \Directus\create_group_route_from_array($this, $name, $endpoints); } }) - ->add($middleware['auth']) - ->add($middleware['table_gateway']); + ->add($middleware['auth']) + ->add($middleware['table_gateway']); $this->group('/pages', function () { $endpointsList = \Directus\get_custom_endpoints('public/extensions/core/pages', true); diff --git a/vendor/autoload.php b/vendor/autoload.php index 957ccc2ac9..3182752400 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInitfef58229980f2c70347e4b7e9e7c5e1c::getLoader(); +return ComposerAutoloaderInit3edaef8c9924f8c649845b93fd58dbe9::getLoader(); diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 68b86ebcfa..339b9aef0e 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -132,6 +132,7 @@ 'Directus\\Authentication\\Exception\\UserInactiveException' => $baseDir . '/src/core/Directus/Authentication/Exception/UserInactiveException.php', 'Directus\\Authentication\\Exception\\UserNotAuthenticatedException' => $baseDir . '/src/core/Directus/Authentication/Exception/UserNotAuthenticatedException.php', 'Directus\\Authentication\\Exception\\UserNotFoundException' => $baseDir . '/src/core/Directus/Authentication/Exception/UserNotFoundException.php', + 'Directus\\Authentication\\Exception\\UserSuspendedException' => $baseDir . '/src/core/Directus/Authentication/Exception/UserSuspendedException.php', 'Directus\\Authentication\\Exception\\UserWithEmailNotFoundException' => $baseDir . '/src/core/Directus/Authentication/Exception/UserWithEmailNotFoundException.php', 'Directus\\Authentication\\Provider' => $baseDir . '/src/core/Directus/Authentication/Provider.php', 'Directus\\Authentication\\Sso\\AbstractSocialProvider' => $baseDir . '/src/core/Directus/Authentication/Sso/AbstractSocialProvider.php', @@ -150,14 +151,26 @@ 'Directus\\Authentication\\User\\User' => $baseDir . '/src/core/Directus/Authentication/User/User.php', 'Directus\\Authentication\\User\\UserInterface' => $baseDir . '/src/core/Directus/Authentication/User/UserInterface.php', 'Directus\\Cache\\Cache' => $baseDir . '/src/core/Directus/Cache/Cache.php', + 'Directus\\Cache\\Exception\\InvalidCacheAdapterException' => $baseDir . '/src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php', + 'Directus\\Cache\\Exception\\InvalidCacheConfigurationException' => $baseDir . '/src/core/Directus/Cache/Exception/InvalidCacheConfigurationException.php', 'Directus\\Cache\\Response' => $baseDir . '/src/core/Directus/Cache/Response.php', 'Directus\\Collection\\Arrayable' => $baseDir . '/src/core/Directus/Collection/Arrayable.php', 'Directus\\Collection\\Collection' => $baseDir . '/src/core/Directus/Collection/Collection.php', 'Directus\\Collection\\CollectionInterface' => $baseDir . '/src/core/Directus/Collection/CollectionInterface.php', 'Directus\\Config\\Config' => $baseDir . '/src/core/Directus/Config/Config.php', 'Directus\\Config\\ConfigInterface' => $baseDir . '/src/core/Directus/Config/ConfigInterface.php', + 'Directus\\Config\\Context' => $baseDir . '/src/core/Directus/Config/Context.php', + 'Directus\\Config\\Exception\\InvalidProjectException' => $baseDir . '/src/core/Directus/Config/Exception/InvalidProjectException.php', 'Directus\\Config\\Exception\\InvalidStatusException' => $baseDir . '/src/core/Directus/Config/Exception/InvalidStatusException.php', 'Directus\\Config\\Exception\\InvalidValueException' => $baseDir . '/src/core/Directus/Config/Exception/InvalidValueException.php', + 'Directus\\Config\\Exception\\UnknownProjectException' => $baseDir . '/src/core/Directus/Config/Exception/UnknownProjectException.php', + 'Directus\\Config\\Schema\\Base' => $baseDir . '/src/core/Directus/Config/Schema/Base.php', + 'Directus\\Config\\Schema\\Exception\\OmitException' => $baseDir . '/src/core/Directus/Config/Schema/Exception/OmitException.php', + 'Directus\\Config\\Schema\\Group' => $baseDir . '/src/core/Directus/Config/Schema/Group.php', + 'Directus\\Config\\Schema\\Node' => $baseDir . '/src/core/Directus/Config/Schema/Node.php', + 'Directus\\Config\\Schema\\Schema' => $baseDir . '/src/core/Directus/Config/Schema/Schema.php', + 'Directus\\Config\\Schema\\Types' => $baseDir . '/src/core/Directus/Config/Schema/Types.php', + 'Directus\\Config\\Schema\\Value' => $baseDir . '/src/core/Directus/Config/Schema/Value.php', 'Directus\\Config\\StatusItem' => $baseDir . '/src/core/Directus/Config/StatusItem.php', 'Directus\\Config\\StatusMapping' => $baseDir . '/src/core/Directus/Config/StatusMapping.php', 'Directus\\Console\\Cli' => $baseDir . '/src/core/Directus/Console/Cli.php', diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 3c7759f027..528f30f32e 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInitfef58229980f2c70347e4b7e9e7c5e1c +class ComposerAutoloaderInit3edaef8c9924f8c649845b93fd58dbe9 { private static $loader; @@ -19,15 +19,15 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInitfef58229980f2c70347e4b7e9e7c5e1c', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInit3edaef8c9924f8c649845b93fd58dbe9', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInitfef58229980f2c70347e4b7e9e7c5e1c', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit3edaef8c9924f8c649845b93fd58dbe9', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::getInitializer($loader)); } else { $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { @@ -39,19 +39,19 @@ public static function getLoader() $loader->register(true); if ($useStaticLoader) { - $includeFiles = Composer\Autoload\ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::$files; + $includeFiles = Composer\Autoload\ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { - composerRequirefef58229980f2c70347e4b7e9e7c5e1c($fileIdentifier, $file); + composerRequire3edaef8c9924f8c649845b93fd58dbe9($fileIdentifier, $file); } return $loader; } } -function composerRequirefef58229980f2c70347e4b7e9e7c5e1c($fileIdentifier, $file) +function composerRequire3edaef8c9924f8c649845b93fd58dbe9($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 77d4227ee9..3438cd99da 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c +class ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9 { public static $files = array ( '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', @@ -453,6 +453,7 @@ class ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c 'Directus\\Authentication\\Exception\\UserInactiveException' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Exception/UserInactiveException.php', 'Directus\\Authentication\\Exception\\UserNotAuthenticatedException' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Exception/UserNotAuthenticatedException.php', 'Directus\\Authentication\\Exception\\UserNotFoundException' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Exception/UserNotFoundException.php', + 'Directus\\Authentication\\Exception\\UserSuspendedException' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Exception/UserSuspendedException.php', 'Directus\\Authentication\\Exception\\UserWithEmailNotFoundException' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Exception/UserWithEmailNotFoundException.php', 'Directus\\Authentication\\Provider' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Provider.php', 'Directus\\Authentication\\Sso\\AbstractSocialProvider' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/Sso/AbstractSocialProvider.php', @@ -471,14 +472,26 @@ class ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c 'Directus\\Authentication\\User\\User' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/User/User.php', 'Directus\\Authentication\\User\\UserInterface' => __DIR__ . '/../..' . '/src/core/Directus/Authentication/User/UserInterface.php', 'Directus\\Cache\\Cache' => __DIR__ . '/../..' . '/src/core/Directus/Cache/Cache.php', + 'Directus\\Cache\\Exception\\InvalidCacheAdapterException' => __DIR__ . '/../..' . '/src/core/Directus/Cache/Exception/InvalidCacheAdapterException.php', + 'Directus\\Cache\\Exception\\InvalidCacheConfigurationException' => __DIR__ . '/../..' . '/src/core/Directus/Cache/Exception/InvalidCacheConfigurationException.php', 'Directus\\Cache\\Response' => __DIR__ . '/../..' . '/src/core/Directus/Cache/Response.php', 'Directus\\Collection\\Arrayable' => __DIR__ . '/../..' . '/src/core/Directus/Collection/Arrayable.php', 'Directus\\Collection\\Collection' => __DIR__ . '/../..' . '/src/core/Directus/Collection/Collection.php', 'Directus\\Collection\\CollectionInterface' => __DIR__ . '/../..' . '/src/core/Directus/Collection/CollectionInterface.php', 'Directus\\Config\\Config' => __DIR__ . '/../..' . '/src/core/Directus/Config/Config.php', 'Directus\\Config\\ConfigInterface' => __DIR__ . '/../..' . '/src/core/Directus/Config/ConfigInterface.php', + 'Directus\\Config\\Context' => __DIR__ . '/../..' . '/src/core/Directus/Config/Context.php', + 'Directus\\Config\\Exception\\InvalidProjectException' => __DIR__ . '/../..' . '/src/core/Directus/Config/Exception/InvalidProjectException.php', 'Directus\\Config\\Exception\\InvalidStatusException' => __DIR__ . '/../..' . '/src/core/Directus/Config/Exception/InvalidStatusException.php', 'Directus\\Config\\Exception\\InvalidValueException' => __DIR__ . '/../..' . '/src/core/Directus/Config/Exception/InvalidValueException.php', + 'Directus\\Config\\Exception\\UnknownProjectException' => __DIR__ . '/../..' . '/src/core/Directus/Config/Exception/UnknownProjectException.php', + 'Directus\\Config\\Schema\\Base' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Base.php', + 'Directus\\Config\\Schema\\Exception\\OmitException' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Exception/OmitException.php', + 'Directus\\Config\\Schema\\Group' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Group.php', + 'Directus\\Config\\Schema\\Node' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Node.php', + 'Directus\\Config\\Schema\\Schema' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Schema.php', + 'Directus\\Config\\Schema\\Types' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Types.php', + 'Directus\\Config\\Schema\\Value' => __DIR__ . '/../..' . '/src/core/Directus/Config/Schema/Value.php', 'Directus\\Config\\StatusItem' => __DIR__ . '/../..' . '/src/core/Directus/Config/StatusItem.php', 'Directus\\Config\\StatusMapping' => __DIR__ . '/../..' . '/src/core/Directus/Config/StatusMapping.php', 'Directus\\Console\\Cli' => __DIR__ . '/../..' . '/src/core/Directus/Console/Cli.php', @@ -2609,10 +2622,10 @@ class ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::$prefixDirsPsr4; - $loader->prefixesPsr0 = ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::$prefixesPsr0; - $loader->classMap = ComposerStaticInitfef58229980f2c70347e4b7e9e7c5e1c::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::$prefixesPsr0; + $loader->classMap = ComposerStaticInit3edaef8c9924f8c649845b93fd58dbe9::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index aad20c4d73..c40ec118f1 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -668,35 +668,39 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.5.2", - "version_normalized": "1.5.2.0", + "version": "1.6.1", + "version_normalized": "1.6.1.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "9f83dded91781a01c63574e387eaa769be769115" + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", - "reference": "9f83dded91781a01c63574e387eaa769be769115", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", "shasum": "" }, "require": { "php": ">=5.4.0", "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5" + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { + "ext-zlib": "*", "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, - "time": "2018-12-04T20:46:45+00:00", + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2019-07-01T23:21:34+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } }, "installation-source": "dist", @@ -1779,27 +1783,27 @@ }, { "name": "ralouphie/getallheaders", - "version": "2.0.5", - "version_normalized": "2.0.5.0", + "version": "3.0.3", + "version_normalized": "3.0.3.0", "source": { "type": "git", "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "~3.7.0", - "satooshi/php-coveralls": ">=1.0" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, - "time": "2016-02-11T07:05:27+00:00", + "time": "2019-03-08T08:55:37+00:00", "type": "library", "installation-source": "dist", "autoload": { diff --git a/vendor/guzzlehttp/psr7/.editorconfig b/vendor/guzzlehttp/psr7/.editorconfig deleted file mode 100644 index 677e36e295..0000000000 --- a/vendor/guzzlehttp/psr7/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/vendor/guzzlehttp/psr7/CHANGELOG.md b/vendor/guzzlehttp/psr7/CHANGELOG.md index 27b65f0951..8a3743dba5 100644 --- a/vendor/guzzlehttp/psr7/CHANGELOG.md +++ b/vendor/guzzlehttp/psr7/CHANGELOG.md @@ -10,6 +10,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [1.6.0] + +### Added + +- Allowed version `^3.0` of `ralouphie/getallheaders` dependency (#244) +- Added MIME type for WEBP image format (#246) +- Added more validation of values according to PSR-7 and RFC standards, e.g. status code range (#250, #272) + +### Changed + +- Tests don't pass with HHVM 4.0, so HHVM support got dropped. Other libraries like composer have done the same. (#262) +- Accept port number 0 to be valid (#270) + +### Fixed + +- Fixed subsequent reads from `php://input` in ServerRequest (#247) +- Fixed readable/writable detection for certain stream modes (#248) +- Fixed encoding of special characters in the `userInfo` component of an URI (#253) + + ## [1.5.2] - 2018-12-04 ### Fixed @@ -209,7 +229,8 @@ Currently unsupported: -[Unreleased]: https://github.com/guzzle/psr7/compare/1.5.2...HEAD +[Unreleased]: https://github.com/guzzle/psr7/compare/1.6.0...HEAD +[1.6.0]: https://github.com/guzzle/psr7/compare/1.5.2...1.6.0 [1.5.2]: https://github.com/guzzle/psr7/compare/1.5.1...1.5.2 [1.5.1]: https://github.com/guzzle/psr7/compare/1.5.0...1.5.1 [1.5.0]: https://github.com/guzzle/psr7/compare/1.4.2...1.5.0 diff --git a/vendor/guzzlehttp/psr7/composer.json b/vendor/guzzlehttp/psr7/composer.json index 2add153ec4..168a055b06 100644 --- a/vendor/guzzlehttp/psr7/composer.json +++ b/vendor/guzzlehttp/psr7/composer.json @@ -18,14 +18,18 @@ "require": { "php": ">=5.4.0", "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5" + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" }, "require-dev": { - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8", + "ext-zlib": "*" }, "provide": { "psr/http-message-implementation": "1.0" }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, "autoload": { "psr-4": { "GuzzleHttp\\Psr7\\": "src/" @@ -39,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } } } diff --git a/vendor/guzzlehttp/psr7/src/LimitStream.php b/vendor/guzzlehttp/psr7/src/LimitStream.php index 3c13d4f411..e4f239e30f 100644 --- a/vendor/guzzlehttp/psr7/src/LimitStream.php +++ b/vendor/guzzlehttp/psr7/src/LimitStream.php @@ -72,7 +72,7 @@ public function seek($offset, $whence = SEEK_SET) { if ($whence !== SEEK_SET || $offset < 0) { throw new \RuntimeException(sprintf( - 'Cannot seek to offset % with whence %s', + 'Cannot seek to offset %s with whence %s', $offset, $whence )); diff --git a/vendor/guzzlehttp/psr7/src/MessageTrait.php b/vendor/guzzlehttp/psr7/src/MessageTrait.php index 1e4da649ad..a7966d10cf 100644 --- a/vendor/guzzlehttp/psr7/src/MessageTrait.php +++ b/vendor/guzzlehttp/psr7/src/MessageTrait.php @@ -66,11 +66,8 @@ public function getHeaderLine($header) public function withHeader($header, $value) { - if (!is_array($value)) { - $value = [$value]; - } - - $value = $this->trimHeaderValues($value); + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; @@ -85,11 +82,8 @@ public function withHeader($header, $value) public function withAddedHeader($header, $value) { - if (!is_array($value)) { - $value = [$value]; - } - - $value = $this->trimHeaderValues($value); + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; @@ -144,11 +138,13 @@ private function setHeaders(array $headers) { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { - if (!is_array($value)) { - $value = [$value]; + if (is_int($header)) { + // Numeric array keys are converted to int by PHP but having a header name '123' is not forbidden by the spec + // and also allowed in withHeader(). So we need to cast it to string again for the following assertion to pass. + $header = (string) $header; } - - $value = $this->trimHeaderValues($value); + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; @@ -160,6 +156,19 @@ private function setHeaders(array $headers) } } + private function normalizeHeaderValue($value) + { + if (!is_array($value)) { + return $this->trimHeaderValues([$value]); + } + + if (count($value) === 0) { + throw new \InvalidArgumentException('Header value can not be an empty array.'); + } + + return $this->trimHeaderValues($value); + } + /** * Trims whitespace from the header values. * @@ -177,7 +186,28 @@ private function setHeaders(array $headers) private function trimHeaderValues(array $values) { return array_map(function ($value) { - return trim($value, " \t"); + if (!is_scalar($value) && null !== $value) { + throw new \InvalidArgumentException(sprintf( + 'Header value must be scalar or null but %s provided.', + is_object($value) ? get_class($value) : gettype($value) + )); + } + + return trim((string) $value, " \t"); }, $values); } + + private function assertHeader($header) + { + if (!is_string($header)) { + throw new \InvalidArgumentException(sprintf( + 'Header name must be a string but %s provided.', + is_object($header) ? get_class($header) : gettype($header) + )); + } + + if ($header === '') { + throw new \InvalidArgumentException('Header name can not be empty.'); + } + } } diff --git a/vendor/guzzlehttp/psr7/src/Request.php b/vendor/guzzlehttp/psr7/src/Request.php index 0006642406..59f337db11 100644 --- a/vendor/guzzlehttp/psr7/src/Request.php +++ b/vendor/guzzlehttp/psr7/src/Request.php @@ -36,6 +36,7 @@ public function __construct( $body = null, $version = '1.1' ) { + $this->assertMethod($method); if (!($uri instanceof UriInterface)) { $uri = new Uri($uri); } @@ -91,6 +92,7 @@ public function getMethod() public function withMethod($method) { + $this->assertMethod($method); $new = clone $this; $new->method = strtoupper($method); return $new; @@ -139,4 +141,11 @@ private function updateHostFromUri() // See: http://tools.ietf.org/html/rfc7230#section-5.4 $this->headers = [$header => [$host]] + $this->headers; } + + private function assertMethod($method) + { + if (!is_string($method) || $method === '') { + throw new \InvalidArgumentException('Method must be a non-empty string.'); + } + } } diff --git a/vendor/guzzlehttp/psr7/src/Response.php b/vendor/guzzlehttp/psr7/src/Response.php index 6e72c06b8c..e7e04d86a6 100644 --- a/vendor/guzzlehttp/psr7/src/Response.php +++ b/vendor/guzzlehttp/psr7/src/Response.php @@ -93,11 +93,11 @@ public function __construct( $version = '1.1', $reason = null ) { - if (filter_var($status, FILTER_VALIDATE_INT) === false) { - throw new \InvalidArgumentException('Status code must be an integer value.'); - } + $this->assertStatusCodeIsInteger($status); + $status = (int) $status; + $this->assertStatusCodeRange($status); - $this->statusCode = (int) $status; + $this->statusCode = $status; if ($body !== '' && $body !== null) { $this->stream = stream_for($body); @@ -125,12 +125,30 @@ public function getReasonPhrase() public function withStatus($code, $reasonPhrase = '') { + $this->assertStatusCodeIsInteger($code); + $code = (int) $code; + $this->assertStatusCodeRange($code); + $new = clone $this; - $new->statusCode = (int) $code; + $new->statusCode = $code; if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { $reasonPhrase = self::$phrases[$new->statusCode]; } $new->reasonPhrase = $reasonPhrase; return $new; } + + private function assertStatusCodeIsInteger($statusCode) + { + if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { + throw new \InvalidArgumentException('Status code must be an integer value.'); + } + } + + private function assertStatusCodeRange($statusCode) + { + if ($statusCode < 100 || $statusCode >= 600) { + throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); + } + } } diff --git a/vendor/guzzlehttp/psr7/src/ServerRequest.php b/vendor/guzzlehttp/psr7/src/ServerRequest.php index 99f453a51d..1a09a6c87c 100644 --- a/vendor/guzzlehttp/psr7/src/ServerRequest.php +++ b/vendor/guzzlehttp/psr7/src/ServerRequest.php @@ -168,7 +168,7 @@ public static function fromGlobals() $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; $headers = getallheaders(); $uri = self::getUriFromGlobals(); - $body = new LazyOpenStream('php://input', 'r+'); + $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); diff --git a/vendor/guzzlehttp/psr7/src/Stream.php b/vendor/guzzlehttp/psr7/src/Stream.php index 111795eb03..d9e7409c7c 100644 --- a/vendor/guzzlehttp/psr7/src/Stream.php +++ b/vendor/guzzlehttp/psr7/src/Stream.php @@ -10,6 +10,17 @@ */ class Stream implements StreamInterface { + /** + * Resource modes. + * + * @var string + * + * @see http://php.net/manual/function.fopen.php + * @see http://php.net/manual/en/function.gzopen.php + */ + const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/'; + const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; + private $stream; private $size; private $seekable; @@ -18,22 +29,6 @@ class Stream implements StreamInterface private $uri; private $customMetadata; - /** @var array Hash of readable and writable stream types */ - private static $readWriteHash = [ - 'read' => [ - 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, - 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, - 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, - 'x+t' => true, 'c+t' => true, 'a+' => true, 'rb+' => true, - ], - 'write' => [ - 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, - 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 'rb+' => true, - 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, - 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true - ] - ]; - /** * This constructor accepts an associative array of options. * @@ -65,8 +60,8 @@ public function __construct($stream, $options = []) $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; - $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); - $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); + $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); + $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); $this->uri = $this->getMetadata('uri'); } @@ -197,6 +192,8 @@ public function rewind() public function seek($offset, $whence = SEEK_SET) { + $whence = (int) $whence; + if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } diff --git a/vendor/guzzlehttp/psr7/src/Uri.php b/vendor/guzzlehttp/psr7/src/Uri.php index 36219568c8..825a25eedb 100644 --- a/vendor/guzzlehttp/psr7/src/Uri.php +++ b/vendor/guzzlehttp/psr7/src/Uri.php @@ -437,9 +437,9 @@ public function withScheme($scheme) public function withUserInfo($user, $password = null) { - $info = $user; - if ($password != '') { - $info .= ':' . $password; + $info = $this->filterUserInfoComponent($user); + if ($password !== null) { + $info .= ':' . $this->filterUserInfoComponent($password); } if ($this->userInfo === $info) { @@ -537,7 +537,9 @@ private function applyParts(array $parts) $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : ''; - $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; + $this->userInfo = isset($parts['user']) + ? $this->filterUserInfoComponent($parts['user']) + : ''; $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : ''; @@ -554,7 +556,7 @@ private function applyParts(array $parts) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { - $this->userInfo .= ':' . $parts['pass']; + $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']); } $this->removeDefaultPort(); @@ -576,6 +578,26 @@ private function filterScheme($scheme) return strtolower($scheme); } + /** + * @param string $component + * + * @return string + * + * @throws \InvalidArgumentException If the user info is invalid. + */ + private function filterUserInfoComponent($component) + { + if (!is_string($component)) { + throw new \InvalidArgumentException('User info must be a string'); + } + + return preg_replace_callback( + '/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $component + ); + } + /** * @param string $host * @@ -606,9 +628,9 @@ private function filterPort($port) } $port = (int) $port; - if (1 > $port || 0xffff < $port) { + if (0 > $port || 0xffff < $port) { throw new \InvalidArgumentException( - sprintf('Invalid port: %d. Must be between 1 and 65535', $port) + sprintf('Invalid port: %d. Must be between 0 and 65535', $port) ); } diff --git a/vendor/guzzlehttp/psr7/src/functions.php b/vendor/guzzlehttp/psr7/src/functions.php index 957bfb42ae..8e6dafe68e 100644 --- a/vendor/guzzlehttp/psr7/src/functions.php +++ b/vendor/guzzlehttp/psr7/src/functions.php @@ -724,6 +724,7 @@ function mimetype_from_extension($extension) 'txt' => 'text/plain', 'wav' => 'audio/x-wav', 'webm' => 'video/webm', + 'webp' => 'image/webp', 'wma' => 'audio/x-ms-wma', 'wmv' => 'video/x-ms-wmv', 'woff' => 'application/x-font-woff', diff --git a/vendor/ralouphie/getallheaders/.travis.yml b/vendor/ralouphie/getallheaders/.travis.yml deleted file mode 100644 index f45b55fa0c..0000000000 --- a/vendor/ralouphie/getallheaders/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - 7.0 - -before_script: - - composer install - -script: - - mkdir -p build/logs - - php vendor/bin/phpunit -c phpunit.xml - -after_script: - - php vendor/bin/coveralls -v \ No newline at end of file diff --git a/vendor/ralouphie/getallheaders/README.md b/vendor/ralouphie/getallheaders/README.md index f3329d6638..9430d76bbc 100644 --- a/vendor/ralouphie/getallheaders/README.md +++ b/vendor/ralouphie/getallheaders/README.md @@ -14,6 +14,14 @@ This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/f ## Install +For PHP version **`>= 5.6`**: + ``` composer require ralouphie/getallheaders ``` + +For PHP version **`< 5.6`**: + +``` +composer require ralouphie/getallheaders "^2" +``` diff --git a/vendor/ralouphie/getallheaders/composer.json b/vendor/ralouphie/getallheaders/composer.json index 5a0d595c99..de8ce62e45 100644 --- a/vendor/ralouphie/getallheaders/composer.json +++ b/vendor/ralouphie/getallheaders/composer.json @@ -9,13 +9,18 @@ } ], "require": { - "php": ">=5.3" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "~3.7.0", - "satooshi/php-coveralls": ">=1.0" + "phpunit/phpunit": "^5 || ^6.5", + "php-coveralls/php-coveralls": "^2.1" }, "autoload": { "files": ["src/getallheaders.php"] + }, + "autoload-dev": { + "psr-4": { + "getallheaders\\Tests\\": "tests/" + } } -} \ No newline at end of file +} diff --git a/vendor/ralouphie/getallheaders/phpunit.xml b/vendor/ralouphie/getallheaders/phpunit.xml deleted file mode 100644 index 7255b23d94..0000000000 --- a/vendor/ralouphie/getallheaders/phpunit.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - ./tests - - - - - src - - - - - - - - \ No newline at end of file diff --git a/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php b/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php deleted file mode 100644 index 8e3d1790af..0000000000 --- a/vendor/ralouphie/getallheaders/tests/GetAllHeadersTest.php +++ /dev/null @@ -1,121 +0,0 @@ - $val) { - $_SERVER[$key] = $val; - } - $result = getallheaders(); - $this->assertEquals($expected, $result, "Error testing $test_type works."); - } - - public function testWorksData() - { - return array( - array( - 'normal case', - array( - 'Key-One' => 'foo', - 'Key-Two' => 'bar', - 'Another-Key-For-Testing' => 'baz' - ), - array( - 'HTTP_KEY_ONE' => 'foo', - 'HTTP_KEY_TWO' => 'bar', - 'HTTP_ANOTHER_KEY_FOR_TESTING' => 'baz' - ) - ), - array( - 'Content-Type', - array( - 'Content-Type' => 'two' - ), - array( - 'HTTP_CONTENT_TYPE' => 'one', - 'CONTENT_TYPE' => 'two' - ) - ), - array( - 'Content-Length', - array( - 'Content-Length' => '222' - ), - array( - 'CONTENT_LENGTH' => '222', - 'HTTP_CONTENT_LENGTH' => '111' - ) - ), - array( - 'Content-Length (HTTP_CONTENT_LENGTH only)', - array( - 'Content-Length' => '111' - ), - array( - 'HTTP_CONTENT_LENGTH' => '111' - ) - ), - array( - 'Content-MD5', - array( - 'Content-Md5' => 'aef123' - ), - array( - 'CONTENT_MD5' => 'aef123', - 'HTTP_CONTENT_MD5' => 'fea321' - ) - ), - array( - 'Content-MD5 (HTTP_CONTENT_MD5 only)', - array( - 'Content-Md5' => 'f123' - ), - array( - 'HTTP_CONTENT_MD5' => 'f123' - ) - ), - array( - 'Authorization (normal)', - array( - 'Authorization' => 'testing' - ), - array( - 'HTTP_AUTHORIZATION' => 'testing', - ) - ), - array( - 'Authorization (redirect)', - array( - 'Authorization' => 'testing redirect' - ), - array( - 'REDIRECT_HTTP_AUTHORIZATION' => 'testing redirect', - ) - ), - array( - 'Authorization (PHP_AUTH_USER + PHP_AUTH_PW)', - array( - 'Authorization' => 'Basic ' . base64_encode('foo:bar') - ), - array( - 'PHP_AUTH_USER' => 'foo', - 'PHP_AUTH_PW' => 'bar' - ) - ), - array( - 'Authorization (PHP_AUTH_DIGEST)', - array( - 'Authorization' => 'example-digest' - ), - array( - 'PHP_AUTH_DIGEST' => 'example-digest' - ) - ) - ); - } -}