/** * Processes this test, when one of its tokens is encountered. * * @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 void */ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { // Merge any custom functions with the defaults, if we haven't already. if (!self::$addedCustomFunctions) { WordPress_Sniff::$sanitizingFunctions = array_merge(WordPress_Sniff::$sanitizingFunctions, array_flip($this->customSanitizingFunctions)); WordPress_Sniff::$unslashingSanitizingFunctions = array_merge(WordPress_Sniff::$unslashingSanitizingFunctions, array_flip($this->customUnslashingSanitizingFunctions)); self::$addedCustomFunctions = true; } $this->init($phpcsFile); $tokens = $phpcsFile->getTokens(); $superglobals = WordPress_Sniff::$input_superglobals; // Handling string interpolation if (T_DOUBLE_QUOTED_STRING === $tokens[$stackPtr]['code']) { foreach ($superglobals as $superglobal) { if (false !== strpos($tokens[$stackPtr]['content'], $superglobal)) { $phpcsFile->addError('Detected usage of a non-sanitized, non-validated input variable: %s', $stackPtr, null, array($tokens[$stackPtr]['content'])); return; } } return; } // Check if this is a superglobal. if (!in_array($tokens[$stackPtr]['content'], $superglobals)) { return; } // If we're overriding a superglobal with an assignment, no need to test if ($this->is_assignment($stackPtr)) { return; } // This superglobal is being validated. if ($this->is_in_isset_or_empty($stackPtr)) { return; } $array_key = $this->get_array_access_key($stackPtr); if (empty($array_key)) { return; } // Check for validation first. if (!$this->is_validated($stackPtr, $array_key, $this->check_validation_in_scope_only)) { $phpcsFile->addError('Detected usage of a non-validated input variable: %s', $stackPtr, 'InputNotValidated', array($tokens[$stackPtr]['content'])); // return; // Should we just return and not look for sanitizing functions ? } if ($this->has_whitelist_comment('sanitization', $stackPtr)) { return; } // If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed. if ($this->is_comparison($stackPtr)) { return; } // Now look for sanitizing functions if (!$this->is_sanitized($stackPtr, true)) { $phpcsFile->addError('Detected usage of a non-sanitized input variable: %s', $stackPtr, 'InputNotSanitized', array($tokens[$stackPtr]['content'])); } return; }
/** * Processes this test, when one of its tokens is encountered. * * @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) { // Merge any custom functions with the defaults, if we haven't already. if (!self::$addedCustomFunctions) { WordPress_Sniff::$escapingFunctions = array_merge(WordPress_Sniff::$escapingFunctions, array_flip($this->customEscapingFunctions)); WordPress_Sniff::$autoEscapedFunctions = array_merge(WordPress_Sniff::$autoEscapedFunctions, array_flip($this->customAutoEscapedFunctions)); WordPress_Sniff::$printingFunctions = array_merge(WordPress_Sniff::$printingFunctions, array_flip($this->customPrintingFunctions)); if (!empty($this->customSanitizingFunctions)) { WordPress_Sniff::$escapingFunctions = array_merge(WordPress_Sniff::$escapingFunctions, array_flip($this->customSanitizingFunctions)); $phpcsFile->addWarning('The customSanitizingFunctions property is deprecated in favor of customEscapingFunctions.', 0, 'DeprecatedCustomSanitizingFunctions'); } self::$addedCustomFunctions = true; } $this->init($phpcsFile); $tokens = $phpcsFile->getTokens(); $function = $tokens[$stackPtr]['content']; // Find the opening parenthesis (if present; T_ECHO might not have it). $open_paren = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, $stackPtr + 1, null, true); // If function, not T_ECHO nor T_PRINT if ($tokens[$stackPtr]['code'] == T_STRING) { // Skip if it is a function but is not of the printing functions. if (!isset(self::$printingFunctions[$tokens[$stackPtr]['content']])) { return; } if (isset($tokens[$open_paren]['parenthesis_closer'])) { $end_of_statement = $tokens[$open_paren]['parenthesis_closer']; } // These functions only need to have the first argument escaped. if (in_array($function, array('trigger_error', 'user_error'))) { $end_of_statement = $phpcsFile->findEndOfStatement($open_paren + 1); } } // Checking for the ignore comment, ex: //xss ok if ($this->has_whitelist_comment('xss', $stackPtr)) { return; } if (isset($end_of_statement, self::$unsafePrintingFunctions[$function])) { $error = $phpcsFile->addError("Expected next thing to be an escaping function (like %s), not '%s'", $stackPtr, 'UnsafePrintingFunction', array(self::$unsafePrintingFunctions[$function], $function)); // If the error was reported, don't bother checking the function's arguments. if ($error) { return $end_of_statement; } } $ternary = false; // This is already determined if this is a function and not T_ECHO. if (!isset($end_of_statement)) { $end_of_statement = $phpcsFile->findNext(array(T_SEMICOLON, T_CLOSE_TAG), $stackPtr); $last_token = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $end_of_statement - 1, null, true); // Check for the ternary operator. We only need to do this here if this // echo is lacking parenthesis. Otherwise it will be handled below. if (T_OPEN_PARENTHESIS !== $tokens[$open_paren]['code'] || T_CLOSE_PARENTHESIS !== $tokens[$last_token]['code']) { $ternary = $phpcsFile->findNext(T_INLINE_THEN, $stackPtr, $end_of_statement); // If there is a ternary skip over the part before the ?. However, if // the ternary is within parentheses, it will be handled in the loop. if ($ternary && empty($tokens[$ternary]['nested_parenthesis'])) { $stackPtr = $ternary; } } } // Ignore the function itself. $stackPtr++; $in_cast = false; // looping through echo'd components $watch = true; for ($i = $stackPtr; $i < $end_of_statement; $i++) { // Ignore whitespaces and comments. if (in_array($tokens[$i]['code'], array(T_WHITESPACE, T_COMMENT))) { continue; } if (T_OPEN_PARENTHESIS === $tokens[$i]['code']) { if ($in_cast) { // Skip to the end of a function call if it has been casted to a safe value. $i = $tokens[$i]['parenthesis_closer']; $in_cast = false; } else { // Skip over the condition part of a ternary (i.e., to after the ?). $ternary = $phpcsFile->findNext(T_INLINE_THEN, $i, $tokens[$i]['parenthesis_closer']); if ($ternary) { $next_paren = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $i, $tokens[$i]['parenthesis_closer']); // We only do it if the ternary isn't within a subset of parentheses. if (!$next_paren || $ternary > $tokens[$next_paren]['parenthesis_closer']) { $i = $ternary; } } } continue; } // Handle arrays for those functions that accept them. if ($tokens[$i]['code'] === T_ARRAY) { $i++; // Skip the opening parenthesis. continue; } if (in_array($tokens[$i]['code'], array(T_DOUBLE_ARROW, T_CLOSE_PARENTHESIS))) { continue; } // Handle magic constants for debug functions. if (in_array($tokens[$i]['code'], array(T_METHOD_C, T_FUNC_C, T_FILE, T_CLASS_C))) { continue; } // Wake up on concatenation characters, another part to check if (in_array($tokens[$i]['code'], array(T_STRING_CONCAT))) { $watch = true; continue; } // Wake up after a ternary else (:). if ($ternary && in_array($tokens[$i]['code'], array(T_INLINE_ELSE))) { $watch = true; continue; } // Wake up for commas. if ($tokens[$i]['code'] === T_COMMA) { $in_cast = false; $watch = true; continue; } if ($watch === false) { continue; } // Allow T_CONSTANT_ENCAPSED_STRING eg: echo 'Some String'; // Also T_LNUMBER, e.g.: echo 45; exit -1; if (in_array($tokens[$i]['code'], array(T_CONSTANT_ENCAPSED_STRING, T_LNUMBER, T_MINUS))) { continue; } $watch = false; // Allow int/double/bool casted variables if (in_array($tokens[$i]['code'], array(T_INT_CAST, T_DOUBLE_CAST, T_BOOL_CAST))) { $in_cast = true; continue; } // Now check that next token is a function call. if (T_STRING === $this->tokens[$i]['code']) { $functionName = $this->tokens[$i]['content']; $is_formatting_function = isset(self::$formattingFunctions[$functionName]); // Skip pointer to after the function. if ($_pos = $this->phpcsFile->findNext(array(T_OPEN_PARENTHESIS), $i, null, null, null, true)) { // If this is a formatting function we just skip over the opening // parenthesis. Otherwise we skip all the way to the closing. if ($is_formatting_function) { $i = $_pos + 1; $watch = true; } else { $i = $this->tokens[$_pos]['parenthesis_closer']; } } // If this is a safe function, we don't flag it. if ($is_formatting_function || isset(self::$autoEscapedFunctions[$functionName]) || isset(self::$escapingFunctions[$functionName])) { continue; } } $this->phpcsFile->addError("Expected next thing to be an escaping function (see Codex for 'Data Validation'), not '%s'", $i, 'OutputNotEscaped', $this->tokens[$i]['content']); } return $end_of_statement; }
/** * Processes this test, when one of its tokens is encountered. * * @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) { if (!isset(self::$methods['all'])) { self::$methods['all'] = array_merge(self::$methods['cachable'], self::$methods['noncachable']); WordPress_Sniff::$cacheGetFunctions = array_merge(WordPress_Sniff::$cacheGetFunctions, array_flip($this->customCacheGetFunctions)); WordPress_Sniff::$cacheSetFunctions = array_merge(WordPress_Sniff::$cacheSetFunctions, array_flip($this->customCacheSetFunctions)); WordPress_Sniff::$cacheDeleteFunctions = array_merge(WordPress_Sniff::$cacheDeleteFunctions, array_flip($this->customCacheDeleteFunctions)); } $tokens = $phpcsFile->getTokens(); // Check for $wpdb variable if ($tokens[$stackPtr]['content'] != '$wpdb') { return; } $is_object_call = $phpcsFile->findNext(array(T_OBJECT_OPERATOR), $stackPtr + 1, null, null, null, true); if (false == $is_object_call) { return; } // This is not a call to the wpdb object $methodPtr = $phpcsFile->findNext(array(T_WHITESPACE), $is_object_call + 1, null, true, null, true); $method = $tokens[$methodPtr]['content']; if (!isset(self::$methods['all'][$method])) { return; } $endOfStatement = $phpcsFile->findNext(array(T_SEMICOLON), $stackPtr + 1, null, null, null, true); $endOfLineComment = ''; for ($i = $endOfStatement + 1; $i < count($tokens); $i++) { if ($tokens[$i]['line'] !== $tokens[$endOfStatement]['line']) { break; } if ($tokens[$i]['code'] === T_COMMENT) { $endOfLineComment .= $tokens[$i]['content']; } } $whitelisted_db_call = false; if (preg_match('/db call\\W*(ok|pass|clear|whitelist)/i', $endOfLineComment, $matches)) { $whitelisted_db_call = true; } // Check for Database Schema Changes $_pos = $stackPtr; while ($_pos = $phpcsFile->findNext(array(T_CONSTANT_ENCAPSED_STRING, T_DOUBLE_QUOTED_STRING), $_pos + 1, $endOfStatement, null, null, true)) { if (preg_match('#\\b(ALTER|CREATE|DROP)\\b#i', $tokens[$_pos]['content'], $matches) > 0) { $phpcsFile->addError('Attempting a database schema change is highly discouraged.', $_pos, 'SchemaChange'); } } // Flag instance if not whitelisted if (!$whitelisted_db_call) { $phpcsFile->addWarning('Usage of a direct database call is discouraged.', $stackPtr, 'DirectQuery'); } if (!isset(self::$methods['cachable'][$method])) { return $endOfStatement; } $whitelisted_cache = false; $cached = $wp_cache_get = false; if (preg_match('/cache\\s+(ok|pass|clear|whitelist)/i', $endOfLineComment, $matches)) { $whitelisted_cache = true; } if (!$whitelisted_cache && !empty($tokens[$stackPtr]['conditions'])) { $scope_function = $phpcsFile->getCondition($stackPtr, T_FUNCTION); if ($scope_function) { $scopeStart = $tokens[$scope_function]['scope_opener']; $scopeEnd = $tokens[$scope_function]['scope_closer']; for ($i = $scopeStart + 1; $i < $scopeEnd; $i++) { if (T_STRING === $tokens[$i]['code']) { if (isset(WordPress_Sniff::$cacheDeleteFunctions[$tokens[$i]['content']])) { if (in_array($method, array('query', 'update', 'replace', 'delete'))) { $cached = true; break; } } elseif (isset(WordPress_Sniff::$cacheGetFunctions[$tokens[$i]['content']])) { $wp_cache_get = true; } elseif (isset(WordPress_Sniff::$cacheSetFunctions[$tokens[$i]['content']])) { if ($wp_cache_get) { $cached = true; break; } } } } } } if (!$cached && !$whitelisted_cache) { $message = 'Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set or wp_cache_delete.'; $phpcsFile->addError($message, $stackPtr, 'NoCaching'); } return $endOfStatement; }