v1.1.0
This commit is contained in:
parent
9b856d1c3d
commit
ddaaff71ad
23 changed files with 879 additions and 798 deletions
|
|
@ -51,7 +51,7 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Check postmeta cache first.
|
||||
$cached_url = get_post_meta( $post_id, '_og_image_fallback_url', true );
|
||||
|
||||
if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
$this->log(
|
||||
'cache_hit',
|
||||
array(
|
||||
|
|
@ -103,7 +103,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
}
|
||||
|
||||
// Detect file extension.
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
// If already a compatible format, save and return.
|
||||
|
|
@ -191,9 +192,14 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Strip query string for URL manipulation.
|
||||
$clean_url = strtok( $image_url, '?' );
|
||||
|
||||
if ( ! is_string( $clean_url ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Parse URL and get base path without extension.
|
||||
$path_info = pathinfo( wp_parse_url( $clean_url, PHP_URL_PATH ) );
|
||||
$filename = $path_info['filename'] ?? '';
|
||||
$url_path = wp_parse_url( $clean_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$filename = $path_info['filename'];
|
||||
$dir = $path_info['dirname'] ?? '';
|
||||
|
||||
if ( empty( $filename ) ) {
|
||||
|
|
@ -224,10 +230,52 @@ class Robotstxt_OG_Image_Resolver {
|
|||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is safe to make server-side HTTP requests to.
|
||||
*
|
||||
* Rejects non-http(s) schemes and URLs that resolve to private or
|
||||
* reserved IP ranges to prevent Server-Side Request Forgery (SSRF).
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param string $url URL to validate.
|
||||
* @return bool True if the URL is safe to request.
|
||||
*/
|
||||
private function is_safe_url( string $url ): bool {
|
||||
$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = wp_parse_url( $url, PHP_URL_HOST );
|
||||
if ( ! is_string( $host ) || '' === $host ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip IPv6 brackets for validation.
|
||||
$host_bare = trim( $host, '[]' );
|
||||
|
||||
// If the host is already an IP address, validate it directly.
|
||||
if ( filter_var( $host_bare, FILTER_VALIDATE_IP ) !== false ) {
|
||||
return (bool) filter_var( $host_bare, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
|
||||
}
|
||||
|
||||
// Resolve hostname to IP and validate it is not private/reserved.
|
||||
$resolved = gethostbyname( $host_bare );
|
||||
|
||||
// gethostbyname() returns the original string on failure.
|
||||
if ( $resolved === $host_bare ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) filter_var( $resolved, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL exists using HTTP HEAD request.
|
||||
*
|
||||
* Uses negative caching to avoid repeated requests for non-existent URLs.
|
||||
* URLs resolving to private or reserved IP ranges are rejected (SSRF protection).
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
|
|
@ -235,6 +283,12 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return bool True if URL returns 200-299 status code.
|
||||
*/
|
||||
private function url_exists( string $url ): bool {
|
||||
// Reject URLs that resolve to private/reserved IP ranges (SSRF protection).
|
||||
if ( ! $this->is_safe_url( $url ) ) {
|
||||
$this->log( 'ssrf_blocked', array( 'url' => $url ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check negative cache to avoid repeated failed requests.
|
||||
$cache_key = self::NEGATIVE_CACHE_PREFIX . md5( $url );
|
||||
|
||||
|
|
@ -333,7 +387,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
private function get_global_fallback(): string {
|
||||
$fallback_url = (string) get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$fallback_url = is_string( $option_value ) ? $option_value : '';
|
||||
|
||||
if ( empty( $fallback_url ) || ! $this->is_valid_url( $fallback_url ) ) {
|
||||
return '';
|
||||
|
|
@ -354,7 +409,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
public function get_homepage_image(): string {
|
||||
$image_url = (string) get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$image_url = is_string( $option_value ) ? $option_value : '';
|
||||
|
||||
if ( ! empty( $image_url ) && $this->is_valid_url( $image_url ) ) {
|
||||
return esc_url_raw( $image_url );
|
||||
|
|
@ -378,7 +434,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
public function ensure_compatible_format( string $image_url ): string {
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
// Already a compatible format.
|
||||
|
|
@ -459,7 +516,7 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Check term meta cache first.
|
||||
$cached_url = get_term_meta( $term_id, '_og_image_fallback_url', true );
|
||||
|
||||
if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
$this->log(
|
||||
'taxonomy_cache_hit',
|
||||
array(
|
||||
|
|
@ -504,7 +561,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
}
|
||||
|
||||
// Detect file extension and resolve if needed.
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
if ( in_array( $extension, array( 'jpg', 'jpeg', 'png' ), true ) ) {
|
||||
|
|
@ -555,8 +613,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $event Event name (e.g., 'cache_hit', 'head_request_error').
|
||||
* @param array $context Additional context data.
|
||||
* @param string $event Event name (e.g., 'cache_hit', 'head_request_error').
|
||||
* @param array<string, mixed> $context Additional context data.
|
||||
* @return void
|
||||
*/
|
||||
private function log( string $event, array $context = array() ): void {
|
||||
|
|
|
|||
Loading…
Reference in a new issue