diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index d415d813d..86f9bdcc4 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: shivammathur/setup-php@v2 with: php-version: 7.1 coverage: none @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: shivammathur/setup-php@v2 with: php-version: 8.0 coverage: none diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index e129028ce..b0692d716 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: shivammathur/setup-php@v2 with: php-version: 7.4 coverage: none - run: composer install --no-progress --prefer-dist - - run: composer phpstan + - run: composer phpstan -- --no-progress continue-on-error: true # is only informative diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa8766a20..73516285b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,8 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src - - run: php coveralls.phar --verbose --config tests/.coveralls.yml + - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar + - env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: php php-coveralls.phar --verbose --config tests/.coveralls.yml diff --git a/.travis.yml b/.travis.yml index 1c6bfc910..dbbb861e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ jobs: - stage: Static Analysis (informative) php: 7.4 script: - - composer run-script phpstan + - composer phpstan -- --no-progress - stage: Code Coverage diff --git a/composer.json b/composer.json index 300527ade..696841b3a 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "nette/forms": "^3.0", "nette/robot-loader": "^3.2", "nette/security": "^3.0", - "latte/latte": "^2.6.2", + "latte/latte": "^2.9.1", "tracy/tracy": "^2.6", "mockery/mockery": "^1.0", "phpstan/phpstan-nette": "^0.12" @@ -39,7 +39,7 @@ "conflict": { "nette/di": "<3.0-stable", "nette/forms": "<3.0", - "latte/latte": "<2.6", + "latte/latte": "<2.6 >=3.0", "tracy/tracy": "<2.5" }, "autoload": { diff --git a/readme.md b/readme.md index ba652fff5..e3a65eaab 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,6 @@ Nette Application MVC ===================== -[](https://packagist.org/packages/nette/application) -[](https://travis-ci.org/nette/application) -[](https://coveralls.io/github/nette/application?branch=master) -[](https://github.com/nette/application/releases) -[](https://github.com/nette/application/blob/master/license.md) - Model-View-Controller is a software architecture that was created to satisfy the need to separate utility code (controller) from application logic code (model) and from code for displaying data (view) in applications with graphical user interface. With this approach we make the application better understandable, simplify future development and enable testing each unit of the application separately. Please, [see documentation](https://doc.nette.org/application). diff --git a/src/Application/MicroPresenter.php b/src/Application/MicroPresenter.php index c71867689..3dce130bd 100644 --- a/src/Application/MicroPresenter.php +++ b/src/Application/MicroPresenter.php @@ -76,10 +76,10 @@ public function run(Application\Request $request): Application\IResponse $params = $request->getParameters(); $callback = $params['callback'] ?? null; - if (!$callback instanceof \Closure) { + if (!is_object($callback) || !is_callable($callback)) { throw new Application\BadRequestException('Parameter callback is not a valid closure.'); } - $reflection = new \ReflectionFunction($callback); + $reflection = Nette\Utils\Callback::toReflection($callback); if ($this->context) { foreach ($reflection->getParameters() as $param) { diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index d27a821ab..c07bb5eaf 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -78,14 +78,7 @@ public function getPresenterClass(string &$name): string throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is abstract."); } - $this->cache[$name] = $class; - - if ($name !== ($realName = $this->unformatPresenterClass($class))) { - trigger_error("Case mismatch on presenter name '$name', correct name is '$realName'.", E_USER_WARNING); - $name = $realName; - } - - return $class; + return $this->cache[$name] = $class; } diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index d3e258a1b..654c36938 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -149,7 +149,7 @@ public function loadState(array $params): void "Value passed to persistent parameter '%s' in %s must be %s, %s given.", $name, $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", - $meta['type'] === 'NULL' ? 'scalar' : $meta['type'], + $meta['type'], is_object($params[$name]) ? get_class($params[$name]) : gettype($params[$name]) )); } diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 8a7270f26..ee13ae908 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -55,7 +55,7 @@ public function getPersistentParams(string $class = null): array ) { $params[$name] = [ 'def' => $default, - 'type' => Nette\Utils\Reflection::getPropertyType($rp) ?: gettype($default), + 'type' => self::getPropertyType($rp, $default), 'since' => $isPresenter ? Nette\Utils\Reflection::getPropertyDeclaringClass($rp)->getName() : null, ]; } @@ -120,7 +120,7 @@ public function saveState(Component $component, array &$params): void "Value passed to persistent parameter '%s' in %s must be %s, %s given.", $name, $component instanceof Presenter ? 'presenter ' . $component->getName() : "component '{$component->getUniqueId()}'", - $meta['type'] === 'NULL' ? 'scalar' : $meta['type'], + $meta['type'], is_object($params[$name]) ? get_class($params[$name]) : gettype($params[$name]) )); } @@ -165,13 +165,13 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, array $a 'Argument $%s passed to %s() must be %s, %s given.', $name, ($method instanceof \ReflectionMethod ? $method->getDeclaringClass()->getName() . '::' : '') . $method->getName(), - $type === 'NULL' ? 'scalar' : $type, + $type, is_object($args[$name]) ? get_class($args[$name]) : gettype($args[$name]) )); } } elseif ($param->isDefaultValueAvailable()) { $res[$i] = $param->getDefaultValue(); - } elseif ($type === 'NULL' || $param->allowsNull()) { + } elseif ($type === 'scalar' || $param->allowsNull()) { $res[$i] = null; } elseif ($type === 'array' || $type === 'iterable') { $res[$i] = []; @@ -190,12 +190,26 @@ public static function combineArgs(\ReflectionFunctionAbstract $method, array $a /** * Non data-loss type conversion. */ - public static function convertType(&$val, string $type): bool + public static function convertType(&$val, string $types): bool + { + foreach (explode('|', $types) as $type) { + if (self::convertSingleType($val, $type)) { + return true; + } + } + return false; + } + + + /** + * Non data-loss type conversion. + */ + private static function convertSingleType(&$val, string $type): bool { static $builtin = [ 'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, - 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, - 'boolean' => 1, 'integer' => 1, 'double' => 1, 'NULL' => 1, + 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, + 'boolean' => 1, 'integer' => 1, 'double' => 1, 'scalar' => 1, ]; if (empty($builtin[$type])) { @@ -207,12 +221,15 @@ public static function convertType(&$val, string $type): bool } elseif ($type === 'callable') { return false; - } elseif ($type === 'NULL') { // means 'not array' + } elseif ($type === 'scalar') { return !is_array($val); } elseif ($type === 'array' || $type === 'iterable') { return is_array($val); + } elseif ($type === 'mixed') { + return true; + } elseif (!is_scalar($val)) { // array, resource, null, etc. return false; @@ -256,9 +273,20 @@ public static function parseAnnotation(\Reflector $ref, string $name): ?array public static function getParameterType(\ReflectionParameter $param): string { - return $param->hasType() - ? $param->getType()->getName() - : gettype($param->isDefaultValueAvailable() ? $param->getDefaultValue() : null); + $default = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + $type = $param->getType(); + return $type + ? ($type instanceof \ReflectionNamedType ? $type->getName() : (string) $type) + : ($default === null ? 'scalar' : gettype($default)); + } + + + public static function getPropertyType(\ReflectionProperty $prop, $default): string + { + $type = PHP_VERSION_ID < 70400 ? null : $prop->getType(); + return $type + ? ($type instanceof \ReflectionNamedType ? $type->getName() : (string) $type) + : ($default === null ? 'scalar' : gettype($default)); } diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index 2e7ceca03..0d986c7fc 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -21,7 +21,7 @@ class Form extends Nette\Forms\Form implements ISignalReceiver public $onAnchor; /** @var bool */ - private $sameSiteProtection = true; + protected $crossOrigin = false; /** @@ -103,9 +103,16 @@ public function isAnchored(): bool /** * Disables CSRF protection using a SameSite cookie. */ + public function allowCrossOrigin(): void + { + $this->crossOrigin = true; + } + + + /** @deprecated use allowCrossOrigin() */ public function disableSameSiteProtection(): void { - $this->sameSiteProtection = false; + $this->crossOrigin = true; } @@ -153,7 +160,7 @@ public function signalReceived(string $signal): void $class = static::class; throw new BadSignalException("Missing handler for signal '$signal' in $class."); - } elseif ($this->sameSiteProtection && !$this->getPresenter()->getHttpRequest()->isSameSite()) { + } elseif (!$this->crossOrigin && !$this->getPresenter()->getHttpRequest()->isSameSite()) { $this->getPresenter()->detectedCsrf(); } elseif (!$this->getPresenter()->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) { diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index f3a12f4dc..56f0616aa 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -214,7 +214,7 @@ public function run(Application\Request $request): Application\IResponse // autoload components foreach ($this->globalParams as $id => $foo) { - $this->getComponent($id, false); + $this->getComponent((string) $id, false); } if ($this->autoCanonicalize) { @@ -1015,7 +1015,7 @@ public static function argsToParams( if ( !$param->isDefaultValueAvailable() && !$param->allowsNull() - && $type !== 'NULL' + && $type !== 'scalar' && $type !== 'array' && $type !== 'iterable' ) { @@ -1030,7 +1030,7 @@ public static function argsToParams( 'Argument $%s passed to %s() must be %s, %s given.', $name, $rm->getDeclaringClass()->getName() . '::' . $rm->getName(), - $type === 'NULL' ? 'scalar' : $type, + $type, is_object($args[$name]) ? get_class($args[$name]) : gettype($args[$name]) )); } @@ -1265,12 +1265,12 @@ private function initGlobalParameters(): void $pos = strrpos($param, '-'); if ($pos) { $this->signalReceiver = substr($param, 0, $pos); - $this->signal = substr($param, $pos + 1); + $this->signal = (string) substr($param, $pos + 1); } else { $this->signalReceiver = $this->getUniqueId(); $this->signal = $param; } - if ($this->signal == null) { // intentionally == + if ($this->signal === '') { $this->signal = null; } } diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index f48e348d6..493375b1a 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -58,7 +58,10 @@ public function getConfigSchema(): Nette\Schema\Schema 'errorPresenter' => Expect::string('Nette:Error')->dynamic(), 'catchExceptions' => Expect::bool(!$this->debugMode)->dynamic(), 'mapping' => Expect::arrayOf('string|array'), - 'scanDirs' => Expect::anyOf(Expect::arrayOf('string'), false)->default($this->scanDirs), + 'scanDirs' => Expect::anyOf( + Expect::arrayOf('string')->default($this->scanDirs), + false + )->default($this->scanDirs), 'scanComposer' => Expect::bool(class_exists(ClassLoader::class)), 'scanFilter' => Expect::string('Presenter'), 'silentLinks' => Expect::bool(), diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index d8c0646bd..3f46d7300 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -95,9 +95,9 @@ public function createTemplate(UI\Control $control = null): UI\ITemplate trigger_error('Filter |null is deprecated.', E_USER_DEPRECATED); }); $latte->addFilter('modifyDate', function ($time, $delta, $unit = null) { - return $time == null // intentionally == - ? null - : Nette\Utils\DateTime::from($time)->modify($delta . $unit); + return $time + ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) + : null; }); if (!isset($latte->getFilters()['translate'])) { diff --git a/tests/.coveralls.yml b/tests/.coveralls.yml index 82764a3f5..845038250 100644 --- a/tests/.coveralls.yml +++ b/tests/.coveralls.yml @@ -1,4 +1,4 @@ # for php-coveralls -service_name: travis-ci +service_name: github-actions coverage_clover: coverage.xml json_path: coverage.json diff --git a/tests/Bridges.Latte/UIMacros.renderSnippets.phpt b/tests/Bridges.Latte/UIMacros.renderSnippets.phpt index 022177f85..c52fb1438 100644 --- a/tests/Bridges.Latte/UIMacros.renderSnippets.phpt +++ b/tests/Bridges.Latte/UIMacros.renderSnippets.phpt @@ -65,7 +65,7 @@ Assert::same([ 'snippet--array2-2' => 'Value 2', 'snippet--array2-3' => 'Value 3', 'snippet--includeSay' => 'Hello include snippet', - 'snippet--nested1' => "\t
Included file #3 (A, B)