plugin()->main_file(); $labels = array( 'Name' => 'Plugin Name', 'PluginURI' => 'Plugin URI', 'Version' => 'Version', 'Description' => 'Description', 'Author' => 'Author', 'AuthorURI' => 'Author URI', 'TextDomain' => 'Text Domain', 'DomainPath' => 'Domain Path', 'Network' => 'Network', 'RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', 'UpdateURI' => 'Update URI', 'RequiresPlugins' => 'Requires Plugins', 'License' => 'License', 'LicenseURI' => 'License URI', ); $restricted_labels = array( 'RestrictedLabel' => 'Restricted Label', ); // Reserved for future use. $plugin_header = $this->get_plugin_data( $plugin_main_file, array_merge( $labels, $restricted_labels ) ); if ( ! empty( $plugin_header['Name'] ) ) { if ( in_array( $plugin_header['Name'], array( 'Plugin Name', 'My Basics Plugin' ), true ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ), esc_html( $labels['Name'] ) ), 'plugin_header_invalid_plugin_name', $plugin_main_file, 0, 0, '', 6 ); } else { $valid_chars_count = preg_match_all( '/[a-z0-9]/i', $plugin_header['Name'] ); if ( intval( $valid_chars_count ) < 5 ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file is not valid. It needs to contain at least 5 latin letters (a-Z) and/or numbers. This is necessary because the initial plugin slug is generated from the name.', 'plugin-check' ), esc_html( $labels['Name'] ) ), 'plugin_header_unsupported_plugin_name', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields', 7 ); } } } if ( ! empty( $plugin_header['PluginURI'] ) ) { if ( true !== $this->is_valid_url( $plugin_header['PluginURI'] ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ), esc_html( $labels['PluginURI'] ) ), 'plugin_header_invalid_plugin_uri', $plugin_main_file, 0, 0, '', 6 ); } elseif ( preg_match( '/\/\/(example\.com|example\.net|example\.org)\//', $plugin_header['PluginURI'], $matches ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: domain */ __( 'The "%1$s" header in the plugin file is not valid. Discouraged domain "%2$s" found. This is the homepage of the plugin, which should be a unique URL, preferably on your own website. ', 'plugin-check' ), esc_html( $labels['PluginURI'] ), esc_html( $matches[1] ), ), 'plugin_header_invalid_plugin_uri_domain', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields', 6 ); } } if ( empty( $plugin_header['Description'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header is missing in the plugin file.', 'plugin-check' ), esc_html( $labels['Description'] ) ), 'plugin_header_missing_plugin_description', $plugin_main_file, 0, 0, __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ), 7 ); } else { if ( str_contains( $plugin_header['Description'], 'This is a short description of what the plugin does' ) || str_contains( $plugin_header['Description'], 'Here is a short description of the plugin' ) || str_contains( $plugin_header['Description'], 'Handle the basics with this plugin' ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file should not contain default text.', 'plugin-check' ), esc_html( $labels['Description'] ) ), 'plugin_header_invalid_plugin_description', $plugin_main_file, 0, 0, '', 6 ); } } if ( empty( $plugin_header['Version'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header is missing in the plugin file.', 'plugin-check' ), esc_html( $labels['Version'] ) ), 'plugin_header_missing_plugin_version', $plugin_main_file, 0, 0, __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ), 7 ); } else { if ( ! preg_match( '/^[a-z0-9.-]+$/i', $plugin_header['Version'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file should only contain numbers, letters, periods, and hyphens.', 'plugin-check' ), esc_html( $labels['Version'] ) ), 'plugin_header_invalid_plugin_version', $plugin_main_file, 0, 0, __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ), 7 ); } } if ( ! empty( $plugin_header['AuthorURI'] ) ) { if ( true !== $this->is_valid_url( $plugin_header['AuthorURI'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file is not valid.', 'plugin-check' ), esc_html( $labels['AuthorURI'] ) ), 'plugin_header_invalid_author_uri', $plugin_main_file, 0, 0, '', 7 ); } } if ( ! empty( $plugin_header['Network'] ) ) { if ( 'true' !== strtolower( $plugin_header['Network'] ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file is not valid. Can only be set to true, and should be left out when not needed.', 'plugin-check' ), esc_html( $labels['Network'] ) ), 'plugin_header_invalid_network', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields', 6 ); } } if ( ! empty( $plugin_header['RequiresWP'] ) ) { $latest_wp_version = $this->get_wordpress_stable_version(); if ( ! preg_match( '!^\d+\.\d(\.\d+)?$!', $plugin_header['RequiresWP'] ) ) { $previous_wp_version = $this->get_wordpress_relative_major_version( $latest_wp_version, -1 ); $this->add_result_error_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: Example version 6.7, 3: Example version 6.6 */ __( 'The "%1$s" header in the plugin file should only contain a WordPress version such as "%2$s" or "%3$s".', 'plugin-check' ), esc_html( $labels['RequiresWP'] ), esc_html( $latest_wp_version ), esc_html( $previous_wp_version ) ), 'plugin_header_invalid_requires_wp', $plugin_main_file, 0, 0, '', 7 ); } else { $acceptable_min_wp_version = $this->get_wordpress_relative_major_version( $latest_wp_version, 1 ); if ( version_compare( $plugin_header['RequiresWP'], $acceptable_min_wp_version, '>' ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: currently used version */ __( '%1$s: %2$s.
The "%1$s" value in your plugin header is not valid. This version of WordPress does not exist (yet).', 'plugin-check' ), esc_html( $labels['RequiresWP'] ), esc_html( $plugin_header['RequiresWP'] ) ), 'plugin_header_nonexistent_requires_wp', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/#header-fields', 7 ); } } } if ( ! empty( $plugin_header['RequiresPHP'] ) ) { if ( ! preg_match( '!^\d+(\.\d+){1,2}$!', $plugin_header['RequiresPHP'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: 1: plugin header field; 2: Example version 5.2.4. 3: Example version 7.0. */ __( 'The "%1$s" header in the plugin file should only contain a PHP version such as "%2$s" or "%3$s".', 'plugin-check' ), esc_html( $labels['RequiresPHP'] ), '5.2.4', '7.0' ), 'plugin_header_invalid_requires_php', $plugin_main_file, 0, 0, '', 7 ); } } if ( ! empty( $plugin_header['RequiresPlugins'] ) ) { if ( ! preg_match( '/^[a-z0-9-]+(?:,\s*[a-z0-9-]+)*$/', $plugin_header['RequiresPlugins'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file must contain a comma-separated list of WordPress.org-formatted slugs.', 'plugin-check' ), esc_html( $labels['RequiresPlugins'] ) ), 'plugin_header_invalid_requires_plugins', $plugin_main_file, 0, 0, '', 7 ); } } if ( empty( $plugin_header['License'] ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'Missing "%s" in Plugin Header.
Please update your Plugin Header with a valid GPLv2 (or later) compatible license.', 'plugin-check' ), esc_html( $labels['License'] ) ), 'plugin_header_no_license', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared', 9 ); } else { $plugin_license = $this->get_normalized_license( $plugin_header['License'] ); if ( ! $this->is_license_gpl_compatible( $plugin_license ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: license */ __( 'Invalid %1$s: %2$s.
Please update your Plugin Header with a valid GPLv2 (or later) compatible license.', 'plugin-check' ), esc_html( $labels['License'] ), esc_html( $plugin_header['License'] ) ), 'plugin_header_invalid_license', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/wordpress-org/common-issues/#no-gpl-compatible-license-declared', 9 ); } } $found_headers = array(); foreach ( $restricted_labels as $restricted_key => $restricted_label ) { if ( array_key_exists( $restricted_key, $plugin_header ) && ! empty( $plugin_header[ $restricted_key ] ) ) { $found_headers[ $restricted_key ] = $restricted_label; } } if ( ! empty( $found_headers ) ) { $this->add_result_error_for_file( $result, sprintf( /* translators: %s: header fields */ __( 'Restricted plugin header field(s) found: %s', 'plugin-check' ), "'" . implode( "', '", array_values( $found_headers ) ) . "'" ), 'plugin_header_restricted_fields', $plugin_main_file, 0, 0, '', 7 ); } if ( ! $result->plugin()->is_single_file_plugin() ) { if ( ! empty( $plugin_header['TextDomain'] ) ) { $plugin_slug = $result->plugin()->slug(); if ( $plugin_slug !== $plugin_header['TextDomain'] ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: plugin header text domain, 3: plugin slug */ __( 'The "%1$s" header in the plugin file does not match the slug. Found "%2$s", expected "%3$s".', 'plugin-check' ), esc_html( $labels['TextDomain'] ), esc_html( $plugin_header['TextDomain'] ), esc_html( $plugin_slug ) ), 'textdomain_mismatch', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/', 6 ); } } if ( ! empty( $plugin_header['DomainPath'] ) ) { if ( ! str_starts_with( $plugin_header['DomainPath'], '/' ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: %s: plugin header field */ __( 'The "%s" header in the plugin file must start with forward slash.', 'plugin-check' ), esc_html( $labels['DomainPath'] ) ), 'plugin_header_invalid_domain_path', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#domain-path', 6 ); } $domain_path = trim( $plugin_header['DomainPath'], '/' ); $target_path = wp_normalize_path( $result->plugin()->path() . $domain_path ); if ( ! is_dir( $target_path ) ) { $this->add_result_warning_for_file( $result, sprintf( /* translators: 1: plugin header field, 2: domain path */ __( 'The "%1$s" header in the plugin file must point to an existing folder. Found: "%2$s"', 'plugin-check' ), esc_html( $labels['DomainPath'] ), $domain_path ), 'plugin_header_nonexistent_domain_path', $plugin_main_file, 0, 0, 'https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#domain-path', 6 ); } } } } /** * Checks if URL is valid. * * @since 1.2.0 * * @param string $url URL. * @return bool true if the URL is valid, otherwise false. */ private function is_valid_url( $url ) { if ( filter_var( $url, FILTER_VALIDATE_URL ) !== $url || ! str_starts_with( $url, 'http' ) ) { return false; } // Detect duplicated protocol (e.g., "https://http://example.com/"). $parsed_url = wp_parse_url( $url ); if ( isset( $parsed_url['scheme'] ) && str_contains( substr( $url, strlen( $parsed_url['scheme'] ) + 3 ), '://' ) ) { return false; } return true; } /** * Parses the plugin contents to retrieve plugin's metadata. * * @since 1.2.0 * * @param string $plugin_file Absolute path to the main plugin file. * @param array $default_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`. * @return string[] Array of file header values keyed by header name. */ private function get_plugin_data( $plugin_file, $default_headers ) { $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' ); // If no text domain is defined fall back to the plugin slug. if ( ! $plugin_data['TextDomain'] ) { $plugin_slug = dirname( plugin_basename( $plugin_file ) ); if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) { $plugin_data['TextDomain'] = $plugin_slug; } } return $plugin_data; } /** * Gets the description for the check. * * Every check must have a short description explaining what the check does. * * @since 1.2.0 * * @return string Description. */ public function get_description(): string { return __( 'Checks adherence to the Headers requirements.', 'plugin-check' ); } /** * Gets the documentation URL for the check. * * Every check must have a URL with further information about the check. * * @since 1.2.0 * * @return string The documentation URL. */ public function get_documentation_url(): string { return __( 'https://developer.wordpress.org/plugins/plugin-basics/header-requirements/', 'plugin-check' ); } }