Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
perpending
/
wp-content
/
plugins
/
seo-by-rank-math-pro
/
includes
/
modules
/
image-seo
:
class-image-seo-pro.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * Image SEO module. * * @since 1.0 * @package RankMathPro * @subpackage RankMathPro\Admin * @author Rank Math <support@rankmath.com> */ namespace RankMathPro; use stdClass; use RankMath\Helper; use RankMath\Traits\Hooker; use RankMath\Helpers\HTML; defined( 'ABSPATH' ) || exit; /** * Image_Seo class. * * @codeCoverageIgnore */ class Image_Seo_Pro { use Hooker; /** * Change the case of the alt attribute. * * @var string */ public $alt_change_case; /** * Change the case of the title attribute. * * @var string */ public $title_change_case; /** * Holds the current content's captions within a loop. * * @var array */ protected $captions = []; /** * Constructor. */ public function __construct() { $this->action( 'rank_math/admin/settings/images', 'add_options' ); $this->filter( 'rank_math/settings/sanitize_fields', 'sanitize_fields', 10, 3 ); if ( Helper::get_settings( 'general.add_avatar_alt' ) ) { $this->filter( 'get_avatar', 'avatar_add_missing_alt', 99, 6 ); } if ( Helper::get_settings( 'general.add_img_caption' ) ) { $this->filter( 'shortcode_atts_caption', 'add_caption', 99, 1 ); $this->filter( 'the_content', 'content_add_caption', 99 ); } if ( Helper::get_settings( 'general.add_img_description' ) ) { $this->filter( 'the_content', 'add_description', 99 ); } $replacements = array_filter( Helper::get_settings( 'general.image_replacements', [] ) ); if ( ! empty( $replacements ) ) { $this->filter( 'the_content', 'attribute_caption_replacements', 100 ); $this->filter( 'post_thumbnail_html', 'attribute_caption_replacements', 20 ); $this->filter( 'woocommerce_single_product_image_thumbnail_html', 'attribute_caption_replacements', 20 ); $this->filter( 'shortcode_atts_caption', 'caption_replacements', 100, 3 ); } $this->action( 'wp_head', 'maybe_change_attributes_case', 110 ); if ( Helper::get_settings( 'general.img_caption_change_case' ) ) { $this->filter( 'shortcode_atts_caption', 'change_caption_case', 110, 1 ); $this->filter( 'the_content', 'change_content_caption_case', 110 ); } if ( Helper::get_settings( 'general.img_description_change_case' ) ) { $this->filter( 'the_content', 'change_description_case', 110 ); } $this->action( 'rank_math/vars/register_extra_replacements', 'register_replacements' ); $this->filter( 'cmb2_field_arguments', 'maybe_exclude_image_vars', 10 ); $this->action( 'rest_api_init', 'init_rest_api' ); } /** * Registers rest routes defined in the Image Seo module. * * @return void */ public function init_rest_api() { $rest = new Image_Seo_Pro\Rest(); $rest->register_routes(); } /** * Registers variable replacements for the Image SEO Pro module. */ public function register_replacements() { rank_math_register_var_replacement( 'imagealt', [ 'name' => esc_html__( 'Image Alt', 'rank-math-pro' ), 'description' => esc_html__( 'Alt text set for the current image.', 'rank-math-pro' ), 'variable' => 'imagealt', 'example' => '', 'nocache' => true, ], [ $this, 'get_imagealt' ] ); rank_math_register_var_replacement( 'imagetitle', [ 'name' => esc_html__( 'Image Title', 'rank-math-pro' ), 'description' => esc_html__( 'Title text set for the current image.', 'rank-math-pro' ), 'variable' => 'imagetitle', 'example' => '', 'nocache' => true, ], [ $this, 'get_imagetitle' ] ); } /** * Filter CMB field arguments to exclude `imagealt` & `imagetitle` when they are not needed. * * @param array $args Arguments array. * @return array */ public function maybe_exclude_image_vars( $args ) { if ( empty( $args['classes'] ) ) { return $args; } $classes = is_array( $args['classes'] ) ? $args['classes'] : explode( ' ', $args['classes'] ); if ( ! in_array( 'rank-math-supports-variables', $classes, true ) ) { return $args; } if ( ! is_string( $args['id'] ) || strpos( $args['id'], 'img_' ) !== false ) { return $args; } if ( ! isset( $args['attributes']['data-exclude-variables'] ) ) { $args['attributes']['data-exclude-variables'] = ''; } $args['attributes']['data-exclude-variables'] .= ',imagealt,imagetitle'; $args['attributes']['data-exclude-variables'] = trim( $args['attributes']['data-exclude-variables'], ',' ); return $args; } /** * Get the alt attribute of the attachment to use as a replacement. * See rank_math_register_var_replacement(). * * @codeCoverageIgnore * * @param string $var_args Variable name, for example %custom%. The '%' signs are optional. * @param object $replacement_args Additional title, description and example values for the variable. * * @return bool Replacement was registered successfully or not. */ public function get_imagealt( $var_args, $replacement_args = null ) { if ( empty( $replacement_args->alttext ) ) { return null; } return $replacement_args->alttext; } /** * Get the title attribute of the attachment to use as a replacement. * See rank_math_register_var_replacement(). * * @codeCoverageIgnore * * @param string $var_args Variable name, for example %custom%. The '%' signs are optional. * @param object $replacement_args Additional title, description and example values for the variable. * * @return bool Replacement was registered successfully or not. */ public function get_imagetitle( $var_args, $replacement_args = null ) { if ( empty( $replacement_args->titletext ) ) { return null; } return $replacement_args->titletext; } /** * Change case of alt & title attributes if needed. * * @return void */ public function maybe_change_attributes_case() { // Change image title and alt casing. $this->alt_change_case = Helper::get_settings( 'general.img_alt_change_case' ); $this->title_change_case = Helper::get_settings( 'general.img_title_change_case' ); if ( $this->alt_change_case || $this->title_change_case ) { $this->filter( 'the_content', 'change_attribute_case', 30 ); $this->filter( 'post_thumbnail_html', 'change_attribute_case', 30 ); $this->filter( 'woocommerce_single_product_image_thumbnail_html', 'change_attribute_case', 30 ); } } /** * Change case of alt & title attributes in a post content string. * * @param string $content Post content. * @return string New post content. */ public function change_attribute_case( $content ) { if ( empty( $content ) ) { return $content; } $stripped_content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content ); preg_match_all( '/<img ([^>]+)\/?>/iU', $stripped_content, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return $content; } foreach ( $matches as $image ) { $is_dirty = false; $attrs = HTML::extract_attributes( $image[0] ); if ( ! isset( $attrs['src'] ) ) { continue; } $this->set_image_attribute( $attrs, 'alt', $this->alt_change_case, $is_dirty ); $this->set_image_attribute( $attrs, 'title', $this->title_change_case, $is_dirty ); if ( $is_dirty ) { $new = '<img' . HTML::attributes_to_string( $attrs ) . '>'; $content = str_replace( $image[0], $new, $content ); } } return $content; } /** * Add alt attribute for avatars if they don't have one. * * @param string $avatar Avatar HTML. * @param mixed $id_or_email User ID or email. * @param int $size Width in px. * @param string $default Fallback. * @param string $alt Alt attribute value. * @param array $args Avatar args. * * @return string New avatar HTML. */ public function avatar_add_missing_alt( $avatar, $id_or_email, $size, $default, $alt, $args ) { // phpcs:ignore if ( is_admin() ) { return $avatar; } if ( empty( $avatar ) ) { return $avatar; } if ( ! empty( $alt ) ) { return $avatar; } if ( ! preg_match( '/<img ([^>]+)\/?>/iU', $avatar ) ) { return $avatar; } $attrs = HTML::extract_attributes( $avatar ); if ( ! empty( $attrs['alt'] ) ) { return $avatar; } $new_alt = ''; if ( is_a( $id_or_email, 'WP_Comment' ) ) { $user = $id_or_email->comment_author; } elseif ( is_int( $id_or_email ) ) { // This is a user ID. $user = get_user_by( 'id', $id_or_email ); } elseif ( is_string( $id_or_email ) ) { $user = get_user_by( 'email', $id_or_email ); } if ( is_a( $user, 'WP_User' ) ) { $new_alt = $user->get( 'display_name' ); } elseif ( is_string( $user ) ) { $new_alt = $user; } else { return $avatar; } // Translators: placeholder is the username or email. $attrs['alt'] = sprintf( __( 'Avatar of %s', 'rank-math-pro' ), $new_alt ); $attrs['alt'] = apply_filters( 'rank_math_pro/images/avatar_alt', $attrs['alt'], $id_or_email ); $new = '<img' . HTML::attributes_to_string( $attrs ) . '>'; // Change image title and alt casing. $this->alt_change_case = Helper::get_settings( 'general.img_alt_change_case' ); $this->title_change_case = Helper::get_settings( 'general.img_title_change_case' ); if ( $this->alt_change_case || $this->title_change_case ) { $new = $this->change_attribute_case( $new ); } return $new; } /** * Add missing caption text if needed. * * @param string $out Shortcode output. * * @return string New shortcode output. */ public function add_caption( $out ) { if ( ! empty( $out['caption'] ) ) { return $out; } $out['caption'] = trim( $this->replace_vars( Helper::get_settings( 'general.img_caption_format' ), $this->get_post() ) ); return $out; } /** * Change case for captions. * * @param string $out Shortcode output. * * @return string New shortcode output. */ public function change_caption_case( $out ) { if ( empty( $out['caption'] ) ) { return $out; } $out['caption'] = $this->change_case( $out['caption'], Helper::get_settings( 'general.img_caption_change_case' ) ); return $out; } /** * Change case for captions in Image Blocks. * * @param string $content Content output. * * @return string New output. */ public function change_content_caption_case( $content ) { $content = preg_replace_callback( '/(<figure[^<]+class="([^"]+ )?(wp-block-image|wp-caption).+<figcaption[^>]*>)([^<]+)(<\/figcaption>)/sU', [ $this, 'caption_case_cb' ], $content ); return $content; } /** * Change case for captions in Image Blocks. * * @param string $content Content output. * * @return string New output. */ public function content_add_caption( $content ) { $content = preg_replace_callback( '/<figure class="([^"]+ )?wp-block-image .+<\/figure>/sU', [ $this, 'add_caption_cb' ], $content ); return $content; } /** * Change case for captions in Image Blocks. * * @param string $matches Content output. * * @return string New output. */ public function caption_case_cb( $matches ) { return $matches[1] . $this->change_case( $matches[4], Helper::get_settings( 'general.img_caption_change_case' ) ) . $matches[5]; } /** * Add caption in Image Blocks. * * @param string $matches Content output. * * @return string New output. */ public function add_caption_cb( $matches ) { if ( stripos( $matches[0], '<figcaption' ) !== false ) { return $matches[0]; } $caption = trim( $this->replace_vars( Helper::get_settings( 'general.img_caption_format' ), $this->get_post( $matches[0] ) ) ); return str_replace( '</figure>', '<figcaption>' . $caption . '</figcaption></figure>', $matches[0] ); } /** * Add missing attachment description if needed. * * @param string $content Original content. * * @return string New content. */ public function add_description( $content ) { if ( get_post_type() !== 'attachment' ) { return $content; } $content_stripped = wp_strip_all_tags( $content ); if ( ! empty( $content_stripped ) ) { return $content; } return $content . trim( $this->replace_vars( Helper::get_settings( 'general.img_description_format' ), $this->get_post() ) ); } /** * Change case for image description. * * @param string $content Original content. * * @return string New content. */ public function change_description_case( $content ) { if ( get_post_type() !== 'attachment' ) { return $content; } $parts = preg_split( '/(<[^>]+>)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE ); $new = ''; foreach ( $parts as $i => $part ) { if ( '<' === substr( trim( $part ), 0, 1 ) ) { $new .= $part; continue; } $new .= $this->change_case( $part, Helper::get_settings( 'general.img_description_change_case' ) ); } return $new; } /** * Search & replace in alt & title attributes. * * @param string $content Original post content. * * @return string New post content. */ public function attribute_caption_replacements( $content ) { $replacements = array_filter( Helper::get_settings( 'general.image_replacements' ) ); foreach ( $replacements as $replacement_id => $replacement ) { if ( ! count( array_intersect( $replacement['replace_in'], [ 'alt', 'title', 'caption' ] ) ) ) { continue; } foreach ( $replacement['replace_in'] as $attr ) { if ( 'caption' === $attr ) { $content = $this->caption_replacement( $content, $replacement['find'], $replacement['replace'] ); } if ( 'alt' === $attr || 'title' === $attr ) { $content = $this->attribute_replacement( $content, $replacement['find'], $replacement['replace'], $attr ); } } } return $content; } /** * Do the replacement in an attribute. * * @param string $content Original content. * @param string $find Search string. * @param string $replace Replacement string. * @param string $attribute Attribute to look for. * * @return string New content. */ public function attribute_replacement( $content, $find, $replace, $attribute ) { $stripped_content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content ); preg_match_all( '/<img ([^>]+)\/?>/iU', $stripped_content, $matches, PREG_SET_ORDER ); if ( empty( $matches ) ) { return $content; } foreach ( $matches as $image ) { $attrs = HTML::extract_attributes( $image[0] ); if ( ! isset( $attrs['src'] ) ) { continue; } if ( empty( $attrs[ $attribute ] ) ) { continue; } $replace = $this->replace_vars( $replace, $this->get_post() ); $attrs[ $attribute ] = str_replace( $find, $replace, $attrs[ $attribute ] ); $new = '<img' . HTML::attributes_to_string( $attrs ) . '>'; $content = str_replace( $image[0], $new, $content ); } return $content; } /** * Search & replace in image captions. * * @param string $out Shortcode output. * @param array $pairs Possible attributes. * @param array $atts Shortcode attributes. * * @return string New shortcode output. */ public function caption_replacements( $out, $pairs, $atts ) { $replacements = array_filter( Helper::get_settings( 'general.image_replacements' ) ); foreach ( $replacements as $replacement_id => $replacement ) { if ( ! in_array( 'caption', $replacement['replace_in'], true ) ) { continue; } $caption = $atts['caption']; if ( empty( $caption ) ) { continue; } $replacement['replace'] = $this->replace_vars( $replacement['replace'], $this->get_post() ); $new_caption = str_replace( $replacement['find'], $replacement['replace'], $caption ); $out = str_replace( $caption, $new_caption, $out ); } return $out; } /** * Add options to Image SEO module. * * @param object $cmb CMB object. */ public function add_options( $cmb ) { $field_ids = wp_list_pluck( $cmb->prop( 'fields' ), 'id' ); $fields_position = array_search( 'img_title_format', array_keys( $field_ids ), true ) + 1; include_once __DIR__ . '/options.php'; } /** * Sanitize the Image SEO options. * * @param string $sanitized_value The sanitized value. * @param string $value Original field value. * @param string $field_id Field ID. * * @return string */ public function sanitize_fields( $sanitized_value, $value, $field_id ) { if ( ! in_array( $field_id, [ 'find', 'replace' ], true ) ) { return $sanitized_value; } return self::is_space( $value ) ? $value : sanitize_text_field( $value ); } /** * Sanitize the current value. Ignore it if the value is a space. * * @param string $value The replacement value. * * @return string */ public static function sanitize_replace_value( $value ) { return self::is_space( $value ) ? $value : sanitize_text_field( $value ); } /** * Do the replacement in the contents captions. * * @param string $content Original content. * @param string $find Search string. * @param string $replace Replacement string. * * @return string New content. */ protected function caption_replacement( $content, $find, $replace ) { if ( empty( $this->captions ) ) { $stripped_content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content ); preg_match_all( '/<figcaption[^>]+>.+?<\/figcaption>/iU', $stripped_content, $this->captions, PREG_SET_ORDER ); if ( empty( $this->captions ) ) { return $content; } } foreach ( $this->captions as $caption ) { $replace = $this->replace_vars( $replace, $this->get_post() ); $new = str_replace( $find, $replace, $caption[0] ); $content = str_replace( $caption[0], $new, $content ); } return $content; } /** * Change image attribute case after checking condition. * * @param array $attrs Array which hold rel attribute. * @param string $attribute Attribute to set. * @param boolean $condition Condition to check. * @param boolean $is_dirty Is dirty variable. */ private function set_image_attribute( &$attrs, $attribute, $condition, &$is_dirty ) { if ( $condition && ! empty( $attrs[ $attribute ] ) ) { $is_dirty = true; $attrs[ $attribute ] = $this->change_case( $attrs[ $attribute ], $condition ); } } /** * Turn first character of every sentence to uppercase. * * @param string $value Original string value. * * @return string New string. */ private function sentence_case( $value ) { $sentences = preg_split( '/([.?!]+)/', $value, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE ); $new_string = ''; foreach ( $sentences as $key => $sentence ) { $new_string .= ( $key & 1 ) === 0 ? $this->mb_ucfirst( trim( $sentence ) ) : $sentence . ' '; } return trim( $new_string ); } /** * Multibyte ucfirst(). * * @param string $value String value. * * @return string New string. */ private function mb_ucfirst( $value ) { return mb_strtoupper( mb_substr( $value, 0, 1 ) ) . mb_strtolower( mb_substr( $value, 1 ) ); } /** * Change case of string. * * @param string $value String to change. * @param string $string_case Case type to change to. * * @return string New string. */ private function change_case( $value, $string_case ) { $cases_hash = [ 'titlecase' => MB_CASE_TITLE, 'sentencecase' => MB_CASE_LOWER, 'lowercase' => MB_CASE_LOWER, 'uppercase' => MB_CASE_UPPER, ]; if ( ! isset( $cases_hash[ $string_case ] ) ) { return $value; } if ( 'sentencecase' === $string_case ) { return $this->sentence_case( $value ); } return mb_convert_case( $value, $cases_hash[ $string_case ] ); } /** * Get post object. * * @param array $image The image array. * * @return object */ private function get_post( $image = [] ) { $post = \get_post(); if ( empty( $post ) ) { $post = new stdClass(); } if ( empty( $image ) ) { return $post; } $attrs = HTML::extract_attributes( $image ); if ( empty( $attrs['src'] ) ) { return $post; } $post->filename = $attrs['src']; // Lazy load support. if ( ! empty( $attrs['data-src'] ) ) { $post->filename = $attrs['data-src']; } elseif ( ! empty( $attrs['data-layzr'] ) ) { $post->filename = $attrs['data-layzr']; } elseif ( ! empty( $attrs['nitro-lazy-srcset'] ) ) { $post->filename = $attrs['nitro-lazy-srcset']; } return $post; } /** * Replace `%variables%` with the context-dependent value. Ignore it if the value is a space. * * @param string $value The string containing the %variables%. * @param array $args Context object, can be post, taxonomy or term. * * @return string */ private function replace_vars( $value, $args = [] ) { return self::is_space( $value ) ? $value : Helper::replace_vars( $value, $args ); } /** * Check if current value is a space. * * @param string $value The value. * * @return boolean */ private static function is_space( $value ) { return 1 === preg_match_all( '/^\s*$/', $value ); } }