Created
September 13, 2016 13:17
-
-
Save JDGrimes/7a29ec88d533459345565ae3caabe7d2 to your computer and use it in GitHub Desktop.
WIP PHPCS sniff for functions that expect slashed data in WordPress
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Sniff for data that is expected to be slashed. | |
* | |
* Some WordPress core functions expect the data passed to them to be slashed, for | |
* legacy reasons. | |
* | |
* @package WordPress-Coding-Standards | |
* @since 0.10.0 | |
*/ | |
class WordPress_Sniffs_WP_ExpectedSlashedSniff extends WordPress_Sniff { | |
/** | |
* Casts that imply the data doesn't need to be slashed. | |
* | |
* @since 0.10.0 | |
* | |
* @var int[] | |
*/ | |
protected $slashed_casts = array( T_INT_CAST, T_DOUBLE_CAST, T_BOOL_CAST ); | |
/** | |
* Tokens that are implicitly slashed and so don't need to be flagged. | |
* | |
* @since 0.10.0 | |
* | |
* @var int[] | |
*/ | |
protected $slashed_tokens = array( | |
T_CONSTANT_ENCAPSED_STRING, | |
T_LNUMBER, | |
T_MINUS, | |
T_TRUE, | |
T_FALSE, | |
T_NULL, | |
); | |
/** | |
* The list functions and their args that are expected to be slashed. | |
* | |
* @since 0.10.0 | |
* | |
* @var array[] | |
*/ | |
public static $expectedSlashedFunctionArgs = array( | |
// Uses add_metadata(). | |
'add_comment_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses wp_unslash(). | |
'add_metadata' => array( 3 => 'meta_key', 4 => 'meta_value' ), | |
// Uses wp_unslash(). | |
'add_ping' => array( 2 => 'uri' ), | |
// Uses add_metadata(). | |
'add_post_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses add_metadata(). | |
'add_term_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses add_metadata(). | |
'add_user_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// These are directly interpolated into a database query. Failure to slash | |
// will result in SQL injection!! | |
'check_comment' => array( 1 => 'author', 2 => 'email' ), | |
// Uses stripslashes(). | |
'comment_exists' => array( 1 => 'comment_author' ), | |
// Uses delete_metadata(). | |
'delete_comment_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses wp_unslash(). | |
'delete_metadata' => array( 3 => 'meta_key', 4 => 'meta_value' ), | |
// Uses delete_metadata(). | |
'delete_post_meta_by_key' => array( 1 => 'post_meta_key' ), | |
// Uses delete_metadata(). | |
'delete_post_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses delete_metadata(). | |
'delete_term_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses delete_metadata(). | |
'delete_user_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses delete_user_meta(). | |
'delete_user_option' => array( 2 => 'option_name' ), | |
// Expects POST data. | |
'edit_post' => array( 1 => 'post_data' ), | |
// Uses get_term_by( 'name' ). | |
'get_cat_ID' => array( 1 => 'category_name' ), | |
// Uses get_search_feed_link(). | |
'get_search_comments_feed_link' => array( 1 => array( 'search_query' ) ), | |
// Uses get_search_link(). | |
'get_search_feed_link' => array( 1 => 'search_query' ), | |
// Uses stripslashes(). | |
'get_search_link' => array( 1 => 'query' ), | |
// Uses wp_unslash() when $field is 'name'. | |
'get_term_by' => array( 2 => 'value' ), | |
// Uses wp_unslash(). | |
'install_blog' => array( 2 => 'blog_title' ), | |
// Uses wp_get_nav_menu_object(). | |
'is_nav_menu' => array( 1 => 'menu' ), | |
// Uses wp_unslash(). | |
'post_exists' => array( 1 => 'title', 2 => 'content', 3 => 'date' ), | |
// Uses update_post_meta() when the $file isn't empty. | |
'update_attached_file' => array( 2 => 'file' ), | |
// Uses update_metadata(). | |
'update_comment_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses wp_unslash(). | |
'update_metadata' => array( 3 => 'meta_key', 4 => 'meta_value' ), | |
// Uses wp_unslash(). | |
'update_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses update_metadata(). | |
'update_post_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses update_metadata(). | |
'update_term_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses update_metadata(). | |
'update_user_meta' => array( 2 => 'meta_key', 3 => 'meta_value' ), | |
// Uses wp_unslash() when a string is passed; also accepts term ID. | |
'term_exists' => array( 1 => 'term' ), | |
// Uses update_user_meta(). | |
'update_user_option' => array( 2 => 'option_name', 3 => 'newvalue' ), | |
// Uses wp_set_object_terms(). | |
'wp_add_object_terms' => array( 2 => 'terms' ), | |
// Uses wp_update_nav_menu_object(). | |
'wp_create_nav_menu' => array( 1 => 'menu_name' ), | |
// Uses wp_unslash(). | |
'wp_create_post_autosave' => array( 1 => 'post_data' ), | |
// Uses wp_get_nav_menu_object(). | |
'wp_delete_nav_menu' => array( 1 => 'menu' ), | |
// Uses wp_get_nav_menu_object(). | |
'wp_get_nav_menu_items' => array( 1 => 'menu' ), | |
// Uses get_term_by( 'name' ) if $menu is not a term ID or slug. | |
'wp_get_nav_menu_object' => array( 1 => 'menu' ), | |
// Uses wp_insert_post(). | |
'wp_insert_attachment' => array( 1 => 'args' ), | |
// Uses wp_unslash(). | |
'wp_insert_comment' => array( 1 => 'commentdata' ), | |
// Uses wp_unslash(). | |
'wp_insert_link' => array( 1 => 'linkdata' ), | |
// Uses wp_unslash(). | |
'wp_insert_post' => array( 1 => 'postarr' ), | |
// Uses wp_unslash(). | |
'wp_insert_term' => array( 1 => 'term' ), | |
// Uses wp_unslash(), update_user_meta(). | |
'wp_insert_user' => array( 1 => 'userdata' ), | |
// Uses wp_insert_comment() and wp_allow_comment(). | |
'wp_new_comment' => array( 1 => 'commentdata' ), | |
// Uses term_exists(). The docs for wp_remove_object_terms() says that it | |
// takes only term slugs or IDs, but it is also possible to pass in the term | |
// names, and in that case they must be slashed. | |
'wp_remove_object_terms' => array( 2 => 'terms' ), | |
// Uses term_exists(), and wp_insert_term() if the term doesn't exist and is | |
// a string. The docs for wp_set_object_terms() says that it takes only term | |
// slugs or IDs, but it is also possible to pass in the term names, and in | |
// that case they must be slashed. | |
'wp_set_object_terms' => array( 2 => 'terms' ), | |
// Uses wp_set_object_terms(). | |
'wp_set_post_terms' => array( 2 => 'terms' ), | |
// Uses update_post_meta(). | |
'wp_update_attachment_metadata' => array( 2 => 'data' ), | |
// Uses wp_unslash(). | |
'wp_update_comment' => array( 1 => 'commentarr' ), | |
// Uses wp_insert_post(). If the $postarr is actually a post object and not | |
// an array, then it should be unslashed instead. | |
'wp_update_post' => array( 1 => 'postarr' ), | |
// Uses wp_insert_user(). | |
'wp_update_user' => array( 1 => 'userdata' ), | |
// Uses install_blog(). | |
'wpmu_create_blog' => array( 3 => 'title' ), | |
// Uses wp_unslash(). | |
'wpmu_validate_blog_signup' => array( 2 => 'blog_title' ), | |
// Uses wp_unslash(). | |
'wpmu_welcome_notification' => array( 4 => 'title' ), | |
// Uses wp_unslash(). | |
'WP_Press_This::side_load_images' => array( 2 => 'content' ), | |
// Uses wp_unslash(). | |
'WP_Customize_Setting::sanitize' => array( 1 => 'value' ), | |
); | |
public $filters = array( | |
// Result passed through wp_unslash(). | |
'pre_comment_author_name' => array( 1 => 'author_cookie' ), | |
// Result passed through wp_unslash(). | |
'pre_comment_author_email' => array( 1 => 'author_email_cookie' ), | |
// Result passed through wp_unslash(). | |
'pre_comment_author_url' => array( 1 => 'author_url_cookie' ), | |
// Result passed through wp_unslash(). | |
'add_ping' => array( 1 => 'new' ), | |
); | |
/** | |
* A list of functions and their args that are expected to be partly slashed. | |
* | |
* Sometimes a function takes an argument that is an array, and some of the | |
* values in the array are expected to be slashed while others do not need to be | |
* (because they are a type of value that doesn't need to be slashed, like an | |
* integer or boolean, perhaps). So it is OK to pass the entire array through | |
* a slashing function, but it's also permissible to slash only those args that | |
* need to be (i.e., when the array is being passed to the function directly, | |
* rather than being assigned to a variable first; then we can check each key | |
* value pair and only flag those that really need to be slashed). | |
* | |
* This is a list of those functions. For each function, there is an array which | |
* is indexed by the number of the argument which is expected to be an array with | |
* mixed slashing. The values are lists of the keys within that array that are | |
* expected to be slashed (any other keys are expected unslashed). | |
* | |
* @since 0.10.0 | |
* | |
* @var array[] | |
*/ | |
public static $partlySlashedFunctionArgs = array( | |
// Uses query_posts(), 'q' is passed as 's'. | |
'_wp_ajax_menu_quick_search' => array( 1 => array( 'q' ) ), | |
// Uses get_posts() with the $args if they are an array. | |
'get_children' => array( 1 => array( 's', 'title' ) ), | |
// Uses get_term_by( 'name' ). All of the other args are either integers or | |
// accept slug-like values. | |
'get_bookmarks' => array( 1 => array( 'category_name' ) ), | |
// Uses wp_unslash() on these. All of the other args are either integers or | |
// accept slug-like values. | |
'get_pages' => array( 1 => array( 'meta_key', 'meta_value' ) ), | |
// Uses WP_Query::query(). | |
'get_posts' => array( 1 => array( 's', 'title' ) ), | |
// Uses WP_Query::query(). | |
'query_posts' => array( 1 => array( 's', 'title' ) ), | |
// Uses wp_unslash() on some of these. All of the other args are either | |
// integers, slugs, or dates. | |
'wp_allow_comment' => array( | |
1 => array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_content', 'comment_agent' ), | |
), | |
// Uses get_posts(). | |
'wp_get_nav_menu_items' => array( 2 => array( 's', 'title' ) ), | |
// Uses get_children(). | |
'wp_get_post_revisions' => array( 2 => array( 's', 'title' ) ), | |
// Uses get_posts(). | |
'wp_get_recent_posts' => array( 1 => array( 's', 'title' ) ), | |
// Uses wp_unslash() on this. All of the other args are integers or slugs. | |
// The 'name' arg is also expected slashed, but this is always overridden by | |
// $term. | |
'wp_insert_term' => array( 3 => array( 'description' ) ), | |
// Uses wp_insert_post() or wp_update_post(). All other values are slugs or | |
// integers. | |
'wp_update_nav_menu_item' => array( | |
3 => array( 'menu-item-description', 'menu-item-attr-title', 'menu-item-title' ) | |
), | |
// Uses get_term_by( 'name' ) with 'menu-name' and also passes the data to | |
// wp_insert_term() if the menu doesn't exist, or else wp_update_term(). | |
'wp_update_nav_menu_object' => array( 2 => array( 'description', 'menu-name' ) ), | |
// Uses wp_unslash() on these. All of the other args are integers or slugs. | |
'wp_update_term' => array( 3 => array( 'description', 'name' ) ), | |
// Uses WP_Query::__construct(). | |
'WP_Customize_Nav_Menus::search_available_items_query' => array( 1 => array( 's' ) ), | |
// Uses WP_Query::query(). | |
'WP_Query::__construct' => array( 1 => array( 's', 'title' ) ), | |
// WP_Query::get_posts() uses stripslashes() on the 'title', and | |
// WP_Query::parse_search() uses stripslashes() on 's'. All other args are | |
// either integers, booleans, or slug-like strings. | |
'WP_Query::parse_query' => array( 1 => array( 's', 'title' ) ), | |
// WP_Query::get_posts() uses stripslashes() on the 'title', and | |
// WP_Query::parse_search() uses stripslashes() on 's'. All other args are | |
// either integers, booleans, or slug-like strings. | |
'WP_Query::query' => array( 1 => array( 's', 'title' ) ), | |
// Uses WP_Query::query(). | |
'_WP_Editors::wp_link_query' => array( 1 => array( 's' ) ), | |
); | |
/** | |
* A list of functions and their args that are expected to be partly slashed and | |
* partly unslashed. | |
* | |
* Sometimes a function takes an argument that is an array, and some of the | |
* values in the array are expected to be slashed while others specifically | |
* expected to be unslashed. | |
* | |
* See self::$partlySlashedFunctionArgs for an explanation of the array format. | |
* | |
* @since 0.10.0 | |
* | |
* @var array[] | |
*/ | |
public static $mixedSlashedFunctionArgs = array( | |
// Uses get_bookmarks(). When 'categorize' is true, passing 'categroy_name' | |
// doesn't make sense, so this only applies when 'categorize' false. In fact, | |
// when 'categorize' is true, the 'category_name' is passed to get_terms() as | |
// 'name__like', which is expected unslashed. So it is only expected slashed | |
// when 'categorize' is false. | |
'wp_list_bookmarks' => array( | |
1 => array( | |
'slashed' => array( 'category_name' ), | |
'unslashed' => array( | |
'title_li', | |
'title_before', | |
'title_after', | |
'class', | |
'category_before', | |
'category_after', | |
), | |
), | |
), | |
// Uses get_pages(). | |
'wp_dropdown_pages' => array( | |
1 => array( | |
'slashed' => array( 'meta_key', 'meta_value' ), | |
'unslashed' => array( | |
'selected', | |
'name', | |
'id', | |
'show_option_none', | |
'show_option_no_change', | |
'option_none_value', | |
), | |
), | |
), | |
// Uses get_pages(). | |
'wp_list_pages' => array( | |
1 => array( | |
'slashed' => array( 'meta_key', 'meta_value' ), | |
'unslashed' => array( 'date_format', 'link_after', 'link_before', 'title_li' ), | |
), | |
), | |
// 'wp_insert_category' => array( 3 => array( 'name', 'description' ) ), | |
); | |
/** | |
* Functions that slash data. | |
* | |
* @since 0.10.0 | |
* | |
* @var array | |
*/ | |
public static $slashingFunctions = array( | |
'wp_slash' => true, | |
); | |
/** | |
* Functions that return slashed data automatically. | |
* | |
* @since 0.10.0 | |
* | |
* @var array | |
*/ | |
public static $autoSlashingFunctions = array( | |
'esc_url_raw' => true, | |
'esc_url' => true, | |
'get_current_user_id' => true, | |
'sanitize_key' => true, | |
'sanitize_title' => true, | |
'sanitize_title_with_dashes' => true, | |
'time' => true, | |
); | |
/** | |
* Returns an array of tokens this test wants to listen for. | |
* | |
* @since 0.10.0 | |
* | |
* @return array | |
*/ | |
public function register() { | |
return array( | |
T_STRING, | |
); | |
} | |
/** | |
* Processes this test, when one of its tokens is encountered. | |
* | |
* @since 0.10.0 | |
* | |
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned. | |
* @param int $stackPtr The position of the current token | |
* in the stack passed in $tokens. | |
* | |
* @return int|void | |
*/ | |
public function process( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { | |
$tokens = $phpcsFile->getTokens(); | |
$function_name = $tokens[ $stackPtr ]['content']; | |
if ( | |
! isset( self::$mixedSlashedFunctionArgs[ $function_name ] ) | |
&& ! isset( self::$partlySlashedFunctionArgs[ $function_name ] ) | |
&& ! isset( self::$expectedSlashedFunctionArgs[ $function_name ] ) | |
) { | |
return; | |
} | |
$this->init( $phpcsFile ); | |
$function_opener = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$stackPtr + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
// If this isn't a function call, ignore it. | |
if ( T_OPEN_PARENTHESIS !== $tokens[ $function_opener ]['code'] ) { | |
return; | |
} | |
if ( $this->has_whitelist_comment( 'slashing', $stackPtr ) ) { | |
return; | |
} | |
// Ignore this if it is a function declaration. | |
$previous_token = $phpcsFile->findPrevious( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$stackPtr - 1, | |
null, | |
true, | |
null, | |
true | |
); | |
if ( T_FUNCTION === $tokens[ $previous_token ]['code'] ) { | |
return; | |
} | |
// Check if we are inside a function. | |
$function_ptr = $phpcsFile->getCondition( $function_opener, T_FUNCTION ); | |
if ( $function_ptr ) { | |
$function_declaration_name = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$function_ptr + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
$function_declaration_name = $tokens[ $function_declaration_name ]['content']; | |
// If we are inside a function that expects slashed arguments we don't | |
// really need to flag any internal function calls, because if any of | |
// them expects slashed data but isn't receiving it, that is likely the | |
// reason that the wrapping function is in this list. | |
if ( | |
isset( self::$expectedSlashedFunctionArgs[ $function_declaration_name ] ) | |
// || isset( self::$partlySlashedFunctionArgs[ $function_declaration_name ] ) | |
// || isset( self::$mixedSlashedFunctionArgs[ $function_declaration_name ] ) | |
) { | |
return; | |
} | |
} | |
if ( | |
isset( self::$mixedSlashedFunctionArgs[ $function_name ] ) | |
|| isset( self::$partlySlashedFunctionArgs[ $function_name ] ) | |
) { | |
$this->process_mixed_function_args( $stackPtr, $function_name ); | |
} | |
if ( isset( self::$expectedSlashedFunctionArgs[ $function_name ] ) ) { | |
$this->process_expected_slashed_args( $stackPtr, $function_opener, $function_name ); | |
} | |
} // end process(). | |
/** | |
* Find the nth parameter of a function call. | |
* | |
* @since 0.10.0 | |
* | |
* @param int $parameter The number parameter to get. | |
* @param int $stackPtr The index of the function. | |
* | |
* @return int|false The pointer of the first token of the parameter or false. | |
*/ | |
protected function find_arg( $parameter, $stackPtr ) { | |
$open_paren = $this->phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$stackPtr + 1, | |
null, | |
true | |
); | |
if ( ! $open_paren || T_OPEN_PARENTHESIS !== $this->tokens[ $open_paren ]['code'] ) { | |
return false; | |
} | |
$start = $open_paren + 1; | |
// If this is the closing parenthesis there were no parameters. | |
if ( T_CLOSE_PARENTHESIS === $this->tokens[ $start ]['code'] ) { | |
return false; | |
} | |
$i = 0; | |
while ( ++$i < $parameter ) { | |
// This will find the next comma. | |
$end = $this->phpcsFile->findEndOfStatement( $start ); | |
// If there wasn't another parameter (we hit the closing parenthesis). | |
if ( T_COMMA !== $this->tokens[ $end ]['code'] ) { | |
return false; | |
} | |
$start = $end + 1; | |
} | |
return $start; | |
} | |
/** | |
* Process a function that expects some arguments with mixed slashing. | |
* | |
* @since 0.10.0 | |
* | |
* @param int $stackPtr The function pointer. | |
* @param string $function_name The function name. | |
*/ | |
protected function process_mixed_function_args( $stackPtr, $function_name ) { | |
$phpcsFile = $this->phpcsFile; | |
$tokens = $this->tokens; | |
if ( isset( self::$mixedSlashedFunctionArgs[ $function_name ] ) ) { | |
$args = self::$mixedSlashedFunctionArgs[ $function_name ]; | |
$is_mixed = true; | |
} else { | |
$args = self::$partlySlashedFunctionArgs[ $function_name ]; | |
$is_mixed = false; | |
} | |
foreach ( $args as $arg_index => $keys ) { | |
$argPtr = $this->find_arg( $arg_index, $stackPtr ); | |
if ( ! $argPtr ) { | |
break; | |
} | |
$arg_start = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$argPtr + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
if ( $is_mixed ) { | |
$slashed_keys = $keys['slashed']; | |
$unslashed_keys = $keys['unslashed']; | |
} else { | |
$slashed_keys = $keys; | |
$unslashed_keys = array(); | |
} | |
// If this is the array declaration itself, we can check per-key. | |
// Otherwise, we can only give a general error/warning. | |
if ( T_ARRAY !== $tokens[ $arg_start ]['code'] ) { | |
if ( $is_mixed ) { | |
$phpcsFile->addWarning( | |
'%s() expects the value of %s to be slashed with wp_slash(), and %s to be unslashed.', | |
$argPtr, | |
'ExpectedMixed', | |
array( | |
$function_name, | |
implode( ', ', $slashed_keys ), | |
implode( ', ', $unslashed_keys ) | |
) | |
); | |
} else { | |
if ( | |
! isset( self::$slashingFunctions[ $tokens[ $arg_start ]['content'] ] ) | |
&& ! isset( self::$autoSlashingFunctions[ $tokens[ $arg_start ]['content'] ] ) | |
) { | |
$phpcsFile->addError( | |
'%s() expects the value of %s to be slashed with wp_slash().', | |
$argPtr, | |
'ExpectedPartlySlashed', | |
array( $function_name, implode( ', ', $slashed_keys ) ) | |
); | |
} | |
} | |
break; | |
} | |
$array_opener = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$arg_start + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
// This likely indicates a syntax error. | |
if ( ! isset( $tokens[ $array_opener ]['parenthesis_closer'] ) ) { | |
$phpcsFile->addError( | |
'Mising closing parenthesis for array.', | |
$argPtr, | |
'MissingArrayClosingParen' | |
); | |
break; | |
} | |
$start = $array_opener; | |
while ( | |
$next_double_arrow = $phpcsFile->findNext( | |
T_DOUBLE_ARROW, | |
$start + 1, | |
$tokens[ $array_opener ]['parenthesis_closer'] | |
) | |
) { | |
$is_slashed = false; | |
$start = $next_double_arrow + 1; | |
$value_ptr = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$next_double_arrow + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
// These tokens are implicitly slashed, so we don't need to slash them. | |
if ( | |
in_array( | |
$tokens[ $value_ptr ]['code'], | |
$this->slashed_tokens + $this->slashed_casts, | |
true | |
) | |
) { | |
continue; | |
} | |
$key_ptr = $phpcsFile->findPrevious( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$next_double_arrow - 1, | |
null, | |
true, | |
null, | |
true | |
); | |
if ( T_CONSTANT_ENCAPSED_STRING !== $tokens[ $key_ptr ]['code'] ) { | |
continue; | |
} | |
$key_name = trim( $tokens[ $key_ptr ]['content'], '\'"' ); | |
if ( | |
isset( self::$slashingFunctions[ $tokens[ $value_ptr ]['content'] ] ) | |
|| isset( self::$autoSlashingFunctions[ $tokens[ $value_ptr ]['content'] ] ) | |
) { | |
$is_slashed = true; | |
} | |
if ( ! $is_slashed && in_array( $key_name, $slashed_keys, true ) ) { | |
$phpcsFile->addError( | |
'%s() expects the value of %s to be slashed with wp_slash().', | |
$value_ptr, | |
'ExpectedKeySlashed', | |
array( $function_name, $key_name ) | |
); | |
} elseif ( $is_slashed && in_array( $key_name, $unslashed_keys, true ) ) { | |
$phpcsFile->addError( | |
'%s() expects the value of %s to be unslashed.', | |
$value_ptr, | |
'ExpectedKeyUnslashed', | |
array( $function_name, $key_name ) | |
); | |
} | |
} | |
} | |
} // end process_mixed_function_args() | |
/** | |
* Process a function that expects some args to be slashed. | |
* | |
* @since 0.10.0 | |
* | |
* @param int $stackPtr The function name pointer. | |
* @param int $function_opener The function openter pointer. | |
* @param string $function_name The function name. | |
*/ | |
protected function process_expected_slashed_args( $stackPtr, $function_opener, $function_name ) { | |
$phpcsFile = $this->phpcsFile; | |
$tokens = $this->tokens; | |
$next_arg_start = false; | |
$last_arg_index = false; | |
// This is probably a parse error. | |
if ( ! isset( $tokens[ $function_opener + 1 ]['nested_parenthesis'] ) ) { | |
$this->phpcsFile->addError( 'Missing function parentheses.', $stackPtr, 'missingParentheses' ); | |
} | |
// Special handling for get_term_by( 'name' ). | |
if ( 'get_term_by' === $function_name ) { | |
$byPtr = $phpcsFile->findNext( | |
PHP_CodeSniffer_Tokens::$emptyTokens, | |
$this->find_arg( 1, $stackPtr ) + 1, | |
null, | |
true, | |
null, | |
true | |
); | |
// If we know what we are getting the term by, and it isn't 'name', then | |
// we can skip this. Otherwise we're getting it by name or the arg is | |
// supplied dynamically, so we don't know whether we are or not. | |
if ( | |
T_CONSTANT_ENCAPSED_STRING === $tokens[ $byPtr ]['code'] | |
&& trim( $tokens[ $byPtr ]['content'], '\'"' ) !== 'name' | |
) { | |
return; | |
} | |
} | |
$nested_parens = count( $tokens[ $function_opener + 1 ]['nested_parenthesis'] ); | |
foreach ( self::$expectedSlashedFunctionArgs[ $function_name ] as $arg_index => $name ) { | |
if ( $next_arg_start && $last_arg_index + 1 === $arg_index ) { | |
$argPtr = $next_arg_start; | |
} else { | |
$argPtr = $this->find_arg( $arg_index, $stackPtr ); | |
} | |
if ( ! $argPtr ) { | |
break; | |
} | |
$in_cast = false; | |
$watch = true; | |
for ( $i = $argPtr; $i <= $tokens[ $function_opener ]['parenthesis_closer']; $i++ ) { | |
if ( T_COMMA === $tokens[ $i ]['code'] ) { | |
// If we aren't in an array, we are at the end of this arg. | |
if ( count( $tokens[ $i ]['nested_parenthesis'] ) === $nested_parens ) { | |
$next_arg_start = $i + 1; | |
$last_arg_index = $arg_index; | |
break; | |
} | |
$watch = true; | |
continue; | |
} | |
// If we're not watching right now, do nothing. | |
if ( ! $watch ) { | |
// Wake up on concatenation characters, another part to check. | |
if ( T_STRING_CONCAT === $tokens[ $i ]['code'] ) { | |
$watch = true; | |
} | |
continue; | |
} | |
// Ignore whitespaces and comments. | |
if ( in_array( $tokens[ $i ]['code'], PHP_CodeSniffer_Tokens::$emptyTokens ) ) { | |
continue; | |
} | |
// Skip to the end of a function call if it has been casted to a safe value. | |
if ( $in_cast && T_OPEN_PARENTHESIS === $tokens[ $i ]['code'] ) { | |
$i = $tokens[ $i ]['parenthesis_closer']; | |
$in_cast = false; | |
continue; | |
} | |
// Handle arrays for those functions that accept them. | |
if ( T_ARRAY === $tokens[ $i ]['code'] ) { | |
$i ++; // Skip the opening parenthesis. | |
continue; | |
} | |
if ( in_array( $tokens[ $i ]['code'], array( T_DOUBLE_ARROW, T_CLOSE_PARENTHESIS, T_STRING_CONCAT ) ) ) { | |
continue; | |
} | |
// Allow tokens that are implicitly slashed. | |
if ( in_array( $tokens[ $i ]['code'], $this->slashed_tokens, true ) ) { | |
continue; | |
} | |
// If we were watching before, stop now. That way we'll error once | |
// for a set of unslashed tokens, instead of for each of the tokens. | |
$watch = false; | |
// Allow int/float/bool casted variables. | |
if ( in_array( $tokens[ $i ]['code'], $this->slashed_casts ) ) { | |
$in_cast = true; | |
continue; | |
} | |
$content = $tokens[ $i ]['content']; | |
// If this is a function call. | |
if ( T_STRING === $tokens[ $i ]['code'] ) { | |
// We can fast-forward to the end of this function call, we don't | |
// need to check each token because we're going to check whether | |
// the result of the function is slashed (below). | |
$paren_opener = $phpcsFile->findNext( PHP_CodeSniffer_Tokens::$emptyTokens, $i + 1, null, true ); | |
if ( isset( $tokens[ $paren_opener ]['parenthesis_closer'] ) ) { | |
$i = $tokens[ $paren_opener ]['parenthesis_closer']; | |
} | |
// If the function is a slashing function we continue to the next | |
// token instead of giving an error. | |
if ( | |
isset( self::$slashingFunctions[ $content ] ) | |
|| isset( self::$autoSlashingFunctions[ $content ] ) | |
) { | |
continue; | |
} | |
} | |
$phpcsFile->addError( | |
'%s() expects the value of the $%s arg to be slashed with wp_slash(); %s found.', | |
$i, | |
'MissingSlashing', | |
array( $function_name, $name, $content ) | |
); | |
} | |
} | |
} // end process_expected_slashed_args() | |
} // end class. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment