| 1 |
<?php |
| 2 |
|
| 3 |
namespace FlyWP\StaticSite\Engine\Discovery; |
| 4 |
|
| 5 |
/** |
| 6 |
* Source URLs of redirect plugins. The crawler fetches them, the origin answers with a 3xx, |
| 7 |
* and the engine writes the rule. |
| 8 |
* |
| 9 |
* - Plugins: Redirection, Rank Math, Yoast SEO Premium, Safe Redirect Manager and Simple 301 Redirects. |
| 10 |
* - One class that returns absolute URLs on the origin. |
| 11 |
* - Absolute sources on a site host (with or without "www.", http or https) move to the origin scheme and host. |
| 12 |
* - No Rank Math row limit. The _redirects limits apply later. |
| 13 |
* - Simple 301 wildcard sources become dynamic rules only when its wildcard option is on. |
| 14 |
* - `wp flywp static site` writes the result to site.json. |
| 15 |
* |
| 16 |
* - A plugin is read only when its classes or functions exist (the plugin is active). |
| 17 |
* - Regex rules and sources with "*" are not crawled. |
| 18 |
* |
| 19 |
* @since 1.8.0 |
| 20 |
*/ |
| 21 |
class RedirectSourceUrls { |
| 22 |
|
| 23 |
const RANK_MATH_PAGE_SIZE = 500; |
| 24 |
|
| 25 |
const CODES = [ 301, 302, 307, 308 ]; |
| 26 |
|
| 27 |
/** |
| 28 |
* Absolute source URLs on the origin. |
| 29 |
* |
| 30 |
* @param string $origin_url Origin URL. |
| 31 |
* @param string[] $site_urls Other URLs of the site (home URL, site URL). |
| 32 |
* |
| 33 |
* @return string[] |
| 34 |
*/ |
| 35 |
public static function all( $origin_url, array $site_urls = [] ) { |
| 36 |
$sources = array_merge( |
| 37 |
self::redirection(), |
| 38 |
self::rank_math(), |
| 39 |
self::yoast_premium(), |
| 40 |
self::safe_redirect_manager(), |
| 41 |
self::simple_301_redirects() |
| 42 |
); |
| 43 |
|
| 44 |
$urls = []; |
| 45 |
|
| 46 |
foreach ( $sources as $source ) { |
| 47 |
$url = self::to_origin( (string) $source, $origin_url, $site_urls ); |
| 48 |
|
| 49 |
if ( $url !== null && strpos( $url, '*' ) === false ) { |
| 50 |
$urls[ $url ] = true; |
| 51 |
} |
| 52 |
} |
| 53 |
|
| 54 |
return array_keys( $urls ); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Simple 301 Redirects wildcard rules, when its wildcard option is on. |
| 59 |
* |
| 60 |
* @param string $origin_url Origin URL. |
| 61 |
* @param string[] $site_urls Other URLs of the site. |
| 62 |
* |
| 63 |
* @return array<int, array{from: string, to: string, status: int}> |
| 64 |
*/ |
| 65 |
public static function wildcard_rules( $origin_url, array $site_urls = [] ) { |
| 66 |
if ( ! class_exists( 'Simple301Redirects' ) || get_option( '301_redirects_wildcard' ) !== 'true' ) { |
| 67 |
return []; |
| 68 |
} |
| 69 |
|
| 70 |
$rules = []; |
| 71 |
|
| 72 |
foreach ( (array) get_option( '301_redirects' ) as $from => $to ) { |
| 73 |
$from_url = self::to_origin( (string) $from, $origin_url, $site_urls ); |
| 74 |
|
| 75 |
if ( $from_url === null || strpos( $from_url, '*' ) === false ) { |
| 76 |
continue; |
| 77 |
} |
| 78 |
|
| 79 |
$to_url = self::to_origin( (string) $to, $origin_url, $site_urls ); |
| 80 |
$rules[] = [ |
| 81 |
'from' => $from_url, |
| 82 |
'to' => $to_url !== null ? $to_url : (string) $to, |
| 83 |
'status' => 301, |
| 84 |
]; |
| 85 |
} |
| 86 |
|
| 87 |
return $rules; |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Move a source onto the origin URL. |
| 92 |
* |
| 93 |
* @param string $source Relative path or absolute URL. |
| 94 |
* @param string $origin_url Origin URL. |
| 95 |
* @param string[] $site_urls Other URLs of the site. |
| 96 |
* |
| 97 |
* @return string|null Null for an empty source or a source on another host. |
| 98 |
*/ |
| 99 |
public static function to_origin( $source, $origin_url, array $site_urls ) { |
| 100 |
$source = trim( $source ); |
| 101 |
|
| 102 |
if ( $source === '' ) { |
| 103 |
return null; |
| 104 |
} |
| 105 |
|
| 106 |
// Plugins store site-root paths ("/blog/old-page" on a site in "/blog"), so resolve them like a browser. |
| 107 |
if ( ! Url::is_http( $source ) ) { |
| 108 |
return Url::resolve( rtrim( $origin_url, '/' ) . '/', $source ); |
| 109 |
} |
| 110 |
|
| 111 |
$hosts = []; |
| 112 |
|
| 113 |
foreach ( array_merge( [ $origin_url ], $site_urls ) as $url ) { |
| 114 |
$hosts[ self::bare_host( $url ) ] = true; |
| 115 |
} |
| 116 |
|
| 117 |
if ( ! isset( $hosts[ self::bare_host( $source ) ] ) ) { |
| 118 |
return null; |
| 119 |
} |
| 120 |
|
| 121 |
$parts = Url::parts( $source ); |
| 122 |
$origin = Url::parts( $origin_url ); |
| 123 |
|
| 124 |
$parts['scheme'] = $origin['scheme']; |
| 125 |
$parts['host'] = $origin['host']; |
| 126 |
$parts['port'] = $origin['port']; |
| 127 |
$parts['user'] = ''; |
| 128 |
$parts['pass'] = ''; |
| 129 |
|
| 130 |
return Url::build( $parts ); |
| 131 |
} |
| 132 |
|
| 133 |
/** |
| 134 |
* Host without "www.". |
| 135 |
* |
| 136 |
* @param string $url URL. |
| 137 |
* |
| 138 |
* @return string |
| 139 |
*/ |
| 140 |
private static function bare_host( $url ) { |
| 141 |
return (string) preg_replace( '/^www\./', '', Url::parts( $url )['host'] ); |
| 142 |
} |
| 143 |
|
| 144 |
/** |
| 145 |
* Redirection plugin. |
| 146 |
* |
| 147 |
* @return string[] |
| 148 |
*/ |
| 149 |
private static function redirection() { |
| 150 |
if ( ! class_exists( 'Red_Item' ) || ! method_exists( 'Red_Item', 'get_all' ) ) { |
| 151 |
return []; |
| 152 |
} |
| 153 |
|
| 154 |
$urls = []; |
| 155 |
|
| 156 |
foreach ( \Red_Item::get_all() as $item ) { |
| 157 |
if ( ! $item->is_enabled() || $item->is_dynamic() || $item->is_regex() ) { |
| 158 |
continue; |
| 159 |
} |
| 160 |
|
| 161 |
if ( $item->get_match_type() !== 'url' || $item->get_action_type() !== 'url' ) { |
| 162 |
continue; |
| 163 |
} |
| 164 |
|
| 165 |
$urls[] = $item->get_url(); |
| 166 |
} |
| 167 |
|
| 168 |
return $urls; |
| 169 |
} |
| 170 |
|
| 171 |
/** |
| 172 |
* Rank Math, all active rules, page by page. |
| 173 |
* |
| 174 |
* @return string[] |
| 175 |
*/ |
| 176 |
private static function rank_math() { |
| 177 |
if ( ! class_exists( '\RankMath\Redirections\DB' ) ) { |
| 178 |
return []; |
| 179 |
} |
| 180 |
|
| 181 |
$urls = []; |
| 182 |
$first_id = null; |
| 183 |
|
| 184 |
for ( $page = 1; ; $page++ ) { |
| 185 |
$result = \RankMath\Redirections\DB::get_redirections( |
| 186 |
[ |
| 187 |
'status' => 'active', |
| 188 |
'limit' => self::RANK_MATH_PAGE_SIZE, |
| 189 |
'paged' => $page, |
| 190 |
] |
| 191 |
); |
| 192 |
$redirects = isset( $result['redirections'] ) ? array_values( (array) $result['redirections'] ) : []; |
| 193 |
$page_id = isset( $redirects[0]['id'] ) ? $redirects[0]['id'] : null; |
| 194 |
|
| 195 |
// Stop when the plugin ignores "paged" and gives the first page again. |
| 196 |
if ( ! $redirects || ( $page > 1 && $page_id === $first_id ) ) { |
| 197 |
break; |
| 198 |
} |
| 199 |
|
| 200 |
$first_id = $page === 1 ? $page_id : $first_id; |
| 201 |
|
| 202 |
foreach ( $redirects as $redirect ) { |
| 203 |
if ( ! in_array( (int) $redirect['header_code'], self::CODES, true ) ) { |
| 204 |
continue; |
| 205 |
} |
| 206 |
|
| 207 |
foreach ( (array) maybe_unserialize( $redirect['sources'] ) as $source ) { |
| 208 |
if ( isset( $source['comparison'], $source['pattern'] ) && $source['comparison'] === 'exact' ) { |
| 209 |
$urls[] = $source['pattern']; |
| 210 |
} |
| 211 |
} |
| 212 |
} |
| 213 |
|
| 214 |
if ( count( $redirects ) < self::RANK_MATH_PAGE_SIZE ) { |
| 215 |
break; |
| 216 |
} |
| 217 |
} |
| 218 |
|
| 219 |
return $urls; |
| 220 |
} |
| 221 |
|
| 222 |
/** |
| 223 |
* Yoast SEO Premium. |
| 224 |
* |
| 225 |
* @return string[] |
| 226 |
*/ |
| 227 |
private static function yoast_premium() { |
| 228 |
if ( ! class_exists( 'WPSEO_Redirect_Manager' ) ) { |
| 229 |
return []; |
| 230 |
} |
| 231 |
|
| 232 |
$urls = []; |
| 233 |
|
| 234 |
foreach ( (array) get_option( 'wpseo-premium-redirects-base' ) as $item ) { |
| 235 |
if ( is_array( $item ) && isset( $item['format'], $item['type'], $item['origin'] ) && $item['format'] === 'plain' && in_array( (int) $item['type'], self::CODES, true ) ) { |
| 236 |
$urls[] = $item['origin']; |
| 237 |
} |
| 238 |
} |
| 239 |
|
| 240 |
return $urls; |
| 241 |
} |
| 242 |
|
| 243 |
/** |
| 244 |
* Safe Redirect Manager. |
| 245 |
* |
| 246 |
* @return string[] |
| 247 |
*/ |
| 248 |
private static function safe_redirect_manager() { |
| 249 |
if ( ! class_exists( 'SRM_Redirect' ) || ! function_exists( 'srm_get_redirects' ) || ! function_exists( 'srm_get_max_redirects' ) ) { |
| 250 |
return []; |
| 251 |
} |
| 252 |
|
| 253 |
$redirects = srm_get_redirects( |
| 254 |
[ |
| 255 |
'posts_per_page' => srm_get_max_redirects(), // phpcs:ignore WordPress.WP.PostsPerPage -- the limit of Safe Redirect Manager itself. |
| 256 |
'post_status' => 'publish', |
| 257 |
] |
| 258 |
); |
| 259 |
$urls = []; |
| 260 |
|
| 261 |
foreach ( (array) $redirects as $item ) { |
| 262 |
if ( empty( $item['enable_regex'] ) && in_array( (int) $item['status_code'], self::CODES, true ) ) { |
| 263 |
$urls[] = $item['redirect_from']; |
| 264 |
} |
| 265 |
} |
| 266 |
|
| 267 |
return $urls; |
| 268 |
} |
| 269 |
|
| 270 |
/** |
| 271 |
* Simple 301 Redirects. Sources with "*" are removed in all(). |
| 272 |
* |
| 273 |
* @return string[] |
| 274 |
*/ |
| 275 |
private static function simple_301_redirects() { |
| 276 |
if ( ! class_exists( 'Simple301Redirects' ) ) { |
| 277 |
return []; |
| 278 |
} |
| 279 |
|
| 280 |
$redirects = get_option( '301_redirects' ); |
| 281 |
|
| 282 |
return is_array( $redirects ) ? array_map( 'strval', array_keys( $redirects ) ) : []; |
| 283 |
} |
| 284 |
} |
| 285 |
|