*/ private static $why = array(); /** * Cache key for a package scan. It includes the plugin version, so an upgrade of ShouldCheck * never shows a scan made by an older scanner. * * @param string $slug Plugin slug. * @param string $version Package version. * @return string */ public static function cache_key( $slug, $version ) { return 'shouldcheck_dirscan_' . md5( $slug . '|' . $version . '|' . ( defined( 'SHOULDCHECK_VERSION' ) ? SHOULDCHECK_VERSION : 'dev' ) ); } /** * Forget every cached package scan. */ public static function clear_cache() { global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\_transient\_shouldcheck\_dirscan\_%' OR option_name LIKE '\_transient\_timeout\_shouldcheck\_dirscan\_%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } /** * Why a package could not be read, in one sentence. Empty when it was read or has not been tried. * * @param object $info Directory metadata. * @return string */ public static function failure_reason( $info ) { $key = self::cache_key( (string) ( $info->slug ?? '' ), (string) ( $info->version ?? '' ) ); return (string) ( self::$why[ $key ] ?? '' ); } /** * Directory listing metadata for a slug. * * @param string $slug Plugin slug. * @return object|WP_Error */ public static function metadata( $slug ) { if ( ! ShouldCheck_Requirements::valid_slug( $slug ) ) { return new WP_Error( 'invalid_slug', 'Invalid WordPress.org plugin slug.' ); } if ( ! function_exists( 'plugins_api' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; } $info = plugins_api( 'plugin_information', array( 'slug' => $slug, 'fields' => array( 'sections' => false, 'description' => false, 'reviews' => false, 'banners' => false, 'icons' => false, ), ) ); if ( is_wp_error( $info ) ) { return $info; } if ( ! is_object( $info ) || empty( $info->slug ) || $info->slug !== $slug ) { return new WP_Error( 'metadata_mismatch', 'WordPress.org returned unexpected plugin metadata.' ); } return $info; } /** * Read the published package and scan it, without installing or running anything. * * The download URL is built from the validated slug and version, never taken from the API * response. Results are cached; failures are cached briefly so a slow network does not stall * every page load. * * @param object $info Directory metadata. * @return array{profile:array,header:array,php_minimum:?array}|null Null when the package could not be read. */ public static function inspect( $info ) { $slug = (string) ( $info->slug ?? '' ); $version = (string) ( $info->version ?? '' ); if ( ! ShouldCheck_Requirements::valid_slug( $slug ) || ! preg_match( '/^[0-9][0-9A-Za-z._-]{0,39}$/D', $version ) ) { return null; } $key = self::cache_key( $slug, $version ); // Downloading the package needs the administrator's consent. Without it, only the listing is read. if ( 'yes' !== ShouldCheck_Settings::get( 'package_lookup' ) ) { self::$why[ $key ] = 'Reading packages from WordPress.org is switched off.'; return null; } $cached = get_transient( $key ); if ( is_array( $cached ) ) { self::$why[ $key ] = (string) ( $cached['why'] ?? '' ); return $cached['scan'] ?? null; } if ( ! function_exists( 'download_url' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $url = 'https://downloads.wordpress.org/plugin/' . rawurlencode( $slug ) . '.' . rawurlencode( $version ) . '.zip'; $limit = function ( $args, $request_url ) use ( $url ) { if ( $request_url === $url ) { $args['limit_response_size'] = self::MAX_BYTES; $args['redirection'] = 2; // At most two redirects. $args['reject_unsafe_urls'] = true; // Refuse redirects to private or local addresses. $args['sslverify'] = true; } return $args; }; add_filter( 'http_request_args', $limit, 10, 2 ); $tmp = download_url( $url, 25 ); remove_filter( 'http_request_args', $limit, 10 ); $scan = null; $why = ''; if ( is_wp_error( $tmp ) ) { $msg = ShouldCheck_Views::plain( $tmp->get_error_message() ); if ( false !== stripos( $msg, 'valid URL' ) ) { // WordPress says this when it will not use an address, usually because this server cannot look up the host name. $why = 'The download failed. WordPress refused the download address, usually because this server cannot look up downloads.wordpress.org.'; } else { $why = 'The download failed: ' . sanitize_text_field( substr( $msg, 0, 160 ) ); } } else { if ( (int) filesize( $tmp ) > self::MAX_BYTES ) { $why = 'The package is larger than the 25 MB limit.'; } else { $got = ShouldCheck_Update_Diff::scan_zip( $tmp ); if ( isset( $got['error'] ) ) { $why = 'The package could not be read as a plugin ZIP.'; } else { $scan = $got; } } wp_delete_file( $tmp ); } self::$why[ $key ] = $why; set_transient( $key, array( 'scan' => $scan, 'why' => $why, ), $scan ? 12 * HOUR_IN_SECONDS : 10 * MINUTE_IN_SECONDS ); return $scan; } }