├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── ContainerHandler.php ├── LICENSE ├── Plugin.php ├── README.md ├── RenderArrayTainter.php ├── codeception.yml ├── composer.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── pretest.sh ├── scripts ├── PsalmDrupalKernel.php ├── autoload.php ├── dump_script.php └── generate_entrypoint.php ├── stubs ├── .gitkeep ├── sanitizers.php ├── sinks.php └── sources.php └── tests ├── _output └── .gitignore ├── _support ├── AcceptanceTester.php ├── FunctionalTester.php ├── Helper │ ├── Acceptance.php │ ├── Functional.php │ └── Unit.php ├── UnitTester.php └── _generated │ └── .gitignore ├── acceptance.suite.yml ├── acceptance └── PsalmPluginDrupal.feature ├── functional.suite.yml ├── functional └── .gitkeep ├── unit.suite.yml └── unit └── .gitkeep /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mortenson 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: '8.1' 17 | tools: composer:v2 18 | coverage: none 19 | env: 20 | fail-fast: true 21 | 22 | - name: Cache Composer packages 23 | id: composer-cache 24 | uses: actions/cache@v2 25 | with: 26 | path: vendor 27 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-php- 30 | 31 | - name: Install dependencies 32 | run: composer install --prefer-dist --no-progress --no-suggest 33 | 34 | - name: Check code style 35 | run: composer run-script cs-check 36 | 37 | - name: Run test suite 38 | run: composer run-script test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .phpunit.result.cache 3 | composer.lock 4 | vendor 5 | tests/_output/* 6 | tests/_run/* 7 | tests/_tmp/* 8 | -------------------------------------------------------------------------------- /ContainerHandler.php: -------------------------------------------------------------------------------- 1 | getMethodId() != 'Drupal::service') { 32 | return; 33 | } 34 | 35 | if (!self::$containerMeta) { 36 | return; 37 | } 38 | 39 | $expr = $event->getExpr(); 40 | if ($expr->args[0]->value instanceof String_) { 41 | $serviceId = $expr->args[0]->value->value; 42 | } elseif ($expr->args[0]->value instanceof ClassConstFetch) { 43 | $serviceId = (string) $expr->args[0]->value->class->getAttribute('resolvedName'); 44 | } else { 45 | return; 46 | } 47 | 48 | $service = self::$containerMeta->get($serviceId); 49 | if ($service) { 50 | $class = $service->getClass(); 51 | if ($class) { 52 | $event->getCodebase()->classlikes->addFullyQualifiedClassName($class); 53 | $event->setReturnTypeCandidate(new Union([new TNamedObject($class)])); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | “Commons Clause” License Condition v1.0 2 | 3 | The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. 4 | 5 | Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. 6 | 7 | For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. 8 | 9 | Software: psalm-plugin-drupal (https://github.com/mortenson/psalm-plugin-drupal) 10 | License: LGPL 2.1 (GNU Lesser General Public License, Version 2.1) 11 | Licensor: Samuel Mortenson (https://mortenson.coffee) 12 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | registerHooksFromClass(RenderArrayTainter::class); 21 | 22 | foreach ($this->getStubFiles() as $file) { 23 | $registration->addStubFile($file); 24 | } 25 | 26 | if (!$config) { 27 | return; 28 | } 29 | 30 | if (isset($config->containerXml)) { 31 | ContainerHandler::init(new ContainerMeta((array) $config->containerXml)); 32 | } 33 | 34 | $registration->registerHooksFromClass(ContainerHandler::class); 35 | 36 | // Add all .theme/.module files for now. Really messy part of core. 37 | $directory = new RecursiveDirectoryIterator('.'); 38 | $files = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) { 39 | if ($current->getFilename()[0] === '.') { 40 | return false; 41 | } 42 | if ($current->isDir()) { 43 | $excluded_dirs = '/(tests|node_modules|bower_components|vendor|files)/'; 44 | return !preg_match($excluded_dirs, $current->getPathname()); 45 | } 46 | return preg_match('/(themes|modules)\/.*(\.module|\.theme)$/', $current->getPathname()); 47 | }); 48 | foreach (new RecursiveIteratorIterator($files) as $file) { 49 | $registration->addStubFile($file->getPathname()); 50 | } 51 | } 52 | 53 | /** @return list */ 54 | private function getStubFiles(): array 55 | { 56 | return glob(__DIR__ . '/stubs/*.php') ?: []; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test Status](https://github.com/mortenson/psalm-plugin-drupal/actions/workflows/tests.yml/badge.svg) 2 | 3 | # psalm-plugin-drupal 4 | 5 | A Drupal integration for Psalm focused on security scanning (SAST) taint 6 | analysis. 7 | 8 | ## Features 9 | 10 | - Stubs for sinks, sources, and sanitizers 11 | - Loading of `.module` and `.theme` files 12 | - Autoloading of modules without an installed site 13 | - Support for `\Drupal::service()` 14 | - Custom script for dumping the Drupal container to XML 15 | - Support for detecting tainted render arrays 16 | - Novel support for Controllers and Form methods. 17 | 18 | ## Installing and running on your Drupal site 19 | 20 | This plugin is meant to be used on your Drupal site, for the scanning of custom 21 | modules. Note that if you follow this guide and run it on a contrib module, and 22 | you find a valid result, you should report your findings to the Drupal Security 23 | Team. 24 | 25 | To install the plugin: 26 | 27 | 1. Run `composer require mortenson/psalm-plugin-drupal:dev-master` 28 | 2. Change directories to the root of your Drupal installation (ex: `cd web`, `cd docroot`). 29 | 3. Create a `psalm.xml` file in the root of your Drupal installation like: 30 | ```xml 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | DrupalContainerDump.xml 50 | 51 | 52 | DrupalContainerDump.xml 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ``` 62 | 4. Run `php ../vendor/mortenson/psalm-plugin-drupal/scripts/dump_script.php && ../vendor/bin/psalm .` 63 | 64 | Note that the path to `vendor` may change based on your Drupal installation. 65 | 66 | ### Generating an entrypoint for seemingly unused class methods 67 | 68 | Drupal's code paths aren't always clear, especially in Drupal 8. Because of 69 | this, things like Controller methods (aka route callbacks) will not be 70 | analyzed when running Psalm. 71 | 72 | To have Psalm analyze these paths, you'll need to generate an entrypoint file 73 | that executes the methods you want to test. 74 | 75 | A script has been included for you to generate this entrypoint for you. To use 76 | it, do the following: 77 | 78 | 1. Run `php ../vendor/mortenson/psalm-plugin-drupal/scripts/generate_entrypoint.php ` 79 | 2. Add `` to your 80 | `psalm.xml` file, under the `` node. 81 | 3. Run Psalm. 82 | 83 | Currently, only `routing.yml` files are parsed to generate the entrypoint, 84 | focusing on Controller and Form methods. 85 | 86 | ## Contributing 87 | 88 | ### Running and writing tests 89 | 90 | Tests use Codeception via [weirdan/codeception-psalm-module]. 91 | 92 | You can run tests with `composer run test`. 93 | 94 | To write tests, edit tests/acceptance/PsalmPluginDrupal.feature and add a new 95 | Scenario. 96 | 97 | To run a single failing test, add the `@failing` tag above the `Scenario:` 98 | line, then run `composer run test-failing`. 99 | 100 | ### Checking code style 101 | 102 | Code style should be checked before committing code. 103 | 104 | To do this, run `composer run cs-check`, or `composer run cs-fix` to 105 | automatically fix issues with `phpcbf`. 106 | 107 | [weirdan/codeception-psalm-module]: https://github.com/weirdan/codeception-psalm-module 108 | -------------------------------------------------------------------------------- /RenderArrayTainter.php: -------------------------------------------------------------------------------- 1 | getExpr(); 15 | $statements_analyzer = $event->getStatementsSource(); 16 | if (!($item instanceof ArrayItem) || !($statements_analyzer instanceof StatementsAnalyzer)) { 17 | return []; 18 | } 19 | $item_key_value = ''; 20 | if ($item->key) { 21 | if ($item_key_type = $statements_analyzer->node_data->getType($item->key)) { 22 | $key_type = $item_key_type; 23 | 24 | if ($key_type->isSingleStringLiteral()) { 25 | $item_key_value = $key_type->getSingleStringLiteral()->value; 26 | } 27 | } 28 | } 29 | 30 | $dangerous_keys = [ 31 | // Code execution. 32 | '#access_callback', 33 | '#ajax', 34 | '#after_build', 35 | '#element_validate', 36 | '#lazy_builder', 37 | '#post_render', 38 | '#pre_render', 39 | '#process', 40 | '#submit', 41 | '#validate', 42 | '#value_callback', 43 | '#file_value_callbacks', 44 | '#date_date_callbacks', 45 | '#date_time_callbacks', 46 | '#captcha_validate', 47 | // Cross site scripting. 48 | '#template', 49 | '#children', 50 | ]; 51 | 52 | if ( 53 | isset($item_key_value[0]) 54 | && $item_key_value[0] === '#' 55 | && !in_array($item_key_value, $dangerous_keys, true) 56 | ) { 57 | // We could/should use a custom taint type here. 58 | return ['html']; 59 | } 60 | 61 | return []; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mortenson/psalm-plugin-drupal", 3 | "description": "Psalm support for Drupal security analysis.", 4 | "type": "psalm-plugin", 5 | "license": "LGPL-2.1-only", 6 | "authors": [ 7 | { 8 | "name": "Sam Mortenson", 9 | "email": "samuel@mortenson.coffee" 10 | } 11 | ], 12 | "require": { 13 | "vimeo/psalm": "^5", 14 | "psalm/plugin-symfony": "^5.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9.0", 18 | "squizlabs/php_codesniffer": "^3.3", 19 | "codeception/codeception": "^4.1", 20 | "codeception/module-phpbrowser": "^1.0.0", 21 | "codeception/module-asserts": "^1.0.0", 22 | "weirdan/codeception-psalm-module": "dev-master" 23 | }, 24 | "extra": { 25 | "psalm": { 26 | "pluginClass": "mortenson\\PsalmPluginDrupal\\Plugin" 27 | } 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "mortenson\\PsalmPluginDrupal\\": "." 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "mortenson\\PsalmPluginDrupal\\Tests\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "cs-check": "phpcs", 41 | "cs-fix": "phpcbf", 42 | "test": "./pretest.sh && ./vendor/bin/codecept run -v", 43 | "test-failing": "./pretest.sh && ./vendor/bin/codecept run -v --group failing", 44 | "test-clean": "rm -rf ./tests/_tmp" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Plugin.php 9 | ContainerHandler.php 10 | RenderArrayTainter.php 11 | scripts 12 | tests/_support 13 | stubs 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pretest.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | if [ -d "./tests/_tmp/drupal" ]; then 6 | exit 0 7 | fi 8 | 9 | mkdir -p ./tests/_tmp 10 | cd ./tests/_tmp 11 | wget https://ftp.drupal.org/files/projects/drupal-10.0.0.tar.gz 12 | tar -xf drupal-10.0.0.tar.gz 13 | mv drupal-10.0.0 drupal 14 | cd drupal 15 | php ../../../scripts/dump_script.php node 16 | -------------------------------------------------------------------------------- /scripts/PsalmDrupalKernel.php: -------------------------------------------------------------------------------- 1 | xpath('//pluginClass[@class="mortenson\PsalmPluginDrupal\Plugin"]/extensions/module') as $node) { 19 | $attributes = $node->attributes(); 20 | if (!empty($attributes['name'])) { 21 | $modules[] = (string) $attributes['name']; 22 | } 23 | } 24 | return $modules; 25 | } 26 | 27 | public function compilePsalmContainer($extra_modules = []) 28 | { 29 | foreach ($this->getPsalmModuleList() as $module) { 30 | $this->moduleList[$module] = $module; 31 | } 32 | foreach ($extra_modules as $module) { 33 | $this->moduleList[$module] = $module; 34 | } 35 | return $this->compileContainer(); 36 | } 37 | 38 | public function dumpContainerXml($extra_modules = []) 39 | { 40 | $container = $this->compilePsalmContainer($extra_modules); 41 | $dumper = new XmlDumper($container); 42 | $dump = $dumper->dump(); 43 | file_put_contents('./DrupalContainerDump.xml', $dump); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/autoload.php: -------------------------------------------------------------------------------- 1 | ['class' => '\Drupal\Component\FileCache\NullFileCache']]); 10 | $request = Request::createFromGlobals(); 11 | $kernel = PsalmDrupalKernel::createFromRequest($request, $autoloader, 'prod'); 12 | $kernel->compilePsalmContainer(); 13 | -------------------------------------------------------------------------------- /scripts/dump_script.php: -------------------------------------------------------------------------------- 1 | ['class' => '\Drupal\Component\FileCache\NullFileCache']]); 10 | $request = Request::createFromGlobals(); 11 | $kernel = PsalmDrupalKernel::createFromRequest($request, $autoloader, 'prod'); 12 | 13 | $kernel->dumpContainerXml(explode(',', $argv[1] ?? '')); 14 | -------------------------------------------------------------------------------- /scripts/generate_entrypoint.php: -------------------------------------------------------------------------------- 1 | getFilename()[0] === '.') { 22 | return false; 23 | } 24 | if ($current->isDir()) { 25 | $excluded_dirs = '/(tests|node_modules|bower_components|vendor|files)/'; 26 | return !preg_match($excluded_dirs, $current->getPathname()); 27 | } 28 | return preg_match('/.*\.routing\.yml$/', $current->getPathname()); 29 | }); 30 | foreach (new RecursiveIteratorIterator($files) as $file) { 31 | $contents = file_get_contents($file->getPathname()); 32 | $routes = Yaml::decode($contents); 33 | foreach ($routes as $route) { 34 | if (isset($route['defaults']['_controller'])) { 35 | $parts = explode("::", $route['defaults']['_controller']); 36 | if (count($parts) !== 2) { 37 | continue; 38 | } 39 | $entrypoint .= ' 40 | $controller = new ' . $parts[0] . '(); 41 | $build = $controller->' . $parts[1] . '(); 42 | \Drupal::service("renderer")->render($build);'; 43 | } 44 | if (isset($route['defaults']['_form'])) { 45 | $entrypoint .= ' 46 | $form = new ' . $route['defaults']['_form'] . '(); 47 | $form_state = new \Drupal\Core\Form\FormState(); 48 | $build = $form->buildForm([], $form_state); 49 | \Drupal::service("renderer")->render($build);'; 50 | } 51 | } 52 | } 53 | } 54 | 55 | file_put_contents('psalm_drupal_entrypoint.module', $entrypoint); 56 | -------------------------------------------------------------------------------- /stubs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mortenson/psalm-plugin-drupal/dbd9bf2d3068de591f91ddfdc6d796ef4e06fa99/stubs/.gitkeep -------------------------------------------------------------------------------- /stubs/sanitizers.php: -------------------------------------------------------------------------------- 1 | writeToFile('tests/_tmp/drupal/composer.lock', '{}'); 28 | } 29 | 30 | /** 31 | * @When I run Psalm in Drupal 32 | */ 33 | public function iRunPsalmInDrupal(): void 34 | { 35 | $this->runShellCommand( 36 | 'cd tests/_tmp/drupal && php ../../../scripts/generate_entrypoint.php ../../../tests/_run' 37 | ); 38 | $this->runPsalmIn('tests/_tmp/drupal'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/_support/FunctionalTester.php: -------------------------------------------------------------------------------- 1 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | DrupalContainerDump.xml 34 | 35 | 36 | DrupalContainerDump.xml 37 | 38 | 39 | 40 | 41 | 42 | 43 | """ 44 | 45 | Scenario: ContainerHandler works 46 | Given I have the following code 47 | """ 48 | \Drupal::service('database')->query($_GET['input']); 49 | """ 50 | When I run Psalm in Drupal 51 | Then I see these errors 52 | | Type | Message | 53 | | TaintedSql | Detected tainted SQL | 54 | And I see no other errors 55 | And I see exit code 2 56 | 57 | Scenario: Database connection SQLi 58 | Given I have the following code 59 | """ 60 | \Drupal::database()->query($_GET['input']); 61 | """ 62 | When I run Psalm in Drupal 63 | Then I see these errors 64 | | Type | Message | 65 | | TaintedSql | Detected tainted SQL | 66 | And I see no other errors 67 | And I see exit code 2 68 | 69 | Scenario: Database condition SQLi 70 | Given I have the following code 71 | """ 72 | \Drupal::database()->select("node")->condition("title", "foo", $_GET['input']); 73 | """ 74 | When I run Psalm in Drupal 75 | Then I see these errors 76 | | Type | Message | 77 | | TaintedSql | Detected tainted SQL | 78 | And I see no other errors 79 | And I see exit code 2 80 | 81 | Scenario: Markup constructor XSS 82 | Given I have the following code 83 | """ 84 | new \Drupal\Core\StringTranslation\TranslatableMarkup($_GET['input']); 85 | """ 86 | When I run Psalm in Drupal 87 | Then I see these errors 88 | | Type | Message | 89 | | TaintedHtml | Detected tainted HTML | 90 | And I see no other errors 91 | And I see exit code 2 92 | 93 | Scenario: MarkupTrait XSS 94 | Given I have the following code 95 | """ 96 | \Drupal\Core\Render\Markup::create($_GET['input']); 97 | """ 98 | When I run Psalm in Drupal 99 | Then I see these errors 100 | | Type | Message | 101 | | TaintedHtml | Detected tainted HTML | 102 | And I see no other errors 103 | And I see exit code 2 104 | 105 | Scenario: Render array bad keys 106 | Given I have the following code 107 | """ 108 | $build = [ 109 | '#markup' => $_GET['input'], 110 | '#template' => $_GET['input'], 111 | ]; 112 | \Drupal::service('renderer')->render($build); 113 | """ 114 | When I run Psalm in Drupal 115 | Then I see these errors 116 | | Type | Message | 117 | | TaintedHtml | Detected tainted HTML | 118 | And I see no other errors 119 | And I see exit code 2 120 | 121 | Scenario: Render array safe keys 122 | Given I have the following code 123 | """ 124 | $build = [ 125 | '#markup' => $_GET['input'], 126 | ]; 127 | \Drupal::service('renderer')->render($build); 128 | """ 129 | When I run Psalm in Drupal 130 | Then I see no other errors 131 | And I see exit code 0 132 | 133 | Scenario: Render array from controller 134 | Given I have the following code in "my_module.routing.yml" 135 | """ 136 | my_module.page: 137 | path: '/my-module' 138 | defaults: 139 | _controller: '\Drupal\my_module\Controller\MyController::build' 140 | my_module.page_response: 141 | path: '/my-module-response' 142 | defaults: 143 | _controller: '\Drupal\my_module\Controller\MyController::buildResponse' 144 | """ 145 | Given I have the following code 146 | """ 147 | namespace Drupal\my_module\Controller; 148 | 149 | use Symfony\Component\HttpFoundation\Response; 150 | use Drupal\Core\Controller\ControllerBase; 151 | 152 | class MyController extends ControllerBase { 153 | 154 | /** 155 | * @return array 156 | */ 157 | public function build() { 158 | return [ 159 | '#markup' => $_GET['input'], 160 | '#template' => $_GET['input'], 161 | ]; 162 | } 163 | 164 | /** 165 | * @return \Symfony\Component\HttpFoundation\Response 166 | */ 167 | public function buildResponse() { 168 | return new Response(''); 169 | } 170 | 171 | } 172 | """ 173 | When I run Psalm in Drupal 174 | Then I see these errors 175 | | Type | Message | 176 | | TaintedHtml | Detected tainted HTML | 177 | And I see no other errors 178 | And I see exit code 2 179 | 180 | Scenario: Render array from form 181 | Given I have the following code in "my_module.routing.yml" 182 | """ 183 | my_module.form: 184 | path: '/my-module-form' 185 | defaults: 186 | _form: '\Drupal\my_module\Form\MyForm' 187 | """ 188 | Given I have the following code 189 | """ 190 | namespace Drupal\my_module\Form; 191 | 192 | use Symfony\Component\HttpFoundation\Response; 193 | use Drupal\Core\Form\FormBase; 194 | 195 | class MyForm extends FormBase { 196 | 197 | /** 198 | * @return array 199 | */ 200 | public function buildForm() { 201 | return [ 202 | '#markup' => $_GET['input'], 203 | '#template' => $_GET['input'], 204 | ]; 205 | } 206 | 207 | } 208 | """ 209 | When I run Psalm in Drupal 210 | Then I see these errors 211 | | Type | Message | 212 | | TaintedHtml | Detected tainted HTML | 213 | And I see no other errors 214 | And I see exit code 2 215 | 216 | Scenario: Node field source 217 | Given I have the following code 218 | """ 219 | /** @var \Drupal\node\Entity\Node $node */ 220 | $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); 221 | echo $node->get('title')->value; 222 | echo $node->title->value; 223 | echo $node->getTitle(); 224 | """ 225 | When I run Psalm in Drupal 226 | Then I see these errors 227 | | Type | Message | 228 | | TaintedHtml | Detected tainted HTML | 229 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 230 | | TaintedHtml | Detected tainted HTML | 231 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 232 | | TaintedHtml | Detected tainted HTML | 233 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 234 | And I see no other errors 235 | And I see exit code 2 236 | 237 | Scenario: Form state source 238 | Given I have the following code 239 | """ 240 | $form_state = new \Drupal\Core\Form\FormState(); 241 | echo $form_state->getUserInput()['foo']; 242 | echo $form_state->getValue('foo'); 243 | echo $form_state->getValues()['foo']; 244 | """ 245 | When I run Psalm in Drupal 246 | Then I see these errors 247 | | Type | Message | 248 | | TaintedHtml | Detected tainted HTML | 249 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 250 | | TaintedHtml | Detected tainted HTML | 251 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 252 | | TaintedHtml | Detected tainted HTML | 253 | | TaintedTextWithQuotes | Detected tainted text with possible quotes | 254 | And I see no other errors 255 | And I see exit code 2 256 | -------------------------------------------------------------------------------- /tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for functional tests 4 | # Emulate web requests and make application process them 5 | # Include one of framework modules (Symfony2, Yii2, Laravel5, Phalcon4) to use it 6 | # Remove this suite if you don't use frameworks 7 | 8 | actor: FunctionalTester 9 | modules: 10 | enabled: 11 | # add a framework module here 12 | - \Helper\Functional 13 | step_decorators: ~ 14 | -------------------------------------------------------------------------------- /tests/functional/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mortenson/psalm-plugin-drupal/dbd9bf2d3068de591f91ddfdc6d796ef4e06fa99/tests/functional/.gitkeep -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | actor: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit 10 | step_decorators: ~ 11 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mortenson/psalm-plugin-drupal/dbd9bf2d3068de591f91ddfdc6d796ef4e06fa99/tests/unit/.gitkeep --------------------------------------------------------------------------------