>|null */ private $categories; /** * Constructor. * * @param string $file Path to alternatives.json. */ public function __construct( string $file ) { $this->file = $file; } /** * Finds alternatives for a plugin. * * Matches the slug against known closed plugins first and falls back to * keywords in the plugin name. * * @param string $slug WordPress.org slug. * @param string $name Plugin name. * @return array */ public function find( string $slug, string $name ): array { $category = $this->match_category( $slug, $name ); if ( null === $category ) { return array(); } $found = array(); foreach ( $category['alternatives'] as $alternative ) { if ( empty( $alternative['slug'] ) || $alternative['slug'] === $slug ) { continue; } $found[] = array( 'slug' => (string) $alternative['slug'], 'name' => (string) ( $alternative['name'] ?? $alternative['slug'] ), 'url' => 'https://wordpress.org/plugins/' . rawurlencode( (string) $alternative['slug'] ) . '/', ); } return $found; } /** * Finds the matching category. * * @param string $slug Slug. * @param string $name Plugin name. * @return array|null */ private function match_category( string $slug, string $name ): ?array { $categories = $this->categories(); foreach ( $categories as $category ) { if ( in_array( $slug, (array) ( $category['closed'] ?? array() ), true ) ) { return $category; } } $haystack = self::lower( $name . ' ' . str_replace( '-', ' ', $slug ) ); foreach ( $categories as $category ) { foreach ( (array) ( $category['keywords'] ?? array() ) as $keyword ) { if ( preg_match( '/\b' . preg_quote( self::lower( (string) $keyword ), '/' ) . '/u', $haystack ) ) { return $category; } } } return null; } /** * UTF-8 safe lower case. * * On PHP < 8, strtolower() depends on the locale and can break multibyte * characters such as "ΓΌ", which makes preg_match() with /u fail. * * @param string $text Text. * @return string */ private static function lower( string $text ): string { if ( function_exists( 'mb_strtolower' ) ) { return mb_strtolower( $text, 'UTF-8' ); } return strtr( $text, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz' ); } /** * Loads the categories. * * @return array> */ private function categories(): array { if ( null === $this->categories ) { $this->categories = array(); if ( is_readable( $this->file ) ) { // Local file shipped with the plugin. $data = json_decode( (string) file_get_contents( $this->file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { foreach ( $data['categories'] as $key => $category ) { if ( is_array( $category ) && ! empty( $category['alternatives'] ) && is_array( $category['alternatives'] ) ) { $this->categories[ (string) $key ] = $category; } } } } } return $this->categories; } }