diff --git a/classes/models/FrmEntryMeta.php b/classes/models/FrmEntryMeta.php index 72413bcfa9..4aced90fe6 100644 --- a/classes/models/FrmEntryMeta.php +++ b/classes/models/FrmEntryMeta.php @@ -393,7 +393,7 @@ public static function getAll( $where = array(), $order_by = '', $limit = '', $s FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit; $cache_key = 'all_' . FrmAppHelper::maybe_json_encode( $where ) . $order_by . $limit; - $results = FrmDb::check_cache( $cache_key, 'frm_entry', $query, ( $limit == ' LIMIT 1' ? 'get_row' : 'get_results' ) ); // phpcs:ignore Universal.Operators.StrictComparisons, SlevomatCodingStandard.Files.LineLength.LineTooLong + $results = FrmDb::check_cache( $cache_key, 'frm_entry', $query, ( $limit === ' LIMIT 1' ? 'get_row' : 'get_results' ) ); if ( ! $results || ! $stripslashes ) { return $results; diff --git a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictComparisonSniff.php b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictComparisonSniff.php new file mode 100644 index 0000000000..964982aeaa --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictComparisonSniff.php @@ -0,0 +1,164 @@ +getTokens(); + + // Check if there's a phpcs:ignore comment for this line. + if ( $this->hasIgnoreComment( $phpcsFile, $stackPtr ) ) { + return; + } + + // Find the string literal on either side of the comparison. + $leftToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); + $rightToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); + + if ( false === $leftToken || false === $rightToken ) { + return; + } + + // Check if either side is a string literal. + $stringToken = false; + + if ( $tokens[ $leftToken ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { + $stringToken = $leftToken; + } elseif ( $tokens[ $rightToken ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { + $stringToken = $rightToken; + } + + if ( false === $stringToken ) { + return; + } + + // Get the string value and check if it's safe for strict comparison. + $stringValue = $tokens[ $stringToken ]['content']; + + if ( ! $this->isSafeStringForStrictComparison( $stringValue ) ) { + return; + } + + // Remove quotes to get the actual string content for the message. + $stringContent = substr( $stringValue, 1, -1 ); + + // Determine the strict operator to use. + $isEqual = $tokens[ $stackPtr ]['code'] === T_IS_EQUAL; + $looseOperator = $isEqual ? '==' : '!='; + $strictOperator = $isEqual ? '===' : '!=='; + + $fix = $phpcsFile->addFixableError( + 'Use strict comparison (%s) instead of loose comparison (%s) when comparing to string "%s"', + $stackPtr, + 'Found', + array( $strictOperator, $looseOperator, $stringContent ) + ); + + if ( true === $fix ) { + $phpcsFile->fixer->replaceToken( $stackPtr, $strictOperator ); + } + } + + /** + * Check if a string value is safe for strict comparison. + * + * A string is safe if it's: + * - Not empty + * - Not numeric (to avoid issues with '0' being falsy or numeric coercion) + * + * @param string $stringValue The string token content (including quotes). + * + * @return bool + */ + private function isSafeStringForStrictComparison( $stringValue ) { + // Remove quotes. + $content = substr( $stringValue, 1, -1 ); + + // Empty string is not safe. + if ( $content === '' ) { + return false; + } + + // Numeric strings are not safe (e.g., '0', '123', '1.5'). + if ( is_numeric( $content ) ) { + return false; + } + + return true; + } + + /** + * Check if there's a phpcs:ignore comment for this line. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token. + * + * @return bool + */ + private function hasIgnoreComment( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $line = $tokens[ $stackPtr ]['line']; + + // Check the previous line for a phpcs:ignore comment. + for ( $i = $stackPtr - 1; $i >= 0; $i-- ) { + if ( $tokens[ $i ]['line'] < $line - 1 ) { + break; + } + + if ( $tokens[ $i ]['line'] === $line - 1 || $tokens[ $i ]['line'] === $line ) { + if ( in_array( $tokens[ $i ]['code'], array( T_COMMENT, T_DOC_COMMENT_STRING ), true ) ) { + if ( strpos( $tokens[ $i ]['content'], 'phpcs:ignore' ) !== false ) { + // Check if it ignores StrictComparisons or our sniff. + if ( strpos( $tokens[ $i ]['content'], 'StrictComparisons' ) !== false || + strpos( $tokens[ $i ]['content'], 'PreferStrictComparison' ) !== false ) { + return true; + } + } + } + } + } + + return false; + } +} diff --git a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictInArraySniff.php b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictInArraySniff.php new file mode 100644 index 0000000000..2bbfc57798 --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/PreferStrictInArraySniff.php @@ -0,0 +1,324 @@ +getTokens(); + + // Check if this is an in_array call. + if ( strtolower( $tokens[ $stackPtr ]['content'] ) !== 'in_array' ) { + return; + } + + // Check if there's a phpcs:ignore comment for this line. + if ( $this->hasIgnoreComment( $phpcsFile, $stackPtr ) ) { + return; + } + + // Find the opening parenthesis. + $openParen = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); + + if ( false === $openParen || $tokens[ $openParen ]['code'] !== T_OPEN_PARENTHESIS ) { + return; + } + + if ( ! isset( $tokens[ $openParen ]['parenthesis_closer'] ) ) { + return; + } + + $closeParen = $tokens[ $openParen ]['parenthesis_closer']; + + // Parse the arguments. + $args = $this->parseArguments( $phpcsFile, $openParen, $closeParen ); + + // in_array needs at least 2 arguments. + if ( count( $args ) < 2 ) { + return; + } + + // If there's already a third argument, skip. + if ( count( $args ) >= 3 ) { + return; + } + + // Check if the second argument (haystack) is an array of safe strings. + $haystackArg = $args[1]; + + if ( ! $this->isArrayOfSafeStrings( $phpcsFile, $haystackArg['start'], $haystackArg['end'] ) ) { + return; + } + + $fix = $phpcsFile->addFixableError( + 'Add strict mode (true) to in_array() when comparing against an array of strings', + $stackPtr, + 'Found' + ); + + if ( true === $fix ) { + $phpcsFile->fixer->beginChangeset(); + + // Find the position just before the closing parenthesis. + $insertPos = $phpcsFile->findPrevious( T_WHITESPACE, $closeParen - 1, null, true ); + + // Add ", true" after the last argument. + $phpcsFile->fixer->addContent( $insertPos, ', true' ); + + $phpcsFile->fixer->endChangeset(); + } + } + + /** + * Parse the arguments of a function call. + * + * @param File $phpcsFile The file being scanned. + * @param int $openParen The position of the opening parenthesis. + * @param int $closeParen The position of the closing parenthesis. + * + * @return array Array of arguments, each with 'start' and 'end' positions. + */ + private function parseArguments( File $phpcsFile, $openParen, $closeParen ) { + $tokens = $phpcsFile->getTokens(); + $args = array(); + $start = $openParen + 1; + $depth = 0; + + for ( $i = $openParen + 1; $i < $closeParen; $i++ ) { + $code = $tokens[ $i ]['code']; + + // Track nesting depth using bracket/paren closers when available. + if ( $code === T_OPEN_PARENTHESIS && isset( $tokens[ $i ]['parenthesis_closer'] ) ) { + ++$depth; + continue; + } + + if ( $code === T_CLOSE_PARENTHESIS ) { + --$depth; + continue; + } + + if ( $code === T_OPEN_SHORT_ARRAY && isset( $tokens[ $i ]['bracket_closer'] ) ) { + ++$depth; + continue; + } + + if ( $code === T_CLOSE_SHORT_ARRAY ) { + --$depth; + continue; + } + + if ( $code === T_OPEN_SQUARE_BRACKET ) { + ++$depth; + continue; + } + + if ( $code === T_CLOSE_SQUARE_BRACKET ) { + --$depth; + continue; + } + + // Comma at depth 0 separates arguments. + if ( $code === T_COMMA && $depth === 0 ) { + $args[] = array( + 'start' => $start, + 'end' => $i - 1, + ); + $start = $i + 1; + } + } + + // Add the last argument. + if ( $start < $closeParen ) { + $args[] = array( + 'start' => $start, + 'end' => $closeParen - 1, + ); + } + + return $args; + } + + /** + * Check if the given range contains an array of safe strings. + * + * @param File $phpcsFile The file being scanned. + * @param int $start The start position. + * @param int $end The end position. + * + * @return bool + */ + private function isArrayOfSafeStrings( File $phpcsFile, $start, $end ) { + $tokens = $phpcsFile->getTokens(); + + // Find the array start. + $arrayStart = $phpcsFile->findNext( T_WHITESPACE, $start, $end + 1, true ); + + if ( false === $arrayStart ) { + return false; + } + + $arrayOpen = false; + $arrayClose = false; + + // Check for short array syntax [ ]. + if ( $tokens[ $arrayStart ]['code'] === T_OPEN_SHORT_ARRAY ) { + $arrayOpen = $arrayStart; + + if ( isset( $tokens[ $arrayStart ]['bracket_closer'] ) ) { + $arrayClose = $tokens[ $arrayStart ]['bracket_closer']; + } + } + + // Check for long array syntax array( ). + if ( $tokens[ $arrayStart ]['code'] === T_ARRAY ) { + $parenOpen = $phpcsFile->findNext( T_WHITESPACE, $arrayStart + 1, null, true ); + + if ( false !== $parenOpen && $tokens[ $parenOpen ]['code'] === T_OPEN_PARENTHESIS ) { + $arrayOpen = $parenOpen; + + if ( isset( $tokens[ $parenOpen ]['parenthesis_closer'] ) ) { + $arrayClose = $tokens[ $parenOpen ]['parenthesis_closer']; + } + } + } + + if ( false === $arrayOpen || false === $arrayClose ) { + return false; + } + + // Check all elements in the array. + $hasElements = false; + + for ( $i = $arrayOpen + 1; $i < $arrayClose; $i++ ) { + $code = $tokens[ $i ]['code']; + + // Skip whitespace and commas. + if ( in_array( $code, array( T_WHITESPACE, T_COMMA ), true ) ) { + continue; + } + + // Skip nested structures. + if ( in_array( $code, array( T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET ), true ) ) { + // Skip to the closing bracket. + if ( isset( $tokens[ $i ]['parenthesis_closer'] ) ) { + $i = $tokens[ $i ]['parenthesis_closer']; + } elseif ( isset( $tokens[ $i ]['bracket_closer'] ) ) { + $i = $tokens[ $i ]['bracket_closer']; + } + continue; + } + + // Must be a string literal. + if ( $code !== T_CONSTANT_ENCAPSED_STRING ) { + return false; + } + + // Check if the string is safe. + if ( ! $this->isSafeString( $tokens[ $i ]['content'] ) ) { + return false; + } + + $hasElements = true; + } + + // Must have at least one element. + return $hasElements; + } + + /** + * Check if a string value is safe for strict comparison. + * + * @param string $stringValue The string token content (including quotes). + * + * @return bool + */ + private function isSafeString( $stringValue ) { + // Remove quotes. + $content = substr( $stringValue, 1, -1 ); + + // Empty string is not safe. + if ( $content === '' ) { + return false; + } + + // Numeric strings are not safe. + if ( is_numeric( $content ) ) { + return false; + } + + return true; + } + + /** + * Check if there's a phpcs:ignore comment for this line. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token. + * + * @return bool + */ + private function hasIgnoreComment( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $line = $tokens[ $stackPtr ]['line']; + + // Check the previous line for a phpcs:ignore comment. + for ( $i = $stackPtr - 1; $i >= 0; $i-- ) { + if ( $tokens[ $i ]['line'] < $line - 1 ) { + break; + } + + if ( $tokens[ $i ]['line'] === $line - 1 || $tokens[ $i ]['line'] === $line ) { + if ( in_array( $tokens[ $i ]['code'], array( T_COMMENT, T_DOC_COMMENT_STRING ), true ) ) { + if ( strpos( $tokens[ $i ]['content'], 'phpcs:ignore' ) !== false ) { + // Check if it ignores StrictInArray or our sniff. + if ( strpos( $tokens[ $i ]['content'], 'StrictInArray' ) !== false || + strpos( $tokens[ $i ]['content'], 'PreferStrictInArray' ) !== false ) { + return true; + } + } + } + } + } + + return false; + } +} diff --git a/phpcs.xml b/phpcs.xml index 45ff01337e..ea7e8302d6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -251,4 +251,6 @@ + +