| 1 |
<?php |
| 2 |
namespace WordPressdotorg\Plugin_Check\Checks; |
| 3 |
use const WordPressdotorg\Plugin_Check\{ PLUGIN_DIR, HAS_VENDOR }; |
| 4 |
use WordPressdotorg\Plugin_Check\{Error, Guideline_Violation, Message, Notice, Warning}; |
| 5 |
use WordPressdotorg\Plugin_Check\PHPCS; |
| 6 |
|
| 7 |
include PLUGIN_DIR . '/inc/class-php-cli.php'; |
| 8 |
include PLUGIN_DIR . '/inc/class-phpcs.php'; |
| 9 |
|
| 10 |
class PHPCS_Checks extends Check_Base { |
| 11 |
|
| 12 |
const NOTICE_TYPES = [ |
| 13 |
// This should be an Error, but this is triggered for all variablse with SQL which isn't always a problem. |
| 14 |
//'WordPress.DB.PreparedSQL.InterpolatedNotPrepared' => Warning::class, |
| 15 |
]; |
| 16 |
|
| 17 |
public function check_against_phpcs() { |
| 18 |
if ( ! HAS_VENDOR ) { |
| 19 |
return new Notice( |
| 20 |
'phpcs_not_tested', |
| 21 |
__( 'PHP Code Sniffer rulesets have not been tested, as the vendor directory is missing. Perhaps you need to run <code>`composer install`</code>.', 'plugin-check' ) |
| 22 |
); |
| 23 |
} |
| 24 |
|
| 25 |
return $this->run_phpcs_standard( |
| 26 |
__DIR__ . '/phpcs/plugin-check.xml' |
| 27 |
); |
| 28 |
} |
| 29 |
|
| 30 |
public function check_against_phpcs_review() { |
| 31 |
if ( ! HAS_VENDOR ) { |
| 32 |
return new Notice( |
| 33 |
'phpcs_not_tested', |
| 34 |
__( 'PHP Code Sniffer rulesets have not been tested, as the vendor directory is missing. Perhaps you need to run <code>`composer install`</code>.', 'plugin-check' ) |
| 35 |
); |
| 36 |
} |
| 37 |
|
| 38 |
return $this->run_phpcs_standard( |
| 39 |
__DIR__ . '/phpcs/plugin-check-needs-review.xml' |
| 40 |
); |
| 41 |
} |
| 42 |
|
| 43 |
protected function run_phpcs_standard( string $standard, array $args = [] ) { |
| 44 |
$phpcs = new PHPCS(); |
| 45 |
$phpcs->set_standard( $standard ); |
| 46 |
|
| 47 |
$args = wp_parse_args( |
| 48 |
$args, |
| 49 |
array( |
| 50 |
'extensions' => 'php', // Only check php files. |
| 51 |
's' => true, // Show the name of the sniff triggering a violation. |
| 52 |
// --ignore-annotations |
| 53 |
) |
| 54 |
); |
| 55 |
|
| 56 |
$report = $phpcs->run_json_report( |
| 57 |
$this->path, |
| 58 |
$args, |
| 59 |
'array' |
| 60 |
); |
| 61 |
|
| 62 |
if ( is_wp_error( $report ) ) { |
| 63 |
return new Error( |
| 64 |
$report->get_error_code(), |
| 65 |
$report->get_error_message() |
| 66 |
); |
| 67 |
} |
| 68 |
|
| 69 |
// If no response, either malformed output or PHP encountered an error. |
| 70 |
if ( ! $report || empty( $report['files'] ) ) { |
| 71 |
return false; |
| 72 |
} |
| 73 |
|
| 74 |
return $this->phpcs_result_to_warnings( $report ); |
| 75 |
} |
| 76 |
|
| 77 |
protected function phpcs_result_to_warnings( $result ) { |
| 78 |
$return = []; |
| 79 |
|
| 80 |
array_walk( $result['files'], function( $output, $filename ) use( &$return ) { |
| 81 |
if ( ! $output['messages'] ) { |
| 82 |
return; |
| 83 |
} |
| 84 |
|
| 85 |
// Ignore the column, and just use the Error + Line number. |
| 86 |
$messages = []; |
| 87 |
foreach ( $output['messages'] as &$message ) { |
| 88 |
$messages[ $message['source'] . ':' . $message['line'] ] = $message; |
| 89 |
} |
| 90 |
|
| 91 |
foreach ( $messages as $message ) { |
| 92 |
switch( strtoupper( $message['type'] ) ) { |
| 93 |
case 'ERROR': |
| 94 |
$notice_class = Error::class; |
| 95 |
break; |
| 96 |
case 'WARNING': |
| 97 |
$notice_class = Warning::class; |
| 98 |
break; |
| 99 |
case 'INFO': |
| 100 |
case 'NOTICE': |
| 101 |
$notice_class = Notice::class; |
| 102 |
break; |
| 103 |
default: |
| 104 |
$notice_class = Message::class; |
| 105 |
} |
| 106 |
|
| 107 |
// Allow for individual notices to be overridden. |
| 108 |
if ( isset( self::NOTICE_TYPES[ $message['source'] ] ) ) { |
| 109 |
$notice_class = self::NOTICE_TYPES[ $message['source'] ]; |
| 110 |
} |
| 111 |
|
| 112 |
$source_code = esc_html( trim( file( $this->path . '/' . $filename )[ $message['line'] - 1 ] ) ); |
| 113 |
|
| 114 |
if ( current_user_can( 'edit_plugins' ) ) { |
| 115 |
$edit_link = sprintf( |
| 116 |
'<a href="%1$s" title="%2$s" aria-label="%2$s" target="_blank">%3$s</a>', |
| 117 |
$this->get_file_editor_url( $filename, $message['line'] ), |
| 118 |
sprintf( |
| 119 |
/* translators: %s is the path to a plugin file. */ |
| 120 |
esc_attr__( 'View %s in the plugin file editor.', 'plugin-check' ), |
| 121 |
$this->slug . '/' . $filename |
| 122 |
), |
| 123 |
esc_html__( 'View in code editor', 'plugin-check' ) |
| 124 |
); |
| 125 |
} |
| 126 |
|
| 127 |
$return[] = new $notice_class( |
| 128 |
$message['source'], |
| 129 |
sprintf( |
| 130 |
/* translators: 1: Type of Error 2: Line 3: File 4: Message 5: Code Example 6: Edit Link */ |
| 131 |
__( '%1$s Line %2$d of file %3$s.<br>%4$s.<br>%5$s%6$s', 'plugin-check' ), |
| 132 |
"<strong>{$message['source']}</strong>", |
| 133 |
$message['line'], |
| 134 |
$filename, |
| 135 |
rtrim( $message['message'], '.' ), |
| 136 |
"<pre class='wp-plugin-check-code'><code>{$source_code}</code></pre>", |
| 137 |
$edit_link ?? '' |
| 138 |
) |
| 139 |
); |
| 140 |
} |
| 141 |
} ); |
| 142 |
|
| 143 |
return $return; |
| 144 |
} |
| 145 |
|
| 146 |
/** |
| 147 |
* Get the URL for opening the plugin file in an external editor. |
| 148 |
* |
| 149 |
* @since 0.2.1 |
| 150 |
* |
| 151 |
* @param array $filename Source of PHPCS error. |
| 152 |
* @param array $line Line number of PHPCS error. |
| 153 |
* |
| 154 |
* @return string|null File editor URL or null if not available. |
| 155 |
*/ |
| 156 |
private function get_file_editor_url( $filename, $line ) { |
| 157 |
if ( ! isset( $filename, $line ) ) { |
| 158 |
return null; |
| 159 |
} |
| 160 |
|
| 161 |
$edit_url = null; |
| 162 |
|
| 163 |
/** |
| 164 |
* Filters the template for the URL for linking to an external editor to open a file for editing. |
| 165 |
* |
| 166 |
* Users of IDEs that support opening files in via web protocols can use this filter to override |
| 167 |
* the edit link to result in their editor opening rather than the plugin editor. |
| 168 |
* |
| 169 |
* The initial filtered value is null, requiring extension plugins to supply the URL template |
| 170 |
* string themselves. If no template string is provided, links to the plugin editors will |
| 171 |
* be provided if available. For example, for an extension plugin to cause file edit links to |
| 172 |
* open in an IDE, the following filters can be used: |
| 173 |
* |
| 174 |
* # PhpStorm |
| 175 |
* add_filter( 'plugin_check_validation_error_source_file_editor_url_template', function () { |
| 176 |
* return 'phpstorm://open?file={{file}}&line={{line}}'; |
| 177 |
* } ); |
| 178 |
* |
| 179 |
* # VS Code |
| 180 |
* add_filter( 'plugin_check_validation_error_source_file_editor_url_template', function () { |
| 181 |
* return 'vscode://file/{{file}}:{{line}}'; |
| 182 |
* } ); |
| 183 |
* |
| 184 |
* For a template to be considered, the string '{{file}}' must be present in the filtered value. |
| 185 |
* |
| 186 |
* @since 0.2.1 |
| 187 |
* |
| 188 |
* @param string|null $editor_url_template Editor URL template. |
| 189 |
*/ |
| 190 |
$editor_url_template = apply_filters( 'plugin_check_validation_error_source_file_editor_url_template', null ); |
| 191 |
|
| 192 |
// Supply the file path to the editor template. |
| 193 |
if ( null !== $editor_url_template && str_contains( $editor_url_template, '{{file}}' ) ) { |
| 194 |
$file_path = WP_PLUGIN_DIR . '/' . $this->slug; |
| 195 |
if ( $this->slug !== $filename ) { |
| 196 |
$file_path .= '/' . $filename; |
| 197 |
} |
| 198 |
|
| 199 |
if ( $file_path && file_exists( $file_path ) ) { |
| 200 |
/** |
| 201 |
* Filters the file path to be opened in an external editor for a given PHPCS error source. |
| 202 |
* |
| 203 |
* This is useful to map the file path from inside of a Docker container or VM to the host machine. |
| 204 |
* |
| 205 |
* @since 0.2.1 |
| 206 |
* |
| 207 |
* @param string|null $editor_url_template Editor URL template. |
| 208 |
* @param array $source Source information. |
| 209 |
*/ |
| 210 |
$file_path = apply_filters( 'plugin_check_validation_error_source_file_path', $file_path, array( $this->slug, $filename, $line) ); |
| 211 |
if ( $file_path ) { |
| 212 |
$edit_url = str_replace( |
| 213 |
[ |
| 214 |
'{{file}}', |
| 215 |
'{{line}}', |
| 216 |
], |
| 217 |
[ |
| 218 |
rawurlencode( $file_path ), |
| 219 |
rawurlencode( $line ), |
| 220 |
], |
| 221 |
$editor_url_template |
| 222 |
); |
| 223 |
} |
| 224 |
|
| 225 |
} |
| 226 |
} |
| 227 |
|
| 228 |
// Fall back to using the plugin editor if no external editor is offered. |
| 229 |
if ( ! $edit_url ) { |
| 230 |
$plugin_data = get_plugins( '/' . $this->slug ); |
| 231 |
|
| 232 |
return esc_url( |
| 233 |
add_query_arg( |
| 234 |
[ |
| 235 |
'plugin' => rawurlencode( $this->slug . '/' . array_key_first( $plugin_data ) ), |
| 236 |
'file' => rawurlencode( $this->slug . '/' . $filename ), |
| 237 |
'line' => rawurlencode( $line ), |
| 238 |
], |
| 239 |
admin_url( 'plugin-editor.php' ) |
| 240 |
) |
| 241 |
); |
| 242 |
} |
| 243 |
|
| 244 |
return $edit_url; |
| 245 |
} |
| 246 |
} |