Key is prefix, value irrelevant. */ protected $prefix_blocklist = array( 'wordpress' => true, 'wp' => true, '_' => true, 'php' => true, // See #1728, the 'php' prefix is reserved by PHP itself. ); /** * Target prefixes after validation. * * All prefixes are lowercased for case-insensitive compare. * * @since 0.12.0 * * @var array */ private $validated_prefixes = array(); /** * Target namespace prefixes after validation with regex indicator. * * All prefixes are lowercased for case-insensitive compare. * If the prefix doesn't already contain a namespace separator, but does contain * non-word characters, these will have been replaced with regex syntax to allow * for namespace separators and the `is_regex` indicator will have been set to `true`. * * @since 1.2.0 * * @var array> */ private $validated_namespace_prefixes = array(); /** * Cache of previously set prefixes. * * Prevents having to do the same prefix validation over and over again. * * @since 0.12.0 * * @var string[] */ private $previous_prefixes = array(); /** * A list of core hooks that are allowed to be called by plugins and themes. * * @since 0.14.0 * @since 3.0.0 Renamed from `$whitelisted_core_hooks` to `$allowed_core_hooks`. * * @var array Key is hook name, value irrelevant. */ protected $allowed_core_hooks = array( 'widget_title' => true, 'add_meta_boxes' => true, ); /** * A list of core constants that are allowed to be defined by plugins and themes. * * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/default-constants.php#L0} * The constants are listed in alphabetic order. * Only overrulable constants are listed, i.e. those defined within core within * a `if ( ! defined() ) {}` wrapper. * * Last update: July 2023 for WP 6.3 at https://github.com/WordPress/wordpress-develop/commit/6281ce432c50345a57768bf53854d9b65b6cdd52 * * @since 1.0.0 * @since 3.0.0 Renamed from `$whitelisted_core_constants` to `$allowed_core_constants`. * * @var array Key is constant name, value irrelevant. */ protected $allowed_core_constants = array( 'ADMIN_COOKIE_PATH' => true, 'AUTH_COOKIE' => true, 'AUTOSAVE_INTERVAL' => true, 'COOKIEHASH' => true, 'COOKIEPATH' => true, 'COOKIE_DOMAIN' => true, 'EMPTY_TRASH_DAYS' => true, 'FORCE_SSL_ADMIN' => true, 'FORCE_SSL_LOGIN' => true, // Deprecated. 'LOGGED_IN_COOKIE' => true, 'MEDIA_TRASH' => true, 'MUPLUGINDIR' => true, // Deprecated. 'PASS_COOKIE' => true, 'PLUGINDIR' => true, // Deprecated. 'PLUGINS_COOKIE_PATH' => true, 'RECOVERY_MODE_COOKIE' => true, 'SCRIPT_DEBUG' => true, 'SECURE_AUTH_COOKIE' => true, 'SHORTINIT' => true, 'SITECOOKIEPATH' => true, 'TEST_COOKIE' => true, 'USER_COOKIE' => true, 'WPMU_PLUGIN_DIR' => true, 'WPMU_PLUGIN_URL' => true, 'WP_CACHE' => true, 'WP_CONTENT_DIR' => true, 'WP_CONTENT_URL' => true, 'WP_CRON_LOCK_TIMEOUT' => true, 'WP_DEBUG' => true, 'WP_DEBUG_DISPLAY' => true, 'WP_DEBUG_LOG' => true, 'WP_DEFAULT_THEME' => true, 'WP_DEVELOPMENT_MODE' => true, 'WP_MAX_MEMORY_LIMIT' => true, 'WP_MEMORY_LIMIT' => true, 'WP_PLUGIN_DIR' => true, 'WP_PLUGIN_URL' => true, 'WP_POST_REVISIONS' => true, 'WP_START_TIMESTAMP' => true, ); /** * A list of functions declared in WP core as "Pluggable", i.e. overloadable from a plugin. * * Note: deprecated functions should still be included in this list as plugins may support older WP versions. * * @since 3.0.0. * * @var array Key is function name, value irrelevant. */ protected $pluggable_functions = array( 'auth_redirect' => true, 'cache_users' => true, 'check_admin_referer' => true, 'check_ajax_referer' => true, 'get_avatar' => true, 'get_currentuserinfo' => true, // Deprecated. 'get_user_by' => true, 'get_user_by_email' => true, // Deprecated. 'get_userdata' => true, 'get_userdatabylogin' => true, // Deprecated. 'graceful_fail' => true, 'install_global_terms' => true, 'install_network' => true, 'is_user_logged_in' => true, // 'lowercase_octets' => true, => unclear if this function is meant to be publicly pluggable. 'maybe_add_column' => true, 'maybe_create_table' => true, 'set_current_user' => true, // Deprecated. 'twenty_twenty_one_entry_meta_footer' => true, 'twenty_twenty_one_post_thumbnail' => true, 'twenty_twenty_one_post_title' => true, 'twenty_twenty_one_posted_by' => true, 'twenty_twenty_one_posted_on' => true, 'twenty_twenty_one_setup' => true, 'twenty_twenty_one_the_posts_navigation' => true, 'twentyeleven_admin_header_image' => true, 'twentyeleven_admin_header_style' => true, 'twentyeleven_comment' => true, 'twentyeleven_content_nav' => true, 'twentyeleven_continue_reading_link' => true, 'twentyeleven_header_style' => true, 'twentyeleven_posted_on' => true, 'twentyeleven_setup' => true, 'twentyfifteen_comment_nav' => true, 'twentyfifteen_entry_meta' => true, 'twentyfifteen_excerpt_more' => true, 'twentyfifteen_fonts_url' => true, 'twentyfifteen_get_color_scheme' => true, 'twentyfifteen_get_color_scheme_choices' => true, 'twentyfifteen_get_link_url' => true, 'twentyfifteen_header_style' => true, 'twentyfifteen_post_thumbnail' => true, 'twentyfifteen_sanitize_color_scheme' => true, 'twentyfifteen_setup' => true, 'twentyfifteen_the_custom_logo' => true, 'twentyfourteen_admin_header_image' => true, 'twentyfourteen_admin_header_style' => true, 'twentyfourteen_excerpt_more' => true, 'twentyfourteen_font_url' => true, 'twentyfourteen_header_style' => true, 'twentyfourteen_list_authors' => true, 'twentyfourteen_paging_nav' => true, 'twentyfourteen_post_nav' => true, 'twentyfourteen_post_thumbnail' => true, 'twentyfourteen_posted_on' => true, 'twentyfourteen_setup' => true, 'twentyfourteen_the_attached_image' => true, 'twentynineteen_comment_count' => true, 'twentynineteen_comment_form' => true, 'twentynineteen_discussion_avatars_list' => true, 'twentynineteen_entry_footer' => true, 'twentynineteen_get_user_avatar_markup' => true, 'twentynineteen_post_thumbnail' => true, 'twentynineteen_posted_by' => true, 'twentynineteen_posted_on' => true, 'twentynineteen_setup' => true, 'twentynineteen_the_posts_navigation' => true, 'twentyseventeen_edit_link' => true, 'twentyseventeen_entry_footer' => true, 'twentyseventeen_fonts_url' => true, 'twentyseventeen_header_style' => true, 'twentyseventeen_posted_on' => true, 'twentyseventeen_time_link' => true, 'twentysixteen_categorized_blog' => true, 'twentysixteen_entry_date' => true, 'twentysixteen_entry_meta' => true, 'twentysixteen_entry_taxonomies' => true, 'twentysixteen_excerpt' => true, 'twentysixteen_excerpt_more' => true, 'twentysixteen_fonts_url' => true, 'twentysixteen_get_color_scheme' => true, 'twentysixteen_get_color_scheme_choices' => true, 'twentysixteen_header_style' => true, 'twentysixteen_post_thumbnail' => true, 'twentysixteen_sanitize_color_scheme' => true, 'twentysixteen_setup' => true, 'twentysixteen_the_custom_logo' => true, 'twentyten_admin_header_style' => true, 'twentyten_comment' => true, 'twentyten_continue_reading_link' => true, 'twentyten_posted_in' => true, 'twentyten_posted_on' => true, 'twentyten_setup' => true, 'twentythirteen_entry_date' => true, 'twentythirteen_entry_meta' => true, 'twentythirteen_excerpt_more' => true, 'twentythirteen_fonts_url' => true, 'twentythirteen_paging_nav' => true, 'twentythirteen_post_nav' => true, 'twentythirteen_the_attached_image' => true, 'twentytwelve_comment' => true, 'twentytwelve_content_nav' => true, 'twentytwelve_entry_meta' => true, 'twentytwelve_get_font_url' => true, 'twentytwenty_customize_partial_blogdescription' => true, 'twentytwenty_customize_partial_blogname' => true, 'twentytwenty_customize_partial_site_logo' => true, 'twentytwenty_generate_css' => true, 'twentytwenty_get_customizer_css' => true, 'twentytwenty_get_theme_svg' => true, 'twentytwenty_the_theme_svg' => true, 'twentytwentytwo_styles' => true, 'twentytwentytwo_support' => true, 'wp_authenticate' => true, 'wp_cache_add_multiple' => true, 'wp_cache_delete_multiple' => true, 'wp_cache_flush_group' => true, 'wp_cache_flush_runtime' => true, 'wp_cache_get_multiple' => true, 'wp_cache_set_multiple' => true, 'wp_cache_supports' => true, 'wp_check_password' => true, 'wp_clear_auth_cookie' => true, 'wp_clearcookie' => true, // Deprecated. 'wp_create_nonce' => true, 'wp_generate_auth_cookie' => true, 'wp_generate_password' => true, 'wp_get_cookie_login' => true, // Deprecated. 'wp_get_current_user' => true, // 'wp_handle_upload_error' => true, => unclear if this function is meant to be publicly pluggable. 'wp_hash' => true, 'wp_hash_password' => true, 'wp_install' => true, 'wp_install_defaults' => true, 'wp_login' => true, // Deprecated. 'wp_logout' => true, 'wp_mail' => true, 'wp_new_blog_notification' => true, 'wp_new_user_notification' => true, 'wp_nonce_tick' => true, 'wp_notify_moderator' => true, 'wp_notify_postauthor' => true, 'wp_parse_auth_cookie' => true, 'wp_password_change_notification' => true, 'wp_rand' => true, 'wp_redirect' => true, 'wp_safe_redirect' => true, 'wp_salt' => true, 'wp_sanitize_redirect' => true, 'wp_set_auth_cookie' => true, 'wp_set_current_user' => true, 'wp_set_password' => true, 'wp_setcookie' => true, // Deprecated. 'wp_text_diff' => true, 'wp_upgrade' => true, 'wp_validate_auth_cookie' => true, 'wp_validate_redirect' => true, 'wp_verify_nonce' => true, ); /** * A list of classes declared in WP core as "Pluggable", i.e. overloadable from a plugin. * * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable.php} * and {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable-deprecated.php} * * Note: deprecated classes should still be included in this list as plugins may support older WP versions. * * @since 3.0.0. * * @var array Key is class name, value irrelevant. */ protected $pluggable_classes = array( 'TwentyTwenty_Customize' => true, 'TwentyTwenty_Non_Latin_Languages' => true, 'TwentyTwenty_SVG_Icons' => true, 'TwentyTwenty_Script_Loader' => true, 'TwentyTwenty_Separator_Control' => true, 'TwentyTwenty_Walker_Comment' => true, 'TwentyTwenty_Walker_Page' => true, 'Twenty_Twenty_One_Customize' => true, 'WP_User_Search' => true, 'wp_atom_server' => true, // Deprecated. ); /** * List of all PHP native functions. * * Using this list rather than a call to `function_exists()` prevents * false negatives from user-defined functions when those would be * autoloaded via a Composer autoload files directives. * * @var array */ private $built_in_functions; /** * Returns an array of tokens this test wants to listen for. * * @since 0.12.0 * * @return array */ public function register() { // Get a list of all PHP native functions. $all_functions = get_defined_functions(); $this->built_in_functions = array_flip( $all_functions['internal'] ); $this->built_in_functions = array_change_key_case( $this->built_in_functions, \CASE_LOWER ); // Make sure the pluggable functions and classes list can be easily compared. $this->pluggable_functions = array_change_key_case( $this->pluggable_functions, \CASE_LOWER ); $this->pluggable_classes = array_change_key_case( $this->pluggable_classes, \CASE_LOWER ); // Set the sniff targets. $targets = array( \T_NAMESPACE => \T_NAMESPACE, \T_FUNCTION => \T_FUNCTION, \T_CONST => \T_CONST, \T_VARIABLE => \T_VARIABLE, \T_DOLLAR => \T_DOLLAR, // Variable variables. \T_FN_ARROW => \T_FN_ARROW, // T_FN_ARROW is only used for skipping over (for now). ); $targets += Tokens::$ooScopeTokens; // T_ANON_CLASS is only used for skipping over test classes. $targets += Collections::listOpenTokensBC(); // Add function call target for hook names and constants defined using define(). $parent = parent::register(); if ( ! empty( $parent ) ) { $targets[] = \T_STRING; } return $targets; } /** * Groups of functions to restrict. * * @since 0.12.0 * * @return array */ public function getGroups() { // Only retrieve functions which are not used for deprecated hooks. $this->target_functions = WPHookHelper::get_functions( false ); $this->target_functions['define'] = true; return parent::getGroups(); } /** * Processes this test, when one of its tokens is encountered. * * @since 0.12.0 * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_token( $stackPtr ) { // Allow overruling the prefixes set in a ruleset via the command line. $cl_prefixes = Helper::getConfigData( 'prefixes' ); if ( ! empty( $cl_prefixes ) ) { $cl_prefixes = trim( $cl_prefixes ); if ( '' !== $cl_prefixes ) { $this->prefixes = array_filter( array_map( 'trim', explode( ',', $cl_prefixes ) ) ); } } $this->prefixes = RulesetPropertyHelper::merge_custom_array( $this->prefixes, array(), false ); if ( empty( $this->prefixes ) ) { // No prefixes passed, nothing to do. return; } $this->validate_prefixes(); if ( empty( $this->validated_prefixes ) ) { // No _valid_ prefixes passed, nothing to do. return; } // Ignore test classes. if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) && true === $this->is_test_class( $this->phpcsFile, $stackPtr ) ) { if ( $this->tokens[ $stackPtr ]['scope_condition'] === $stackPtr && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) { // Skip forward to end of test class. return $this->tokens[ $stackPtr ]['scope_closer']; } return; } if ( \T_ANON_CLASS === $this->tokens[ $stackPtr ]['code'] ) { // Token was only registered to allow skipping over test classes. return; } /* * Ignore the contents of arrow functions which do not declare closures. * * - Parameters declared by arrow functions do not need to be prefixed (handled elsewhere). * - New variables declared within an arrow function are local to the arrow function, so can be ignored. * - A `global` statement is not allowed within an arrow function. * * Note: this does mean some convoluted code may get ignored (false negatives), but this is currently * not reliably solvable as PHPCS does not add arrow functions to the 'conditions' array. */ if ( \T_FN_ARROW === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) { $has_closure = $this->phpcsFile->findNext( \T_CLOSURE, ( $stackPtr + 1 ), $this->tokens[ $stackPtr ]['scope_closer'] ); if ( false !== $has_closure ) { // Skip to the start of the closure. return $has_closure; } // Skip the arrow function completely. return $this->tokens[ $stackPtr ]['scope_closer']; } if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) { // Disallow excluding function groups for this sniff. $this->exclude = array(); return parent::process_token( $stackPtr ); } elseif ( \T_DOLLAR === $this->tokens[ $stackPtr ]['code'] ) { return $this->process_variable_variable( $stackPtr ); } elseif ( \T_VARIABLE === $this->tokens[ $stackPtr ]['code'] ) { return $this->process_variable_assignment( $stackPtr ); } elseif ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) ) { return $this->process_list_assignment( $stackPtr ); } elseif ( \T_NAMESPACE === $this->tokens[ $stackPtr ]['code'] ) { $namespace_name = Namespaces::getDeclaredName( $this->phpcsFile, $stackPtr ); if ( false === $namespace_name || '' === $namespace_name || '\\' === $namespace_name ) { return; } foreach ( $this->validated_namespace_prefixes as $key => $prefix_info ) { if ( false === $prefix_info['is_regex'] ) { if ( stripos( $namespace_name, $prefix_info['prefix'] ) === 0 ) { $this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key ); return; } } else { // Ok, so this prefix should be used as a regex. $regex = '`^' . $prefix_info['prefix'] . '`i'; if ( preg_match( $regex, $namespace_name ) > 0 ) { $this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key ); return; } } } // Still here ? In that case, we have a non-prefixed namespace name. $recorded = $this->phpcsFile->addError( self::ERROR_MSG, $stackPtr, 'NonPrefixedNamespaceFound', array( 'Namespaces declared', $namespace_name, ) ); if ( true === $recorded ) { $this->record_potential_prefix_metric( $stackPtr, $namespace_name ); } return; } else { // Namespaced methods, classes and constants do not need to be prefixed. $namespace = Namespaces::determineNamespace( $this->phpcsFile, $stackPtr ); if ( '' !== $namespace && '\\' !== $namespace ) { return; } $item_name = ''; $error_text = 'Unknown syntax used'; $error_code = 'NonPrefixedSyntaxFound'; switch ( $this->tokens[ $stackPtr ]['code'] ) { case \T_FUNCTION: // Methods in a class do not need to be prefixed. if ( Scopes::isOOMethod( $this->phpcsFile, $stackPtr ) === true ) { return; } if ( DeprecationHelper::is_function_deprecated( $this->phpcsFile, $stackPtr ) === true ) { /* * Deprecated functions don't have to comply with the naming conventions, * otherwise functions deprecated in favour of a function with a compliant * name would still trigger an error. */ return; } $item_name = FunctionDeclarations::getName( $this->phpcsFile, $stackPtr ); $item_lc = strtolower( $item_name ); if ( isset( $this->built_in_functions[ $item_lc ] ) ) { // Backfill for PHP native function. return; } if ( isset( $this->pluggable_functions[ $item_lc ] ) ) { // Pluggable function should not be prefixed. return; } $error_text = 'Functions declared in the global namespace'; $error_code = 'NonPrefixedFunctionFound'; break; case \T_CLASS: case \T_INTERFACE: case \T_TRAIT: case \T_ENUM: $item_name = ObjectDeclarations::getName( $this->phpcsFile, $stackPtr ); $error_text = 'Classes declared'; $error_code = 'NonPrefixedClassFound'; switch ( $this->tokens[ $stackPtr ]['code'] ) { case \T_CLASS: if ( isset( $this->pluggable_classes[ strtolower( $item_name ) ] ) ) { // Pluggable class should not be prefixed. return; } if ( class_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native class. return; } break; case \T_INTERFACE: if ( interface_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native interface. return; } $error_text = 'Interfaces declared'; $error_code = 'NonPrefixedInterfaceFound'; break; case \T_TRAIT: // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.trait_existsFound if ( function_exists( '\trait_exists' ) && trait_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native trait. return; } $error_text = 'Traits declared'; $error_code = 'NonPrefixedTraitFound'; break; case \T_ENUM: // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.enum_existsFound if ( function_exists( '\enum_exists' ) && enum_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native enum. return; } $error_text = 'Enums declared'; $error_code = 'NonPrefixedEnumFound'; break; } break; case \T_CONST: // Constants in an OO construct do not need to be prefixed. if ( true === Scopes::isOOConstant( $this->phpcsFile, $stackPtr ) ) { return; } $constant_name_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); if ( false === $constant_name_ptr ) { // Live coding. return; } $item_name = $this->tokens[ $constant_name_ptr ]['content']; if ( \defined( '\\' . $item_name ) ) { // Backfill for PHP native constant. return; } if ( isset( $this->allowed_core_constants[ $item_name ] ) ) { // Defining a WP Core constant intended for overruling. return; } $error_text = 'Global constants defined'; $error_code = 'NonPrefixedConstantFound'; break; default: // Left empty on purpose. break; } if ( empty( $item_name ) || $this->is_prefixed( $stackPtr, $item_name ) === true ) { return; } $recorded = $this->phpcsFile->addError( self::ERROR_MSG, $stackPtr, $error_code, array( $error_text, $item_name, ) ); if ( true === $recorded ) { $this->record_potential_prefix_metric( $stackPtr, $item_name ); } } } /** * Handle variable variables defined in the global namespace. * * @since 0.12.0 * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ protected function process_variable_variable( $stackPtr ) { static $indicators = array( \T_OPEN_CURLY_BRACKET => true, \T_VARIABLE => true, ); // Is this a variable variable ? // Not concerned with nested ones as those will be recognized on their own token. $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); if ( false === $next_non_empty || ! isset( $indicators[ $this->tokens[ $next_non_empty ]['code'] ] ) ) { return; } if ( \T_OPEN_CURLY_BRACKET === $this->tokens[ $next_non_empty ]['code'] && isset( $this->tokens[ $next_non_empty ]['bracket_closer'] ) ) { // Skip over the variable part. $next_non_empty = $this->tokens[ $next_non_empty ]['bracket_closer']; } $maybe_assignment = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true ); while ( false !== $maybe_assignment && \T_OPEN_SQUARE_BRACKET === $this->tokens[ $maybe_assignment ]['code'] && isset( $this->tokens[ $maybe_assignment ]['bracket_closer'] ) ) { $maybe_assignment = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->tokens[ $maybe_assignment ]['bracket_closer'] + 1 ), null, true, null, true ); } if ( false === $maybe_assignment ) { return; } if ( ! isset( Tokens::$assignmentTokens[ $this->tokens[ $maybe_assignment ]['code'] ] ) ) { // Not an assignment. return; } $error = self::ERROR_MSG; /* * Local variable variables in a function do not need to be prefixed. * But a variable variable could evaluate to the name of an imported global * variable. * Not concerned with imported variable variables (global.. ) as that has been * forbidden since PHP 7.0. Presuming cross-version code and if not, that * is for the PHPCompatibility standard to detect. */ $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); if ( false !== $functionPtr ) { $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] ); if ( false === $has_global ) { // No variable import happening. return; } $error = 'Variable variable which could potentially override an imported global variable detected. ' . $error; } $variable_name = $this->phpcsFile->getTokensAsString( $stackPtr, ( ( $next_non_empty - $stackPtr ) + 1 ) ); // Still here ? In that case, the variable name should be prefixed. $recorded = $this->phpcsFile->addWarning( $error, $stackPtr, 'NonPrefixedVariableFound', array( 'Global variables defined', $variable_name, ) ); if ( true === $recorded ) { $this->record_potential_prefix_metric( $stackPtr, $variable_name ); } // Skip over the variable part of the variable. return ( $next_non_empty + 1 ); } /** * Check that defined global variables are prefixed. * * @since 0.12.0 * @since 2.2.0 Added $in_list parameter. * * @param int $stackPtr The position of the current token in the stack. * @param bool $in_list Whether or not this is a variable in a list assignment. * Defaults to false. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ protected function process_variable_assignment( $stackPtr, $in_list = false ) { /* * We're only concerned with variables which are being defined. * `is_assigment()` will not recognize property assignments, which is good in this case. * However it will also not recognize $b in `foreach( $a as $b )` as an assignment, so * we need a separate check for that. */ if ( false === $in_list && false === VariableHelper::is_assignment( $this->phpcsFile, $stackPtr ) && Context::inForeachCondition( $this->phpcsFile, $stackPtr ) !== 'afterAs' ) { return; } $is_error = true; $variable_name = substr( $this->tokens[ $stackPtr ]['content'], 1 ); // Strip the dollar sign. // Bow out early if we know for certain no prefix is needed. if ( 'GLOBALS' !== $variable_name && $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true ) { return; } if ( 'GLOBALS' === $variable_name ) { $array_open = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); if ( false === $array_open || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $array_open ]['code'] ) { // Live coding or something very silly. return; } $array_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $array_open + 1 ), null, true, null, true ); if ( false === $array_key ) { // No key found, nothing to do. return; } $stackPtr = $array_key; $variable_name = TextStrings::stripQuotes( $this->tokens[ $array_key ]['content'] ); // Check whether a prefix is needed. if ( isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] ) && $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true ) { return; } if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $array_key ]['code'] ) { // If the array key is a double quoted string, try again with only // the part before the first variable (if any). $exploded = explode( '$', $variable_name ); $first = rtrim( $exploded[0], '{' ); if ( '' !== $first ) { if ( $this->variable_prefixed_or_allowed( $array_key, $first ) === true ) { return; } } else { // If the first part was dynamic, throw a warning. $is_error = false; } } elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] ) ) { // Dynamic array key, throw a warning. $is_error = false; } } else { // Function parameters do not need to be prefixed. if ( false === $in_list ) { $functionPtr = Parentheses::getLastOwner( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); if ( false !== $functionPtr ) { return; } unset( $functionPtr ); } // Properties in a class do not need to be prefixed. if ( false === $in_list && true === Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) { return; } // Local variables in a function do not need to be prefixed unless they are being imported. $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); if ( false !== $functionPtr ) { $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] ); if ( false === $has_global || Conditions::getLastCondition( $this->phpcsFile, $has_global, Collections::functionDeclarationTokens() ) !== $functionPtr ) { // No variable import happening in the current scope. return; } // Ok, this may be an imported global variable. $end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), ( $has_global + 1 ) ); if ( false === $end_of_statement ) { // No semi-colon - live coding. return; } for ( $ptr = ( $has_global + 1 ); $ptr <= $end_of_statement; $ptr++ ) { // Move the stack pointer to the next variable. $ptr = $this->phpcsFile->findNext( \T_VARIABLE, $ptr, $end_of_statement, false, null, true ); if ( false === $ptr ) { // Reached the end of the global statement without finding the variable, // so this must be a local variable. return; } if ( substr( $this->tokens[ $ptr ]['content'], 1 ) === $variable_name ) { break; } } unset( $has_global, $end_of_statement, $ptr ); } } // Still here ? In that case, the variable name should be prefixed. $recorded = MessageHelper::addMessage( $this->phpcsFile, self::ERROR_MSG, $stackPtr, $is_error, 'NonPrefixedVariableFound', array( 'Global variables defined', '$' . $variable_name, ) ); if ( true === $recorded ) { $this->record_potential_prefix_metric( $stackPtr, $variable_name ); } } /** * Check that global variables declared via a list construct are prefixed. * * {@internal No need to take special measures for nested lists. Nested or not, * each list part can only contain one variable being written to.} * * @since 2.2.0 * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ protected function process_list_assignment( $stackPtr ) { $list_open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr ); if ( false === $list_open_close ) { // Short array, not short list. return; } $var_pointers = ListHelper::get_list_variables( $this->phpcsFile, $stackPtr ); foreach ( $var_pointers as $ptr ) { $this->process_variable_assignment( $ptr, true ); } // No need to re-examine these variables. return $list_open_close['closer']; } /** * Process the parameters of a matched function. * * @since 0.12.0 * * @param int $stackPtr The position of the current token in the stack. * @param string $group_name The name of the group which was matched. * @param string $matched_content The token content (function name) which was matched * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { if ( 'define' === $matched_content ) { $target_param = PassedParameters::getParameterFromStack( $parameters, 1, 'constant_name' ); } else { $target_param = WPHookHelper::get_hook_name_param( $matched_content, $parameters ); } if ( false === $target_param ) { return; } $is_error = true; $clean_content = TextStrings::stripQuotes( $target_param['clean'] ); if ( ( 'define' !== $matched_content && isset( $this->allowed_core_hooks[ $clean_content ] ) ) || ( 'define' === $matched_content && isset( $this->allowed_core_constants[ $clean_content ] ) ) ) { return; } if ( $this->is_prefixed( $target_param['start'], $clean_content ) === true ) { return; } else { // This may be a dynamic hook/constant name. $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $target_param['start'], ( $target_param['end'] + 1 ), true ); if ( false === $first_non_empty ) { return; } $first_non_empty_content = TextStrings::stripQuotes( $this->tokens[ $first_non_empty ]['content'] ); // Try again with just the first token if it's a text string. if ( isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) && $this->is_prefixed( $target_param['start'], $first_non_empty_content ) === true ) { return; } if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $first_non_empty ]['code'] ) { // If the first part of the parameter is a double quoted string, try again with only // the part before the first variable (if any). $exploded = explode( '$', $first_non_empty_content ); $first = rtrim( $exploded[0], '{' ); if ( '' !== $first ) { if ( $this->is_prefixed( $target_param['start'], $first ) === true ) { return; } } else { // Start of hook/constant name is dynamic, throw a warning. $is_error = false; } } elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) ) { // Dynamic hook/constant name, throw a warning. $is_error = false; } } if ( 'define' === $matched_content ) { if ( \defined( '\\' . $clean_content ) ) { // Backfill for PHP native constant. return; } if ( strpos( $clean_content, '\\' ) !== false ) { // Namespaced or unreachable constant. return; } $data = array( 'Global constants defined' ); $error_code = 'NonPrefixedConstantFound'; if ( false === $is_error ) { $error_code = 'VariableConstantNameFound'; } } else { $data = array( 'Hook names invoked' ); $error_code = 'NonPrefixedHooknameFound'; if ( false === $is_error ) { $error_code = 'DynamicHooknameFound'; } } $data[] = $clean_content; $recorded = MessageHelper::addMessage( $this->phpcsFile, self::ERROR_MSG, $first_non_empty, $is_error, $error_code, $data ); if ( true === $recorded ) { $this->record_potential_prefix_metric( $stackPtr, $clean_content ); } } /** * Check if a function/class/constant/variable name is prefixed with one of the expected prefixes. * * @since 0.12.0 * @since 0.14.0 Allows for other non-word characters as well as underscores to better support hook names. * @since 1.0.0 Does not require a word seperator anymore after a prefix. * This allows for improved code style independent checking, * i.e. allows for camelCase naming and the likes. * @since 1.0.1 - Added $stackPtr parameter. * - The function now also records metrics about the prefixes encountered. * * @param int $stackPtr The position of the token to record the metric against. * @param string $name Name to check for a prefix. * * @return bool True when the name is one of the prefixes or starts with an allowed prefix. * False otherwise. */ private function is_prefixed( $stackPtr, $name ) { foreach ( $this->validated_prefixes as $prefix ) { if ( stripos( $name, $prefix ) === 0 ) { $this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $prefix ); return true; } } return false; } /** * Check if a variable name might need a prefix. * * Prefix is not needed for: * - superglobals, * - WP native globals, * - variables which are already prefixed. * * @since 0.12.0 * @since 1.0.1 Added $stackPtr parameter. * @since 3.0.0 Renamed from `variable_prefixed_or_whitelisted()` to `variable_prefixed_or_allowed()`. * * @param int $stackPtr The position of the token to record the metric against. * @param string $name Variable name without the dollar sign. * * @return bool True if the variable name is allowed or already prefixed. * False otherwise. */ private function variable_prefixed_or_allowed( $stackPtr, $name ) { // Ignore superglobals and WP global variables. if ( Variables::isSuperglobalName( $name ) || WPGlobalVariablesHelper::is_wp_global( $name ) ) { return true; } return $this->is_prefixed( $stackPtr, $name ); } /** * Validate an array of prefixes as passed through a custom property or via the command line. * * Checks that the prefix: * - is not one of the blocked ones. * - complies with the PHP rules for valid function, class, variable, constant names. * * @since 0.12.0 * * @return void */ private function validate_prefixes() { if ( $this->previous_prefixes === $this->prefixes ) { return; } // Set the cache *before* validation so as to not break the above compare. $this->previous_prefixes = $this->prefixes; // Validate the passed prefix(es). $prefixes = array(); $ns_prefixes = array(); foreach ( $this->prefixes as $key => $prefix ) { $prefixLC = strtolower( $prefix ); if ( isset( $this->prefix_blocklist[ $prefixLC ] ) ) { $this->phpcsFile->addError( 'The "%s" prefix is not allowed.', 0, 'ForbiddenPrefixPassed', array( $prefix ) ); continue; } $prefix_length = strlen( $prefix ); if ( function_exists( 'iconv_strlen' ) ) { $prefix_length = iconv_strlen( $prefix, Helper::getEncoding( $this->phpcsFile ) ); } if ( $prefix_length < self::MIN_PREFIX_LENGTH ) { $this->phpcsFile->addError( 'The "%s" prefix is too short. Short prefixes are not unique enough and may cause name collisions with other code.', 0, 'ShortPrefixPassed', array( $prefix ) ); continue; } /* * Validate the prefix against characters allowed for function, class, constant names etc. * Note: this does not use the PHPCSUtils `NamingConventions::isValidIdentifierName()` method * as we want to allow namespace separators in the prefixes. */ if ( preg_match( '`^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\\\\]*$`', $prefix ) !== 1 ) { $this->phpcsFile->addWarning( 'The "%s" prefix is not a valid namespace/function/class/variable/constant prefix in PHP.', 0, 'InvalidPrefixPassed', array( $prefix ) ); } // Lowercase the prefix to allow for direct compare. $prefixes[ $key ] = $prefixLC; /* * Replace non-word characters in the prefix with a regex snippet, but only if the * string doesn't already contain namespace separators. */ $is_regex = false; if ( strpos( $prefix, '\\' ) === false && preg_match( '`[_\W]`', $prefix ) > 0 ) { $prefix = preg_replace( '`([_\W])`', '[\\\\\\\\$1]', $prefixLC ); $is_regex = true; } $ns_prefixes[ $prefixLC ] = array( 'prefix' => $prefix, 'is_regex' => $is_regex, ); } // Set the validated prefixes caches. $this->validated_prefixes = $prefixes; $this->validated_namespace_prefixes = $ns_prefixes; } /** * Record the "potential prefix" metric. * * @since 1.0.1 * * @param int $stackPtr The position of the token to record the metric against. * @param string $construct_name Name of the global construct to try and distill a potential prefix from. * * @return void */ private function record_potential_prefix_metric( $stackPtr, $construct_name ) { if ( preg_match( '`^([A-Z]*[a-z0-9]*+)`', ltrim( $construct_name, '\$_' ), $matches ) > 0 && isset( $matches[1] ) && '' !== $matches[1] ) { $this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: potential prefixes - start of non-prefixed construct', strtolower( $matches[1] ) ); } } }