/home/bonphmya/mercandestockages.store/wp-content/plugins/aioseo-image-seo/app/Image/Image.php
<?php
namespace AIOSEO\Plugin\Addon\ImageSeo\Image;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Adds support for Image SEO.
*
* @since 1.0.0
*/
class Image {
/**
* The supported image extensions.
*
* @since 1.1.6
*
* @var array
*/
private $supportedExtensions = [ 'png', 'jpg', 'jpeg', 'gif', 'heic', 'svg', 'ico', 'webp' ];
/**
* Class constructor.
*
* @since 1.0.0
*/
public function __construct() {
// Filter images embedded into the post content or 3rd party plugins.
add_filter( 'the_content', [ $this, 'filterContent' ], 99999 );
add_filter( 'seedprod_lpage_content', [ $this, 'filterContent' ] );
add_filter( 'woocommerce_short_description', [ $this, 'filterContent' ] );
// Filter images on attachment pages.
add_filter( 'wp_get_attachment_image_attributes', [ $this, 'filterImageAttributes' ], 10, 2 );
// Filter attachment data on upload.
add_filter( 'wp_insert_attachment_data', [ $this, 'filterImageData' ], 10, 2 );
add_filter( 'wp_unique_filename', [ $this, 'filterFilename' ], 20, 2 );
// Filter embedded attachment caption and description smart tags on-the-fly.
add_action( 'template_redirect', [ $this, 'parseEmbeddedImageSmartTags' ] );
// Bulk actions.
add_filter( 'bulk_actions-upload', [ $this, 'registerBulkActions' ] );
add_filter( 'handle_bulk_actions-upload', [ $this, 'handleBulkActions' ], 10, 3 );
}
/**
* Filters the content of the requested post.
*
* @since 1.0.0
*
* @param string $content The post content.
* @return string The filtered post content.
*/
public function filterContent( $content ) {
if ( is_admin() || ! is_singular() ) {
return $content;
}
return preg_replace_callback( '/(<img.[^>]*+>)(<figcaption(.*?)>(.*?)<\/figcaption>)?/', [ $this, 'filterEmbeddedImages' ], $content );
}
/**
* Filters the attributes of image attachment pages.
*
* @since 1.0.0
*
* @param array $attributes The image attributes.
* @param \WP_Post $post The post object.
* @return array The filtered image attributes
*/
public function filterImageAttributes( $attributes, $post = null ) {
if ( is_admin() || ! is_singular() ) {
return $attributes;
}
$attributes['title'] = $this->getAttribute( 'title', $post->ID );
$attributes['alt'] = $this->getAttribute( 'altTag', $post->ID );
return $attributes;
}
/**
* Filters the attributes of images that are embedded in the post content.
* Callback function for filterContent().
*
* @since 1.0.0
*
* @param array $images The HTML image tag (first match of Regex pattern).
* @return string The filtered HTML image tag.
*/
public function filterEmbeddedImages( $images ) {
$image = ! empty( $images[1] ) ? $images[1] : '';
$captionAttributes = ! empty( $images[3] ) ? $images[3] : '';
$caption = ! empty( $images[4] ) ? $images[4] : '';
$id = $this->imageId( $image );
if ( ! $id ) {
return $images[0];
}
if ( ! $this->isExcluded( 'title' ) ) {
$title = $this->findExistingAttribute( 'title', $image );
$image = $this->insertAttribute(
$image,
'title',
$this->getAttribute( 'title', $id, $title )
);
}
if ( ! $this->isExcluded( 'altTag' ) ) {
$altTag = $this->findExistingAttribute( 'alt', $image );
$image = $this->insertAttribute(
$image,
'alt',
$this->getAttribute( 'altTag', $id, $altTag )
);
}
$output = $image;
if ( ! empty( $caption ) ) {
$caption = aioseoImageSeo()->tags->replaceTags( $caption, $id, 'caption' );
$output .= "<figcaption$captionAttributes>$caption</figcaption>";
}
return $output;
}
/**
* Tries to extract the attachment page ID of an image.
*
* @since 1.0.0
*
* @param string $image The image HTML tag.
* @return mixed The ID of the attachment page or false if no ID could be found.
*/
private function imageId( $image ) {
$imageId = false;
// Check if class contains an ID.
if ( preg_match( '#wp-image-(\d+)#', $this->findExistingAttribute( 'class', $image ), $matches ) ) {
$imageId = intval( $matches[1] );
}
// Check for SeedProd image.
if ( ! $imageId && preg_match( '#sp-image-block-([a-z0-9]+)#', $this->findExistingAttribute( 'class', $image ), $matches ) ) {
$imageId = intval( $matches[1] );
}
// Allow WPML to find the translated attachment page.
if ( aioseo()->helpers->isWpmlActive() ) {
$imageId = apply_filters( 'wpml_object_id', $imageId, 'attachment', true );
}
return $imageId;
}
/**
* Inserts a given value for a given image attribute.
*
* @since 1.0.0
*
* @param string $image The image HTML.
* @param string $attributeName The attribute name.
* @param string $value The attribute value.
* @return array The modified image attributes.
*/
private function insertAttribute( $image, $attributeName, $value ) {
if ( empty( $value ) ) {
return $image;
}
$value = esc_attr( $value );
$image = preg_replace( $this->attributeRegex( $attributeName, true ), '${1}' . $value . '${2}', $image, 1, $count );
// Attribute does not exist. Let's append it at the beginning of the tag.
if ( ! $count ) {
$image = preg_replace( '/<img /', '<img ' . $this->attributeToHtml( $attributeName, $value ) . ' ', $image );
}
return $image;
}
/**
* Returns the value of a given image attribute.
*
* @since 1.0.0
*
* @param string $attributeName The attribute name.
* @param int $id The attachment page ID.
* @param string $value The value, if it already exists.
* @return string The attribute value.
*/
private function getAttribute( $attributeName, $id, $value = '' ) {
$format = aioseo()->options->image->$attributeName->format;
if ( $value ) {
// If the value already exists on the image (e.g. alt text on Image Block), use that to replace the relevant tag in the format.
$tag = 'title' === $attributeName ? '#image_title' : '#alt_tag';
$format = aioseo()->helpers->pregReplace( "/$tag/", $value, $format );
// Because some HTML entities are already decoded, we must decode them so that we can strip them.
$format = aioseo()->helpers->decodeHtmlEntities( $format );
}
$attribute = aioseoImageSeo()->tags->replaceTags( $format, $id, $attributeName );
$snakeName = aioseo()->helpers->toSnakeCase( $attributeName );
return apply_filters( "aioseo_image_seo_$snakeName", $attribute, [ $id ] );
}
/**
* Returns the value of the given attribute if it already exists.
*
* @since 1.0.6
*
* @param string $attributeName The attribute name, "title" or "alt".
* @param string $image The image HTML.
* @return string The value.
*/
private function findExistingAttribute( $attributeName, $image ) {
preg_match( $this->attributeRegex( $attributeName ), $image, $value );
return ! empty( $value ) ? $value[1] : false;
}
/**
* Returns a regex string to match an attribute.
*
* @since 1.0.7
*
* @param string $attributeName The attribute name.
* @param bool $groupReplaceValue Regex groupings without the value.
* @return string The regex string.
*/
private function attributeRegex( $attributeName, $groupReplaceValue = false ) {
$regex = $groupReplaceValue ? "/(\s%s=['\"]).*?(['\"])/" : "/\s%s=['\"](.*?)['\"]/";
return sprintf( $regex, trim( $attributeName ) );
}
/**
* Returns an attribute as HTML.
*
* @since 1.0.7
*
* @param string $attributeName The attribute name.
* @param string $value The attribute value.
* @return string The HTML formatted attribute.
*/
private function attributeToHtml( $attributeName, $value ) {
return sprintf( '%s="%s"', $attributeName, esc_attr( $value ) );
}
/**
* Filter image caption and description data.
*
* @since 1.1.0
*
* @param array $processedData An array of slashed, sanitized, and processed attachment image post data.
* @param array $unprocessedData An array of slashed and sanitized attachment post data, but not processed.
* @param bool $bulk Whether a bulk action is triggering this method.
* @return array An array of slashed, sanitized, modified and processed attachment image post data.
*/
public function filterImageData( $processedData, $unprocessedData = [], $bulk = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// If the attachment is not an image, return.
if ( ! strstr( $processedData['post_mime_type'], 'image/' ) ) {
return $processedData;
}
// If the post already has an ID, an existing attachment is being updated.
// In that case, we don't want to update the caption/description (only when uploading new images), except if we're running the bulk action.
if ( ! $bulk && ! empty( $unprocessedData['ID'] ) ) {
return $processedData;
}
if ( $bulk || aioseo()->options->image->caption->autogenerate ) {
$processedData['post_excerpt'] = aioseo()->options->image->caption->format;
}
if ( $bulk || aioseo()->options->image->description->autogenerate ) {
$processedData['post_content'] = aioseo()->options->image->description->format;
}
if ( ! $bulk ) {
// Harmonize the attachment post title/name with the filename after it's been sanitized.
$processedData['post_title'] = $this->filterFilenameHelper( $processedData['post_title'] );
$processedData['post_name'] = $this->filterFilenameHelper( $processedData['post_name'] );
}
return $processedData;
}
/**
* Filters the filename of new images when uploaded.
*
* @since 1.1.0
*
* @param string $filename The filename.
* @param string $extension The file extension.
* @return string Modified uploaded file data.
*/
public function filterFilename( $filename, $extension = '' ) {
// Ignore files that are not images.
if ( ! $this->isImage( $filename, $extension ) ) {
return $filename;
}
// First, remove the extension part of the filename temporarily so that we don't mess with it.
// We'll add it back at the end.
$filename = preg_replace( "/{$extension}/i", '', $filename );
$filename = $this->filterFilenameHelper( $filename );
// Now, add back the extension.
$filename .= $extension;
return $filename;
}
/**
* Helper method for filterFileanmeHelper().
*
* @since 1.1.1
*
* @param string $string The string.
* @return string The filtered string.
*/
private function filterFilenameHelper( $string ) {
$words = aioseo()->options->image->filename->wordsToStrip;
$casing = aioseo()->options->image->filename->casing;
// First, strip all words that are on the blacklist.
if ( ! empty( $words ) ) {
$words = explode( "\n", $words );
foreach ( $words as $word ) {
$escapedWord = preg_quote( $word );
// Strip the word, but not if it's part of another, longer word.
$string = preg_replace( "/(\b|\_|[0-9])({$escapedWord})(\b|\_|[0-9])/i", '$1$3', $string );
}
}
// Next, change the casing.
if ( ! empty( $casing ) ) {
$string = aioseo()->helpers->convertCase( $string, $casing );
}
// Finally, strip the punctuation.
if ( aioseo()->options->image->filename->stripPunctuation ) {
$string = aioseoImageSeo()->helpers->stripPunctuation( $string, 'filename' );
}
return ltrim( $string, '-' );
}
/**
* Checks whether the given file is an image.
*
* @since 1.1.6
*
* @param string $filename The filename.
* @param string $extension The file extension.
* @return bool Whether the file is an image.
*/
private function isImage( $filename, $extension = '' ) {
if ( ! $extension ) {
preg_match( '/.*\.(.*?)$/', $filename, $matches );
if ( ! empty( $matches[1] ) ) {
$extension = $matches[1];
}
}
return in_array( ltrim( strtolower( $extension ), '.' ), $this->supportedExtensions, true );
}
/**
* Parses the attachment caption and description when they are loaded.
*
* @since 1.1.0
*
* @return void
*/
public function parseEmbeddedImageSmartTags() {
if ( false !== stripos( strval( get_post_mime_type() ), 'image/' ) ) {
global $wp_query;
$wp_query->post->post_content = aioseoImageSeo()->tags->replaceTags(
$wp_query->post->post_content,
$wp_query->post->ID,
'description'
);
$wp_query->post->post_excerpt = aioseoImageSeo()->tags->replaceTags(
$wp_query->post->post_excerpt,
$wp_query->post->ID,
'caption'
);
}
}
/**
* Register the bulk action.
*
* @since 1.1.0
*
* @param array $bulkActions List of bulk actions.
* @return array Modified list of bulk actions.
*/
public function registerBulkActions( $bulkActions ) {
$bulkActions['autogenerate_attributes'] = __( 'Autogenerate image attributes', 'aioseo-image-seo' );
return $bulkActions;
}
/**
* Handle bulk actions.
*
* @since 1.1.0
*
* @param string $sendBack The redirect URL.
* @param string $actionName The action being taken.
* @param array $posts The items the bulk action is being applied to.
* @return string The redirect URL.
*/
public function handleBulkActions( $sendBack, $actionName = '', $posts = [] ) {
if ( 'autogenerate_attributes' !== $actionName ) {
return $sendBack;
}
foreach ( $posts as $id ) {
$post = get_post( $id, ARRAY_A );
if ( empty( $post['post_type'] ) || 'attachment' !== $post['post_type'] ) {
continue;
}
$filteredData = $this->filterImageData( $post, null, true );
wp_update_post( $filteredData );
}
return $sendBack;
}
/**
* Check if the post or term should be excluded.
*
* @since 1.1.0
*
* @param string $attributeName Image attribute name eg. title, alt...
* @return bool Whether the post/term is excluded or not.
*/
private function isExcluded( $attributeName ) {
$postId = get_the_ID();
$excludedPosts = aioseo()->options->image->{$attributeName}->advancedSettings->excludePosts;
if ( ! $postId ) {
return false;
}
foreach ( $excludedPosts as $p ) {
$post = json_decode( $p );
if ( $post->value === $postId ) {
return true;
}
}
$excludedTermIds = [];
$excludedTerms = aioseo()->options->image->$attributeName->advancedSettings->excludeTerms;
foreach ( $excludedTerms as $t ) {
$term = json_decode( $t );
if ( is_object( $term ) ) {
$excludedTermIds[] = (int) $term->value;
}
}
// Check if there is at least one excluded term assigned to the post.
$excludedTermRelationships = [];
if ( count( $excludedTermIds ) ) {
$excludedTermRelationships = aioseo()->core->db->start( 'term_relationships' )
->select( 'object_id' )
->where( 'object_id =', $postId )
->whereIn( 'term_taxonomy_id', $excludedTermIds )
->limit( 1 )
->run()
->result();
}
return ! empty( $excludedTermRelationships );
}
}