Файловый менеджер - Редактировать - /var/www/xthruster/html/wp-content/uploads/flags/inc.tar
Назад
traits/validator.php 0000644 00000002150 14720403740 0010547 0 ustar 00 <?php /** * Validation traits. * * @package \Optml\Inc\Traits * @author Optimole <friends@optimole.com> */ trait Optml_Validator { /** * Check if the value is a valid numeric. * * @param mixed $value The value to check. * * @return bool */ public function is_valid_numeric( $value ) { if ( isset( $value ) && ! empty( $value ) && is_numeric( $value ) ) { return true; } return false; } /** * Check if the URl is a GIF url. * * @param string $url URL to check. * * @return bool Is Gif? */ public function is_valid_gif( $url ) { $type = wp_check_filetype( $url, [ 'gif' => 'image/gif' ] ); if ( empty( $type['ext'] ) ) { return false; } return true; } /** * Check if the url has an accepted mime type extension. * * @param mixed $url The url to check. * * @return bool|string */ public function is_valid_mimetype_from_url( $url, $filters = [] ) { $type = wp_check_filetype( $url, Optml_Config::$all_extensions ); if ( empty( $type['ext'] ) ) { return false; } return Optml_Filters::should_do_extension( $filters, $type['ext'] ); } } traits/dam_offload_utils.php 0000644 00000017604 14720403740 0012247 0 ustar 00 <?php trait Optml_Dam_Offload_Utils { use Optml_Normalizer; /** * Checks that the attachment is a DAM image. * * @param int $post_id The attachment ID. * * @return bool */ private function is_dam_imported_image( $post_id ) { $meta = get_post_meta( $post_id, Optml_Dam::OM_DAM_IMPORTED_FLAG, true ); if ( empty( $meta ) ) { return false; } return true; } /** * Checks if the attachment is offloaded using the old method. * * @param int $id The attachment ID. * * @return bool */ private function is_legacy_offloaded_attachment( $id ) { $id = apply_filters( 'optml_ensure_source_attachment_id', $id ); return ! $this->is_new_offloaded_attachment( $id ) && ! empty( get_post_meta( $id, Optml_Media_Offload::META_KEYS['offloaded'] ) ); } /** * Check if it's a newly offloaded attachment * * @param int $id The attachment ID. * * @return bool */ private function is_new_offloaded_attachment( $id ) { $id = apply_filters( 'optml_ensure_source_attachment_id', $id ); return ! empty( get_post_meta( $id, Optml_Media_Offload::OM_OFFLOADED_FLAG, true ) ); } /** * Get all registered image sizes. * * @return array */ private function get_all_image_sizes() { $additional_sizes = wp_get_additional_image_sizes(); $intermediate = get_intermediate_image_sizes(); $all = []; foreach ( $intermediate as $size ) { if ( isset( $additional_sizes[ $size ] ) ) { $all[ $size ] = [ 'width' => $additional_sizes[ $size ]['width'], 'height' => $additional_sizes[ $size ]['height'], 'crop' => isset( $additional_sizes[ $size ]['crop'] ) ? $additional_sizes[ $size ]['crop'] : false, ]; } else { $all[ $size ] = [ 'width' => (int) get_option( $size . '_size_w' ), 'height' => (int) get_option( $size . '_size_h' ), 'crop' => get_option( $size . '_crop' ), ]; } if ( ! empty( $additional_sizes[ $size ]['crop'] ) ) { $all[ $size ]['crop'] = is_array( $additional_sizes[ $size ]['crop'] ) ? $additional_sizes[ $size ]['crop'] : (bool) $additional_sizes[ $size ]['crop']; } else { $all[ $size ]['crop'] = (bool) get_option( $size . '_crop' ); } } return $all; } /** * Get the dimension from optimized url. * * @param string $url The image url. * * @return array Contains the width and height values in this order. */ private function parse_dimension_from_optimized_url( $url ) { $catch = []; $height = 'auto'; $width = 'auto'; preg_match( '/\/w:(.*)\/h:(.*)\/q:/', $url, $catch ); if ( isset( $catch[1] ) && isset( $catch[2] ) ) { $width = $catch[1]; $height = $catch[2]; } return [ $width, $height ]; } /** * Check if we're in the attachment edit page. * * /wp-admin/post.php?post=<id>&action=edit * * Send whatever comes from the DAM. * * @param int $attachment_id attachment id. * * @return bool */ private function is_attachment_edit_page( $attachment_id ) { if ( ! is_admin() ) { return false; } if ( ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); if ( ! isset( $screen->base ) ) { return false; } if ( $screen->base !== 'post' ) { return false; } if ( $screen->post_type !== 'attachment' ) { return false; } if ( $screen->id !== 'attachment' ) { return false; } if ( ! isset( $_GET['post'] ) ) { return false; } if ( (int) sanitize_text_field( $_GET['post'] ) !== $attachment_id ) { return false; } return true; } /** * Used to filter the image metadata. Adds optimized image url for all image sizes. * * @param array $metadata The attachment metadata. * @param int $id The attachment id. * * @return mixed */ private function get_altered_metadata_for_remote_images( $metadata, $id ) { $post = get_post( $id ); $sizes_meta = []; // SVG files don't have a width/height so we add a dummy one. These are vector images so it doesn't matter. $is_svg = ( $post->post_mime_type === Optml_Config::$image_extensions['svg'] ); if ( $is_svg ) { $metadata['width'] = 150; $metadata['height'] = 150; } if ( ! isset( $metadata['height'] ) || ! isset( $metadata['width'] ) ) { return $metadata; } $sizes = Optml_App_Replacer::image_sizes(); foreach ( $sizes as $size => $args ) { // check if the image is portrait or landscape using attachment metadata. $dimensions = $this->size_to_dimension( $size, $metadata ); $sizes_meta[ $size ] = [ 'file' => $metadata['file'], 'width' => $dimensions['width'], 'height' => $dimensions['height'], 'mime-type' => $post->post_mime_type, ]; } $metadata['sizes'] = $sizes_meta; return $metadata; } /** * Get the scaled URL. * * @param string $url Original URL. * * @return string */ private function get_scaled_url( $url ) { $extension = pathinfo( $url, PATHINFO_EXTENSION ); return str_replace( '.' . $extension, '-scaled.' . $extension, $url ); } /** * Check if the URL is a scaled image. * * @param string $url The URL to check. * * @return bool */ private function is_scaled_url( $url ) { return strpos( $url, '-scaled.' ) !== false; } /** * Check if the image has been offloaded completely. * * @param int $id The attachment ID. * * @return bool */ private function is_completed_offload( $id ) { // This is the flag that is used to mark the image as offloaded at the end. $completed = ! empty( get_post_meta( $id, Optml_Media_Offload::OM_OFFLOADED_FLAG, true ) ); if ( $completed ) { return true; } // In some rare cases the image is offloaded but the flag is not set so we can alternatively check this using the file path. $meta = wp_get_attachment_metadata( $id ); if ( ! isset( $meta['file'] ) ) { return false; } if ( Optml_Media_Offload::is_uploaded_image( $meta['file'] ) ) { return true; } return false; } /** * Get the attachment ID from URL. * * @param string $input_url The attachment URL. * * @return int */ private function attachment_url_to_post_id( $input_url ) { $cached = Optml_Attachment_Cache::get_cached_attachment_id( $input_url ); if ( $cached !== false ) { return (int) $cached; } $url = $this->strip_image_size( $input_url ); $attachment_id = attachment_url_to_postid( $url ); if ( $attachment_id === 0 && ! $this->is_scaled_url( $url ) ) { $scaled_url = $this->get_scaled_url( $url ); $attachment_id = attachment_url_to_postid( $scaled_url ); } /* * TODO: The logic is a mess, we need to refactor at some point. * Websites may transition between 'www' subdomains and apex domains, potentially breaking references to hosted images. This can cause issues when attempting to match attachment IDs if images are linked using outdated domains. The logic is checking for alternative domains and consider the use of 'scaled' prefixes in image URLs for large images, which might affect ID matching. */ if ( $attachment_id === 0 ) { if ( strpos( $url, 'www.' ) !== false ) { $variant_url = str_replace( 'www.', '', $url ); $attachment_id = attachment_url_to_postid( $variant_url ); } else { $variant_url = str_replace( '://', '://www.', $url ); $attachment_id = attachment_url_to_postid( $variant_url ); } if ( $attachment_id === 0 && ! $this->is_scaled_url( $variant_url ) ) { $scaled_url = $this->get_scaled_url( $variant_url ); $attachment_id = attachment_url_to_postid( $scaled_url ); } } Optml_Attachment_Cache::set_cached_attachment_id( $input_url, $attachment_id ); return $attachment_id; } /** * Strips the image size from the URL. * * @param string $url URL to strip. * * @return string */ private function strip_image_size( $url ) { if ( preg_match( '#(-\d+x\d+(?:_c)?|(@2x))\.(' . implode( '|', array_keys( Optml_Config::$image_extensions ) ) . '){1}$#i', $url, $src_parts ) ) { $url = str_replace( $src_parts[1], '', $url ); } return $url; } } traits/normalizer.php 0000644 00000021002 14720403740 0010741 0 ustar 00 <?php use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty; use Optimole\Sdk\ValueObject\Position; /** * Normalization traits. * * @package \Optml\Inc\Traits * @author Optimole <friends@optimole.com> */ trait Optml_Normalizer { /** * Normalize value to boolean. * * @param mixed $value Value to process. * * @return bool */ public function to_boolean( $value ) { if ( in_array( $value, [ 'yes', 'enabled', 'true', '1' ], true ) ) { return true; } if ( in_array( $value, [ 'no', 'disabled', 'false', '0' ], true ) ) { return false; } return boolval( $value ); } /** * Return domain hash. * * @param string $domain Full url. * * @return string Domain hash. */ public function to_domain_hash( $domain ) { $domain_parts = parse_url( $domain ); $domain = isset( $domain_parts['host'] ) ? $domain_parts['host'] : ''; $prefix = substr( $domain, 0, 4 ); if ( $prefix === 'www.' ) { $domain = substr( $domain, 4 ); } return base64_encode( $domain ); } /** * Strip slashes on unicode encoded strings. * * @param string $content Input string. * * @return string Decoded string. */ public function strip_slashes( $content ) { return html_entity_decode( stripslashes( preg_replace( '/\\\u([\da-fA-F]{4})/', '&#x\1;', $content ) ) ); } /** * Normalize value to positive integer. * * @param mixed $value Value to process. * * @return integer */ public function to_positive_integer( $value ) { $integer = (int) $value; return ( $integer > 0 ) ? $integer : 0; } /** * Normalize value to map. * * @param mixed $value Value to process. * @param array $map Associative list from witch to return. * @param mixed $initial Default. * * @return mixed */ public function to_map_values( $value, $map, $initial ) { if ( in_array( $value, $map, true ) ) { return $value; } return $initial; } /** * Normalize value to an accepted quality. * * @param mixed $value Value to process. * * @return mixed */ public function to_accepted_quality( $value ) { if ( is_numeric( $value ) ) { return intval( $value ); } $value = trim( $value ); $accepted_qualities = [ 'eco' => 'eco', 'auto' => 'auto', 'mauto' => 'mauto', 'high_c' => 55, 'medium_c' => 75, 'low_c' => 90, ]; if ( array_key_exists( $value, $accepted_qualities ) ) { return $accepted_qualities[ $value ]; } // Legacy values. return 60; } /** * Normalize value to an accepted minify. * * @param mixed $value Value to process. * * @return mixed */ public function to_accepted_minify( $value ) { if ( is_numeric( $value ) ) { return $this->to_bound_integer( $value, 0, 1 ); } return 'auto'; } /** * Normalize value to an integer within bounds. * * @param mixed $value Value to process. * @param integer $min Lower bound. * @param integer $max Upper bound. * * @return integer */ public function to_bound_integer( $value, $min, $max ) { $integer = absint( $value ); if ( $integer < $min ) { $integer = $min; } if ( $integer > $max ) { $integer = $max; } return $integer; } /** * Convert image size to dimensions. * * This function takes an image size and its metadata, and returns the dimensions * of the image based on the specified size. It handles different cases such as * custom sizes, predefined sizes, and full size. * * @param mixed $size The size of the image. Can be an array of width and height, a predefined size, or 'full'. * @param array $image_meta Metadata of the image, including width and height. * * @return array The dimensions of the image, including width, height, and optional resize parameters. */ public function size_to_dimension( $size, $image_meta ) { // default size $sizes = [ 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : false, 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : false, ]; $image_args = Optml_App_Replacer::image_sizes(); switch ( $size ) { case is_array( $size ): $width = isset( $size[0] ) ? (int) $size[0] : false; $height = isset( $size[1] ) ? (int) $size[1] : false; if ( ! $width || ! $height ) { break; } $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $width, $height ); if ( $image_resized ) { $width = $image_resized[6]; $height = $image_resized[7]; } else { $width = $image_meta['width']; $height = $image_meta['height']; } list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $width, $height, $size ); break; case 'full' !== $size && isset( $image_args[ $size ] ): $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $image_args[ $size ]['width'], $image_args[ $size ]['height'], $image_args[ $size ]['crop'] ); if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image. $sizes['width'] = $image_resized[6]; $sizes['height'] = $image_resized[7]; } // There are cases when the image meta is missing and image size is non existent, see SVG image handling. if ( ! $sizes['width'] || ! $sizes['height'] ) { break; } list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $sizes['width'], $sizes['height'], $size, 'display' ); $sizes['resize'] = $this->to_optml_crop( $image_args[ $size ]['crop'] ); break; } return $sizes; } /** * Normalize arguments for crop. * * @param array|bool $crop_args Crop arguments. * * @return array */ public function to_optml_crop( $crop_args = [] ) { $enlarge = false; if ( isset( $crop_args['enlarge'] ) ) { $enlarge = $crop_args['enlarge']; $crop_args = $crop_args['crop']; } if ( $crop_args === true ) { return [ 'type' => ResizeTypeProperty::FILL, 'enlarge' => $enlarge, 'gravity' => Position::CENTER, ]; } if ( $crop_args === false || ! is_array( $crop_args ) || count( $crop_args ) !== 2 ) { return []; } $allowed_x = [ 'left' => true, 'center' => true, 'right' => true, ]; $allowed_y = [ 'top' => true, 'center' => true, 'bottom' => true, ]; $allowed_gravities = [ 'left' => Position::WEST, 'right' => Position::EAST, 'top' => Position::NORTH, 'bottom' => Position::SOUTH, 'lefttop' => Position::NORTH_WEST, 'leftbottom' => Position::SOUTH_WEST, 'righttop' => Position::NORTH_EAST, 'rightbottom' => Position::SOUTH_EAST, 'centertop' => [ 0.5, 0 ], 'centerbottom' => [ 0.5, 1 ], 'leftcenter' => [ 0, 0.5 ], 'rightcenter' => [ 1, 0.5 ], ]; $gravity = Position::CENTER; $key_search = ( $crop_args[0] === true ? '' : ( isset( $allowed_x[ $crop_args[0] ] ) ? $crop_args[0] : '' ) ) . ( $crop_args[1] === true ? '' : ( isset( $allowed_y[ $crop_args[1] ] ) ? $crop_args[1] : '' ) ); if ( array_key_exists( $key_search, $allowed_gravities ) ) { $gravity = $allowed_gravities[ $key_search ]; } return [ 'type' => ResizeTypeProperty::FILL, 'enlarge' => $enlarge, 'gravity' => $gravity, ]; } /** * Normalize arguments for watermark. * * @param array $watermark_args Watermark arguments. * * @return array */ public function to_optml_watermark( $watermark_args = [] ) { $allowed_gravities = [ 'left' => Position::WEST, 'right' => Position::EAST, 'top' => Position::NORTH, 'bottom' => Position::SOUTH, 'left_top' => Position::NORTH_WEST, 'left_bottom' => Position::SOUTH_WEST, 'right_top' => Position::NORTH_EAST, 'right_bottom' => Position::SOUTH_EAST, ]; $gravity = Position::CENTER; if ( isset( $watermark_args['position'] ) && array_key_exists( $watermark_args['position'], $allowed_gravities ) ) { $gravity = $allowed_gravities[ $watermark_args['position'] ]; } return [ 'opacity' => 1, 'position' => $gravity, ]; } /** * If missing, add schema to urls. * * @param string $url Url to check. * * @return string */ public function add_schema( $url ) { $schema_url = $url; $should_add_schema = false; if ( function_exists( 'str_starts_with' ) ) { $should_add_schema = str_starts_with( $schema_url, '//' ); } else { $should_add_schema = substr( $schema_url, 0, strlen( '//' ) ) === '//'; } if ( $should_add_schema ) { $schema_url = is_ssl() ? 'https:' : 'http:' . $schema_url; } return $schema_url; } } settings.php 0000644 00000054304 14720403740 0007124 0 ustar 00 <?php use Optimole\Sdk\ValueObject\Position; /** * Class Optml_Settings. */ class Optml_Settings { use Optml_Normalizer; const FILTER_EXT = 'extension'; const FILTER_URL = 'page_url'; const FILTER_URL_MATCH = 'page_url_match'; const FILTER_FILENAME = 'filename'; const FILTER_CLASS = 'class'; const FILTER_TYPE_LAZYLOAD = 'lazyload'; const FILTER_TYPE_OPTIMIZE = 'optimize'; const OPTML_USER_EMAIL = 'optml_user_email'; /** * Holds an array of possible settings to alter via wp cli or wp-config constants. * * @var array Whitelisted settings. */ public static $whitelisted_settings = [ 'image_replacer' => 'bool', 'quality' => 'int', 'lazyload' => 'bool', 'lazyload_placeholder' => 'bool', 'network_optimization' => 'bool', 'autoquality' => 'bool', 'img_to_video' => 'bool', 'resize_smart' => 'bool', 'retina_images' => 'bool', 'native_lazyload' => 'bool', 'video_lazyload' => 'bool', 'bg_replacer' => 'bool', 'scale' => 'bool', 'cdn' => 'bool', ]; /** * Holds the status of the auto connect hook. * * @var boolean Whether or not the auto connect action is hooked. */ private static $auto_connect_hooked = false; /** * Default settings schema. * * @var array Settings schema. */ private $default_schema = [ 'api_key' => '', 'service_data' => '', 'cache_buster' => '', 'cache_buster_assets' => '', 'cache_buster_images' => '', 'cdn' => 'disabled', 'admin_bar_item' => 'enabled', 'lazyload' => 'disabled', 'scale' => 'disabled', 'network_optimization' => 'disabled', 'lazyload_placeholder' => 'enabled', 'bg_replacer' => 'enabled', 'video_lazyload' => 'enabled', 'retina_images' => 'disabled', 'limit_dimensions' => 'enabled', 'limit_height' => 1080, 'limit_width' => 1920, 'resize_smart' => 'disabled', 'no_script' => 'disabled', 'filters' => [], 'cloud_sites' => [ 'all' => 'true' ], 'watchers' => '', 'quality' => 'auto', 'wm_id' => - 1, 'wm_opacity' => 1, 'wm_position' => Position::SOUTH_EAST, 'wm_x' => 0, 'wm_y' => 0, 'wm_scale' => 0, 'image_replacer' => 'enabled', 'img_to_video' => 'disabled', 'css_minify' => 'enabled', 'js_minify' => 'disabled', 'report_script' => 'disabled', 'avif' => 'enabled', 'autoquality' => 'enabled', 'native_lazyload' => 'disabled', 'offload_media' => 'disabled', 'transfer_status' => 'disabled', 'cloud_images' => 'enabled', 'strip_metadata' => 'enabled', 'skip_lazyload_images' => 3, 'defined_image_sizes' => [], 'banner_frontend' => 'disabled', 'offloading_status' => 'disabled', 'rollback_status' => 'disabled', 'best_format' => 'enabled', 'offload_limit_reached' => 'disabled', 'offload_limit' => 50000, 'placeholder_color' => '', 'show_offload_finish_notice' => '', ]; /** * Option key. * * @var string Option name. */ private $namespace; /** * Holds all options from db. * * @var array All options. */ private $options; /** * Optml_Settings constructor. */ public function __construct() { $this->default_schema['cloud_sites'] = $this->get_cloud_sites_whitelist_default(); $this->namespace = OPTML_NAMESPACE . '_settings'; $this->default_schema = apply_filters( 'optml_default_settings', $this->default_schema ); $this->options = wp_parse_args( get_option( $this->namespace, $this->default_schema ), $this->default_schema ); if ( defined( 'OPTIML_ENABLED_MU' ) && defined( 'OPTIML_MU_SITE_ID' ) && $this->to_boolean( constant( 'OPTIML_ENABLED_MU' ) ) && constant( 'OPTIML_MU_SITE_ID' ) ) { switch_to_blog( constant( 'OPTIML_MU_SITE_ID' ) ); $this->options = wp_parse_args( get_option( $this->namespace, $this->default_schema ), $this->default_schema ); restore_current_blog(); } if ( defined( 'OPTIML_USE_ENV' ) && constant( 'OPTIML_USE_ENV' ) && $this->to_boolean( constant( 'OPTIML_USE_ENV' ) ) ) { if ( defined( 'OPTIML_API_KEY' ) && constant( 'OPTIML_API_KEY' ) !== '' ) { if ( ! $this->is_connected() && ! self::$auto_connect_hooked ) { self::$auto_connect_hooked = true; add_action( 'plugins_loaded', [ $this, 'auto_connect' ] ); } } foreach ( self::$whitelisted_settings as $key => $type ) { $env_key = 'OPTIML_' . strtoupper( $key ); if ( defined( $env_key ) && constant( $env_key ) ) { $value = constant( $env_key ); if ( $type === 'bool' && ( (string) $value === '' || ! in_array( $value, [ 'on', 'off', ], true ) ) ) { continue; } if ( $type === 'int' && ( (string) $value === '' || (int) $value > 100 || (int) $value < 0 ) ) { continue; } $sanitized_value = ( $type === 'bool' ) ? ( $value === 'on' ? 'enabled' : 'disabled' ) : (int) $value; $this->options[ $key ] = $sanitized_value; } } } add_action( 'init', [ $this, 'register_settings' ] ); } /** * Check if the user is connected to Optimole. * * @return bool Connection status. */ public function is_connected() { $service_data = $this->get( 'service_data' ); if ( ! isset( $service_data['cdn_key'] ) ) { return false; } if ( empty( $service_data ['cdn_key'] ) || empty( $service_data['cdn_secret'] ) ) { return false; } return true; } /** * Get setting value by key. * * @param string $key Key to search against. * * @return mixed|null Setting value. */ public function get( $key ) { if ( ! $this->is_allowed( $key ) ) { return null; } return isset( $this->options[ $key ] ) ? $this->options[ $key ] : ''; } /** * Check if key is allowed. * * @param string $key Is key allowed or not. * * @return bool Is key allowed or not. */ private function is_allowed( $key ) { return isset( $this->default_schema[ $key ] ); } /** * Auto connect action. */ public function auto_connect() { $request = new WP_REST_Request( 'POST' ); $request->set_param( 'api_key', constant( 'OPTIML_API_KEY' ) ); Optml_Main::instance()->rest->connect( $request ); remove_action( 'plugins_loaded', [ $this, 'auto_connect' ] ); self::$auto_connect_hooked = false; } /** * Process settings. * * @param array $new_settings List of settings. * * @return array */ public function parse_settings( $new_settings ) { $sanitized = []; foreach ( $new_settings as $key => $value ) { switch ( $key ) { case 'admin_bar_item': case 'lazyload': case 'scale': case 'image_replacer': case 'cdn': case 'network_optimization': case 'lazyload_placeholder': case 'retina_images': case 'limit_dimensions': case 'resize_smart': case 'bg_replacer': case 'video_lazyload': case 'report_script': case 'avif': case 'offload_media': case 'cloud_images': case 'autoquality': case 'img_to_video': case 'css_minify': case 'js_minify': case 'native_lazyload': case 'strip_metadata': case 'no_script': case 'banner_frontend': case 'offloading_status': case 'rollback_status': case 'best_format': case 'offload_limit_reached': $sanitized_value = $this->to_map_values( $value, [ 'enabled', 'disabled' ], 'enabled' ); break; case 'offload_limit': $sanitized_value = absint( $value ); break; case 'limit_height': case 'limit_width': $sanitized_value = $this->to_bound_integer( $value, 100, 5000 ); break; case 'quality': $sanitized_value = $this->to_bound_integer( $value, 1, 100 ); break; case 'wm_id': $sanitized_value = intval( $value ); break; case 'show_offload_finish_notice': $sanitized_value = $this->to_map_values( $value, [ 'offload', 'rollback' ], '' ); break; case 'cache_buster_assets': case 'cache_buster_images': case 'cache_buster': $sanitized_value = is_string( $value ) ? sanitize_text_field( $value ) : ''; break; case 'cloud_sites': $current_sites = $this->get( 'cloud_sites' ); $sanitized_value = array_replace_recursive( $current_sites, $value ); if ( isset( $value['all'] ) && $value['all'] === 'true' ) { $sanitized_value = [ 'all' => 'true' ]; } break; case 'defined_image_sizes': $current_sizes = $this->get( 'defined_image_sizes' ); foreach ( $value as $size_name => $size_value ) { if ( $size_value === 'remove' ) { unset( $current_sizes[ $size_name ] ); unset( $value[ $size_name ] ); } } $sanitized_value = array_replace_recursive( $current_sizes, $value ); break; case 'filters': $current_filters = $this->get_filters(); $sanitized_value = array_replace_recursive( $current_filters, $value ); // Remove falsy vars. foreach ( $sanitized_value as $filter_type => $filter_values ) { foreach ( $filter_values as $filter_rule_type => $filter_rules_value ) { $sanitized_value[ $filter_type ][ $filter_rule_type ] = array_filter( $filter_rules_value, function ( $value ) { return ( $value !== 'false' && $value !== false ); } ); } } break; case 'watchers': case 'placeholder_color': case 'transfer_status': $sanitized_value = sanitize_text_field( $value ); break; case 'skip_lazyload_images': $sanitized_value = $this->to_bound_integer( $value, 0, 100 ); break; case 'wm_opacity': case 'wm_scale': case 'wm_x': case 'wm_y': $sanitized_value = floatval( $value ); break; case 'wm_position': $sanitized_value = $this->to_map_values( $value, [ Position::NORTH, Position::NORTH_EAST, Position::NORTH_WEST, Position::CENTER, Position::EAST, Position::WEST, Position::SOUTH_EAST, Position::SOUTH, Position::SOUTH_WEST, ], Position::SOUTH_EAST ); break; default: $sanitized_value = ''; break; } $sanitized[ $key ] = $sanitized_value; $this->update( $key, $sanitized_value ); } return $sanitized; } /** * Return filter definitions. * * @return mixed|null Filter values. */ public function get_filters() { $filters = $this->get( 'filters' ); if ( ! isset( $filters[ self::FILTER_TYPE_LAZYLOAD ] ) ) { $filters[ self::FILTER_TYPE_LAZYLOAD ] = []; } if ( ! isset( $filters[ self::FILTER_TYPE_OPTIMIZE ] ) ) { $filters[ self::FILTER_TYPE_OPTIMIZE ] = []; } foreach ( $filters as $filter_key => $filter_rules ) { if ( ! isset( $filter_rules[ self::FILTER_EXT ] ) ) { $filters[ $filter_key ][ self::FILTER_EXT ] = []; } if ( ! isset( $filter_rules[ self::FILTER_FILENAME ] ) ) { $filters[ $filter_key ][ self::FILTER_FILENAME ] = []; } if ( ! isset( $filter_rules[ self::FILTER_URL ] ) ) { $filters[ $filter_key ][ self::FILTER_URL ] = []; } if ( ! isset( $filter_rules[ self::FILTER_URL_MATCH ] ) ) { $filters[ $filter_key ][ self::FILTER_URL_MATCH ] = []; } if ( ! isset( $filter_rules[ self::FILTER_CLASS ] ) ) { $filters[ $filter_key ][ self::FILTER_CLASS ] = []; } } return $filters; } /** * Update frontend banner setting from remote. * * @param bool $value Value. * * @return bool */ public function update_frontend_banner_from_remote( $value ) { if ( ! $this->is_main_mu_site() ) { return false; } $opts = $this->options; $opts['banner_frontend'] = $value ? 'enabled' : 'disabled'; $update = update_option( $this->namespace, $opts, false ); if ( $update ) { $this->options = $opts; } return $update; } /** * Update settings. * * @param string $key Settings key. * @param mixed $value Settings value. * * @return bool Update result. */ public function update( $key, $value ) { if ( ! $this->is_allowed( $key ) ) { return false; } if ( ! $this->is_main_mu_site() ) { return false; } $opt = $this->options; if ( $key === 'banner_frontend' ) { $api = new Optml_Api(); $service_data = $this->get( 'service_data' ); $application = isset( $service_data['cdn_key'] ) ? $service_data['cdn_key'] : ''; $response = $api->update_extra_visits( $opt['api_key'], $value, $application ); } $opt[ $key ] = $value; $update = update_option( $this->namespace, $opt, false ); if ( $update ) { $this->options = $opt; } if ( apply_filters( 'optml_dont_trigger_settings_updated', false ) === false ) { do_action( 'optml_settings_updated' ); } return $update; } /** * Check that we're on the main OPTML blog. * * @return bool */ private function is_main_mu_site() { // If we try to update from a website which is not the main OPTML blog, bail. if ( defined( 'OPTIML_ENABLED_MU' ) && constant( 'OPTIML_ENABLED_MU' ) && defined( 'OPTIML_MU_SITE_ID' ) && constant( 'OPTIML_MU_SITE_ID' ) && intval( constant( 'OPTIML_MU_SITE_ID' ) ) !== get_current_blog_id() ) { return false; } return true; } /** * Return site settings. * * @return array Site settings. */ public function get_site_settings() { $service_data = $this->get( 'service_data' ); $whitelist = []; if ( isset( $service_data['whitelist'] ) ) { $whitelist = $service_data['whitelist']; } return [ 'quality' => $this->get_quality(), 'admin_bar_item' => $this->get( 'admin_bar_item' ), 'lazyload' => $this->get( 'lazyload' ), 'network_optimization' => $this->get( 'network_optimization' ), 'retina_images' => $this->get( 'retina_images' ), 'limit_dimensions' => $this->get( 'limit_dimensions' ), 'limit_height' => $this->get( 'limit_height' ), 'limit_width' => $this->get( 'limit_width' ), 'lazyload_placeholder' => $this->get( 'lazyload_placeholder' ), 'skip_lazyload_images' => $this->get( 'skip_lazyload_images' ), 'bg_replacer' => $this->get( 'bg_replacer' ), 'video_lazyload' => $this->get( 'video_lazyload' ), 'resize_smart' => $this->get( 'resize_smart' ), 'no_script' => $this->get( 'no_script' ), 'image_replacer' => $this->get( 'image_replacer' ), 'cdn' => $this->get( 'cdn' ), 'filters' => $this->get_filters(), 'cloud_sites' => $this->get( 'cloud_sites' ), 'defined_image_sizes' => $this->get( 'defined_image_sizes' ), 'watchers' => $this->get_watchers(), 'watermark' => $this->get_watermark(), 'img_to_video' => $this->get( 'img_to_video' ), 'scale' => $this->get( 'scale' ), 'css_minify' => $this->get( 'css_minify' ), 'js_minify' => $this->get( 'js_minify' ), 'native_lazyload' => $this->get( 'native_lazyload' ), 'report_script' => $this->get( 'report_script' ), 'avif' => $this->get( 'avif' ), 'autoquality' => $this->get( 'autoquality' ), 'offload_media' => $this->get( 'offload_media' ), 'cloud_images' => $this->get( 'cloud_images' ), 'strip_metadata' => $this->get( 'strip_metadata' ), 'whitelist_domains' => $whitelist, 'banner_frontend' => $this->get( 'banner_frontend' ), 'offloading_status' => $this->get( 'offloading_status' ), 'rollback_status' => $this->get( 'rollback_status' ), 'best_format' => $this->get( 'best_format' ), 'offload_limit_reached' => $this->get( 'offload_limit_reached' ), 'placeholder_color' => $this->get( 'placeholder_color' ), 'show_offload_finish_notice' => $this->get( 'show_offload_finish_notice' ), ]; } /** * Return quality factor. * * @return string Quality factor. */ public function get_quality() { $quality = $this->get( 'quality' ); if ( $this->get( 'autoquality' ) === 'enabled' ) { return 'mauto'; } // Legacy compat. if ( $quality === 'low' ) { return 'high_c'; } if ( $quality === 'medium' ) { return 'medium_c'; } if ( $quality === 'high' ) { return 'low_c'; } return $quality; } /** * Return filter definitions. * * @return mixed|null Filter values. */ public function get_watchers() { return $this->get( 'watchers' ); } /** * Return an watermark array. * * @return array */ public function get_watermark() { return [ 'id' => $this->get( 'wm_id' ), 'opacity' => $this->get( 'wm_opacity' ), 'position' => $this->get( 'wm_position' ), 'x_offset' => $this->get( 'wm_x' ), 'y_offset' => $this->get( 'wm_y' ), 'scale' => $this->get( 'wm_scale' ), ]; } /** * Check if smart cropping is enabled. * * @return bool Is smart cropping enabled. */ public function is_smart_cropping() { return $this->get( 'resize_smart' ) === 'enabled'; } /** * Check if best format is enabled. * * @return bool */ public function is_best_format() { return $this->get( 'best_format' ) === 'enabled'; } /** * Check if offload limit was reached. * * @return bool */ public function is_offload_limit_reached() { return $this->get( 'offload_limit_reached' ) === 'enabled'; } /** * Get numeric quality used by the service. * * @return int Numeric quality. */ public function get_numeric_quality() { $value = $this->get_quality(); return (int) $this->to_accepted_quality( $value ); } /** * Check if replacer is enabled. * * @return bool Replacer enabled */ public function is_enabled() { $status = $this->get( 'image_replacer' ); $service_data = $this->get( 'service_data' ); if ( empty( $service_data ) ) { return false; } if ( isset( $service_data['status'] ) && $service_data['status'] === 'inactive' ) { return false; } return $this->to_boolean( $status ); } /** * Check if lazyload is enabled. * * @return bool Lazyload enabled */ public function use_lazyload() { $status = $this->get( 'lazyload' ); return $this->to_boolean( $status ); } /** * Check if replacer is enabled. * * @return bool Replacer enabled */ public function use_cdn() { $status = $this->get( 'cdn' ); return $this->to_boolean( $status ); } /** * Return cdn url. * * @return string CDN url. */ public function get_cdn_url() { $service_data = $this->get( 'service_data' ); if ( ! isset( $service_data['cdn_key'] ) ) { return ''; } if ( isset( $service_data['is_cname_assigned'] ) && $service_data['is_cname_assigned'] === 'yes' && ! empty( $service_data['domain'] ) ) { return strtolower( $service_data['domain'] ); } if ( defined( 'OPTML_CUSTOM_DOMAIN' ) && constant( 'OPTML_CUSTOM_DOMAIN' ) ) { return parse_url( strtolower( OPTML_CUSTOM_DOMAIN ), PHP_URL_HOST ); } return sprintf( '%s.%s', strtolower( $service_data['cdn_key'] ), Optml_Config::$base_domain ); } /** * Reset options to defaults. * * @return bool Reset action status. */ public function reset() { $reset_schema = $this->default_schema; $reset_schema['filters'] = $this->options['filters']; $update = update_option( $this->namespace, $reset_schema ); if ( $update ) { $this->options = $reset_schema; } return $update; } /** * Get raw settings value. * * @return array */ public function get_raw_settings() { return get_option( $this->namespace, false ); } /** * Get settings for CSAT. * * @return void */ public function register_settings() { register_setting( 'optml_settings', 'optml_csat', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'default' => '{}', ] ); } /** * Clear cache. * * @param string $type Cache type. * * @return string|WP_Error */ public function clear_cache( $type = '' ) { $token = $this->get( 'cache_buster' ); $token_images = $this->get( 'cache_buster_images' ); if ( ! empty( $token_images ) ) { $token = $token_images; } if ( ! empty( $type ) && $type === 'assets' ) { $token = $this->get( 'cache_buster_assets' ); } $request = new Optml_Api(); $data = $request->get_cache_token( $token, $type ); if ( $data === false || is_wp_error( $data ) || empty( $data ) || ! isset( $data['token'] ) ) { $extra = ''; if ( is_wp_error( $data ) ) { /** * Error from api. * * @var WP_Error $data Error object. */ $extra = sprintf( /* translators: Error details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() ); } return new WP_Error( 'optimole_cache_buster_error', __( 'Can not get new token from Optimole service', 'optimole-wp' ) . $extra ); } if ( ! empty( $type ) && $type === 'assets' ) { set_transient( 'optml_cache_lock_assets', 'yes', 5 * MINUTE_IN_SECONDS ); $this->update( 'cache_buster_assets', $data['token'] ); } else { set_transient( 'optml_cache_lock', 'yes', 5 * MINUTE_IN_SECONDS ); $this->update( 'cache_buster_images', $data['token'] ); } return $data['token']; } /** * Utility to check if offload is enabled. * * @return bool */ public function is_offload_enabled() { return $this->get( 'offload_media' ) === 'enabled' || $this->get( 'rollback_status' ) !== 'disabled'; } /** * Get cloud sites whitelist for current domain only. * * @return array */ public function get_cloud_sites_whitelist_default() { $site_url = get_site_url(); $site_url = preg_replace( '/^https?:\/\//', '', $site_url ); $site_url = trim( $site_url, '/' ); return [ 'all' => 'false', $site_url => 'true', ]; } } cli.php 0000644 00000001514 14720403740 0006026 0 ustar 00 <?php /** * CLI class. * * Author: Bogdan Preda <bogdan.preda@themeisle.com> * Created on: 19/07/2018 * * @package \Optimole\Inc * @author Optimole <friends@optimole.com> */ /** * Class Optml_Cli */ class Optml_Cli { /** * Api version. * * @var string Version string. */ const CLI_NAMESPACE = 'optimole'; /** * CLI controllers * * @var array List of CLI controllers. */ private $commands = [ 'setting', 'media', ]; /** * Optml_Cli constructor. */ public function __construct() { foreach ( $this->commands as $command ) { $class_name = 'Optml_Cli_' . ucfirst( $command ); $controller = new $class_name(); try { \WP_CLI::add_command( self::CLI_NAMESPACE . ' ' . $command, $controller ); } catch ( \Exception $e ) { // TODO Log this exception. } } } } attachment_cache.php 0000644 00000003760 14720403740 0010537 0 ustar 00 <?php /** * Class Optml_Attachment_Cache. */ class Optml_Attachment_Cache { const CACHE_GROUP = 'om_att'; /** * Local cache map. * * @var array */ private static $cache_map = []; /** * Reset the memory cache. */ public static function reset() { self::$cache_map = []; } /** * Get the cached attachment ID. * * @param string $url the URL of the attachment. * * @return bool|mixed */ public static function get_cached_attachment_id( $url ) { // We cache also in memory to avoid calling DB every time when not using Object Cache. $cache_key = self::get_cache_key( $url ); if ( isset( self::$cache_map[ $cache_key ] ) ) { return self::$cache_map[ $cache_key ]; } $value = wp_using_ext_object_cache() ? wp_cache_get( $cache_key, self::CACHE_GROUP ) : get_transient( self::CACHE_GROUP . $cache_key ); self::$cache_map[ $cache_key ] = $value; return $value; } /** * Set the cached attachment ID. * * @param string $url the URL of the attachment. * @param int $id the attachment ID. * * @return void */ public static function set_cached_attachment_id( $url, $id ) { $cache_key = self::get_cache_key( $url ); // We cache also in memory to avoid calling DB every time when not using Object Cache. self::$cache_map[ $cache_key ] = $id; // If the ID is not found we cache for 10 minutes, otherwise for a week. // We try to reduce the cache time when is not found to // avoid caching for situation when this might be temporary. $expiration = $id === 0 ? ( 10 * MINUTE_IN_SECONDS ) : WEEK_IN_SECONDS; wp_using_ext_object_cache() ? wp_cache_set( $cache_key, $id, self::CACHE_GROUP, $expiration ) : set_transient( self::CACHE_GROUP . $cache_key, $id, $expiration ); } /** * Generate cache key for URL. * * @param string $url the URL to generate the cache key for. * * @return string */ private static function get_cache_key( $url ) { $url = strtok( $url, '?' ); return 'id_' . crc32( $url ); } } config.php 0000644 00000007453 14720403740 0006534 0 ustar 00 <?php use Optimole\Sdk\Optimole; /** * Class Optml_Config holds configuration for the service. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ class Optml_Config { /** * A list of allowed extensions. * * @var array */ public static $image_extensions = [ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpe' => 'image/jpeg', 'png' => 'image/png', 'heic' => 'image/heif', 'ico' => 'image/x-icon', 'avif' => 'image/avif', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'gif' => 'image/gif', ]; /** * CSS/Js mimetypes. * * @var string[] */ public static $assets_extensions = [ 'css' => 'text/css', 'js' => 'text/javascript', ]; /** * All extensions that Optimole process. * * @var string[] */ public static $all_extensions = [ 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'ico' => 'image/x-icon', 'heic' => 'image/heif', 'avif' => 'image/avif', 'svg' => 'image/svg+xml', 'gif' => 'image/gif', 'css' => 'text/css', 'js' => 'text/javascript', ]; /** * A string of allowed chars. * * @var string */ public static $chars = '\/:,~\\\\.\-\–\d_@%^A-Za-z\p{L}\p{M}\p{N}\x{0080}-\x{017F}\x{2200}-\x{22FF}\x{2022}'; /** * Service api key. * * @var string Service key. */ public static $key = ''; /** * Service secret key used for signing the requests. * * @var string Secret key. */ public static $secret = ''; /** * Service url. * * @var string Service url. */ public static $service_url = ''; /** * Base domain. * * @var string Base domain. */ public static $base_domain = 'i.optimole.com'; /** * Service settings. * * @param array $service_settings Service settings. * * @throws \InvalidArgumentException In case that key or secret is not provided. */ public static function init( $service_settings = [] ) { if ( empty( $service_settings['key'] ) && ! defined( 'OPTML_KEY' ) ) { throw new \InvalidArgumentException( 'Optimole SDK requires service api key.' ); // @codeCoverageIgnore } if ( empty( $service_settings['secret'] ) && ! defined( 'OPTML_SECRET' ) ) { throw new \InvalidArgumentException( 'Optimole SDK requires service secret key.' ); // @codeCoverageIgnore } if ( defined( 'OPTML_KEY' ) ) { self::$key = constant( 'OPTML_KEY' ); } if ( defined( 'OPTML_SECRET' ) ) { self::$secret = constant( 'OPTML_SECRET' ); } if ( defined( 'OPTML_BASE_DOMAIN' ) ) { self::$base_domain = constant( 'OPTML_BASE_DOMAIN' ); } if ( ! empty( $service_settings['key'] ) ) { self::$key = trim( strtolower( $service_settings['key'] ) ); } if ( ! empty( $service_settings['secret'] ) ) { self::$secret = trim( $service_settings['secret'] ); } self::$service_url = sprintf( 'https://%s.%s', self::$key, self::$base_domain ); if ( isset( $service_settings['domain'] ) && ! empty( $service_settings['domain'] ) ) { self::$service_url = sprintf( 'https://%s', $service_settings['domain'] ); } elseif ( defined( 'OPTML_CUSTOM_DOMAIN' ) ) { self::$service_url = constant( 'OPTML_CUSTOM_DOMAIN' ); } $options = [ 'domain' => parse_url( self::$service_url, PHP_URL_HOST ) ]; if ( defined( 'OPTIML_API_ROOT' ) ) { $options['dashboard_api_url'] = OPTIML_API_ROOT; } if ( defined( 'OPTIML_UPLOAD_API_ROOT' ) ) { $options['upload_api_url'] = OPTIML_UPLOAD_API_ROOT; } if ( isset( $service_settings['key'], $service_settings['secret'], $service_settings['api_key'] ) ) { $options['upload_api_credentials'] = [ 'userKey' => $service_settings['key'], 'secret' => $service_settings['secret'], ]; $options['dashboard_api_key'] = $service_settings['api_key']; } Optimole::init( self::$key, $options ); } } logger.php 0000644 00000014354 14720403740 0006544 0 ustar 00 <?php /** * Class Optml_Logger * Handles the logging operations. */ class Optml_Logger { const LOG_TYPE_OFFLOAD = 'offload'; const LOG_TYPE_ROLLBACK = 'rollback'; const LOG_SEPARATOR = '=========='; /** * Upload directory. * * @var string */ private $upload_dir; /** * Log filenames. * * @var array */ private $log_filenames = [ self::LOG_TYPE_OFFLOAD => 'offload.log', self::LOG_TYPE_ROLLBACK => 'rollback.log', ]; /** * The single instance of the class. * * @var Optml_Logger */ private static $instance = null; /** * Ensures only one instance of the class is loaded. * * @return Optml_Logger An instance of the class. */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Optml_Logger constructor. * Initializes the upload directory. * * @return void */ private function __construct() { $upload_dir = wp_upload_dir(); $this->upload_dir = trailingslashit( $upload_dir['basedir'] ) . 'optimole-logs/'; if ( ! file_exists( $this->upload_dir ) ) { if ( false === wp_mkdir_p( $this->upload_dir ) ) { return; } } add_action( 'wp_ajax_optml_fetch_logs', [ $this, 'fetch_logs_ajax_handler' ] ); } /** * Adds a log entry. * * @param string $type Type of log (offload/rollback). * @param string $message Log message. * @return bool Returns true on success, false on failure. */ public function add_log( $type, $message ) { if ( ! array_key_exists( $type, $this->log_filenames ) ) { return false; } $this->send_log( $type, $message ); if ( ! $this->has_permission() ) { return false; } $file_path = $this->upload_dir . $this->log_filenames[ $type ]; $handle = @fopen( $file_path, 'a' ); if ( $handle === false ) { return false; } fwrite( $handle, '[' . gmdate( 'M d H:i:s' ) . '] ' . $message . "\n" ); fclose( $handle ); return true; } /** * Retrieves the log. * * @param string $type Type of log (offload/rollback). * @param int $lines Number of lines to return from the end of the log file. * @param string $separator Separator line to start retrieving log entries from. * * @return false|string Returns log data or false on failure. */ public function get_log( $type, $lines = 500, $separator = '' ) { if ( ! array_key_exists( $type, $this->log_filenames ) ) { return false; } $file_path = $this->upload_dir . $this->log_filenames[ $type ]; if ( $this->has_logs( $type ) ) { $data = $this->tailCustom( $file_path, $lines, true, $separator ); return nl2br( esc_textarea( $data ) ); } return false; } /** * Checks if the log file exists. * * @param string $type Type of log (offload/rollback). * @return bool Returns true if log file exists, false otherwise. */ public function has_logs( $type ) { if ( ! array_key_exists( $type, $this->log_filenames ) ) { return false; } $file_path = $this->upload_dir . $this->log_filenames[ $type ]; return file_exists( $file_path ); } /** * Checks for permission to write to log file. * * @return bool Returns true if permission is granted, false otherwise. */ public function has_permission() { return is_writable( $this->upload_dir ); } /** * Reads the last n lines from a file, or from a specific point defined by a separator. * * @param string $filepath Path to the file. * @param int $lines Number of lines to read from the end of the file. * @param bool $adaptive Use adaptive buffers. * @param string $separator Separator line to start retrieving log entries from. * @return false|string Returns last n lines from the file or false on failure. */ private function tailCustom( $filepath, $lines = 1, $adaptive = true, $separator = '' ) { $f = @fopen( $filepath, 'rb' ); if ( $f === false ) { return false; } if ( ! $adaptive ) { $buffer = 4096; } else { $buffer = min( 64, filesize( $filepath ) ); } fseek( $f, -1, SEEK_END ); $output = ''; $read = ''; $separator_found = false; while ( $buffer > 0 && ftell( $f ) > 0 ) { $seek = min( ftell( $f ), $buffer ); fseek( $f, -$seek, SEEK_CUR ); $output = ( $read = fread( $f, $seek ) ) . $output; fseek( $f, -mb_strlen( $read, '8bit' ), SEEK_CUR ); if ( ! empty( $separator ) && strpos( $output, $separator ) !== false ) { $separator_found = true; break; } $buffer = min( $buffer * 2, 4096 ); } $pos = -1; if ( ! empty( $separator ) && $separator_found ) { $pos = strrpos( $output, $separator ); if ( $pos !== false ) { $output = substr( $output, $pos + strlen( $separator ) ); $pos = -1; // we reset the position to start from the beginning of the chunk when slicing. } } // Extract the desired number of lines $output_lines = explode( "\n", $output ); if ( empty( $separator ) || ! $separator_found ) { $output = implode( "\n", array_slice( $output_lines, -$lines ) ); // fetch last n lines } else { // Calculate the start position for slicing $start_pos = max( $pos + 1, count( $output_lines ) - $lines ); $output = implode( "\n", array_slice( $output_lines, $start_pos, $lines ) ); // fetch from separator } fclose( $f ); return trim( $output ); } /** * Fetches the logs via AJAX. * * @return void */ public function fetch_logs_ajax_handler() { // Check for nonce security $nonce = $_REQUEST['nonce']; if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { wp_die( __( 'Security check failed', 'optimole-wp' ) ); } // Check for necessary parameters if ( ! isset( $_REQUEST['type'] ) || ! in_array( $_REQUEST['type'], array_keys( $this->log_filenames ), true ) ) { wp_die( __( 'Invalid log type', 'optimole-wp' ) ); } $lines = 10000; if ( isset( $_REQUEST['lines'] ) ) { $lines = intval( $_REQUEST['lines'] ); } // Get log type $type = $_REQUEST['type']; // Fetch the logs $logs = $this->get_log( $type, $lines, self::LOG_SEPARATOR ); // Return the logs echo $logs; wp_die(); } /** * Send logs to Axiom. * * @param string $type Type of log (offload/rollback). * @param string $message Log message. * @return void */ public function send_log( $type, $message ) { $request = new Optml_Api(); $request->send_log( $type, $message ); } } tag_replacer.php 0000644 00000036467 14720403740 0007726 0 ustar 00 <?php /** * The class handles the img tag replacements. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Tag_Replacer extends Optml_App_Replacer { use Optml_Normalizer; use Optml_Validator; /** * Cached object instance. * * @var Optml_Tag_Replacer */ protected static $instance = null; /** * The number of images skipped from lazyload. * * @var integer */ public static $lazyload_skipped_images = 0; /** * Class instance method. * * @codeCoverageIgnore * @static * @since 1.0.0 * @access public * @return Optml_Tag_Replacer */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] ); } return self::$instance; } /** * The initialize method. */ public function init() { parent::init(); add_filter( 'optml_content_images_tags', [ $this, 'process_image_tags' ], 1, 2 ); if ( $this->settings->use_lazyload() ) { return; } add_filter( 'optml_tag_replace', [ $this, 'regular_tag_replace' ], 1, 6 ); add_filter( 'image_downsize', [ $this, 'filter_image_downsize' ], PHP_INT_MAX, 3 ); add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_attr' ], PHP_INT_MAX, 5 ); add_filter( 'wp_calculate_image_sizes', [ $this, 'filter_sizes_attr' ], 1, 2 ); } /** * Called by hook to replace image tags in content. * * @param string $content The content to process. * @param array $images List of image tags. * * @return mixed */ /** * Replace in given content the given image tags with video tags is image is gif * * @param string $image_url Image url to process. * @param string $image_tag The tag to replace. * @param string $content The content to process. * * @return bool */ public function img_to_video( $image_url, $image_tag, &$content ) { if ( $this->settings->get( 'img_to_video' ) === 'disabled' ) { return false; } if ( false === Optml_Filters::should_do_image( $image_tag, apply_filters( 'optml_gif_to_video_flags', [ 'lazyload' => true, 'placeholder' => true, 'original-src' => true ] ) ) ) { return false; } $link_mp4 = apply_filters( 'optml_content_url', $image_url, [ 'width' => 'auto', 'height' => 'auto', 'format' => 'mp4', ] ); $link_png = apply_filters( 'optml_content_url', $image_url, [ 'width' => 'auto', 'height' => 'auto', 'quality' => 'eco', ] ); $video_tag = $image_tag; $video_tag = str_replace( [ 'src=', '<img', '/>', ], [ 'original-src=', '<video autoplay muted loop playsinline poster="' . $link_png . '"', '><source src="' . $link_mp4 . '" type="video/mp4"></video>', ], $video_tag ); $content = str_replace( $image_tag, $video_tag, $content ); return true; } /** * Method invoked by `optml_content_images_tags` filter. * * @param string $content The content to be processed. * @param array $images A list of images. * * @return mixed */ public function process_image_tags( $content, $images = [] ) { $image_sizes = self::image_sizes(); $sizes2crop = self::size_to_crop(); foreach ( $images[0] as $index => $tag ) { $width = $height = false; $crop = null; $image_tag = $images['img_tag'][ $index ]; $is_slashed = strpos( $images['img_url'][ $index ], '\/' ) !== false; $src = $tmp = $is_slashed ? $this->strip_slashes( $images['img_url'][ $index ] ) : $images['img_url'][ $index ]; if ( strpos( $src, $this->upload_resource['content_path'] ) === 0 ) { $src = $tmp = untrailingslashit( $this->upload_resource['content_host'] ) . $src; $new_src = $is_slashed ? addcslashes( $src, '/' ) : $src; $image_tag = str_replace( [ '"' . $images['img_url'][ $index ], "'" . $images['img_url'][ $index ], ], [ '"' . $new_src, "'" . $new_src, ], $image_tag ); $images['img_url'][ $index ] = $new_src; } if ( ( apply_filters( 'optml_ignore_image_link', false, $src ) || ( false !== strpos( $src, Optml_Config::$service_url ) && ! $this->url_has_dam_flag( $src ) ) || ! $this->can_replace_url( $src ) || ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src ) ) { continue; // @codeCoverageIgnore } $resize = apply_filters( 'optml_default_crop', [] ); list( $width, $height, $resize ) = self::parse_dimensions_from_tag( $images['img_tag'][ $index ], $image_sizes, [ 'width' => $width, 'height' => $height, 'resize' => $resize, ] ); if ( false === $width && false === $height ) { list( $width, $height, $crop ) = $this->parse_dimensions_from_filename( $tmp ); } if ( empty( $resize ) && isset( $sizes2crop[ $width . $height ] ) ) { $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] ); } elseif ( $crop === true ) { $resize = $this->to_optml_crop( $crop ); } $optml_args = [ 'width' => $width, 'height' => $height, 'resize' => $resize ]; $is_gif = $this->is_valid_gif( $images['img_url'][ $index ] ); $should_lazy_gif = $is_gif ? $this->should_lazy_gif( $images['img_url'][ $index ], $optml_args ) : null; if ( $should_lazy_gif === true && $this->img_to_video( $images['img_url'][ $index ], $images['img_tag'][ $index ], $content ) ) { continue; } $tmp = $this->strip_image_size_from_url( $tmp ); $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args ); if ( $new_url === $tmp && ! Optml_Media_Offload::is_not_processed_image( $src ) ) { continue; // @codeCoverageIgnore } $image_tag = str_replace( [ 'width="' . $width . '"', 'width=\"' . $width . '\"', 'height="' . $height . '"', 'height=\"' . $height . '\"', ], [ 'width="' . $optml_args['width'] . '"', 'width=\"' . $optml_args['width'] . '\"', 'height="' . $optml_args['height'] . '"', 'height=\"' . $optml_args['height'] . '\"', ], $image_tag ); // If the image is in header or has a class excluded from lazyload or is an excluded gif, we need to do the regular replace. if ( $images['in_header'][ $index ] || $should_lazy_gif === false ) { $image_tag = $this->regular_tag_replace( $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag ); } else { $image_tag = apply_filters( 'optml_tag_replace', $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag ); } if ( strpos( $image_tag, 'decoding=' ) === false ) { $pattern = '/<img(.*?)/is'; $replace = '<img decoding=async $1'; $image_tag = preg_replace( $pattern, $replace, $image_tag ); } $content = str_replace( $images['img_tag'][ $index ], $image_tag, $content ); } return $content; } /** * Check if we should lazyload a gif. * * @param string $url URL to check. * @param array $optml_args The width/height that we find for the image. * @return bool Should we lazyload the gif ? */ public function should_lazy_gif( $url, $optml_args = [] ) { if ( strpos( $url, '/plugins/' ) !== false ) { return false; } if ( $this->is_valid_numeric( $optml_args['width'] ) && $this->is_valid_numeric( $optml_args['height'] ) && min( $optml_args['height'], $optml_args['width'] ) <= 20 ) { return false; } return true; } /** * Check replacement is allowed for this tag. * * @param string $url Url. * @param string $tag Html tag. * * @return bool We can replace? */ public function can_replace_tag( $url, $tag = '' ) { foreach ( self::possible_tag_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { self::$ignored_url_map[ crc32( $url ) ] = true; return false; } } return true; } /** * Extract image dimensions from img tag. * * @param string $tag The HTML img tag. * @param array $image_sizes WordPress supported image sizes. * @param array $args Default args to use. * * @return array */ private function parse_dimensions_from_tag( $tag, $image_sizes, $args = [] ) { if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $tag, $width_string ) ) { if ( ctype_digit( $width_string[1] ) === true ) { $args['width'] = $width_string[1]; } } if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $tag, $height_string ) ) { if ( ctype_digit( $height_string[1] ) === true ) { $args['height'] = $height_string[1]; } } if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $tag, $size ) ) { $size = array_pop( $size ); if ( false === $args['width'] && false === $args['height'] && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) { $args['width'] = (int) $image_sizes[ $size ]['width']; $args['height'] = (int) $image_sizes[ $size ]['height']; } if ( 'full' !== $size && array_key_exists( $size, $image_sizes ) ) { $args['resize'] = $this->to_optml_crop( $image_sizes[ $size ]['crop'] ); } } else { $args['resize'] = apply_filters( 'optml_parse_resize_from_tag', [], $tag ); } return [ $args['width'], $args['height'], $args['resize'] ]; } /** * Replaces the tags by default. * * @param string $new_tag The new tag. * @param string $original_url The original URL. * @param string $new_url The optimized URL. * @param array $optml_args Options passed for URL optimization. * @param bool $is_slashed Url needs to slashed. * @param string $full_tag Full tag, wrapper included. * * @return string */ public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) { $pattern = '/(?<!\/)' . preg_quote( $original_url, '/' ) . '/i'; $replace = $is_slashed ? addcslashes( $new_url, '/' ) : $new_url; if ( $this->settings->get( 'lazyload' ) === 'enabled' && $this->settings->get( 'native_lazyload' ) === 'enabled' && apply_filters( 'optml_should_load_eager', '__return_true' ) && ! $this->is_valid_gif( $original_url ) ) { if ( strpos( $new_tag, 'loading=' ) === false ) { $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img loading=\"eager\"' : '<img loading="eager"', $new_tag ); } else { $new_tag = $is_slashed ? str_replace( 'loading=\"lazy\"', 'loading=\"eager\"', $new_tag ) : str_replace( 'loading="lazy"', 'loading="eager"', $new_tag ); } } // If the image is between the first images we add the fetchpriority attribute to improve the LCP. if ( self::$lazyload_skipped_images < Optml_Lazyload_Replacer::get_skip_lazyload_limit() ) { if ( strpos( $new_tag, 'fetchpriority=' ) === false ) { $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img fetchpriority=\"high\"' : '<img fetchpriority="high"', $new_tag ); } } ++self::$lazyload_skipped_images; return preg_replace( $pattern, $replace, $new_tag ); } /** * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes. * * @param array $sources Array of image sources. * @param array $size_array Array of width and height values in pixels (in that order). * @param string $image_src The 'src' of the image. * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. * @param int $attachment_id Image attachment ID. * * @return array */ public function filter_srcset_attr( $sources = [], $size_array = [], $image_src = '', $image_meta = [], $attachment_id = 0 ) { if ( ! is_array( $sources ) ) { return $sources; } if ( Optml_Media_Offload::is_uploaded_image( $image_src ) ) { return $sources; } $original_url = null; $cropping = null; if ( count( $size_array ) === 2 ) { $sizes = self::size_to_crop(); $cropping = isset( $sizes[ $size_array[0] . $size_array[1] ] ) ? $this->to_optml_crop( $sizes[ $size_array[0] . $size_array[1] ] ) : null; } foreach ( $sources as $i => $source ) { $url = $source['url']; if ( Optml_Media_Offload::is_uploaded_image( $url ) ) { continue; } list( $width, $height, $file_crop ) = $this->parse_dimensions_from_filename( $url ); if ( empty( $width ) ) { $width = $image_meta['width']; } if ( empty( $height ) ) { $height = $image_meta['height']; } if ( $original_url === null ) { if ( ! empty( $attachment_id ) ) { $original_url = wp_get_attachment_url( $attachment_id ); } else { $original_url = $this->strip_image_size_from_url( $source['url'] ); } } $args = []; if ( 'w' === $source['descriptor'] ) { if ( $height && ( $source['value'] === $width ) ) { $args['width'] = $width; $args['height'] = $height; } else { $args['width'] = $source['value']; } } if ( $cropping !== null ) { $args['resize'] = $cropping; } else { $args['resize'] = $this->to_optml_crop( $file_crop ); } $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $original_url, $args ); } return $sources; } /** * Filters sizes attribute of the images. * * @param array $sizes An array of media query breakpoints. * @param array $size Width and height of the image. * * @return mixed An array of media query breakpoints. */ public function filter_sizes_attr( $sizes, $size ) { if ( ! doing_filter( 'the_content' ) ) { return $sizes; } $content_width = false; if ( isset( $GLOBALS['content_width'] ) ) { $content_width = $GLOBALS['content_width']; } if ( ! $content_width ) { $content_width = 1000; } if ( is_array( $size ) && $size[0] < $content_width ) { return $sizes; } return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width ); } /** * This filter will replace all the images retrieved via "wp_get_image" type of functions. * * @param array $image The filtered value. * @param int $attachment_id The related attachment id. * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions. * * @return array */ public function filter_image_downsize( $image, $attachment_id, $size ) { $image_url = wp_get_attachment_url( $attachment_id ); if ( Optml_Media_Offload::is_uploaded_image( $image_url ) ) { return $image; } if ( $image_url === false ) { return $image; } $image_meta = wp_get_attachment_metadata( $attachment_id ); $sizes = $this->size_to_dimension( $size, $image_meta ); $image_url = $this->strip_image_size_from_url( $image_url ); $new_url = apply_filters( 'optml_content_url', $image_url, $sizes ); if ( $new_url === $image_url ) { return $image; } return [ $new_url, $sizes['width'], $sizes['height'], $size === 'full', ]; } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @codeCoverageIgnore * @access public * @since 1.0.0 * @return void */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @codeCoverageIgnore * @access public * @since 1.0.0 * @return void */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } } main.php 0000644 00000012614 14720403740 0006206 0 ustar 00 <?php /** * Main Plugin Class */ final class Optml_Main { /** * Optml_Main The single instance of Starter_Plugin. * * @var object * @access private * @since 1.0.0 */ private static $_instance = null; /** * Holds the manager class. * * @access public * @since 1.0.0 * @var Optml_Manager Manager instance. */ public $manager; /** * Holds the media_offload class. * * @access public * @since 1.0.0 * @var Optml_Media_Offload instance. */ public $media_offload; /** * Holds the rest class. * * @access public * @since 1.0.0 * @var Optml_Rest REST instance. */ public $rest; /** * Holds the admin class. * * @access public * @since 1.0.0 * @var Optml_Admin Admin instance. */ public $admin; /** * Holds the Dam class. * * @access public * @since 4.0 * @var Optml_Dam Dam instance. */ public $dam; /** * Holds the cli class. * * @access public * @since 1.0.0 * @var Optml_Cli Cli instance. */ public $cli; /** * Optml_Main constructor. */ public function __construct() { register_activation_hook( OPTML_BASEFILE, [ $this, 'activate' ] ); register_deactivation_hook( OPTML_BASEFILE, [ $this, 'deactivate' ] ); } /** * Main Starter_Plugin Instance * * Ensures only one instance of Starter_Plugin is loaded or can be loaded. * * @return Optml_Main Plugin instance. * @since 1.0.0 */ public static function instance() { $vendor_file = OPTML_PATH . 'vendor/autoload.php'; if ( is_readable( $vendor_file ) ) { include_once $vendor_file; } if ( null === self::$_instance ) { add_filter( 'themeisle_sdk_products', [ __CLASS__, 'register_sdk' ] ); add_filter( 'themeisle_sdk_ran_promos', '__return_true' ); add_filter( 'optimole-wp_uninstall_feedback_icon', [ __CLASS__, 'change_icon' ] ); add_filter( 'optimole_wp_uninstall_feedback_after_css', [ __CLASS__, 'adds_uf_css' ] ); add_filter( 'optimole_wp_feedback_review_message', [ __CLASS__, 'change_review_message' ] ); add_filter( 'optimole_wp_logger_heading', [ __CLASS__, 'change_review_message' ] ); add_filter( 'optml_register_conflicts', [ __CLASS__, 'register_conflicts' ] ); self::$_instance = new self(); self::$_instance->manager = Optml_Manager::instance(); self::$_instance->rest = new Optml_Rest(); self::$_instance->admin = new Optml_Admin(); self::$_instance->dam = new Optml_Dam(); self::$_instance->media_offload = Optml_Media_Offload::instance(); if ( class_exists( 'WP_CLI' ) ) { self::$_instance->cli = new Optml_Cli(); } } return self::$_instance; } /** * Register Conflicts to watch for * * @param array $conflicts_to_register A list of class names of conflicts to register. * * @return array * @since 2.0.6 * @access public */ public static function register_conflicts( $conflicts_to_register = [] ) { $conflicts_to_register = array_merge( $conflicts_to_register, [ 'Optml_Jetpack_Photon', 'Optml_Jetpack_Lazyload', 'Optml_Wprocket', 'Optml_Divi', 'Optml_w3_total_cache_cdn', ] ); return $conflicts_to_register; } /** * Change review message. * * @param string $message Old review message. * * @return string New review message. */ public static function change_review_message( $message ) { return str_replace( '{product}', 'Optimole', $message ); } /** * Register product into SDK. * * @param array $products All products. * * @return array Registered product. */ public static function register_sdk( $products ) { $products[] = OPTML_BASEFILE; return $products; } /** * Adds aditional CSS for uninstall feedback popup. */ public static function adds_uf_css() { ?> <style type="text/css"> body.plugins-php .optimole_wp-container #TB_title { background-position: 30px 10px; background-size: 80px; } body.plugins-php .optimole_wp-container input.button:hover, body.plugins-php .optimole_wp-container input.button { background: #5080C1; } </style> <?php } /** * Change icon for uninstall feedback. * * @return string Registered product. */ public static function change_icon( $old_icon ) { return OPTML_URL . 'assets/img/logo.png'; } /** * Load the localisation file. * * @access public * @since 1.0.0 */ public function load_plugin_textdomain() { load_plugin_textdomain( 'optimole-wp', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); } /** * Install routine actions. * * @access public * @since 1.0.0 */ public function activate() { update_option( OPTML_NAMESPACE . '-version', OPTML_VERSION ); if ( is_multisite() ) { return; } set_transient( 'optml_fresh_install', true, MINUTE_IN_SECONDS ); } /** * Deactivate routine actions. * * @access public * @since 3.11.0 */ public function deactivate() { // Clear registered cron events. wp_clear_scheduled_hook( 'optml_daily_sync' ); wp_clear_scheduled_hook( 'optml_pull_image_data_init' ); } /** * Cloning is forbidden. * * @access public * @since 1.0.0 */ public function __clone() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Unserializing instances of this class is forbidden. * * @access public * @since 1.0.0 */ public function __wakeup() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } } url_replacer.php 0000644 00000030256 14720403740 0007743 0 ustar 00 <?php use Optimole\Sdk\Optimole; use Optimole\Sdk\Resource\Image; use Optimole\Sdk\Resource\ImageProperty\GravityProperty; use Optimole\Sdk\ValueObject\Position; /** * The class handles the url replacements. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Url_Replacer extends Optml_App_Replacer { use Optml_Dam_Offload_Utils; use Optml_Validator; use Optml_Normalizer; /** * Cached object instance. * * @var Optml_Url_Replacer */ protected static $instance = null; /** * Class instance method. * * @codeCoverageIgnore * @static * @return Optml_Url_Replacer * @since 1.0.0 * @access public */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] ); } return self::$instance; } /** * The initialize method. */ public function init() { add_filter( 'optml_replace_image', [ $this, 'build_url' ], 10, 2 ); parent::init(); add_filter( 'optml_content_url', [ $this, 'build_url' ], 1, 2 ); } /** * Returns a signed image url authorized to be used in our CDN. * * @param string $url The url which should be signed. * @param array $args Dimension params; Supports `width` and `height`. * * @return string */ public function build_url( $url, $args = [ 'width' => 'auto', 'height' => 'auto', ] ) { if ( apply_filters( 'optml_dont_replace_url', false, $url ) ) { return $url; } $original_url = $url; $is_slashed = strpos( $url, '\/' ) !== false; // We do a little hack here, for json unicode chars we first replace them with html special chars, // we then strip slashes to normalize the URL and last we convert html special chars back to get a clean URL $url = $is_slashed ? html_entity_decode( stripslashes( preg_replace( '/\\\u([\da-fA-F]{4})/', '&#x\1;', $url ) ) ) : ( $url ); $is_uploaded = Optml_Media_Offload::is_not_processed_image( $url ); if ( $is_uploaded === true ) { $attachment_id = []; preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id ); if ( ! isset( $attachment_id[1] ) ) { return $url; } $id_and_filename = []; preg_match( '/\/(' . Optml_Media_Offload::KEYS['uploaded_flag'] . '.*)/', $url, $id_and_filename ); if ( isset( $id_and_filename[0] ) ) { $id_and_filename = $id_and_filename[0]; } $sizes = $this->parse_dimension_from_optimized_url( $url ); if ( isset( $sizes[0] ) && $sizes[0] !== false ) { $args['width'] = $sizes[0]; } if ( isset( $sizes[1] ) && $sizes[1] !== false ) { $args['height'] = $sizes[1]; } $unoptimized_url = false; if ( $attachment_id[1] === 'media_cloud' ) { $tmp_url = explode( '/', $id_and_filename, 3 ); if ( isset( $tmp_url[2] ) ) { $unoptimized_url = $tmp_url[2]; } } else { $unoptimized_url = Optml_Media_Offload::get_original_url( (int) $attachment_id[1] ); } if ( $unoptimized_url !== false ) { $url = $unoptimized_url; } } if ( strpos( $url, Optml_Config::$service_url ) !== false && ! $this->url_has_dam_flag( $url ) ) { return $original_url; } if ( ! $this->can_replace_url( $url ) ) { return $original_url; } // Remove any query strings that might affect conversion. $url = strtok( $url, '?' ); $ext = $this->is_valid_mimetype_from_url( $url, self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ] ); if ( false === $ext ) { return $original_url; } if ( isset( $args['quality'] ) && ! empty( $args['quality'] ) ) { $args['quality'] = $this->to_accepted_quality( $args['quality'] ); } if ( isset( $args['minify'] ) && ! empty( $args['minify'] ) ) { $args['minify'] = $this->to_accepted_minify( $args['minify'] ); } // this will authorize the image if ( ! empty( $this->site_mappings ) ) { $url = str_replace( array_keys( $this->site_mappings ), array_values( $this->site_mappings ), $url ); } if ( substr( $url, 0, 2 ) === '//' ) { $url = sprintf( '%s:%s', is_ssl() ? 'https' : 'http', $url ); } $normalized_ext = strtolower( $ext ); if ( isset( Optml_Config::$image_extensions[ $normalized_ext ] ) ) { $new_url = $this->normalize_image( $url, $original_url, $args, $is_uploaded, $normalized_ext ); if ( $is_uploaded ) { $new_url = str_replace( '/' . $url, $id_and_filename, $new_url ); } } else { $asset = Optimole::asset( $url, $this->active_cache_buster_assets ); if ( stripos( $url, '.css' ) || stripos( $url, '.js' ) ) { $asset->quality(); } $asset->minify( ( $this->is_css_minify_on && str_ends_with( strtolower( $url ), '.css' ) ) || ( $this->is_js_minify_on && str_ends_with( strtolower( $url ), '.js' ) ) ); return $asset->getUrl(); } return $is_slashed ? addcslashes( $new_url, '/' ) : $new_url; } /** * Apply extra normalization to image. * * @param string $url Image url. * @param string $original_url Original image url. * @param array $args Args. * * @return string */ private function normalize_image( $url, $original_url, $args, $is_uploaded = false, $ext = '' ) { $new_url = $this->strip_image_size_from_url( $url ); if ( $new_url !== $url || $is_uploaded === true ) { if ( ! isset( $args['quality'] ) || $args['quality'] !== 'eco' ) { if ( $is_uploaded === true ) { $crop = false; } if ( $is_uploaded !== true ) { list($args['width'], $args['height'], $crop) = $this->parse_dimensions_from_filename( $url ); } if ( ! $crop ) { $sizes2crop = self::size_to_crop(); $crop = isset( $sizes2crop[ $args['width'] . $args['height'] ] ) ? $sizes2crop[ $args['width'] . $args['height'] ] : false; } if ( $crop ) { $args['resize'] = $this->to_optml_crop( $crop ); } } $url = $new_url; } if ( ! isset( $args['width'] ) ) { $args['width'] = $this->limit_dimensions_enabled ? $this->limit_width : 'auto'; } if ( ! isset( $args['height'] ) ) { $args['height'] = $this->limit_dimensions_enabled ? $this->limit_height : 'auto'; } $args['width'] = (int) $args['width']; $args['height'] = (int) $args['height']; if ( $this->limit_dimensions_enabled && $args['height'] !== 0 && $args['width'] !== 0 ) { if ( $args['width'] > $this->limit_width || $args['height'] > $this->limit_height ) { $scale = min( $this->limit_width / $args['width'], $this->limit_height / $args['height'] ); if ( $scale < 1 ) { $args['width'] = (int) floor( $scale * $args['width'] ); $args['height'] = (int) floor( $scale * $args['height'] ); } } if ( isset( $args['resize'], $args['resize']['enlarge'] ) ) { $args['resize']['enlarge'] = false; } } if ( empty( $args['quality'] ) ) { $args['quality'] = $this->to_accepted_quality( $this->settings->get_quality() ); } if ( isset( $args['resize'], $args['resize']['gravity'] ) && $this->settings->is_smart_cropping() ) { $args['resize']['gravity'] = GravityProperty::SMART; } $args = apply_filters( 'optml_image_args', $args, $original_url ); $image = Optimole::image( apply_filters( 'optml_processed_url', $url ), $this->active_cache_buster ); $image->width( ! empty( $args['width'] ) && is_int( $args['width'] ) ? $args['width'] : 'auto' ); $image->height( ! empty( $args['height'] ) && is_int( $args['height'] ) ? $args['height'] : 'auto' ); $image->quality( $args['quality'] ); if ( ! empty( $args['resize'] ) ) { $this->apply_resize( $image, $args['resize'] ); } if ( apply_filters( 'optml_apply_watermark_for', true, $url ) ) { $this->apply_watermark( $image ); } if ( ! empty( $args['format'] ) ) { $image->format( (string) $args['format'] ); } elseif ( $this->settings->is_best_format() ) { // If format is not already set, we use best format if it's enabled. $image->format( 'best' ); } if ( $this->settings->get( 'strip_metadata' ) === 'disabled' ) { $image->stripMetadata( false ); } if ( ! apply_filters( 'optml_should_avif_ext', true, $ext, $original_url ) || $this->settings->get( 'avif' ) === 'disabled' ) { $image->ignoreAvif(); } if ( apply_filters( 'optml_keep_copyright', false ) === true ) { $image->keepCopyright(); } if ( $this->is_dam_url( $image->getSource() ) ) { return $this->get_dam_url( $image ); } $offloaded_id = $this->is_offloaded_url( $image->getSource() ); if ( $offloaded_id !== 0 ) { return $this->get_offloaded_url( $offloaded_id, $image->getUrl(), $image->getSource() ); } return $image->getUrl(); } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Apply resize to image. * * @param Image $image Image object. * @param mixed $resize Resize arguments. */ private function apply_resize( Image $image, $resize ) { if ( ! is_array( $resize ) || empty( $resize['type'] ) ) { return; } $image->resize( $resize['type'], $resize['gravity'] ?? Position::CENTER, $resize['enlarge'] ?? false ); } /** * Apply watermark to image. * * @param Image $image Image object. */ private function apply_watermark( Image $image ) { $settings = $this->settings->get_site_settings(); if ( empty( $settings['watermark'] ) ) { return; } $watermark = $settings['watermark']; if ( ! isset( $watermark['id'], $watermark['opacity'], $watermark['position'] ) || $watermark['id'] <= 0 ) { return; } $image->watermark( $watermark['id'], $watermark['opacity'], $watermark['position'], $watermark['x_offset'] ?? 0, $watermark['y_offset'] ?? 0, $watermark['scale'] ?? 0 ); } /** * Get the DAM image URL. * * @param Image $image Image object. * * @return string */ private function get_dam_url( Image $image ) { // Remove DAM flag. $url = str_replace( Optml_Dam::URL_DAM_FLAG, '', $image->getSource() ); foreach ( $image->getProperties() as $property ) { [ $name, $value ] = explode( ':', $property ); // Check if the property exists in the URL, if so, replace it with the current property value. Otherwise, add it before the /q: param. $url = strpos( $url, '/' . $name . ':' ) !== false ? preg_replace( '/\/' . $name . ':.*?\//', '/' . $name . ':' . $value . '/', $url ) : str_replace( '/q:', '/' . $name . ':' . $value . '/q:', $url ); } return $url; } /** * Check if this contains the DAM flag. * * @param string $url The URL to check. * * @return bool */ private function is_dam_url( $url ) { return is_string( $url ) && strpos( $url, Optml_Dam::URL_DAM_FLAG ) !== false; } /** * Check if the URL is offloaded. * * @param string $source_url The source image URL. * * @return int */ private function is_offloaded_url( $source_url ) { $attachment_id = 0; if ( ! self::$offload_enabled ) { return 0; } if ( strpos( $source_url, Optml_Media_Offload::KEYS['not_processed_flag'] ) !== false ) { $attachment_id = (int) Optml_Media_Offload::get_attachment_id_from_url( $source_url ); } else { $attachment_id = $this->attachment_url_to_post_id( $source_url ); } if ( $attachment_id === 0 ) { return 0; } if ( ! $this->is_completed_offload( $attachment_id ) ) { return 0; } return (int) $attachment_id; } /** * Get the offloaded URL for an image. * * @param int $id The attachment ID. * @param string $optimized_url The optimized image URL. * @param string $source_url The source image URL. * * @return string */ private function get_offloaded_url( $id, $optimized_url, $source_url ) { $suffix = wp_get_attachment_metadata( $id )['file']; return str_replace( $source_url, ltrim( $suffix, '/' ), $optimized_url ); } } manager.php 0000644 00000157504 14720403740 0006704 0 ustar 00 <?php /** * Class Optml_Manager. Adds hooks for processing tags and urls. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Manager { /** * Holds allowed compatibilities objects. * * @var Optml_compatibility[] Compatibilities objects. */ public static $loaded_compatibilities = []; /** * Cached object instance. * * @var Optml_Manager */ protected static $instance = null; /** * Holds the url replacer class. * * @access public * @since 1.0.0 * @var Optml_Url_Replacer Replacer instance. */ public $url_replacer; /** * Holds the tag replacer class. * * @access public * @since 1.0.0 * @var Optml_Tag_Replacer Replacer instance. */ public $tag_replacer; /** * Holds the lazyload replacer class. * * @access public * @since 1.0.0 * @var Optml_Lazyload_Replacer Replacer instance. */ public $lazyload_replacer; /** * Holds the hero preloader class. * * @access public * @since 3.9.0 * @var Optml_Hero_Preloader Preloader instance. */ public $hero_preloader; /** * Holds plugin settings. * * @var Optml_Settings WordPress settings. */ protected $settings; /** * Possible integrations with different plugins. * * @var array Integrations classes. */ private $possible_compatibilities = [ 'shortcode_ultimate', 'foogallery', 'envira', 'beaver_builder', 'jet_elements', 'revslider', 'metaslider', 'essential_grid', 'yith_quick_view', 'cache_enabler', 'elementor_builder', 'divi_builder', 'thrive', 'master_slider', 'pinterest', 'sg_optimizer', 'wp_fastest_cache', 'swift_performance', 'w3_total_cache', 'translate_press', 'give_wp', 'woocommerce', 'smart_search_woocommerce', 'facetwp', 'wp_rest_cache', 'wp_bakery', 'elementor_builder_late', 'wpml', 'otter_blocks', 'spectra', ]; /** * The current state of the buffer. * * @var boolean Buffer state. */ private static $ob_started = false; /** * Class instance method. * * @codeCoverageIgnore * @static * @return Optml_Manager * @since 1.0.0 * @access public */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); self::$instance->url_replacer = Optml_Url_Replacer::instance(); self::$instance->tag_replacer = Optml_Tag_Replacer::instance(); self::$instance->lazyload_replacer = Optml_Lazyload_Replacer::instance(); self::$instance->hero_preloader = Optml_Hero_Preloader::instance(); add_action( 'after_setup_theme', [ self::$instance, 'init' ] ); add_action( 'wp_footer', [ self::$instance, 'banner' ] ); } return self::$instance; } /** * The initialize method. */ public function init() { $this->settings = new Optml_Settings(); $added_sizes = $this->settings->get( 'defined_image_sizes' ); foreach ( $added_sizes as $key => $value ) { add_image_size( $key, $value['width'], $value['height'], true ); } foreach ( $this->possible_compatibilities as $compatibility_class ) { $compatibility_class = 'Optml_' . $compatibility_class; $compatibility = new $compatibility_class(); /** * Check if we should load compatibility. * * @var Optml_compatibility $compatibility Class to register. */ if ( $compatibility->will_load() ) { if ( $compatibility->should_load_early() ) { $compatibility->register(); continue; } self::$loaded_compatibilities[ $compatibility_class ] = $compatibility; } } if ( ! $this->should_replace() ) { return; } $this->register_hooks(); } /** * Render banner. * * @return void */ public function banner() { if ( defined( 'REST_REQUEST' ) ) { return; } $has_banner = $this->settings->get( 'banner_frontend' ) === 'enabled'; if ( ! $has_banner ) { return; } $string = __( 'Optimized by Optimole', 'optimole-wp' ); $div_style = [ 'display' => 'flex', 'position' => 'fixed', 'align-items' => 'center', 'bottom' => '15px', 'right' => '15px', 'background-color' => '#fff', 'padding' => '8px 6px', 'font-size' => '12px', 'font-weight' => '600', 'color' => '#444', 'border' => '1px solid #E9E9E9', 'z-index' => '9999999999', 'border-radius' => '4px', 'text-decoration' => 'none', 'font-family' => 'Arial, Helvetica, sans-serif', ]; $logo = OPTML_URL . 'assets/img/logo.svg'; $link = tsdk_translate_link( 'https://optimole.com/wordpress/?from=badgeOn' ); $css = ''; foreach ( $div_style as $key => $value ) { $css .= $key . ':' . $value . ';'; } $output = '<a style="' . esc_attr( $css ) . '" href="' . esc_url( $link ) . '" rel="nofollow" target="_blank">'; $output .= ' <svg style="margin-right:6px" width="20" height="20" viewBox="0 0 251 250" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M217.696 99.0926C240.492 152.857 211.284 215.314 153.249 239.999C95.2141 264.683 30.4436 242.239 7.64869 188.474C-15.1462 134.71 14.8767 75.1972 72.9116 50.4991C130.947 25.8007 194.902 45.3142 217.696 99.0926Z" fill="#EDF0FF"/> <path d="M217.696 99.0926C240.492 152.857 211.284 215.314 153.249 239.999C95.2141 264.683 30.4436 242.239 7.64869 188.474C-15.1462 134.71 14.8767 75.1972 72.9116 50.4991C130.947 25.8007 194.902 45.3142 217.696 99.0926Z" fill="#D7EEFC"/> <path d="M181.711 75.8034C176.438 78.931 171.418 82.6928 167.832 87.6665C164.246 92.6402 162.221 98.9662 163.346 104.996C163.458 105.602 163.627 106.236 164.091 106.645C164.71 107.18 165.624 107.11 166.425 107.011C178.786 105.334 190.57 99.628 199.556 90.9633C204.956 85.7784 210.103 79.1566 217.542 78.2969C220.987 77.8883 224.475 78.8464 227.582 80.3685C229.186 81.1431 230.732 82.1012 231.899 83.4397C234.571 86.4971 234.796 90.9771 234.402 95.0352C233.967 99.487 232.968 103.926 231.393 108.11C230.001 111.815 226.851 114.661 224.193 117.578C218.203 124.171 211.678 130.301 204.52 135.612C194.437 143.079 184.833 150.772 173.654 156.703C167.93 159.732 162.671 162.029 157.974 166.651C153.488 171.06 149.832 176.33 147.231 182.064C145.093 186.784 143.842 191.546 141.409 196.182C138.836 201.07 135.938 205.804 132.634 210.228C128.584 215.667 124.689 218.809 118.319 221.373C108.63 225.276 98.0689 226.332 87.6486 226.98C79.9569 227.459 72.2365 227.727 64.5448 227.262C62.5477 227.149 58.1321 226.713 57.6544 224.134C57.1197 221.232 61.0852 219.893 63.11 219.471C75.6818 216.85 88.4785 213.145 98.477 205.072C100.502 203.437 102.569 201.056 101.852 198.562C96.5926 198.323 91.5721 200.507 86.5096 201.944C80.9129 203.536 75.0631 204.24 69.2552 204.029C67.3714 203.958 64.9242 203.226 64.9242 201.338C64.9385 199.774 66.6681 198.914 68.0885 198.266C72.9537 196.083 77.0602 192.56 81.096 189.08C84.5268 186.122 88.1269 182.909 89.3643 178.541C89.533 177.95 89.6457 177.288 89.3643 176.752C88.7456 175.541 86.9597 175.865 85.7084 176.386C79.3382 179.021 73.1085 182.007 67.0617 185.332C59.4121 189.545 51.3682 194.448 42.678 193.73C40.9623 193.589 38.7826 192.575 39.12 190.884C39.303 189.94 40.2308 189.348 41.0607 188.869C63.2649 175.935 82.277 156.238 87.3534 131.019C91.5019 110.406 85.9334 89.0332 87.8178 68.0968C88.0987 64.9266 88.5631 61.7426 89.6458 58.7416C90.3911 56.6705 91.4173 54.6978 92.5287 52.7957C99.8831 40.0872 110.711 29.4076 123.493 22.208C135.291 15.5862 148.328 15.0649 160.829 9.92232C168.788 6.6536 176.874 2.80726 185.48 2.97633C186.071 2.99042 186.689 3.03269 187.153 3.38492C187.561 3.70897 187.772 4.21618 187.941 4.72339C189.263 8.6261 189.249 12.8529 190.261 16.8542C190.36 17.2628 190.514 17.7136 190.88 17.9109C191.245 18.1081 191.71 17.9954 192.103 17.8686C195.478 16.7978 198.853 15.727 202.228 14.6563C203.283 14.3181 204.422 13.9941 205.448 14.3745C207.431 15.1071 207.656 17.7981 207.459 19.9116C205.533 41.0312 199.936 64.9968 181.711 75.8034Z" fill="#577BF9"/> <path d="M118.165 39.6223C108.461 45.8356 99.8553 53.7539 92.8244 62.8836C97.6475 64.3067 102.4 66.0112 107.027 67.9977C103.132 71.8867 98.4348 74.9718 93.3303 77.0008C87.6496 79.2549 83.7963 82.0873 80.3649 87.1594C87.6772 89.1315 95.2572 85.9895 102.387 83.4258C109.572 80.8472 116.941 78.8043 124.436 77.3248C129.006 76.423 133.858 75.6341 137.429 72.6331C137.711 72.3937 138.006 72.0835 137.992 71.7175C137.978 71.4073 137.739 71.1541 137.514 70.9424C134.195 67.885 129.47 66.6172 126.391 63.306C126.053 62.94 125.716 62.4607 125.843 61.9819C125.941 61.6015 126.306 61.3477 126.644 61.1365C130.005 59.009 133.352 56.8816 136.712 54.7541C132.41 50.1187 128.036 45.5397 123.593 41.0453C122.242 39.6786 119.781 38.5796 118.165 39.6223Z" fill="#EE686B"/> <path d="M165.793 91.1041C163.67 96.2326 163.205 101.953 163.74 107.49C168.296 107.011 172.782 105.897 177.015 104.165C177.703 103.883 178.435 103.545 178.815 102.897C179.067 102.46 179.124 101.924 179.152 101.417C179.461 95.4293 177.085 89.3432 178.617 83.5524C178.969 82.2277 180.08 78.7904 177.788 78.4095C176.551 78.1983 174.553 80.4385 173.639 81.1291C170.152 83.7779 167.466 87.0465 165.793 91.1041Z" fill="#F6E5C1"/> <path d="M171.558 156.534C169.041 157.929 166.524 159.338 164.274 161.128C161.222 163.565 158.762 166.665 156.498 169.863C150.17 178.824 145.276 188.7 140.411 198.534C139.44 200.479 138.484 202.423 137.514 204.368C136.318 206.776 135.109 209.242 134.8 211.919C134.687 212.905 134.771 214.061 135.573 214.652C136.108 215.047 136.839 215.075 137.499 215.033C140.298 214.892 143.012 214.061 145.656 213.131C164.893 206.34 181.81 193.138 193.073 176.104C194.283 174.273 195.45 172.385 196.223 170.328C196.913 168.482 197.292 166.552 197.63 164.607C198.572 159.226 199.275 153.801 199.697 148.349C199.838 146.587 199.964 142.488 197.574 142.192C195.563 141.938 190.782 145.827 188.967 146.827C183.174 150.068 177.366 153.308 171.558 156.534Z" fill="#F6E5C1"/> <path d="M58.9198 140.29C55.4043 141.952 51.6495 143.798 49.7369 147.194C47.7403 150.744 48.2744 155.365 50.4259 158.831C52.5778 162.297 56.1071 164.72 59.862 166.298C60.9728 166.763 62.1538 167.172 63.3492 167.031C64.6287 166.89 65.7539 166.143 66.7807 165.354C71.59 161.607 74.9644 156.351 78.241 151.223C82.2209 145.01 86.2987 138.486 87.1005 131.146C87.8878 123.862 79.7317 130.498 77.102 131.738C71.0415 134.583 64.9804 137.43 58.9198 140.29Z" fill="#336093"/> <path d="M136.515 18.9961C142.604 15.6429 149.129 13.149 155.626 10.6694C164.471 7.30208 173.358 3.92065 182.667 2.24404C183.427 2.10315 184.228 1.99043 184.931 2.2863C185.663 2.58218 186.155 3.27255 186.577 3.93474C188.995 7.82333 190.163 12.4869 189.853 17.0659C189.825 17.5167 189.769 17.9957 189.474 18.3339C189.277 18.5734 188.981 18.7002 188.7 18.827C185.747 20.1655 182.653 21.2645 179.827 22.8565C176.986 24.4486 174.357 26.6183 172.923 29.5348C170.265 34.9309 172.135 41.7078 169.618 47.1744C168.183 50.3022 165.469 52.627 162.727 54.6982C158.621 57.812 153.756 60.714 148.651 60.0097C145.698 59.6011 143.04 58.0232 140.495 56.4591C139.103 55.6137 137.724 54.7827 136.332 53.9374C130.088 50.1473 123.353 45.7373 121.286 38.7209C119.725 33.3952 121.286 30.7183 125.068 27.1396C128.5 23.9273 132.409 21.2504 136.515 18.9961Z" fill="#D6EDFB"/> <path d="M199.416 58.9386C199.894 59.8829 200.78 60.7847 201.835 60.6575C202.285 60.6011 202.693 60.3761 203.086 60.1362C204.605 59.235 206.18 58.2907 207.08 56.7691C207.924 55.3604 208.064 53.6553 208.177 52.0351C208.416 48.6115 208.5 44.8355 206.391 42.1304C206.152 41.8346 205.857 41.5246 205.477 41.4823C202.468 41.102 200.682 45.7232 200.161 47.8648C199.275 51.4434 197.574 55.3322 199.416 58.9386Z" fill="#F6E5C1"/> <path d="M220.565 71.295C217.767 72.5633 214.673 74.9163 215.193 77.9594C218.892 77.7764 222.647 77.8046 226.19 78.9031C229.72 80.0023 233.024 82.3127 234.571 85.6803C236.99 90.9354 234.656 97.163 235.696 102.841C240.646 100.178 244.626 95.7538 246.763 90.555C247.663 88.343 248.254 85.8915 247.733 83.5667C247.298 81.6366 246.13 79.8895 245.891 77.9312C245.638 75.8319 246.468 73.789 247.199 71.8025C247.523 70.9428 247.79 69.8581 247.171 69.1675C246.552 68.4775 245.47 68.6185 244.569 68.7733C240.28 69.5341 236.146 69.2665 231.885 68.6041C228.089 68.0264 224.067 69.7171 220.565 71.295Z" fill="#F6E5C1"/> <path d="M40.5681 149.138C40.4415 149.8 40.3994 150.49 40.1606 151.124C39.1338 153.773 35.2808 154.125 33.7621 156.52C33.4246 157.056 33.2137 157.704 33.3121 158.338C33.495 159.578 34.7324 160.381 35.9277 160.79C37.123 161.198 38.4167 161.409 39.387 162.212C40.9341 163.494 41.1166 165.876 42.5088 167.341C43.451 168.341 44.8289 168.778 46.1509 169.102C49.0477 169.82 52.029 170.201 55.0103 170.229C55.8397 170.243 56.7681 170.173 57.3166 169.553C58.0055 168.778 57.682 167.482 56.9649 166.735C56.2478 165.988 55.2492 165.594 54.3352 165.087C52.0428 163.818 50.2292 161.705 49.3147 159.239C48.3587 156.619 48.4571 153.745 49.1604 151.068C49.6382 149.264 52.5631 145.235 51.2416 143.53C50.0601 142.008 46.4605 143.009 45.0821 143.643C42.8743 144.672 41.0464 146.686 40.5681 149.138Z" fill="#F6E5C1"/> <path d="M25.8872 210.694C26.0419 211.68 27.1809 212.116 28.1371 212.37C30.6543 213.018 33.1855 213.554 35.7448 214.019C36.9823 214.23 38.2338 214.427 39.4715 214.23C40.7089 214.033 41.932 213.37 42.5087 212.272C42.8321 211.651 42.9449 210.933 42.9726 210.214C43.0151 209.242 42.9023 208.242 42.4384 207.383C41.5665 205.79 39.6257 205.086 37.812 205.044C36.012 205.016 34.2401 205.509 32.4402 205.748C30.134 206.058 25.3247 207.242 25.8872 210.694Z" fill="#6B8FCA"/> <path d="M151.745 39.27C154.262 38.2133 157.186 38.2133 159.816 39.4954C161.026 38.2274 162.165 36.8748 163.219 35.48C163.304 35.3673 164.57 33.5639 164.471 33.578C163.29 33.6766 162.109 33.9584 160.998 34.381C160.351 34.6206 159.577 33.9725 158.959 33.7471C158.129 33.4371 157.271 33.2257 156.399 33.113C154.599 32.8876 152.392 33.099 150.746 33.8738C148.721 34.8319 146.935 36.3394 145.599 38.1147C143.631 40.7212 142.548 43.9617 143.11 47.2022C143.153 47.4276 143.251 47.6953 143.476 47.7094C143.617 47.7094 143.729 47.6249 143.827 47.5263C145.332 46.1596 146.373 44.328 147.512 42.6655C148.623 41.1157 150.099 39.9604 151.745 39.27Z" fill="#1D445C"/> <path opacity="0.5" d="M132.213 48.3997C132.606 48.8223 133.07 49.2732 133.605 49.8226C135.475 51.7106 137.022 53.8942 138.991 55.6695C140.284 56.825 142.028 58.121 143.814 58.3045C144.404 58.3609 148.567 58.4875 148.68 58.1635C151.45 49.4423 153.812 40.6788 156.287 31.8449C156.962 29.4356 164.232 9.25987 162.446 8.69632C161.997 8.55544 161.504 8.75266 161.068 8.93582C156.147 11.0915 151.239 13.2471 146.317 15.4028C142.942 16.8821 142.155 18.8828 140.72 22.0811C138.681 26.6459 136.727 31.239 134.842 35.8603C133.717 38.6217 132.649 41.4114 131.537 44.187C130.694 46.2158 131.116 47.188 132.213 48.3997Z" fill="white"/> <path opacity="0.5" d="M157.693 55.6844C161.448 54.2187 165.498 52.5142 167.453 48.9778C168.507 47.0616 168.817 44.8496 169.098 42.6799C170.799 29.8165 172.501 16.939 174.203 4.07556C174.258 3.61062 170.687 4.97727 170.335 5.35768C169.407 6.3439 169.393 7.59788 169.028 8.87995C167.973 12.5995 166.693 16.2627 165.625 19.9822C163.473 27.4918 161.616 35.0859 160.042 42.7363C159.184 47.0335 158.396 51.3588 157.693 55.6844Z" fill="white"/> <path d="M199.289 106.546C190.261 110.969 181.725 112.703 171.839 112.914C164.077 113.083 155.879 112.351 149.481 107.941C147.948 106.884 146.556 105.644 145.346 104.249C148.44 104.63 151.52 106.475 154.529 107.349C157.623 108.25 160.829 108.73 164.049 108.405C170.785 107.729 177.267 105.292 183.188 102.094C189.361 98.7685 195.042 94.6402 200.737 90.5544C202.551 89.2584 204.225 87.8635 205.589 86.1446C207.122 84.2145 208.823 82.4674 211.073 81.3965C213.956 80.0298 217.275 79.9452 220.467 80.0718C223.617 80.199 226.865 80.5512 229.579 82.1573C233.995 84.7636 235.978 90.7236 234.121 95.4291C232.883 92.8229 229.973 91.2167 227.09 91.2449C222.59 91.287 217.809 95.4573 214.209 97.7965C209.385 100.938 204.478 103.953 199.317 106.489C199.331 106.531 199.303 106.531 199.289 106.546Z" fill="#6C99CE"/> <path d="M179.096 24.4344C173.091 28.0553 171.249 34.4096 171.46 41.0033C171.685 47.8084 176.649 52.4015 183.23 54.1485C203.058 59.4036 207.361 34.4519 208.05 20.1513C208.106 18.8551 208.078 17.3898 207.15 16.474C206.503 15.8401 205.575 15.6287 204.675 15.516C198.994 14.8115 192.792 16.474 188.081 19.6864C185.241 21.6166 181.964 22.7015 179.096 24.4344Z" fill="#6C99CE"/> <path d="M86.9743 69.4919C86.5242 72.7041 86.3556 76.1137 87.7473 79.044C93.9488 75.7897 99.9397 72.1263 105.663 68.0826C102.119 67.0963 98.5756 66.1105 95.032 65.1098C94.4973 64.9688 93.9206 64.7858 93.6397 64.307C93.3445 63.8277 93.4567 63.194 93.6254 62.6583C94.4409 60.052 96.3115 57.9246 98.224 55.9663C103.764 50.2881 110.036 45.3146 116.828 41.2288C118.192 40.4115 119.655 39.524 120.26 38.0728C121.202 35.8326 119.796 33.0288 121.005 30.9295C121.806 29.5347 123.452 28.9571 124.872 28.1822C127.656 26.6746 128.331 23.3918 124.829 22.2224C122.074 21.3066 119.936 23.6595 117.77 25.0403C112.427 28.4499 107.449 31.8594 103.075 36.5088C94.5537 45.54 88.7038 57.1638 86.9743 69.4919Z" fill="#336093"/> <path d="M94.9195 86.8914C88.4791 87.3143 86.6794 88.0751 87.1576 94.5139C87.7056 102.023 88.4227 109.589 88.4791 117.127C88.5494 126.341 87.3821 135.795 83.1213 143.967C79.9437 150.053 75.2605 155.238 71.1407 160.677C67.1608 165.932 63.3639 170.99 58.6674 175.696C61.6625 172.695 68.328 171.469 72.111 169.313C76.7655 166.665 81.1386 163.522 85.1467 159.958C100.123 146.658 112.005 129.314 115.422 109.265C117.053 99.6988 116.857 89.9206 116.66 80.2275C109.826 82.5237 102.133 86.4264 94.9195 86.8914Z" fill="#336093"/> <path d="M43.1137 204.283C42.9169 204.156 42.7062 204.015 42.5094 203.93C41.0746 203.184 39.6685 203.282 38.0093 203.297C35.9984 203.325 34.0297 203.423 32.0609 203.776C28.1798 204.452 24.383 205.79 20.8674 207.396C18.8143 208.341 12.0785 210.679 15.7488 213.666C17.1128 214.793 18.8705 215.286 20.6002 215.653C25.1142 216.653 29.7688 216.991 34.3812 216.681C36.8562 216.512 38.8391 216.132 41.131 215.061C41.9891 214.652 42.8605 214.244 43.5638 213.61C46.2919 211.186 46.1371 206.382 43.1137 204.283ZM40.5686 209.862C38.9514 212.328 33.889 211.595 31.5125 211.37C30.711 211.285 29.8672 211.158 29.3188 210.567C29.2063 210.44 29.1079 210.271 29.1079 210.116C29.1079 209.806 29.4032 209.608 29.6844 209.482C31.4 208.622 33.2844 208.059 35.1968 207.847C36.4624 207.72 38.6983 207.312 39.8233 208.059C40.8075 208.721 40.9203 209.327 40.5686 209.862Z" fill="#1D445C"/> <path d="M185.1 49.0198C186.155 48.2309 187.336 47.2164 187.238 45.9061C187.111 44.3282 185.1 43.4687 183.722 43.8773C182.372 44.2718 180.966 45.8498 180.052 46.8501C175.453 51.894 170.869 56.9519 166.271 61.9958C165.933 62.3624 164.302 63.7993 165.708 63.7429C166.679 63.7009 167.986 62.5034 168.746 61.9538C170.743 60.4887 172.529 58.7554 174.455 57.2195C177.943 54.4302 181.5 51.6968 185.1 49.0198Z" fill="#1D445C"/> <path d="M175.411 66.871C175.017 67.5334 173.302 70.1258 175.271 68.7171C176.775 67.618 177.802 66.0821 178.983 64.6733C180.234 63.18 181.514 61.7005 182.836 60.2636C184.074 58.9107 185.48 57.7414 186.703 56.3608C187.772 55.1489 190.233 52.4158 188.475 50.7533C187.913 50.2179 186.703 50.6828 186.141 51.0632C185.142 51.7395 184.748 52.8384 184.059 53.7822C183.3 54.8249 182.695 55.9522 181.936 56.9806C179.63 60.179 177.45 63.4758 175.411 66.871Z" fill="#1D445C"/> <path d="M248.746 78.0012C248.605 77.7336 248.451 77.508 248.296 77.325C248.914 75.8035 249.407 74.1692 249.744 72.3801C249.871 71.7316 250.18 70.21 249.463 68.7449C248.83 67.491 247.55 66.5749 246.06 66.2791C245.005 66.0817 244.007 66.1945 243.093 66.3637C242.137 66.5328 241.222 66.7722 240.323 67.068C237.046 65.1241 233.615 65.5045 230.563 65.8423L229.917 65.9269C226.359 66.3355 222.843 67.5612 219.75 69.5057C215.77 71.9715 212.226 75.5923 209.189 80.2697C206.067 82.3407 203.241 84.9614 200.583 87.399C199.05 88.8221 197.601 90.1601 196.153 91.372C192.778 94.1336 189.727 96.6133 186.338 98.6844C184.903 99.5861 183.398 100.403 181.852 101.122C179.756 94.373 179.883 87.1596 182.274 81.397C183.469 78.579 185.269 76.4377 187.195 74.1548C188.081 73.1126 188.981 72.0135 189.895 70.8016C190.5 69.9845 191.105 69.1535 191.723 68.3081C193.327 66.0817 194.803 63.7856 196.223 61.4464C196.8 60.4888 197.826 59.5306 198.684 60.9394C199.092 61.5874 199.205 62.4189 199.753 62.9541C200.681 63.7994 202.144 63.3063 203.254 62.7147C205.715 61.3762 208.106 59.5163 209.076 56.8536C209.793 54.8671 209.611 52.6549 209.414 50.5416C209.104 47.1884 208.795 43.8492 208.5 40.5242C208.289 38.2981 207.909 36.072 208.148 33.8459C208.373 31.5071 209.245 29.1824 209.709 26.8718C209.821 26.3363 209.99 25.6742 210.159 24.9556C210.651 22.969 211.143 20.9684 211.607 18.9959C212.086 16.9389 212.901 14.9523 213.435 12.9939C214.139 10.4579 214.87 7.1328 213.323 4.77991C211.874 2.582 208.682 1.8071 206.573 3.44144C206.404 3.55415 206.278 3.68096 206.137 3.82185C205.589 4.3995 205.153 5.13215 204.422 5.44209C203.831 5.7098 203.17 5.65346 202.509 5.62526C200.892 5.59707 199.219 5.89297 197.925 6.86509C195.267 8.89393 195.998 11.2891 197.756 13.318C197.32 13.6279 196.912 13.9097 196.561 14.0647C195.605 14.5578 194.634 14.9523 193.65 15.2764C192.919 12.8389 192.173 10.4156 191.414 7.96405C190.669 5.56892 189.867 1.34215 187.322 0.271376C186.689 0.00368107 185.972 -0.0104081 185.283 0.00368111C176.255 0.0882161 167.733 3.61051 159.577 7.17509C157.721 7.97815 155.893 8.73896 154.107 9.49978C150.985 10.8101 147.877 12.1204 144.812 13.5575C143.546 14.1492 142.407 14.9382 141.071 15.2905C139.707 15.6567 138.287 15.7695 136.909 16.1076C135.531 16.4598 134.167 16.9389 132.774 17.277C130.426 17.8265 128.092 18.531 125.828 19.3622C121.342 21.0106 117.039 23.1663 113.059 25.7728C105.114 30.9435 98.3362 37.8754 93.3157 45.9344C90.2083 50.922 87.7893 56.3461 86.1438 61.9959C85.3426 64.7719 84.5409 67.5612 84.0488 70.4074C83.5423 73.3238 83.5705 76.1275 83.7535 79.0721C83.7535 79.1705 83.7673 79.2551 83.7392 79.3541C83.7115 79.4669 83.6407 79.5653 83.5705 79.6637C82.7268 80.8192 81.4755 81.5236 80.4349 82.5099C77.594 85.1726 74.5004 87.5118 72.1101 90.6251C72.124 90.5969 74.1349 90.6958 74.3318 90.7097C75.1191 90.724 75.9065 90.7379 76.6944 90.7522C77.4818 90.7522 78.2691 90.7522 79.0565 90.7522C79.8444 90.7379 80.6317 90.724 81.4191 90.7097C82.291 90.6815 83.1486 90.5692 84.0067 90.4985C84.1754 90.4846 84.3722 90.4846 84.4989 90.6112C84.5973 90.7097 84.6111 90.8506 84.6393 90.9778C84.9346 93.2319 85.6379 95.4721 85.6379 97.7964C85.6661 98.1492 85.7225 98.5014 85.7363 98.8535C85.9331 100.53 86.0736 102.235 86.1582 103.926C86.2002 104.362 86.2428 104.813 86.2566 105.25C86.3412 106.701 86.3832 108.152 86.397 109.631C86.4396 111.576 86.4252 113.506 86.3268 115.45C86.2146 118.367 85.947 121.298 85.5251 124.2C79.1411 128.201 71.7867 132.047 62.3931 136.359C60.9445 137.007 59.4118 137.725 57.8647 138.542C57.5413 138.641 57.1338 138.726 56.7539 138.782C55.7133 138.994 54.6727 139.163 53.6603 139.303C48.9494 140.05 44.0981 140.839 39.7809 144.531C37.2074 146.715 35.6043 149.504 35.2809 152.378C35.2668 152.547 35.2809 152.702 35.2809 152.871C34.2403 153.618 33.2278 154.491 32.2294 155.52C31.8216 155.943 30.8654 156.929 30.6966 158.394C30.556 159.691 31.0482 161.029 32.0607 161.987C32.7497 162.635 33.5232 163.016 34.2544 163.34C34.9434 163.635 35.6184 163.861 36.3215 164.058C37.7699 166.848 40.3151 168.13 42.5654 169.229L43.0432 169.468C47.4449 171.652 51.9866 171.708 56.3884 171.751C56.9226 171.751 57.429 171.765 57.9493 171.765C57.8365 171.878 57.7243 171.99 57.5977 172.103C52.4793 176.654 46.8821 180.74 40.9763 184.234C35.309 187.615 29.3045 190.067 23.2436 192.673C22.7515 192.885 22.2171 193.152 22.0343 193.659C21.7671 194.392 22.414 195.125 23.0468 195.547C24.2139 196.35 25.7045 196.984 27.0685 197.337C28.2779 197.647 29.5295 197.675 30.7529 197.816C32.9888 198.055 35.2809 198.027 37.5309 197.901C45.9399 197.407 54.462 194.646 62.0415 191.011C63.8976 190.123 65.7256 189.179 67.5535 188.249C69.6071 187.192 71.7303 186.094 73.8397 185.121C74.2754 184.91 74.8239 184.628 75.4426 184.319C77.0599 183.515 78.6346 182.67 80.2381 181.825C80.927 181.472 81.5739 181.064 82.249 180.697C82.938 180.317 83.894 179.584 84.6813 179.528C85.7501 179.443 84.5973 181.965 84.344 182.444C83.7955 183.515 82.98 184.417 82.1224 185.262C81.841 185.558 81.5457 185.84 81.2643 186.108C79.6614 187.615 77.8893 188.926 76.0613 190.151C74.444 191.222 72.7847 192.222 71.0972 193.152C69.5789 193.998 68.2148 194.913 66.7805 195.815C64.868 197.013 53.4635 201.282 55.9383 204.551C56.7683 205.65 58.7228 206.1 59.9603 206.382C65.6974 207.721 71.7303 206.706 77.3833 205.452C83.3316 204.128 89.1251 202.198 94.975 200.549C95.5656 200.38 96.3816 200.324 96.6205 200.873C98.1394 204.339 86.5518 208.975 84.527 209.904C77.046 213.342 69.2272 215.723 61.1557 217.386C57.9913 218.034 54.8132 218.696 51.6212 219.161C48.8367 219.569 45.659 219.344 43.1416 220.781C39.7389 222.725 43.7183 225.825 45.8133 226.952C48.2179 228.249 50.946 228.84 53.6039 229.432C63.1385 231.531 73.1928 231.08 82.8677 230.418C101.978 229.08 120.849 225.008 139.102 219.231C141.015 218.626 142.913 218.005 144.826 217.372C146.851 216.681 148.946 216.005 150.985 215.202C157.538 212.666 163.543 209.51 169.013 205.762C171.094 204.339 173.105 202.817 175.032 201.226C176.283 200.197 177.492 199.126 178.659 198.013C180.094 196.675 181.458 195.266 182.794 193.814C189.91 186.023 195.436 176.781 199.205 166.284C201.961 158.578 202.607 151.406 201.44 141.628C207.585 137.007 213.45 131.991 218.976 126.538C224.348 121.213 230.381 115.211 234.191 107.405L234.318 107.448C234.529 106.898 235.162 106.293 235.767 105.715C236.709 104.813 237.679 103.954 238.621 103.108C242.91 99.2339 247.34 95.2327 249.294 88.7093C250.462 84.863 250.25 81.073 248.746 78.0012ZM222.323 73.6478C224.812 72.0981 227.624 71.1118 230.465 70.7878L231.112 70.7032C231.449 70.675 231.801 70.633 232.125 70.5904C231.73 70.9006 231.379 71.2384 231.013 71.6049C230.296 72.3237 229.509 73.0563 228.651 73.8027C228.383 74 228.116 74.2112 227.849 74.4086C226.907 75.1273 225.937 75.8317 224.995 76.5361C223.265 76.1839 221.479 76.0429 219.665 76.2259C219.356 76.2403 219.075 76.2967 218.779 76.3387C219.918 75.3103 221.099 74.4086 222.323 73.6478ZM203.93 57.9245C203.662 58.6709 202.045 60.3334 201.188 59.4599C200.99 59.2487 200.892 58.9529 200.808 58.6709C200.414 57.3606 199.739 55.9801 200.006 54.6554C200.175 53.8664 201.243 50.3584 202.157 50.4429C203.691 50.5698 204.323 56.7829 203.93 57.9245ZM206.77 49.6821C206.798 50.3584 206.812 50.9361 206.404 51.5138C205.434 52.8804 204.914 49.8512 204.787 49.3017C204.436 47.8364 204.408 46.2866 205.055 44.9059C205.406 44.1169 206.081 44.4973 206.278 45.2018C206.658 46.6389 206.728 48.1887 206.77 49.6821ZM136.149 23.913C142.519 19.6299 149.073 16.8825 156.033 13.966C157.848 13.2052 159.704 12.4304 161.56 11.6132C168.422 8.62628 175.13 6.11837 182.498 4.52631C183.118 4.3995 183.75 4.25861 184.369 4.42768C185.325 4.70947 185.719 6.09023 186.113 6.92148C186.647 8.07678 186.802 9.373 187.153 10.5987C187.8 12.698 188.7 14.6846 189.066 16.8543C189.08 16.953 189.094 17.0516 189.038 17.1361C189.009 17.1784 188.939 17.2207 188.869 17.2489C187.392 17.8828 185.902 18.545 184.425 19.1791C183.82 19.4468 183.089 19.9258 182.414 19.9821C181.866 20.0103 181.261 19.8694 180.685 19.8835C176.691 19.9821 174.624 22.5887 172.472 25.5051C170.026 28.7738 169.632 32.7892 169.35 36.7342C169.041 40.9891 169.027 44.3564 165.962 47.7801C163.557 50.4852 160.337 52.3309 157.13 53.9936C154.234 55.5151 151.168 56.952 147.892 56.9802C142.449 57.0792 138.864 52.6549 135.137 49.3863C132.31 46.9066 130.004 43.7506 127.515 40.9046C125.87 39.0307 123.001 36.4524 122.945 33.7613C122.917 32.5356 123.873 31.7466 124.745 31.0281C126.798 29.3796 129.09 28.0411 131.383 26.7449C132.971 25.8714 134.63 24.9415 136.149 23.913ZM92.5284 57.9102C94.4127 53.1902 97.1828 48.5409 100.234 44.4409C103.441 40.1156 107.238 36.2129 111.484 32.8878C113.622 31.2253 115.858 29.7037 118.192 28.337C119.893 27.3508 121.468 26.3223 123.24 25.491C124.126 25.0824 125.026 24.6597 125.982 24.5752C123.409 26.3786 120.794 28.1398 118.291 30.0137C117.657 30.4927 116.997 30.9999 116.659 31.6903C116.322 32.4511 116.378 33.3528 116.659 34.1559C117.292 35.917 118.909 37.0583 116.856 38.5376C113.368 41.0173 109.895 43.4829 106.408 45.9626C103.638 47.9351 101.106 50.288 98.6315 52.6409C97.5207 53.6696 96.3955 54.6836 95.4251 55.8391C94.2861 57.1914 93.3721 58.8965 91.9235 59.9392C92.0362 59.1359 92.2469 58.6289 92.5284 57.9102ZM88.7878 74.8033C88.816 74.3948 88.8581 73.9718 88.9001 73.5632C88.9847 72.7605 89.0831 71.9571 89.1954 71.1682C89.3363 70.1818 89.4486 69.1955 89.6034 68.2235C89.6316 68.0262 89.9551 65.6311 90.1939 65.6875C91.0238 65.9131 91.8676 66.1525 92.7114 66.3216C94.1456 66.6174 95.5517 66.9978 96.9722 67.3356C98.35 67.674 99.7423 67.9134 101.12 68.2374C101.556 68.3363 101.992 68.4491 102.428 68.5475C102.456 68.5475 101.852 68.9423 101.81 68.9705C101.486 69.1391 101.163 69.2945 100.839 69.4493C100.192 69.7733 99.5311 70.0834 98.8847 70.4074C97.6047 71.0272 96.3252 71.6613 95.0734 72.3657C92.922 73.5351 90.8408 74.7469 88.7176 75.9865C88.8017 75.9163 88.7735 74.9438 88.7878 74.8033ZM85.8209 85.187C85.4831 85.2151 85.1458 85.2572 84.8079 85.2854C84.6255 85.2997 84.4568 85.3279 84.2738 85.3418C84.26 85.3418 83.9365 85.3561 83.9365 85.3279C84.2456 84.8768 84.7377 84.5954 85.174 84.257C85.5533 83.9612 85.8767 83.5947 86.2284 83.285C90.1801 79.7201 95.2001 77.7054 99.8125 75.2119C103.905 72.9855 107.87 70.5058 111.653 67.7724C111.836 67.632 112.033 67.5048 112.216 67.3638C111.695 67.1106 111.048 67.026 110.486 66.8286C109.783 66.5749 109.066 66.3498 108.348 66.1381C106.858 65.7013 105.367 65.3209 103.863 64.9549C102.4 64.6027 100.938 64.2506 99.4891 63.842C99.1092 63.743 95.2283 62.4045 95.2001 62.4471C95.8049 61.4607 96.5923 60.6436 97.394 59.8264C98.6171 58.6007 99.8827 57.4314 101.12 56.2195C102.597 54.7682 104.242 53.5142 105.803 52.1477C110.542 47.9914 115.661 44.5959 120.906 41.1582C122.354 43.1448 124.014 44.9764 125.673 46.7939C127.586 48.8932 129.484 50.9924 131.396 53.0776C131.875 53.6132 132.423 54.1909 132.999 54.7543C132.62 54.9799 132.24 55.2049 131.875 55.4023C127.586 58.2623 123.17 60.9537 118.656 63.4334C118.726 63.3908 119.19 63.6446 119.275 63.6866C119.485 63.7712 119.683 63.8558 119.893 63.9404C122.439 65.0252 124.913 66.2791 127.361 67.5756C128.584 68.2235 129.793 68.8859 131.017 69.5339C131.635 69.8579 132.24 70.1818 132.859 70.5058C133.421 70.8016 134.026 71.0416 134.532 71.464C134.462 71.5204 134.363 71.5624 134.293 71.6049C134.195 71.6752 134.096 71.7459 133.998 71.8023C133.815 71.9151 133.618 72.0274 133.407 72.1263C133.056 72.2955 132.69 72.4359 132.325 72.5631C131.453 72.8871 130.581 73.2111 129.709 73.5351C128.893 73.8308 128.12 74.1974 127.29 74.4794C126.896 74.606 126.559 74.7188 126.306 74.8172C121.806 76.4797 117.039 78.0294 111.695 79.5791C111.203 79.7201 110.725 79.8611 110.233 80.0021C102.316 82.3551 94.0892 84.3836 85.8209 85.187ZM42.9868 148.222C44.8711 146.587 46.9805 145.728 49.2585 145.122C48.9494 145.517 48.6398 145.869 48.3589 146.292C47.9652 146.898 47.67 147.588 47.4167 148.321C47.2337 148.334 47.0651 148.363 46.8821 148.391C45.5744 148.504 44.2247 148.616 42.8883 148.912C42.5931 148.982 42.3404 149.039 42.059 149.123C42.326 148.799 42.6356 148.518 42.9868 148.222ZM36.209 158.803C36.1668 158.774 36.0825 158.732 35.984 158.69C38.0793 156.591 40.2028 155.337 42.7899 154.689L46.6571 153.731C46.7416 155.83 47.1497 157.929 47.8104 159.691C47.5854 159.719 47.3742 159.761 47.1353 159.789C43.2262 160.057 39.5277 160.198 36.209 158.803ZM45.293 165.072C46.0383 164.904 46.8539 164.763 47.6976 164.65C47.9652 164.636 48.2179 164.622 48.4855 164.608C49.2867 164.537 50.0885 164.495 50.8902 164.439C51.7898 165.326 52.8448 166.115 54.012 166.791C50.9184 166.693 47.9791 166.411 45.293 165.072ZM57.9211 163.269C55.4883 162.24 53.646 160.691 52.7602 158.873C51.4243 156.21 51.0024 151.195 52.4511 148.969C54.1524 146.391 56.9369 144.531 59.9459 142.938C60.2694 142.812 60.579 142.643 60.9025 142.445C62.1117 141.839 63.3071 141.29 64.5025 140.726C72.2644 137.19 78.649 133.921 84.3158 130.61C83.5567 133.738 82.5581 136.866 81.2786 139.937C77.594 148.743 71.9691 157.281 64.8254 165.016C62.3931 164.608 60.0587 164.171 57.9211 163.269ZM194.62 164.622C193.987 166.439 193.27 168.229 192.483 169.99C192.188 170.68 191.878 171.342 191.569 172.004C187.842 179.81 182.752 186.925 176.648 193.011C175.791 193.871 174.905 194.73 173.991 195.547C172.416 196.984 170.785 198.351 169.083 199.648C168.887 199.816 168.69 199.957 168.492 200.098C167.143 201.113 165.765 202.057 164.386 202.944C158.551 206.635 152.349 209.129 145.487 210.877C144.052 211.257 139.314 212.891 138.371 210.975C138.16 210.524 138.188 209.96 138.287 209.453C138.962 206.016 141.493 202.789 142.998 199.661C147.947 189.362 150.521 178.274 158.705 169.82C163.838 164.509 170.236 160.437 176.691 157.14C183.694 153.562 190.444 149.49 196.884 144.967C197.531 152.547 196.842 158.408 194.62 164.622ZM232.645 95.5988C232.349 98.5014 231.604 101.347 230.423 104.024C227.048 111.675 220.945 117.719 215.545 123.045C203.367 135.076 189.558 145.066 174.469 152.772C167.986 156.07 161.419 159.676 156.118 164.763C148.552 171.99 144.967 180.162 141.352 189.743C138.681 196.773 134.617 203.282 129.54 208.834C121.412 217.71 109.811 222.303 98.2234 224.487C92.2469 225.614 86.1438 226.149 80.0694 226.318C74.9643 226.445 67.4695 227.46 62.8006 225.05C62.6038 224.952 62.3788 224.839 62.1963 224.712C62.1819 224.698 62.1681 224.698 62.1537 224.684C61.8585 224.472 61.5776 224.191 61.5212 223.853C61.3382 223.035 62.2521 222.444 63.0257 222.148C70.7179 219.189 78.9022 217.738 86.4252 214.314C91.741 211.919 96.6205 208.566 100.698 204.381C102.316 202.719 110.205 193.829 105.086 192.236C104.664 192.11 104.2 192.236 103.778 192.363C98.7299 193.97 93.8222 195.843 88.7033 197.238C85.5251 198.126 82.3474 198.985 79.1693 199.873C76.4832 200.605 73.6146 201.211 70.8445 201.324C70.5349 201.338 70.2114 201.352 69.9162 201.254C67.4131 200.408 72.3772 197.999 72.8975 197.717C75.1191 196.449 77.3833 195.224 79.4784 193.744C83.3598 191.039 86.6503 187.714 89.4768 183.938C91.3893 181.36 93.3019 178.556 93.9062 175.315C94.1313 174.146 94.708 170.173 93.3859 169.37C93.1471 169.243 92.8518 169.257 92.5704 169.285C90.9531 169.553 89.5896 170.596 88.2394 171.511C83.5285 174.738 78.3111 177.372 73.2491 179.951C72.7006 180.233 72.1942 180.486 71.8005 180.669C69.6071 181.698 67.4551 182.811 65.3463 183.882C59.1586 187.066 52.6899 189.7 45.659 190.236C45.2089 190.264 44.745 190.306 44.337 190.109C43.9151 189.926 43.5917 189.489 43.7044 189.052C43.7885 188.77 44.0135 188.559 44.2524 188.376C46.2213 186.812 48.3728 185.502 50.4401 184.065C54.096 181.514 57.5413 178.711 60.8461 175.752C72.0394 165.805 80.6456 154.083 85.7645 141.839C89.5188 132.865 91.0095 123.524 91.2766 114.549C91.3191 113.013 91.3473 111.477 91.3047 109.984C91.2909 108.11 91.2063 106.25 91.1079 104.419C90.9813 102.291 90.7988 100.206 90.5876 98.163C90.2923 95.4296 89.9551 92.7951 89.5752 90.2591C94.1313 89.2025 98.6735 87.7932 103.201 86.5537C108.039 85.2433 112.848 83.8905 117.644 82.4253C119.092 81.9747 120.54 81.5236 121.975 81.0448C123.479 80.5516 124.97 80.0303 126.447 79.4951C127.937 78.9593 129.414 78.396 130.876 77.8044C132.339 77.2122 133.787 76.5925 135.235 75.9301C136.67 75.2683 138.09 74.5778 139.482 73.8452C140.185 73.4787 140.875 73.0983 141.549 72.7179C141.887 72.5205 142.239 72.3237 142.576 72.1263C142.745 72.0274 142.913 71.9289 143.082 71.8305C143.11 71.8162 143.518 71.5768 143.504 71.5624C143.237 71.3512 142.956 71.1964 142.674 70.999C142.464 70.858 142.266 70.6894 142.056 70.5622C141.001 69.8999 139.932 69.2381 138.877 68.5757C136.529 67.1106 134.195 65.6875 131.692 64.5182C130.398 63.9122 129.104 63.3063 127.937 62.4891C127.923 62.4753 128.893 61.8693 128.978 61.8268C129.202 61.7001 129.442 61.5735 129.681 61.4464C130.019 61.2777 130.314 61.0378 130.637 60.8266C131.523 60.277 132.451 59.7982 133.351 59.263C134.392 58.6289 135.46 58.0368 136.543 57.4734C138.273 59.1923 140.635 60.277 142.857 61.1931C145.29 62.1933 148.426 62.0385 150.971 61.6299C154.881 60.9958 158.565 59.3471 161.94 57.2478C169.646 52.4577 173.541 46.0471 174.188 37.0864C174.427 33.7614 174.666 30.6336 176.339 28.4075C178.308 25.801 182.175 24.3779 185.916 22.9972C188.826 21.9405 191.794 20.6444 194.705 19.3763C196.237 18.7282 197.756 18.0519 199.289 17.432C199.922 17.1643 200.808 16.6148 201.511 16.6007C202.678 16.5726 204.576 17.5306 205.532 18.1505C206.756 18.9396 206.278 20.6866 205.954 21.856C204.253 27.9144 203.114 34.029 201.005 39.9888C197.784 49.034 193.327 57.6 187.758 65.4199C183.61 71.2528 177.324 75.1694 171.952 79.7063C164.738 85.8067 160.688 95.162 160.646 105.687C160.646 105.786 160.491 105.828 160.407 105.841C160.069 105.912 159.717 105.87 159.38 105.841C158.818 105.799 158.255 105.729 157.707 105.645C156.99 105.546 156.272 105.419 155.555 105.306C154.74 105.165 153.924 105.01 153.108 104.856C152.251 104.686 151.407 104.517 150.549 104.348C149.72 104.179 148.89 104.01 148.046 103.841C147.287 103.686 146.541 103.531 145.782 103.362C145.164 103.235 144.559 103.094 143.94 102.968C143.603 102.897 143.279 102.798 142.956 102.756C142.941 102.756 142.913 102.756 142.885 102.798C142.871 102.869 142.913 102.953 142.941 103.024C143.026 103.165 143.082 103.305 143.195 103.418C143.701 103.968 144.193 104.532 144.784 105.01C148.552 108.025 153.052 109.857 157.848 110.378C159.225 110.533 160.618 110.604 162.01 110.576C178.463 110.336 192.16 101.812 203.873 90.9634C208.809 86.3983 213.928 81.6789 220.115 81.073C224.094 80.6782 228.088 82.1998 230.268 84.9193C232.279 87.4272 233.109 91.2028 232.645 95.5988ZM177.338 102.968C173.527 104.306 169.547 105.151 165.54 105.517C165.638 96.5425 169.041 88.5822 175.102 83.4819C175.692 82.9887 176.297 82.5243 176.873 82.045C174.919 88.4694 175.074 95.9653 177.338 102.968ZM234.107 81.8901C233.094 80.6218 231.843 79.5653 230.437 78.6917C230.887 78.3396 231.323 77.973 231.745 77.6208C235.654 74.6624 239.423 71.9853 243.923 71.182C244.302 71.1118 244.668 71.0698 245.019 71.098C245.005 71.182 244.991 71.2948 244.949 71.4358C244.19 75.3103 242.769 78.1847 240.393 80.7346L236.146 85.2715C235.626 84.1022 234.965 82.9611 234.107 81.8901ZM244.625 87.3144C243.318 91.696 240.59 94.6832 237.299 97.7124C237.383 97.191 237.468 96.6553 237.524 96.1058C237.693 94.556 237.735 92.7387 237.538 90.8506C238.213 90.3011 238.888 89.7239 239.634 89.1461C240.871 88.1598 242.179 87.1596 243.332 85.9759C244.021 85.2854 244.597 84.5672 245.16 83.8341C245.16 84.9475 244.977 86.1169 244.625 87.3144Z" fill="#1D445C"/> <path d="M57.1758 149.912C57.063 150.124 56.9369 150.335 56.9508 150.574C56.9646 150.814 57.1476 151.054 57.3865 151.039C57.5131 151.026 57.6115 150.955 57.7099 150.885C60.0161 149.222 62.3506 147.531 64.2493 145.418C66.1475 143.305 67.6381 140.698 67.9472 137.866C67.9898 137.415 67.9334 136.851 67.5115 136.711C67.3147 136.64 67.1035 136.697 66.9066 136.739C65.6272 137.091 63.7567 137.768 62.8709 138.81C62.1399 139.669 62.0973 140.811 61.6755 141.882C60.5364 144.742 58.6803 147.249 57.1758 149.912Z" fill="#1D445C"/> <path d="M66.4287 147.926C66.2744 148.194 66.1196 148.49 66.1334 148.786C66.1478 149.095 66.3867 149.42 66.6963 149.392C66.8506 149.377 66.9916 149.279 67.1182 149.194C70.1272 147.024 73.1505 144.827 75.6398 142.079C78.1285 139.332 80.0554 135.936 80.4491 132.245C80.5055 131.668 80.4347 130.921 79.8867 130.738C79.6335 130.653 79.3521 130.709 79.0988 130.78C77.4395 131.245 74.9929 132.104 73.8395 133.471C72.8835 134.598 72.8414 136.077 72.2929 137.472C70.8023 141.191 68.3976 144.46 66.4287 147.926Z" fill="#1D445C"/> <path d="M106.605 99.9099C106.521 100.036 106.366 100.107 106.212 100.177C102.71 101.713 99.1663 103.249 95.37 103.996C93.9916 104.278 92.5711 104.447 91.1512 104.475C89.4919 104.517 87.8044 104.362 86.2009 103.967C86.1169 102.263 85.9759 100.572 85.7791 98.8953C86.5106 98.4447 87.2841 98.0925 88.0294 97.9654C88.9152 97.8244 89.7728 97.9936 90.6309 98.233C91.2635 98.4022 91.8965 98.5713 92.5711 98.6698C96.8181 99.2901 101.093 98.8671 105.326 98.9379C105.649 98.9379 106.015 98.9517 106.282 99.1209C106.577 99.2757 106.774 99.6561 106.605 99.9099Z" fill="#1D445C"/> <path d="M100.727 110.97C100.642 111.097 100.487 111.167 100.333 111.238C97.3938 112.506 94.4407 113.802 91.305 114.62C90.7002 114.803 90.0958 114.929 89.4771 115.056C88.4642 115.267 87.4236 115.408 86.3691 115.479C86.4676 113.534 86.4814 111.604 86.4394 109.66C86.524 109.674 86.6224 109.688 86.7064 109.702C88.2535 109.928 89.8005 110.012 91.3471 110.026C94.047 110.069 96.7613 109.913 99.4612 109.956C99.7846 109.956 100.136 109.97 100.417 110.139C100.712 110.308 100.895 110.702 100.727 110.97Z" fill="#1D445C"/> <path d="M182.837 193.871C181.5 195.322 180.136 196.731 178.702 198.07C177.071 197.351 175.524 196.519 174.005 195.618C172.74 194.871 171.502 194.082 170.251 193.293C170.124 193.209 169.97 193.11 169.913 192.969C169.787 192.687 170.026 192.349 170.349 192.222C170.659 192.11 170.996 192.138 171.348 192.194C173.12 192.462 174.905 192.786 176.663 193.096C178.702 193.448 180.756 193.772 182.837 193.871Z" fill="#1D445C"/> <path d="M175.074 201.282C173.147 202.888 171.151 204.396 169.055 205.818C167.902 205.283 166.777 204.649 165.737 203.945C165.287 203.649 164.837 203.325 164.415 203.015C161.743 201 159.409 198.619 157.117 196.252C157.004 196.139 156.892 196.026 156.85 195.858C156.807 195.561 157.13 195.266 157.454 195.224C157.792 195.195 158.129 195.308 158.439 195.435C161.996 196.816 165.413 198.619 169.112 199.718C169.534 199.845 169.955 199.957 170.406 200.07C171.966 200.465 173.598 200.352 174.92 201.169C174.948 201.197 175.018 201.226 175.074 201.282Z" fill="#1D445C"/> </svg>'; $output .= '<span>' . esc_html( $string ) . '</span>'; $output .= '</a>'; echo $output; } /** * Check if we should rewrite the urls. * * @return bool If we can replace the image. */ public function should_replace() { if ( apply_filters( 'optml_should_replace_page', false ) ) { return false; } if ( apply_filters( 'optml_force_replacement', false ) === true ) { return true; } if ( is_customize_preview() && $this->settings->get( 'offload_media' ) !== 'enabled' ) { return false; } if ( ( is_admin() && ! self::is_ajax_request() ) || ! $this->settings->is_connected() || ! $this->settings->is_enabled() ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) && ! $this->settings->is_offload_enabled() ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'optml_off', $_GET ) && 'true' === $_GET['optml_off'] ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'ct_builder', $_GET ) && ! empty( $_GET['ct_builder'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'et_fb', $_GET ) && ! empty( $_GET['et_fb'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'tve', $_GET ) && $_GET['tve'] === 'true' ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] === 'true' || $_GET['trp-edit-translation'] === 'preview' ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'context', $_GET ) && $_GET['context'] === 'edit' ) { return false; // @codeCoverageIgnore } // avada if ( array_key_exists( 'fb-edit', $_GET ) && ! empty( $_GET['fb-edit'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'builder', $_GET ) && ! empty( $_GET['builder'] ) && array_key_exists( 'builder_id', $_GET ) && ! empty( $_GET['builder_id'] ) ) { return false; // @codeCoverageIgnore } // Motion.page iFrame & builder if ( ( array_key_exists( 'motionpage_iframe', $_GET ) && $_GET['motionpage_iframe'] === 'true' ) || ( array_key_exists( 'page', $_GET ) && $_GET['page'] === 'motionpage' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return false; // @codeCoverageIgnore } /** * Disable replacement on POST request and when user is logged in, but allows for sample image call widget in dashboard */ if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'POST' && is_user_logged_in() && ( ! isset( $_GET['quality'] ) || ! current_user_can( 'manage_options' ) ) ) { return false; // @codeCoverageIgnore } if ( class_exists( 'FLBuilderModel', false ) ) { $post_data = FLBuilderModel::get_post_data(); if ( isset( $_GET['fl_builder'] ) || isset( $post_data['fl_builder'] ) ) { return false; } } $filters = $this->settings->get_filters(); return Optml_Filters::should_do_page( $filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL ], $filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL_MATCH ] ); } /** * Check if we are in a ajax contex where we should enable replacement. * * @return bool Is ajax request? */ public static function is_ajax_request() { if ( apply_filters( 'optml_force_replacement_on', false ) === true ) { return true; } if ( ! function_exists( 'is_user_logged_in' ) ) { return false; } // Disable for logged in users to avoid unexpected results. if ( is_user_logged_in() ) { return false; } if ( ! function_exists( 'wp_doing_ajax' ) ) { return false; } if ( ! wp_doing_ajax() ) { return false; } if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { return false; } return true; } /** * Register frontend replacer hooks. */ public function register_hooks() { do_action( 'optml_replacer_setup' ); if ( $this->settings->get( 'native_lazyload' ) === 'disabled' ) { add_filter( 'wp_lazy_loading_enabled', '__return_false' ); } add_filter( 'the_content', [ $this, 'process_images_from_content' ], PHP_INT_MAX ); /** * When we have to process cdn images, i.e MIRROR is defined, * we need this as late as possible for other replacers to occur. * Otherwise, we can hook first to avoid any other plugins to take care of replacement. */ add_action( self::is_ajax_request() ? 'init' : 'template_redirect', [ $this, 'process_template_redirect_content', ], defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN ); add_action( 'template_redirect', [ $this, 'register_after_setup' ] ); add_action( 'rest_api_init', [ $this, 'process_template_redirect_content' ], PHP_INT_MIN ); add_action( 'shutdown', [ $this, 'close_buffer' ] ); foreach ( self::$loaded_compatibilities as $registered_compatibility ) { $registered_compatibility->register(); } } /** * Run after Optimole is fully setup. */ public function register_after_setup() { do_action( 'optml_after_setup' ); } /** * Filter raw HTML content for urls. * * @param string $html HTML to filter. * * @return mixed Filtered content. */ public function replace_content( $html ) { if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() && ( apply_filters( 'optml_force_replacement', false ) !== true ) ) { return $html; } $html = $this->add_html_class( $html ); $html = $this->process_images_from_content( $html ); if ( $this->settings->get( 'video_lazyload' ) === 'enabled' ) { $html = apply_filters( 'optml_video_replace', $html ); if ( Optml_Lazyload_Replacer::found_iframe() === true ) { if ( strpos( $html, Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT ) !== false ) { $html = str_replace( Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT, Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_CLASS, $html ); } else { $html = preg_replace( '/<head>(.*)<\/head>/ism', '<head> $1' . Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_STYLE . '</head>', $html ); } } } $html = apply_filters( 'optml_url_pre_process', $html ); $html = $this->process_urls_from_content( $html ); $html = apply_filters( 'optml_url_post_process', $html ); return $html; } /** * Adds a filter that allows adding classes to the HTML tag. * * @param string $content The HTML content. * * @return mixed */ public function add_html_class( $content ) { if ( empty( $content ) ) { return $content; } $additional_html_classes = apply_filters( 'optml_additional_html_classes', [] ); if ( ! $additional_html_classes ) { return $content; } if ( preg_match( '/<html.*>/ismU', $content, $matches, PREG_OFFSET_CAPTURE ) === 1 ) { $add_classes = implode( ' ', $additional_html_classes ); foreach ( $matches as $match ) { if ( strpos( $match[0], 'class' ) !== false ) { $new_tag = str_replace( [ 'class="', "class='" ], [ 'class="' . $add_classes, "class='" . $add_classes ], $match[0] ); } else { $new_tag = str_replace( 'html ', 'html class="' . $add_classes . '" ', $match[0] ); } $content = str_replace( $match[0], $new_tag, $content ); } } return $content; } /** * Adds a filter with detected images tags and the content. * * @param string $content The HTML content. * * @return mixed */ public function process_images_from_content( $content ) { if ( self::should_ignore_image_tags() ) { return $content; } $images = self::parse_images_from_html( $content ); if ( empty( $images ) ) { return $content; } return apply_filters( 'optml_content_images_tags', $content, $images ); } /** * Check if we are on a amp endpoint. * * IMPORTANT: This needs to be used after parse_query hook, otherwise will return false positives. * * @return bool */ public static function should_ignore_image_tags() { // Ignore image tag replacement in feed context as we don't need it. if ( is_feed() ) { return true; } // Ignore image tags replacement in amp context as they are not available. if ( function_exists( 'is_amp_endpoint' ) ) { return is_amp_endpoint(); } if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) { return ampforwp_is_amp_endpoint(); } return apply_filters( 'optml_should_ignore_image_tags', false ) === true; } /** * Match all images and any relevant <a> tags in a block of HTML. * * @param string $content Some HTML. * * @return array An array of $images matches, where $images[0] is * an array of full matches, and the link_url, img_tag, * and img_url keys are arrays of those matches. */ public static function parse_images_from_html( $content ) { $images = []; $header_start = null; $header_end = null; if ( preg_match( '/<header.*<\/header>/ismU', $content, $matches, PREG_OFFSET_CAPTURE ) === 1 ) { $header_start = $matches[0][1]; $header_end = $header_start + strlen( $matches[0][0] ); } $regex = '/(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag>(?:<noscript\s*>\s*)?<img[^>]*?\s?(?:' . implode( '|', array_merge( [ 'src' ], Optml_Tag_Replacer::possible_src_attributes() ) ) . ')=["\'\\\\]*?(?P<img_url>[' . Optml_Config::$chars . ']{10,}).*?>(?:\s*<\/noscript\s*>)?){1}(?:\s*<\/a>)?/ismu'; if ( preg_match_all( $regex, $content, $images, PREG_OFFSET_CAPTURE ) ) { if ( OPTML_DEBUG ) { do_action( 'optml_log', $images ); } foreach ( $images as $key => $unused ) { // Simplify the output as much as possible, mostly for confirming test results. if ( is_numeric( $key ) && $key > 0 ) { unset( $images[ $key ] ); continue; } $is_no_script = false; foreach ( $unused as $url_key => $url_value ) { if ( $key === 'img_url' ) { $images[ $key ][ $url_key ] = rtrim( $url_value[0], '\\' ); continue; } $images[ $key ][ $url_key ] = $url_value[0]; if ( $key === 0 ) { $images['in_header'][ $url_key ] = $header_start !== null ? ( $url_value[1] > $header_start && $url_value[1] < $header_end ) : false; // Check if we are in the noscript context. if ( $is_no_script === false ) { $is_no_script = strpos( $images[0][ $url_key ], '<noscript' ) !== false ? true : false; } if ( $is_no_script ) { $images['in_header'][ $url_key ] = true; $is_no_script = strpos( $images[0][ $url_key ], '</noscript' ) !== false ? false : true; } } } } return $images; } return []; } /** * Process url replacement from raw html strings. * * @param string $html Raw html. * * @return string Processed string. */ public function process_urls_from_content( $html ) { $extracted_urls = $this->extract_urls_from_content( $html ); if ( OPTML_DEBUG ) { do_action( 'optml_log', 'matched urls' ); do_action( 'optml_log', $extracted_urls ); } return $this->do_url_replacement( $html, $extracted_urls ); } /** * Method to extract assets from content. * * @param string $content The HTML content. * * @return array */ public function extract_urls_from_content( $content ) { $extensions = array_keys( Optml_Config::$image_extensions ); if ( $this->settings->use_cdn() && ! self::should_ignore_image_tags() ) { $extensions = array_merge( $extensions, array_keys( Optml_Config::$assets_extensions ) ); } $regex = '/(?:[(|\s\';",=\]])((?:http|\/|\\\\){1}(?:[' . Optml_Config::$chars . ']{10,}\.(?:' . implode( '|', $extensions ) . ')))(?=(?:http|>|%3F|\?|"|&|,|\s|\'|\)|\||\\\\|}|\[))/Uu'; preg_match_all( $regex, $content, $urls ); return $this->normalize_urls( $urls[1] ); } /** * Normalize extracted urls. * * @param array $urls Raw urls extracted. * * @return array Normalized array. */ private function normalize_urls( $urls ) { $urls = array_map( function ( $value ) { $value = str_replace( '"', '', $value ); return rtrim( $value, '\\";\'' ); }, $urls ); $urls = array_unique( $urls ); return array_values( $urls ); } /** * Process string content and replace possible urls. * * @param string $html String content. * @param array $extracted_urls Urls to check. * * @return string Processed html. */ public function do_url_replacement( $html, $extracted_urls ) { $extracted_urls = apply_filters( 'optml_extracted_urls', $extracted_urls ); if ( empty( $extracted_urls ) ) { return $html; } $slashed_config = addcslashes( Optml_Config::$service_url, '/' ); $extracted_urls = array_filter( $extracted_urls, function ( $value ) use ( $slashed_config ) { return strpos( $value, Optml_Config::$service_url ) === false && strpos( $value, $slashed_config ) === false || Optml_Media_Offload::is_not_processed_image( $value ) || $this->tag_replacer->url_has_dam_flag( $value ); } ); $upload_resource = $this->tag_replacer->get_upload_resource(); $urls = array_combine( $extracted_urls, $extracted_urls ); $urls = array_map( function ( $url ) use ( $upload_resource ) { $is_slashed = strpos( $url, '\/' ) !== false; $is_relative = strpos( $url, $is_slashed ? addcslashes( $upload_resource['content_path'], '/' ) : $upload_resource['content_path'] ) === 0; if ( $is_relative ) { $url = $upload_resource['content_host'] . $url; } return apply_filters( 'optml_content_url', $url ); }, $urls ); foreach ( $urls as $origin => $replace ) { $html = preg_replace( '/(?<![\/|:|\\w])' . preg_quote( $origin, '/' ) . '/m', $replace, $html ); } return $html; } /** * Init html replacer handler. */ public function process_template_redirect_content() { // Early exit if function was already called, we don't want duplicate ob_start if ( self::$ob_started === true ) { return; } self::$ob_started = true; // We no longer need this if the handler was started. remove_filter( 'the_content', [ $this, 'process_images_from_content' ], PHP_INT_MAX ); ob_start( [ &$this, 'replace_content' ] ); } /** * Close the buffer and flush the content. */ public function close_buffer() { if ( self::$ob_started && ob_get_length() ) { ob_end_flush(); } } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } } filters.php 0000644 00000004372 14720403740 0006734 0 ustar 00 <?php /** * Class Optml_Filters. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Filters { /** * Generic method to check if the page is allowed to do the action. * * @param array $contains_flags Contains flags array. * @param array $match_flags Exact path match flags array. * @return bool Should do action on page? */ public static function should_do_page( $contains_flags, $match_flags ) { if ( empty( $contains_flags ) && empty( $match_flags ) ) { return true; } if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return true; } $check_against = [ $_SERVER['REQUEST_URI'] ]; // This code is designed to handle ajax requests on pages that are excluded. // For ajax requests, the referer is set to the page URL and they use a POST method. // If an ajax request uses a GET method, it can be managed using the available exclusion rules. if ( isset( $_SERVER['HTTP_REFERER'] ) && $_SERVER['REQUEST_METHOD'] === 'POST' ) { $check_against[] = $_SERVER['HTTP_REFERER']; } foreach ( $check_against as $check ) { foreach ( $match_flags as $rule_flag => $status ) { if ( $rule_flag === $check ) { return false; } } foreach ( $contains_flags as $rule_flag => $status ) { if ( strpos( $check, $rule_flag ) !== false ) { return false; } if ( $rule_flag === 'home' && ( empty( $check ) || $check === '/' ) ) { return false; } } } return true; } /** * Check if image qualifies for processing. * * @param string $image_url Image url. * @param array $flags Flags array. * * @return bool Should we process the image? */ public static function should_do_image( $image_url, $flags ) { foreach ( $flags as $rule_flag => $status ) { if ( strpos( $image_url, $rule_flag ) !== false ) { return false; } } return true; } /** * * Check if the image should be processed based on the extension. * * @param array $flags Flags array. * @param string $ext Extension string. * * @return bool|string Should we do the processing? */ public static function should_do_extension( $flags, $ext ) { foreach ( $flags as $rule_flag => $status ) { if ( $rule_flag === $ext ) { return false; } } return $ext; } } api.php 0000644 00000033214 14720403740 0006032 0 ustar 00 <?php /** * The class defines way of connecting this user to the Optimole Dashboard. * * @codeCoverageIgnore * @package \Optimole\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Api { /** * Optimole root api url. * * @var string Api root. */ private $api_root = 'https://dashboard.optimole.com/api/'; /** * Optimole onboard api root url. * * @var string Api root. */ private $onboard_api_root = 'https://onboard.i.optimole.com/onboard_api/'; /** * Optimole offload conflicts api root url. * * @var string Api root. */ private $upload_conflicts_api = 'https://conflicts.i.optimole.com/offload_api/'; /** * Hold the user api key. * * @var string Api key. */ private $api_key; /** * Optml_Api constructor. */ public function __construct() { $settings = new Optml_Settings(); $this->api_key = $settings->get( 'api_key' ); if ( defined( 'OPTIML_API_ROOT' ) ) { $this->api_root = constant( 'OPTIML_API_ROOT' ); } if ( defined( 'OPTIML_ONBOARD_API_ROOT' ) ) { $this->onboard_api_root = constant( 'OPTIML_ONBOARD_API_ROOT' ); } if ( defined( 'OPTIML_UPLOAD_CONFLICTS_API_ROOT' ) ) { $this->upload_conflicts_api = constant( 'OPTIML_UPLOAD_CONFLICTS_API_ROOT' ); } } /** * Connect to the service. * * @param string $api_key Api key. * * @return array|bool|WP_Error */ public function connect( $api_key = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } return $this->request( '/optml/v2/account/connect', 'POST', [ 'sample_image' => $this->get_sample_image() ] ); } /** * Get sample image. * * @return string */ private function get_sample_image() { $accepted_mimes = [ 'image/jpeg', 'image/png', 'image/webp' ]; $args = [ 'post_type' => 'attachment', 'post_status' => 'any', 'number' => '1', 'no_found_rows' => true, 'fields' => 'ids', 'post_mime_type' => $accepted_mimes, 'post_parent__not_in' => [ 0 ], ]; $image_result = new WP_Query( $args ); $original_image_url = 'none'; if ( ! empty( $image_result->posts ) ) { $original_image_url = wp_get_attachment_image_url( $image_result->posts[0], 'full' ); } return $original_image_url; } /** * Get user data from service. * * @return array|string|bool|WP_Error User data. */ public function get_user_data( $api_key = '', $application = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } return $this->request( '/optml/v2/account/details', 'POST', [ 'application' => $application ] ); } /** * Toggle the extra visits. * * @param string $api_key Api key. * @param string $status Status of the visits toggle. * * @return array|bool|string */ public function update_extra_visits( $api_key = '', $status = 'enabled', $application = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } return $this->request( '/optml/v2/account/extra_visits', 'POST', [ 'extra_visits' => $status, 'application' => $application ] ); } /** * Get cache token from service. * * @return array|bool|WP_Error User data. */ public function get_cache_token( $token = '', $type = '', $api_key = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } $lock = get_transient( 'optml_cache_lock' ); if ( ! empty( $type ) && $type === 'assets' ) { $lock = get_transient( 'optml_cache_lock_assets' ); } if ( $lock === 'yes' ) { return new WP_Error( 'cache_throttle', __( 'You can clear cache only once per 5 minutes.', 'optimole-wp' ) ); } return $this->request( '/optml/v1/cache/tokens', 'POST', [ 'token' => $token, 'type' => $type ] ); } /** * Request constructor. * * @param string $path The request url. * @param string $method The request method type. * @param array|string $params The request method type. * * @return array|string|boolean|WP_Error Api data. */ public function request( $path, $method = 'GET', $params = [], $extra_headers = [] ) { $headers = [ 'Optml-Site' => get_home_url(), ]; if ( ! empty( $this->api_key ) ) { $headers['Authorization'] = 'Bearer ' . $this->api_key; } if ( is_array( $headers ) ) { $headers = array_merge( $headers, $extra_headers ); } $url = trailingslashit( $this->api_root ) . ltrim( $path, '/' ); // If there is a extra, add that as a url var. if ( 'GET' === $method && ! empty( $params ) ) { foreach ( $params as $key => $val ) { $url = add_query_arg( [ $key => $val ], $url ); } } $url = tsdk_translate_link( $url, 'query' ); $args = $this->build_args( $method, $url, $headers, $params ); $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $response = wp_remote_retrieve_body( $response ); if ( empty( $response ) ) { return false; } $response = json_decode( $response, true ); if ( isset( $response['id'] ) && is_numeric( $response['id'] ) ) { return true; } if ( ! isset( $response['code'] ) ) { return false; } if ( intval( $response['code'] ) !== 200 ) { if ( isset( $response['error'] ) && $response['error'] === 'domain_not_accessible' ) { return new WP_Error( 'domain_not_accessible', sprintf( /* translators: 1 start of the italic tag, 2 is the end of italic tag, 3 is starting anchor tag, 4 is the ending anchor tag. */ __( 'It seems Optimole is having trouble reaching your website. This issue often occurs if your website is private, local, or protected by a firewall. But don\'t stress – it\'s an easy fix! Ensure your website is live and accessible to the public. If a firewall is in place, just tweak the settings to allow the %1$sOptimole(1.0)%2$s user agent access to your website. %3$sLearn More%4$s', 'optimole-wp' ), '<i>', '</i>', '<a href="https://docs.optimole.com/article/1976-resolving-optimole-access-to-your-website" target="_blank">', '</a>' ) ); } if ( $path === 'optml/v2/account/complete_register_remote' && isset( $response['error'] ) ) { if ( strpos( $response['error'], 'This email address is already registered.' ) !== false ) { return 'email_registered'; } if ( $response['error'] === 'ERROR: Site already whitelisted.' ) { return 'site_exists'; } } if ( $path === '/optml/v2/account/details' && isset( $response['code'] ) && $response['code'] === 'not_allowed' ) { return 'disconnect'; } return isset( $response['error'] ) ? new WP_Error( 'api_error', wp_kses( $response['error'], [ 'a' => [ 'href' => [], 'target' => [], ], ] ) ) : false; } return $response['data']; } /** * Builds Request arguments array. * * @param string $method Request method (GET | POST | PUT | UPDATE | DELETE). * @param string $url Request URL. * @param array $headers Headers Array. * @param array|string $params Additional params for the Request. * * @return array */ private function build_args( $method, $url, $headers, $params ) { $args = [ 'method' => $method, 'timeout' => 45, 'user-agent' => 'Optimle WP (v' . OPTML_VERSION . ') ', 'sslverify' => false, 'headers' => $headers, ]; if ( $method !== 'GET' ) { $args['body'] = $params; } return $args; } /** * Check if the optimized url is available. * * @param string $url The optimized url to check. * @return bool Whether or not the url is valid. */ public function check_optimized_url( $url ) { $response = wp_remote_get( $url, [ 'timeout' => 30 ] ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 || ! empty( wp_remote_retrieve_header( $response, 'x-not-found-o' ) ) ) { $this->log_offload_error( $response ); return false; } return true; } /** * Send a list of images to upload. * * @param array $images List of Images. * @return array */ public function call_onboard_api( $images = [] ) { $settings = new Optml_Settings(); $token_images = $settings->get( 'cache_buster_images' ); $body = [ 'secret' => Optml_Config::$secret, 'userKey' => Optml_Config::$key, 'images' => $images, 'cache_buster' => $token_images, ]; $body = wp_json_encode( $body ); $options = [ 'body' => $body, 'headers' => [ 'Content-Type' => 'application/json', ], 'timeout' => 60, 'blocking' => true, 'sslverify' => false, 'data_format' => 'body', ]; return wp_remote_post( $this->onboard_api_root, $options ); } /** * Send a list of images with alt/title values to update. * * @param array $images List of images. * @return array */ public function call_data_enrich_api( $images = [] ) { return $this->request( 'optml/v2/media/add_data', 'POST', [ 'images' => $images, 'key' => Optml_Config::$key ] ); } /** * Register user remotely on optimole.com. * * @param string $email User email. * * @return array|bool|string|WP_Error Api response. */ public function create_account( $email ) { return $this->request( 'optml/v2/account/complete_register_remote', 'POST', [ 'email' => $email, 'version' => OPTML_VERSION, 'sample_image' => $this->get_sample_image(), 'site' => get_home_url(), ] ); } /** * Get the optimized images from API. * * @param string $api_key the api key. * * @return array|bool|WP_Error */ public function get_optimized_images( $api_key = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } $app_key = ''; $settings = new Optml_Settings(); $service_data = $settings->get( 'service_data' ); if ( isset( $service_data['cdn_key'] ) ) { $app_key = $service_data['cdn_key']; } return $this->request( '/optml/v1/stats/images', 'GET', [], [ 'application' => $app_key ] ); } /** * Get the watermarks from API. * * @param string $api_key The API key. * * @return array|bool|WP_Error */ public function get_watermarks( $api_key = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } return $this->request( '/optml/v1/settings/watermark' ); } /** * Remove the watermark from the API. * * @param integer $post_id The watermark post ID. * @param string $api_key The API key. * * @return array|bool|WP_Error */ public function remove_watermark( $post_id, $api_key = '' ) { if ( ! empty( $api_key ) ) { $this->api_key = $api_key; } return $this->request( '/optml/v1/settings/watermark', 'DELETE', [ 'watermark' => $post_id ] ); } /** * Add watermark. * * @param array $file The file to be uploaded. * * @return array|bool|mixed|object */ public function add_watermark( $file ) { $headers = [ 'Content-Disposition' => 'attachment; filename=' . $file['file']['name'], ]; $response = $this->request( 'wp/v2/media', 'POST', file_get_contents( $file['file']['tmp_name'] ), $headers ); if ( $response === false ) { return false; } return $response; } /** * Call the images endpoint. * * @param integer $page Page used to advance the search. * @param array $domains Domains to filter by. * @param string $search The string to search inside the originURL. * @return mixed The decoded json response from the api. */ public function get_cloud_images( $page = 0, $domains = [], $search = '' ) { $params = [ 'key' => Optml_Config::$key ]; $params['page'] = $page; $params['size'] = 40; if ( $search !== '' ) { $params['search'] = $search; } if ( ! empty( $domains ) ) { $params['domains'] = implode( ',', $domains ); } return $this->request( 'optml/v2/media/browser', 'GET', $params ); } /** * Get offload conflicts. * * @return array The decoded conflicts list. */ public function get_offload_conflicts() { $conflicts_list = wp_remote_retrieve_body( wp_remote_get( $this->upload_conflicts_api ) ); return json_decode( $conflicts_list, true ); } /** * Get offload conflicts. * * @param array $error_response The error to send as a string. */ public function log_offload_error( $error_response ) { $headers = wp_remote_retrieve_headers( $error_response ); $body = wp_remote_retrieve_body( $error_response ); $headers_to_log = 'no_headers_returned'; if ( ! empty( $headers ) ) { $headers_to_log = wp_json_encode( $headers->getAll() ); } wp_remote_post( $this->upload_conflicts_api, [ 'headers' => [ 'Content-Type' => 'application/json' ], 'timeout' => 15, 'blocking' => true, 'sslverify' => false, 'data_format' => 'body', 'body' => [ 'error_body' => wp_json_encode( $body ), 'error_headers' => $headers_to_log, 'error_site' => wp_json_encode( get_home_url() ), ], ] ); } /** * Send Offloading Logs * * @param string $type Type of log (offload/rollback). * @param string $message Log message. * * @return mixed */ public function send_log( $type, $message ) { return $this->request( 'optml/v2/logs', 'POST', [ 'type' => $type, 'message' => $message, 'site' => get_home_url(), ] ); } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @access public * @return void * @since 1.0.0 */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @access public * @return void * @since 1.0.0 */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } } cli/cli_media.php 0000644 00000006525 14720403740 0007743 0 ustar 00 <?php /** * CLI commands responsible for the Optimole media. */ if ( ! class_exists( 'WP_CLI' ) ) { return; } /** * Class Optml_Cli_Media */ class Optml_Cli_Media extends WP_CLI_Command { /** * Move all existing images to our servers. */ public function move_to_optimole() { $this->update_images_template( 'offload' ); } /** * Move all existing images from our servers to your media library. */ public function rollback_images() { $this->update_images_template( 'rollback' ); } /** * Template for bulk image processing to avoid duplicate code. * * @param string $action The action to perform rollback/offload. * @return mixed WP_CLI::error If it fails. */ private function update_images_template( $action ) { $strings = [ 'offload' => [ 'info' => __( 'Moving all images to Optimole Cloud', 'optimole-wp' ), 'success' => __( 'All images have been uploaded to Optimole Cloud', 'optimole-wp' ), ], 'rollback' => [ 'info' => __( 'Moving all images back to your media library', 'optimole-wp' ), 'success' => __( 'All images have been uploaded to your media library', 'optimole-wp' ), ], 'limit_exceeded' => __( 'You have reached the maximum offloading limit of images. To increase the offload limit and for more information, contact our support.', 'optimole-wp' ), ]; $settings = new Optml_Settings(); if ( $settings->get( 'offload_media' ) === 'disabled' ) { return \WP_CLI::error( __( 'You need to have the offload_media option enabled in order to use this command', 'optimole-wp' ) ); } WP_CLI::line( $strings[ $action ]['info'] ); $number_of_images_for = 'offload_images'; if ( $action === 'rollback' ) { $number_of_images_for = 'rollback_images'; } $number_of_images = Optml_Media_Offload::number_of_images_and_pages( $number_of_images_for ); Optml_Media_Offload::record_process_meta( $number_of_images ); $batch = 5; $possible_batch = ceil( $number_of_images / 10 ); if ( $possible_batch < $batch ) { $batch = $possible_batch; } $total_progress = ceil( $number_of_images / $batch ); $progress = \WP_CLI\Utils\make_progress_bar( __( 'Progress bar', 'optimole-wp' ), (int) $total_progress ); $tick = 0; $page = 1; while ( $tick < $total_progress ) { $posts_to_update = $action === 'offload' ? [] : Optml_Media_Offload::instance()->update_content( $page, $number_of_images_for, $batch ); // Kept for backward compatibility with old offloading mechanism where pages were modified. if ( $action === 'rollback' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) { $page = $posts_to_update['page']; if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) { foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) { Optml_Media_Offload::instance()->rollback_and_update_images( $images ); Optml_Media_Offload::instance()->update_page( $post_id ); } } } else { $action === 'rollback' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch ); } if ( Optml_Media_Offload::instance()->settings->is_offload_limit_reached() ) { WP_CLI::error( $strings['limit_exceeded'], true ); } $progress->tick(); ++$tick; } $progress->finish(); WP_CLI::line( $strings[ $action ]['success'] ); } } cli/cli_setting.php 0000644 00000010134 14720403740 0010330 0 ustar 00 <?php /** * CLI commands responsible for the Optimole settings. */ if ( ! class_exists( 'WP_CLI' ) ) { return; } /** * Class Optml_Cli_Setting */ class Optml_Cli_Setting extends WP_CLI_Command { /** * Connect to service * <apikey> * : The api key to use. */ public function connect( $args ) { if ( empty( $args ) || ! isset( $args[0] ) || $args[0] === '' ) { return \WP_CLI::error( 'No argument passed. Required one argument ( api key )' ); } if ( sizeof( $args ) > 1 ) { return \WP_CLI::error( 'To many arguments passed' ); } $api_key = $args[0]; $request = new Optml_Api(); $data = $request->get_user_data( $api_key ); if ( $data === false || is_wp_error( $data ) ) { $extra = ''; if ( is_wp_error( $data ) ) { /** * Error from api. * * @var WP_Error $data Error object. */ $extra = sprintf( /* translators: errors details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() ); } return \WP_CLI::error( __( 'Can not connect to Optimole service', 'optimole-wp' ) . $extra ); } $settings = new Optml_Settings(); $settings->update( 'service_data', $data ); $settings->update( 'api_key', $api_key ); \WP_CLI::success( sprintf( 'Connected API key %s to Optimole Service', $args[0] ) ); } /** * Disconnect from service. */ public function disconnect() { $settings = new Optml_Settings(); $settings->reset(); \WP_CLI::success( 'Disconnected from Optimole Service' ); } /** * Update settings. * * <setting_name> * : The setting name to update. * * <setting_value> * : The setting value to update. */ public function update( $args ) { if ( empty( $args ) || ! isset( Optml_Settings::$whitelisted_settings[ $args[0] ] ) ) { return \WP_CLI::error( sprintf( 'Setting must be one of: %s', implode( ',', array_keys( Optml_Settings::$whitelisted_settings ) ) ) ); } if ( Optml_Settings::$whitelisted_settings[ $args[0] ] === 'bool' && ( ! isset( $args[1] ) || $args[1] === '' || ! in_array( $args[1], [ 'on', 'off', ], true ) ) ) { return \WP_CLI::error( 'No argument passed. Required one argument ( on/off )' ); } if ( Optml_Settings::$whitelisted_settings[ $args[0] ] === 'int' && ( ! isset( $args[1] ) || $args[1] === '' || (int) $args[1] > 100 || (int) $args[1] < 0 ) ) { return \WP_CLI::error( 'Invalid argument, must be between 0 and 100.' ); } $value = ( Optml_Settings::$whitelisted_settings[ $args[0] ] === 'bool' ) ? ( $args[1] === 'on' ? 'enabled' : 'disabled' ) : (int) $args[1]; $new_value = $this->update_setting( [ $args[0] => $value ] ); \WP_CLI::success( sprintf( 'Setting %s updated to: %s', $args[0], $new_value[ $args[0] ] ) ); } /** * Check settings. * * <setting_name> * : The setting name to check. */ public function get( $args ) { if ( empty( $args ) || ! isset( Optml_Settings::$whitelisted_settings[ $args[0] ] ) ) { return \WP_CLI::error( sprintf( 'Setting must be one of: %s', implode( ',', array_keys( Optml_Settings::$whitelisted_settings ) ) ) ); } $value = ( new Optml_Settings() )->get( $args[0] ); \WP_CLI::success( sprintf( 'Setting %s is set to: %s', $args[0], $value ) ); } /** * Utility method to update setting * * @param mixed $new_setting The setting to parse. * * @return array */ private function update_setting( $new_setting ) { if ( empty( $new_setting ) ) { \WP_CLI::error( __( 'No setting to update', 'optimole-wp' ) ); } $settings = new Optml_Settings(); return $settings->parse_settings( $new_setting ); } /** * Clear cache. * * --type=<type> * : The type of cache to clear. Default is images. */ public function clear_cache( $args, $assoc_args ) { $type = ''; // If assoc_args has a type key, use that. if ( isset( $assoc_args['type'] ) && $assoc_args['type'] === 'assets' ) { $type = $assoc_args['type']; } $settings = new Optml_Settings(); $token = $settings->clear_cache( $type ); if ( is_wp_error( $token ) ) { \WP_CLI::error( $token->get_error_message() ); } \WP_CLI::success( sprintf( 'Cache type %s cleared', $assoc_args['type'] ) ); } } rest.php 0000644 00000064345 14720403740 0006247 0 ustar 00 <?php /** * Optimole Rest related actions. * * @package Optimole/Inc * @copyright Copyright (c) 2017, Marius Cristea * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License */ use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty; use Optimole\Sdk\ValueObject\Position; /** * Class Optml_Rest * * @codeCoverageIgnore */ class Optml_Rest { /** * Rest api namespace. * * @var string Namespace. */ private $namespace; /** * Upload conflicts api. * * @var array upload_conflicts_api. */ public static $rest_routes = [ 'service_routes' => [ 'update_option' => 'POST', 'request_update' => 'GET', 'check_redirects' => 'POST_PUT_PATCH', 'connect' => [ 'POST', 'args' => [ 'api_key' => [ 'type' => 'string', 'required' => true, ], ], ], 'select_application' => [ 'POST', 'args' => [ 'api_key' => [ 'type' => 'string', 'required' => true, ], 'application' => [ 'type' => 'string', 'required' => true, ], ], ], 'register_service' => [ 'POST', 'args' => [ 'email' => [ 'type' => 'string', 'required' => true, ], ], ], 'disconnect' => 'GET', ], 'image_routes' => [ 'poll_optimized_images' => 'GET', 'get_sample_rate' => 'POST', 'upload_onboard_images' => [ 'POST', 'args' => [ 'offset' => [ 'type' => 'number', 'required' => false, 'default' => 0, ], ], ], ], 'media_cloud_routes' => [ 'number_of_images_and_pages' => 'POST', 'get_offload_conflicts' => 'GET', ], 'watermark_routes' => [ 'poll_watermarks' => 'GET', 'add_watermark' => 'POST', 'remove_watermark' => 'POST', ], 'conflict_routes' => [ 'poll_conflicts' => 'GET', 'dismiss_conflict' => 'POST', ], 'cache_routes' => [ 'clear_cache_request' => 'POST', ], 'dam_routes' => [ 'insert_images' => [ 'POST', 'args' => [ 'images' => [ 'type' => 'array', 'required' => true, ], ], 'permission_callback' => 'upload_files', ], ], 'notification_dismiss_routes' => [ 'dismiss_notice' => [ 'POST', 'args' => [ 'key' => [ 'type' => 'string', 'required' => true, ], ], ], ], ]; /** * Optml_Rest constructor. */ public function __construct() { $this->namespace = OPTML_NAMESPACE . '/v1'; add_action( 'rest_api_init', [ $this, 'register' ] ); } /** * Method to register a specific rest route. * * @param string $route The route name. * @param string $method The route access method GET, POST, POST_PUT_PATCH. * @param array $args Optional argument to include required args. * @param string $permission_callback Optional permission callback. */ private function reqister_route( $route, $method = 'GET', $args = [], $permission_callback = 'manage_options' ) { $wp_method_constant = false; if ( $method === 'GET' ) { $wp_method_constant = \WP_REST_Server::READABLE; } if ( $method === 'POST' ) { $wp_method_constant = \WP_REST_Server::CREATABLE; } if ( $method === 'POST_PUT_PATCH' ) { $wp_method_constant = \WP_REST_Server::EDITABLE; } if ( $wp_method_constant !== false ) { $params = [ 'methods' => $wp_method_constant, 'permission_callback' => function () use ( $permission_callback ) { return current_user_can( $permission_callback ); }, 'callback' => [ $this, $route ], ]; if ( ! empty( $args ) ) { $params['args'] = $args; } register_rest_route( $this->namespace, '/' . $route, [ $params, ] ); } } /** * Register rest routes. */ public function register() { $this->register_service_routes(); $this->register_image_routes(); $this->register_watermark_routes(); $this->register_conflict_routes(); $this->register_cache_routes(); $this->register_media_offload_routes(); $this->register_dam_routes(); $this->register_notification_routes(); } /** * Method to register service specific routes. */ public function register_service_routes() { foreach ( self::$rest_routes['service_routes'] as $route => $details ) { if ( is_array( $details ) ) { $this->reqister_route( $route, $details[0], $details['args'] ); } else { $this->reqister_route( $route, $details ); } } } /** * Method to register image specific routes. */ public function register_image_routes() { foreach ( self::$rest_routes['image_routes'] as $route => $details ) { if ( is_array( $details ) ) { $this->reqister_route( $route, $details[0], $details['args'] ); } else { $this->reqister_route( $route, $details ); } } } /** * Method to register media offload specific routes. */ public function register_media_offload_routes() { foreach ( self::$rest_routes['media_cloud_routes'] as $route => $details ) { $this->reqister_route( $route, $details ); } } /** * Method to register watermark specific routes. */ public function register_watermark_routes() { foreach ( self::$rest_routes['watermark_routes'] as $route => $details ) { $this->reqister_route( $route, $details ); } } /** * Method to register conflicts specific routes. */ public function register_conflict_routes() { foreach ( self::$rest_routes['conflict_routes'] as $route => $details ) { $this->reqister_route( $route, $details ); } } /** * Method to register cache specific routes. */ public function register_cache_routes() { foreach ( self::$rest_routes['cache_routes'] as $route => $details ) { $this->reqister_route( $route, $details ); } } /** * Register DAM routes. * * @return void */ public function register_dam_routes() { foreach ( self::$rest_routes['dam_routes'] as $route => $details ) { $permission = isset( $details['permission_callback'] ) ? $details['permission_callback'] : 'manage_options'; $args = isset( $details['args'] ) ? $details['args'] : []; $this->reqister_route( $route, $details[0], $args, $permission ); } } /** * Register notification dismiss routes. * * @return void */ public function register_notification_routes() { foreach ( self::$rest_routes['notification_dismiss_routes'] as $route => $details ) { $this->reqister_route( $route, $details[0], isset( $details['args'] ) ? $details['args'] : [] ); } } /** * Clear Cache request. * * @param WP_REST_Request $request clear cache rest request. * * @return WP_Error|WP_REST_Response */ public function clear_cache_request( WP_REST_Request $request ) { $settings = new Optml_Settings(); $type = $request->get_param( 'type' ); $response = $settings->clear_cache( $type ); if ( is_wp_error( $response ) ) { wp_send_json_error( $response->get_error_message() ); } return $this->response( $response, '200' ); } /** * Connect to optimole service. * * @param WP_REST_Request $request connect rest request. * * @return WP_Error|WP_REST_Response */ public function connect( WP_REST_Request $request ) { $api_key = $request->get_param( 'api_key' ); $original_request = $request; $request = new Optml_Api(); $data = $request->connect( $api_key ); if ( $data === false || is_wp_error( $data ) ) { $extra = ''; if ( is_wp_error( $data ) ) { if ( $data->get_error_code() === 'domain_not_accessible' ) { return $this->response( $data->get_error_message(), 400 ); } /** * Error from api. * * @var WP_Error $data Error object. */ $extra = sprintf( /* translators: Error details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() ); } return $this->response( __( 'Can not connect to Optimole service', 'optimole-wp' ) . $extra, 400 ); } $settings = new Optml_Settings(); $settings->update( 'api_key', $api_key ); if ( isset( $data['extra_visits'] ) ) { $settings->update_frontend_banner_from_remote( $data['extra_visits'] ); } if ( $data['app_count'] === 1 ) { return $this->select_application( $original_request ); } return $this->response( $data ); } /** * Select application. * * @param WP_REST_Request $request Rest request. * * @return WP_REST_Response */ public function select_application( WP_REST_Request $request ) { $api_key = $request->get_param( 'api_key' ); $application = $request->get_param( 'application' ); $request = new Optml_Api(); $data = $request->get_user_data( $api_key, $application ); if ( $data === false || is_wp_error( $data ) ) { $extra = ''; if ( is_wp_error( $data ) ) { /** * Error from api. * * @var WP_Error $data Error object. */ $extra = sprintf( /* translators: Error details */ __( '. ERROR details: %s', 'optimole-wp' ), $data->get_error_message() ); } wp_send_json_error( __( 'Can not connect to Optimole service', 'optimole-wp' ) . $extra ); } $settings = new Optml_Settings(); $settings->update( 'service_data', $data ); return $this->response( $data ); } /** * Wrapper for api response. * * @param mixed $data data from api. * * @return WP_REST_Response */ private function response( $data, $code = 'success' ) { return new WP_REST_Response( [ 'data' => $data, 'code' => $code ], 200 ); } /** * Connect to optimole service. * * @param WP_REST_Request $request connect rest request. * * @return WP_Error|WP_REST_Response */ public function register_service( WP_REST_Request $request ) { $email = $request->get_param( 'email' ); $api = new Optml_Api(); $user = $api->create_account( $email ); $auto_connect = $request->get_param( 'auto_connect' ); if ( ! empty( $auto_connect ) && $auto_connect === 'true' ) { delete_option( Optml_Settings::OPTML_USER_EMAIL ); } if ( $user === false || is_wp_error( $user ) ) { if ( $user->get_error_code() === 'domain_not_accessible' ) { return new WP_REST_Response( [ 'data' => null, 'message' => $user->get_error_message(), 'code' => 'domain_not_accessible', ], 200 ); } return new WP_REST_Response( [ 'data' => null, 'message' => __( 'Error creating account.', 'optimole-wp' ) . ' ' . $user->get_error_message(), 'code' => 'error', ], 200 ); } if ( $user === 'email_registered' ) { return new WP_REST_Response( [ 'data' => null, 'message' => __( 'Error: This email is already registered. Please choose another one.', 'optimole-wp' ), 'code' => 'email_registered', ], 200 ); } if ( $user === 'site_exists' ) { return new WP_REST_Response( [ 'data' => null, 'message' => sprintf( /* translators: 1 start anchor tag to register page, 2 is ending anchor tag. */ __( 'Error: This site has been previously registered. You can login to your account from %1$shere%2$s', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/login', 'query' ) ) . '" target="_blank"> ', '</a>' ), 'code' => 'site_exists', ], 200 ); } $user_data = $user['res']; $settings = new Optml_Settings(); $settings->update( 'api_key', $user_data['api_key'] ); if ( $user_data['app_count'] === 1 ) { $settings->update( 'service_data', $user_data ); } if ( isset( $user_data['extra_visits'] ) ) { $settings->update_frontend_banner_from_remote( $user_data['extra_visits'] ); } return $this->response( $user_data ); } /** * Return image samples. * * @param WP_REST_Request $request Rest request. * * @return WP_REST_Response Image urls. */ public function get_sample_rate( WP_REST_Request $request ) { add_filter( 'optml_dont_replace_url', '__return_true' ); $image_sample = get_transient( 'optimole_sample_image' ); if ( $image_sample === false || $request->get_param( 'force' ) === 'yes' ) { $image_sample = $this->fetch_sample_image(); set_transient( 'optimole_sample_image', $image_sample ); } $image = [ 'id' => $image_sample['id'] ]; $image['original'] = $image_sample['url']; remove_filter( 'optml_dont_replace_url', '__return_true' ); $image['optimized'] = apply_filters( 'optml_replace_image', $image['original'], [ 'width' => $image_sample['width'], 'height' => $image_sample['height'], 'quality' => $request->get_param( 'quality' ), ] ); $optimized = wp_remote_get( $image['optimized'], [ 'timeout' => 10, 'headers' => [ 'Accept' => 'text/html,application/xhtml+xml,image/webp,image/apng ', ], ] ); $original = wp_remote_get( $image['original'] ); $image['optimized_size'] = (int) wp_remote_retrieve_header( $optimized, 'content-length' ); $image['original_size'] = (int) wp_remote_retrieve_header( $original, 'content-length' ); return $this->response( $image ); } /** * Crawl & upload initial load. * * @return WP_REST_Response If there are more posts left to receive. */ public function upload_onboard_images( WP_REST_Request $request ) { $offset = absint( $request->get_param( 'offset' ) ); // Arguments for get_posts function $args = [ 'post_type' => 'attachment', 'post_mime_type' => 'image', 'post_status' => 'inherit', 'posts_per_page' => 100, 'offset' => $offset, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'orderby' => [ 'parent' => 'DESC', ], ]; // Initialize an array to store the image URLs $image_urls = []; add_filter( 'optml_dont_replace_url', '__return_true' ); // If site has a logo, get the logo url if offset is 0, ie first request if ( $offset === 0 ) { $custom_logo_id = get_theme_mod( 'custom_logo' ); if ( $custom_logo_id ) { $image_urls[] = wp_get_attachment_url( $custom_logo_id ); } } $query = new \WP_Query( $args ); if ( $query->have_posts() ) { $image_ids = $query->get_posts(); $image_urls = array_map( 'wp_get_attachment_url', $image_ids ); } remove_filter( 'optml_dont_replace_url', '__return_true' ); if ( count( $image_urls ) === 0 ) { return $this->response( true ); } $api = new Optml_Api(); $api->call_onboard_api( $image_urls ); // Check if image_url array has at least 100 items, if not, we have reached the end of the images return $this->response( count( $image_urls ) < 100 ? true : false ); } /** * Return sample image data. * * @return array Image data. */ private function fetch_sample_image() { $accepted_mimes = [ 'image/jpeg' ]; $args = [ 'post_type' => 'attachment', 'post_status' => 'any', 'number' => '5', 'no_found_rows' => true, 'fields' => 'ids', 'post_mime_type' => $accepted_mimes, 'post_parent__not_in' => [ 0 ], ]; $image_result = new WP_Query( $args ); if ( empty( $image_result->posts ) ) { $rand_id = rand( 1, 3 ); $original_image_url = OPTML_URL . 'assets/img/' . $rand_id . '.jpg'; return [ 'url' => $original_image_url, 'width' => '700', 'height' => '465', 'id' => - 1, ]; } $attachment_id = $image_result->posts[ array_rand( $image_result->posts, 1 ) ]; $original_image_url = wp_get_attachment_image_url( $attachment_id, 'full' ); $metadata = wp_get_attachment_metadata( $attachment_id ); $width = 'auto'; $height = 'auto'; $size = 'full'; if ( isset( $metadata['sizes'] ) && isset( $metadata['sizes'][ $size ] ) ) { $width = $metadata['sizes'][ $size ]['width']; $height = $metadata['sizes'][ $size ]['height']; } return [ 'url' => $original_image_url, 'id' => $attachment_id, 'width' => $width, 'height' => $height, ]; } /** * Disconnect from optimole service. * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param WP_REST_Request $request disconnect rest request. */ public function disconnect( WP_REST_Request $request ) { $settings = new Optml_Settings(); $settings->reset(); wp_send_json_success( 'Disconnected' ); } /** * Get optimized images from API. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function poll_optimized_images( WP_REST_Request $request ) { $api_key = $request->get_param( 'api_key' ); $request = new Optml_Api(); $images = $request->get_optimized_images( $api_key ); if ( ! isset( $images['list'] ) || empty( $images['list'] ) ) { return $this->response( [] ); } $final_images = array_splice( $images['list'], 0, 10 ); foreach ( $final_images as $index => $value ) { $final_images[ $index ]['url'] = Optml_Media_Offload::instance()->get_media_optimized_url( $value['url'], $value['key'], 140, 140, [ 'type' => ResizeTypeProperty::FILL, 'enlarge' => false, 'gravity' => Position::CENTER, ] ); unset( $final_images[ $index ]['key'] ); } return $this->response( $final_images ); } /** * Get watermarks from API. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function poll_watermarks( WP_REST_Request $request ) { $api_key = $request->get_param( 'api_key' ); $request = new Optml_Api(); $watermarks = $request->get_watermarks( $api_key ); if ( ! isset( $watermarks['watermarks'] ) || empty( $watermarks['watermarks'] ) ) { return $this->response( [] ); } $final_images = array_splice( $watermarks['watermarks'], 0, 10 ); return $this->response( $final_images ); } /** * Add watermark. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function add_watermark( WP_REST_Request $request ) { $file = $request->get_file_params(); $request = new Optml_Api(); $response = $request->add_watermark( $file ); if ( $response === false ) { return $this->response( __( 'Error uploading image. Please try again.', 'optimole-wp' ), 'error' ); } return $this->response( __( 'Watermark image uploaded succesfully !', 'optimole-wp' ) ); } /** * Remove watermark. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function remove_watermark( WP_REST_Request $request ) { $post_id = $request->get_param( 'postID' ); $api_key = $request->get_param( 'api_key' ); $request = new Optml_Api(); return $this->response( $request->remove_watermark( $post_id, $api_key ) ); } /** * Get conflicts from API. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function poll_conflicts( WP_REST_Request $request ) { $conflicts_to_register = apply_filters( 'optml_register_conflicts', [] ); $manager = new Optml_Conflict_Manager( $conflicts_to_register ); return $this->response( [ 'count' => $manager->get_conflict_count(), 'conflicts' => $manager->get_conflict_list(), ] ); } /** * Dismiss conflict. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function dismiss_conflict( WP_REST_Request $request ) { $conflict_id = $request->get_param( 'conflictID' ); $conflicts_to_register = apply_filters( 'optml_register_conflicts', [] ); $manager = new Optml_Conflict_Manager( $conflicts_to_register ); $manager->dismiss_conflict( $conflict_id ); return $this->response( [ 'count' => $manager->get_conflict_count(), 'conflicts' => $manager->get_conflict_list(), ] ); } /** * Request stats update for app. * * @param WP_REST_Request $request rest request. * * @return WP_REST_Response */ public function request_update( WP_REST_Request $request ) { do_action( 'optml_daily_sync' ); $settings = new Optml_Settings(); $service_data = $settings->get( 'service_data' ); if ( empty( $service_data ) ) { return $this->response( '', 'disconnected' ); } return $this->response( $service_data ); } /** * Update options method. * * @param WP_REST_Request $request option update rest request. * * @return WP_REST_Response */ public function update_option( WP_REST_Request $request ) { $new_settings = $request->get_param( 'settings' ); if ( empty( $new_settings ) ) { wp_send_json_error( 'No option key set.' ); } $settings = new Optml_Settings(); $sanitized = $settings->parse_settings( $new_settings ); return $this->response( $sanitized ); } /** * Update options method. * * @param WP_REST_Request $request option update rest request. * * @return WP_REST_Response */ public function check_redirects( WP_REST_Request $request ) { if ( empty( $request->get_param( 'images' ) ) ) { return $this->response( __( 'No images available on the current page.' ), 'noImagesFound' ); } // 'ok' if no issues found, 'log' is there are issues we need to notify, 'deactivated' if the user's account is disabled $status = 'ok'; $result = ''; foreach ( $request->get_param( 'images' ) as $domain => $value ) { $args = [ 'method' => 'GET', 'redirection' => 0, ]; $processed_images = 0; if ( isset( $value['src'] ) ) { $processed_images = count( $value['src'] ); } if ( isset( $value['ignoredUrls'] ) && $value['ignoredUrls'] > $processed_images ) { $result .= '<li>❌ ' . sprintf( /* translators: 1 is the domain name, 2 is starting anchor tag, 3 is the ending anchor tag. */__( 'The images from: %1$s are not optimized by Optimole. If you would like to do so, you can follow this: %2$sWhy Optimole does not optimize all the images from my site?%3$s.', 'optimole-wp' ), $domain, '<a target="_blank" href="https://docs.optimole.com/article/1290-how-to-optimize-images-using-optimole-from-my-domain">', '</a>' ) . '</li>'; $status = 'log'; continue; } if ( $processed_images > 0 ) { $response = wp_remote_get( $value['src'][ rand( 0, $processed_images - 1 ) ], $args ); if ( ! is_wp_error( $response ) && is_array( $response ) ) { $headers = $response['headers']; // array of http header lines $status_code = $response['response']['code']; if ( $status_code === 301 ) { $status = 'deactivated'; $result = '<li>❌ ' . sprintf( /* translators: 1 is starting anchor tag, 2 is the ending anchor tag. */ __( 'Your account is currently disabled due to exceeding quota and Optimole is no longer able to optimize the images. In order to fix this you will need to %1$supgrade%2$s.', 'optimole-wp' ), '<a target="_blank" href="' . esc_url( tsdk_translate_link( 'https://optimole.com/pricing' ) ) . '">', '</a>' ) . '</li>'; break; } if ( $status_code === 302 ) { if ( isset( $headers['x-redirect-o'] ) ) { $optimole_code = (int) $headers['x-redirect-o']; if ( $optimole_code === 1 ) { $status = 'log'; $result .= '<li>❌ ' . sprintf( /* translators: 1 is the domain, 2 is starting anchor tag, 3 is the ending anchor tag. */ __( 'The domain: %1$s is not allowed to optimize images using your Optimole account. You can add this to the allowed list %2$shere%3$s.', 'optimole-wp' ), '<b>' . $domain . '</b>', '<a target="_blank" href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/whitelist', 'query' ) ) . '">', '</a>' ) . '</li>'; } if ( $optimole_code === 4 ) { $status = 'log'; $result .= '<li>❌ ' . sprintf( /* translators: 1 is the domain, 2 is starting anchor tag, 3 is the ending anchor tag. */ __( 'We are not able to download the images from %1$s. Please check %2$sthis%3$s document for a more advanced guide on how to solve this.', 'optimole-wp' ), '<b>' . $domain . '</b>', '<a target="_blank" href="https://docs.optimole.com/article/1291-why-optimole-is-not-able-to-download-the-images-from-my-site">', '</a>' ) . '<br />' . '</li>'; } } } } } } if ( $result === '' ) { $result = __( 'No issues detected, everything is running smoothly.', 'optimole-wp' ); } return $this->response( '<ul>' . $result . '</ul>', $status ); } /** * Get total number of images. * * @param WP_REST_Request $request rest request object. * * @return WP_REST_Response */ public function number_of_images_and_pages( WP_REST_Request $request ) { $action = 'offload_images'; $refresh = false; $images = []; if ( ! empty( $request->get_param( 'action' ) ) ) { $action = $request->get_param( 'action' ); } if ( ! empty( $request->get_param( 'refresh' ) ) ) { $refresh = $request->get_param( 'refresh' ); } if ( ! empty( $request->get_param( 'images' ) ) ) { $images = $request->get_param( 'images' ); } return $this->response( Optml_Media_Offload::get_image_count( $action, $refresh, $images ) ); } /** * Get conflicts list. * * @param WP_REST_Request $request rest request object. * * @return WP_REST_Response */ public function get_offload_conflicts( WP_REST_Request $request ) { $request = new Optml_Api(); $decoded_list = $request->get_offload_conflicts(); $active_conflicts = []; if ( isset( $decoded_list['plugins'] ) ) { foreach ( $decoded_list['plugins'] as $slug ) { if ( is_plugin_active( $slug ) ) { $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $slug ); if ( ! empty( $plugin_data['Name'] ) ) { $active_conflicts[] = $plugin_data['Name']; } } } } return $this->response( $active_conflicts ); } /** * Insert images request. * * @param WP_REST_Request $request insert images rest request. * * @return WP_REST_Response */ public function insert_images( WP_REST_Request $request ) { $images = $request->get_param( 'images' ); if ( ! is_array( $images ) || empty( $images ) ) { return $this->response( [ 'error' => 'No images found' ] ); } $insert = Optml_Main::instance()->dam->insert_attachments( $images ); return $this->response( $insert ); } /** * Dismiss a notification (set the notification key to 'yes'). * * @param WP_REST_Request $request the incoming request. * * @return WP_REST_Response */ public function dismiss_notice( WP_REST_Request $request ) { $key = $request->get_param( 'key' ); if ( empty( $key ) ) { return $this->response( [ 'error' => 'Invalid key' ], 'error' ); } $result = update_option( $key, 'yes' ); if ( ! $result ) { return $this->response( [ 'error' => 'Could not dismiss notice' ], 'error' ); } return $this->response( [ 'success' => 'Notice dismissed' ] ); } } dam.php 0000644 00000050511 14720403740 0006021 0 ustar 00 <?php /** * Dam class. * * Author: Andrei Baicus <andrei@themeisle.com> * Created on: 04/07/2023 * * @package \Optimole\Inc * @author Optimole <friends@optimole.com> */ use Optimole\Sdk\Resource\ImageProperty\GravityProperty; use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty; use Optimole\Sdk\ValueObject\Position; /** * Class Optml_Dam */ class Optml_Dam { use Optml_Dam_Offload_Utils; use Optml_Normalizer; /** * Hold the settings object. * * @var Optml_Settings Settings object. */ private $settings; /** * The dam endpoint. * * @var string */ private $dam_endpoint = 'https://dashboard.optimole.com/dam'; const OM_DAM_IMPORTED_FLAG = 'om-dam-imported'; const URL_DAM_FLAG = '/dam:1'; const IS_EDIT_FLAG = 'om-dam-edit'; /** * Optml_Dam constructor. */ public function __construct() { $this->settings = Optml_Main::instance()->admin->settings; if ( ! $this->settings->is_connected() ) { return; } if ( $this->settings->get( 'cloud_images' ) === 'enabled' ) { add_action( 'admin_menu', [ $this, 'add_menu' ] ); add_action( 'print_media_templates', [ $this, 'print_media_template' ] ); add_action( 'wp_enqueue_media', [ $this, 'enqueue_media_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_page_scripts' ] ); } if ( defined( 'OPTML_DAM_ENDPOINT' ) ) { $this->dam_endpoint = constant( 'OPTML_DAM_ENDPOINT' ); } add_filter( 'wp_get_attachment_image_src', [ $this, 'alter_attachment_image_src' ], 10, 4 ); add_filter( 'wp_get_attachment_metadata', [ $this, 'alter_attachment_metadata' ], 10, 2 ); add_filter( 'image_downsize', [ $this, 'catch_downsize' ], 10, 3 ); add_filter( 'wp_prepare_attachment_for_js', [ $this, 'alter_attachment_for_js' ], 10, 3 ); add_filter( 'wp_image_src_get_dimensions', [ $this, 'alter_img_tag_w_h' ], 10, 4 ); add_filter( 'get_attached_file', [ $this, 'alter_attached_file_response' ], 10, 2 ); add_filter( 'wp_calculate_image_srcset', [ $this, 'disable_dam_images_srcset' ], 1, 5 ); add_filter( 'elementor/image_size/get_attachment_image_html', [ $this, 'alter_elementor_image_size', ], 10, 4 ); } /** * Catch image downsize for the DAM imported images. * * @param array $image { * Array of image data. * * @type string $0 Image source URL. * @type int $1 Image width in pixels. * @type int $2 Image height in pixels. * @type bool $3 Whether the image is a resized image. * * @param int $id attachment id. * @param string|int[] $size image size. * * @return array $image. */ public function catch_downsize( $image, $id, $size ) { return $this->alter_attachment_image_src( $image, $id, $size, false ); } /** * Insert attachments. * * @param array $images Images array. * * @return array */ public function insert_attachments( $images ) { $ids = []; $existing = $this->check_existing_attachments( $images ); foreach ( $images as $image ) { if ( ! isset( $image['isEdit'] ) && array_key_exists( $image['meta']['resourceS3'], $existing ) ) { $ids[] = $existing[ $image['meta']['resourceS3'] ]; continue; } $id = $this->insert_attachment( $image ); if ( $id === 0 ) { continue; } $ids[] = $id; } return $ids; } /** * Insert single attachment * * @param array $image Image data. * * @return int */ private function insert_attachment( $image ) { $filename = basename( $image['url'] ); $name = pathinfo( $filename, PATHINFO_FILENAME ); $args = [ 'post_title' => $name, 'post_type' => 'attachment', 'post_mime_type' => $image['meta']['mimeType'], 'guid' => $image['url'], ]; $id = wp_insert_attachment( $args ); if ( $id === 0 ) { return $id; } update_post_meta( $id, self::OM_DAM_IMPORTED_FLAG, $image['meta']['resourceS3'] ); if ( isset( $image['isEdit'] ) ) { update_post_meta( $id, self::IS_EDIT_FLAG, true ); } $metadata = []; $metadata['file'] = '/id:' . $image['meta']['resourceS3'] . '/' . get_home_url() . '/' . $filename; $metadata['mime-type'] = $image['meta']['mimeType']; if ( isset( $image['meta']['filesize'] ) ) { $metadata['filesize'] = $image['meta']['fileSize']; } if ( isset( $image['meta']['originalWidth'] ) && isset( $image['meta']['originalHeight'] ) ) { $metadata['width'] = $image['meta']['originalWidth']; $metadata['height'] = $image['meta']['originalHeight']; } wp_update_attachment_metadata( $id, $metadata ); return $id; } /** * Alter attachment image src for DAM imported images. * * @param array|false $image { * Array of image data. * * @type string $0 Image source URL. * @type int $1 Image width in pixels. * @type int $2 Image height in pixels. * @type bool $3 Whether the image is a resized image. * } * * @param int $attachment_id attachment id. * @param string|int[] $size image size. * @param bool $icon Whether the image should be treated as an icon. * * @return array $image. */ public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) { // Skip if not DAM image. if ( ! $this->is_dam_imported_image( $attachment_id ) ) { return $image; } $image_url = wp_get_attachment_url( $attachment_id ); $incoming_size = $this->parse_dimension_from_optimized_url( $image_url ); $width = $incoming_size[0]; $height = $incoming_size[1]; // Skip resize in single attachment view on backend. if ( $this->is_attachment_edit_page( $attachment_id ) ) { return [ $image_url, $width, $height, false, ]; } $metadata = wp_get_attachment_metadata( $attachment_id ); // Use the original size if the requested size is full. if ( $size === 'full' ) { $image_url = $this->replace_dam_url_args( [ 'width' => $metadata['width'], 'height' => $metadata['height'], 'crop' => false, ], $image_url ); return [ $image_url, $metadata['width'], $metadata['height'], false, ]; } $sizes = $this->size_to_dimension( $size, $metadata ); $image_url = $this->replace_dam_url_args( [ 'width' => $sizes['width'], 'height' => $sizes['height'], 'crop' => $sizes['resize'] ?? false, ], $image_url ); return [ $image_url, $sizes['width'], $sizes['height'], $size === 'full', ]; } /** * Alter the attachment metadata. * * @param array $metadata attachment metadata. * @param int $id attachment ID. * * @return array */ public function alter_attachment_metadata( $metadata, $id ) { if ( ! $this->is_dam_imported_image( $id ) ) { return $metadata; } return $this->get_altered_metadata_for_remote_images( $metadata, $id ); } /** * Check if the images are already imported. * * @param array $images List of images to check. * * @return array List of images that are already imported. */ private function check_existing_attachments( $images ) { $already_imported = $this->get_dam_imported_attachments( $images ); // All DAM imports are already in the DB. if ( count( $already_imported ) === count( $images ) ) { return $already_imported; } // Get the remaining images. $remaining = array_filter( $images, function ( $image ) use ( $already_imported ) { return ! array_key_exists( $image['meta']['resourceS3'], $already_imported ); } ); // Offloaded images. if ( $this->settings->get( 'offload_media' ) === 'enabled' ) { $offloaded = $this->get_offloaded_attachments( $remaining ); $already_imported = array_merge( $already_imported, $offloaded ); } return $already_imported; } /** * Check if the images are already imported. * * @param array $images List of images to check. * * @return array */ private function get_dam_imported_attachments( $images ) { global $wpdb; $s3_ids = []; foreach ( $images as $image ) { $s3_ids[] = esc_sql( strval( $image['meta']['resourceS3'] ) ); } $meta_values_str = "'" . join( "', '", $s3_ids ) . "'"; // Select all the posts that have the flag and are in the list of images. $found_attachments = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation. "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value IN ( {$meta_values_str} )", self::OM_DAM_IMPORTED_FLAG ) ); if ( empty( $found_attachments ) ) { return []; } $map = []; // Remap this in a key/value array. // Also ensures that if there are multiple attachments with the same S3 ID, we only get the one. // Shouldn't happen, but just in case. foreach ( $found_attachments as $attachment ) { // Skip edits. if ( ! empty( get_post_meta( $attachment->post_id, self::IS_EDIT_FLAG, true ) ) ) { continue; } $map[ $attachment->meta_value ] = (int) $attachment->post_id; } return $map; } /** * Get the offloaded attachments. * * [ S3 ID => Attachment Post ID ] * * @param array $images List of images to check. * * @return array */ private function get_offloaded_attachments( $images ) { global $wpdb; $map = []; foreach ( $images as $image ) { $like = '%id:' . $image['meta']['resourceS3'] . '%'; $found_attachments = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value LIKE %s", '_wp_attachment_metadata', $like ) ); if ( empty( $found_attachments ) ) { return []; } $map[ $image['meta']['resourceS3'] ] = (int) $found_attachments[0]->post_id; } return $map; } /** * Adds menu item for DAM. * * @return void */ public function add_menu() { if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) { return; } add_submenu_page( 'optimole', __( 'Cloud Library', 'optimole-wp' ), __( 'Cloud Library', 'optimole-wp' ), 'manage_options', 'optimole-dam', [ $this, 'render_dashboard_page' ] ); } /** * Add media template to be used in the media library. * * @return void */ public function print_media_template() { ?> <script type="text/html" id="tmpl-optimole-dam"> <?php $this->render_dashboard_page(); ?> </script> <?php } /** * Render the dashboard page. * * @return void */ public function render_dashboard_page() { ?> <style> .notice:not(.optml-notice-optin){ display: none !important; } </style> <iframe id="om-dam" style="display: none;" src="<?php echo( $this->build_iframe_url() ); ?>"></iframe> <div class="om-dam-loader"> <img src="<?php echo esc_url( OPTML_URL . 'assets/img/logo.png' ); ?>" alt="Optimole Logo" class="om-dam-logo"> <p><?php echo esc_html__( 'Loading', 'optimole-wp' ); ?>...</p> </div> <?php } /** * Build the iFrame URL. * * @return string */ public function build_iframe_url() { $api_key = $this->settings->get( 'api_key' ); $connected_sites = $this->settings->get( 'cloud_sites' ); if ( empty( $api_key ) ) { return ''; } if ( isset( $connected_sites['all'] ) && $connected_sites['all'] === 'true' ) { $connected_sites = []; } else { foreach ( $connected_sites as $site => $status ) { if ( $status !== 'true' ) { unset( $connected_sites[ $site ] ); } } } $data = [ 'site' => get_site_url(), 'token' => $api_key, 'sites' => array_keys( $connected_sites ), ]; $data = json_encode( $data ); $data = rtrim( base64_encode( $data ), '=' ); return tsdk_translate_link( add_query_arg( [ 'data' => $data, ], $this->dam_endpoint ), 'query' ); } /** * Enqueue script for generating cloud media tab. * * @return void */ public function enqueue_media_scripts() { $asset_file = include OPTML_PATH . 'assets/build/media/media-modal.asset.php'; wp_register_script( OPTML_NAMESPACE . '-media-modal', OPTML_URL . 'assets/build/media/media-modal.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_localize_script( OPTML_NAMESPACE . '-media-modal', 'optmlMediaModal', $this->get_localized_vars() ); wp_enqueue_script( OPTML_NAMESPACE . '-media-modal' ); wp_enqueue_style( OPTML_NAMESPACE . '-media-modal', OPTML_URL . 'assets/build/media/media-modal.css' ); } /** * Enqueue script for generating admin page. * * @return void */ public function enqueue_admin_page_scripts() { $screen = get_current_screen(); if ( $screen->id !== 'optimole_page_optimole-dam' ) { return; } $asset_file = include OPTML_PATH . 'assets/build/media/admin-page.asset.php'; wp_register_script( OPTML_NAMESPACE . '-admin-page', OPTML_URL . 'assets/build/media/admin-page.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_localize_script( OPTML_NAMESPACE . '-admin-page', 'optmlAdminPage', [ 'siteUrl' => get_site_url(), ] ); wp_enqueue_script( OPTML_NAMESPACE . '-admin-page' ); wp_enqueue_style( OPTML_NAMESPACE . '-admin-page', OPTML_URL . 'assets/build/media/admin-page.css' ); } /** * Get localized variables for the media modal. * * @return array */ private function get_localized_vars() { $routes = array_keys( Optml_Rest::$rest_routes['dam_routes'] ); foreach ( $routes as $route ) { $routes[ $route ] = OPTML_NAMESPACE . '/v1/' . $route; } return [ 'nonce' => wp_create_nonce( 'wp_rest' ), 'routes' => $routes, ]; } /** * Alter the image size for the image widget. * * @param string $html the attachment image HTML string. * @param array $settings Control settings. * @param string $image_size_key Optional. Settings key for image size. * Default is `image`. * @param string $image_key Optional. Settings key for image. Default * is null. If not defined uses image size key * as the image key. * * @return string */ public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) { if ( ! isset( $settings['image'] ) ) { return $html; } $image = $settings['image']; if ( ! isset( $image['id'] ) ) { return $html; } if ( ! $this->is_dam_imported_image( $image['id'] ) ) { return $html; } if ( ! isset( $settings['image_size'] ) ) { return $html; } if ( $settings['image_size'] === 'custom' ) { if ( ! isset( $settings['image_custom_dimension'] ) ) { return $html; } $custom_dimensions = $settings['image_custom_dimension']; if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) { return $html; } return $this->replace_dam_url_args( $custom_dimensions, $html ); } $all_sizes = Optml_App_Replacer::image_sizes(); if ( ! isset( $all_sizes[ $image_size_key ] ) ) { return $html; } return $this->replace_dam_url_args( $all_sizes[ $image_size_key ], $html ); } /** * Needed as some blocks might use the image sizes. * * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js(). * @param WP_Post $attachment Attachment object. * @param array|false $meta Array of attachment meta data, or false if there is none. * * @return array */ public function alter_attachment_for_js( $response, $attachment, $meta ) { if ( ! $this->is_dam_imported_image( $attachment->ID ) ) { return $response; } $sizes = Optml_App_Replacer::image_sizes(); $meta = []; if ( isset( $response['width'] ) ) { $meta['width'] = $response['width']; } if ( isset( $response['height'] ) ) { $meta['height'] = $response['height']; } foreach ( $sizes as $size => $args ) { if ( isset( $response['sizes'][ $size ] ) ) { continue; } $args = $this->size_to_dimension( $size, $meta ); $response['sizes'][ $size ] = array_merge( $args, [ 'url' => $this->replace_dam_url_args( $args, $response['url'] ), 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape', ] ); } $response['url'] = $this->replace_dam_url_args( $meta, $response['url'] ); return $response; } /** * We have to short-circuit the logic that adds width and height to the img tag. * It compares the URL basename, and the `file` param for each image. * This happens for any image that gets its size set non-explicitly * e.g. an image block with its size set from the sidebar to `thumbnail`). * * Optimole has a single basename for all image resizes in its URL. * * @param array|false $dimensions Array with first element being the width * and second element being the height, or * false if dimensions could not be determined. * @param string $image_src The image URL (will be Optimole URL). * @param array $image_meta The image metadata. * @param int $attachment_id The image attachment ID. Default 0. */ public function alter_img_tag_w_h( $dimensions, $image_src, $image_meta, $attachment_id ) { if ( ! $this->is_dam_imported_image( $attachment_id ) ) { return $dimensions; } // Get the dimensions from the optimized URL. $incoming_size = $this->parse_dimension_from_optimized_url( $image_src ); $width = $incoming_size[0]; $height = $incoming_size[1]; $sizes = Optml_App_Replacer::image_sizes(); // If this is an image size. Return its dimensions. foreach ( $sizes as $size => $args ) { if ( (int) $args['width'] !== (int) $width ) { continue; } if ( (int) $args['height'] !== (int) $height ) { continue; } return [ $args['width'], $args['height'], ]; } // Fall-through with the original dimensions. return $dimensions; } /** * Replace the image size params in DAM URLs inside a string. * * @param array $args The arguments to replace. * - width: The width of the image. * - height: The height of the image. * - crop: Whether to crop the image. * @param string $subject The string to replace the arguments in. * * @return string */ public function replace_dam_url_args( $args, $subject ) { $args = wp_parse_args( $args, [ 'width' => 'auto', 'height' => 'auto', 'dam' => true ] ); $width = $args['width']; $height = $args['height']; $crop = $args['crop'] ?? $args['resize'] ?? false; $gravity = Position::CENTER; if ( $this->settings->get( 'resize_smart' ) === 'enabled' ) { $gravity = GravityProperty::SMART; } if ( $width === 0 ) { $width = 'auto'; } if ( $height === 0 ) { $height = 'auto'; } // Use the proper replacement for the image size. $replacement = '/w:' . $width . '/h:' . $height; if ( $crop === true ) { $replacement .= '/g:' . $gravity . '/rt:fill'; } elseif ( is_array( $crop ) && ! empty( $crop ) ) { $replacement .= '/' . ( new GravityProperty( $crop['gravity'] ) ) . '/' . new ResizeTypeProperty( $crop['type'] ) . ( $crop['enlarge'] ? '/el:1' : '' ); } $replacement .= '/q:'; if ( $args['dam'] ) { $replacement = self::URL_DAM_FLAG . $replacement; } return preg_replace( '/\/w:(.*)\/h:(.*)\/q:/', $replacement, $subject ); } /** * Elementor checks if the file exists before requesting a specific image size. * * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload. * * Also needed because some * * @param string $file The file path. * @param int $id The attachment ID. * * @return bool|string */ public function alter_attached_file_response( $file, $id ) { if ( ! $this->is_dam_imported_image( $id ) ) { return $file; } $metadata = wp_get_attachment_metadata( $id ); if ( isset( $metadata['file'] ) ) { $uploads = wp_get_upload_dir(); return $uploads['basedir'] . '/' . $metadata['file']; } return true; } /** * Alter the srcSet for DAM images. * * @param array $sources Initial source array. * @param array $size_array Requested size. * @param string $image_src Image source URL. * @param array $image_meta Image meta data. * @param int $attachment_id Image attachment ID. * * @return array */ public function disable_dam_images_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) { if ( ! $this->is_dam_imported_image( $attachment_id ) ) { return $sources; } return []; } } hero_preloader.php 0000644 00000007662 14720403740 0010263 0 ustar 00 <?php /** * Optimole Hero Preloader. * * @package Optimole/Inc * @copyright Copyright (c) 2023, Hardeep Asrani * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License */ /** * Class Optml_Hero_Preloader */ class Optml_Hero_Preloader { /** * Hold the settings object. * * @var Optml_Settings Settings object. */ public $settings; /** * Cached object instance. * * @var Optml_Hero_Preloader */ protected static $instance = null; /** * Has flagged preloading image. * * @var bool */ protected static $has_flagged_preloading_image = false; /** * Has flagged preloading logo to prevent footer logos from being targetted. * * @var bool */ protected static $has_flagged_preloading_logo = false; /** * Class instance method. * * @static * @return Optml_Hero_Preloader * @since 3.9.0 * @access public */ public static function instance() { if ( null === self::$instance || ( self::$instance->settings !== null && ( ! self::$instance->settings->is_connected() ) ) ) { self::$instance = new self(); self::$instance->settings = new Optml_Settings(); if ( self::$instance->settings->is_connected() && ! function_exists( 'wp_get_loading_optimization_attributes' ) ) { self::$instance->init(); } } return self::$instance; } /** * The initialize method. * * @since 3.9.0 * @access public */ public function init() { add_filter( 'get_header_image_tag_attributes', [ $this, 'add_preload' ] ); add_filter( 'post_thumbnail_html', [ $this, 'add_preload_to_thumbnail' ] ); add_filter( 'wp_get_attachment_image_attributes', [ $this, 'add_preload_to_image_attributes' ], 10, 2 ); add_filter( 'get_custom_logo_image_attributes', [ $this, 'add_preload_to_logo' ] ); add_filter( 'wp_content_img_tag', [ $this, 'add_preload_to_thumbnail' ] ); } /** * Add preload attribute to image. * * @since 3.9.0 * @access public * * @param array $attr Image attributes. * * @return array */ public function add_preload( $attr ) { if ( self::$has_flagged_preloading_image ) { return $attr; } self::$has_flagged_preloading_image = true; $attr['fetchpriority'] = 'high'; return $attr; } /** * Add preload attribute to thumbnail. * * @since 3.9.0 * @access public * * @param string $html The post thumbnail HTML. * * @return string */ public function add_preload_to_thumbnail( $html ) { if ( self::$has_flagged_preloading_image ) { return $html; } if ( ! empty( $html ) && strpos( $html, 'loading="lazy"' ) === false && strpos( $html, 'fetchpriority=' ) === false ) { self::$has_flagged_preloading_image = true; $html = str_replace( '<img', '<img fetchpriority="high"', $html ); } return $html; } /** * Filter attachment image attributes. * * @since 3.9.0 * @access public * * @param array $attr Image attributes. * @param object $attachment Image attachment. * * @return array */ public function add_preload_to_image_attributes( $attr, $attachment ) { if ( self::$has_flagged_preloading_image ) { return $attr; } global $wp_query; $post = null; $queried_post = get_queried_object(); if ( is_singular() && $queried_post instanceof WP_Post ) { $post = $queried_post; } elseif ( $wp_query->is_main_query() && $wp_query->post_count > 0 && isset( $wp_query->posts[0] ) ) { $post = $wp_query->posts[0]; } if ( $post instanceof WP_Post && $attachment instanceof WP_Post && (int) get_post_thumbnail_id( $post ) === $attachment->ID ) { $attr = $this->add_preload( $attr ); } return $attr; } /** * Add preload attribute to logo. * * @since 3.9.0 * @access public * * @param array $attr Image attributes. * * @return array */ public function add_preload_to_logo( $attr ) { if ( self::$has_flagged_preloading_logo ) { return $attr; } self::$has_flagged_preloading_logo = true; $attr['fetchpriority'] = 'high'; return $attr; } } conflicts/w3_total_cache_cdn.php 0000644 00000002625 14720403740 0012752 0 ustar 00 <?php /** * Class Optml_w3_total_cache_cdn * * External css/js needs to be added to a list in order to be processed by w3 total cache. */ class Optml_w3_total_cache_cdn extends Optml_Abstract_Conflict { /** * Optml_Jetpack_Lazyload constructor. */ public function __construct() { $this->severity = self::SEVERITY_MEDIUM; parent::__construct(); } /** * Set the message property * * @since 2.0.6 * @access public */ public function define_message() { /* translators: 1 is new line tag, 2 is the starting anchor tag, 3 is the ending anchor tag and 4 is the domain name of Optimole */ $this->message = sprintf( __( 'It seems your are using W3 Total Cache. %1$s If you are using the CSS or JavaScript minify/combine option from %2$sW3 Total Cache -> Performance -> Minify page %3$s %1$s add this line: %4$s to Include external files/libraries and check Use regular expressions for file matching checkbox.', 'optimole-wp' ), '<br/>', '<a target="_blank" href="' . admin_url( 'admin.php?page=w3tc_minify' ) . '">', '</a>', 'https://' . Optml_Main::instance()->admin->settings->get_cdn_url() . '/*' ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.0.6 * @access public */ public function is_conflict_valid() { return Optml_Main::instance()->admin->settings->get( 'cdn' ) === 'enabled' && is_plugin_active( 'w3-total-cache/w3-total-cache.php' ); } } conflicts/conflicting_plugins.php 0000644 00000013067 14720403740 0013311 0 ustar 00 <?php /** * The Conflicting Plugins class, documents and displays dashboard notice for conflicting plugins. * * @package \Optimole\Inc\Conflicts * @author Hardeep Asrani <hardeep@optimole.com> */ /** * Class Optml_Conflicting_Plugins * * @since 3.8.0 */ class Optml_Conflicting_Plugins { /** * Option key. * * @since 3.8.0 * @access private * @var string */ private $option_main = 'optml_dismissed_plugin_conflicts'; /** * Optml_Conflicting_Plugins constructor. * * @since 3.8.0 * @access public */ public function __construct() { add_action( 'wp_ajax_optml_dismiss_conflict_notice', [ $this, 'dismiss_notice' ] ); add_filter( 'all_plugins', [ $this, 'filter_conflicting_plugins' ] ); } /** * Define conflicting plugins. * * @since 3.8.0 * @access private * @return array */ private function defined_plugins() { $plugins = [ 'wp-smush' => 'wp-smushit/wp-smush.php', 'wp-smush-pro' => 'wp-smush-pro/wp-smush.php', 'kraken' => 'kraken-image-optimizer/kraken.php', 'tinypng' => 'tiny-compress-images/tiny-compress-images.php', 'shortpixel' => 'shortpixel-image-optimiser/wp-shortpixel.php', 'ewww' => 'ewww-image-optimizer/ewww-image-optimizer.php', 'ewww-cloud' => 'ewww-image-optimizer-cloud/ewww-image-optimizer-cloud.php', 'imagerecycle' => 'imagerecycle-pdf-image-compression/wp-image-recycle.php', 'imagify' => 'imagify/imagify.php', // 'plugin-slug' => 'plugin-folder/plugin-file.php' ]; return apply_filters( 'optml_conflicting_defined_plugins', $plugins ); } /** * Get a list of active conflicting plugins. * * @since 3.8.0 * @access private * @return array */ private function get_active_plugins() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; $conflicting_plugins = $this->defined_plugins(); $conflicting_plugins = array_filter( $conflicting_plugins, 'is_plugin_active' ); return apply_filters( 'optml_conflicting_active_plugins', $conflicting_plugins ); } /** * Get a list of dismissed conflicting plugins. * * @since 3.8.0 * @access private * @return array */ private function get_dismissed_notices() { $dismissed_conflicts = get_option( $this->option_main, '{}' ); $dismissed_conflicts = json_decode( $dismissed_conflicts, true ); if ( empty( $dismissed_conflicts ) ) { return []; } return $dismissed_conflicts; } /** * Get a list of undismissed conflicting plugins. * * @since 3.8.0 * @access private * @param boolean $show_dismissed Also show the dismissed plugins. * @return array */ public function get_conflicting_plugins( $show_dismissed = false ) { $conflicting_plugins = $this->get_active_plugins(); if ( true === $show_dismissed ) { return $conflicting_plugins; } $dismissed_conflicts = $this->get_dismissed_notices(); $conflicting_plugins = array_diff_key( $conflicting_plugins, $dismissed_conflicts ); return $conflicting_plugins; } /** * Checks if there are any conflicting plugins. * * @since 3.8.0 * @access public * @return boolean */ public function has_conflicting_plugins() { $conflicting_plugins = $this->get_conflicting_plugins(); return ! empty( $conflicting_plugins ); } /** * Dismiss conflicting plugins. * * @since 3.8.0 * @access public */ public function dismiss_conflicting_plugins() { $options = get_option( $this->option_main, '{}' ); $options = json_decode( $options, true ); if ( empty( $options ) ) { $options = []; } $conflicting_plugins = $this->get_conflicting_plugins(); foreach ( $conflicting_plugins as $slug => $file ) { $conflicting_plugins[ $slug ] = true; } $options = array_merge( $options, $conflicting_plugins ); update_option( $this->option_main, wp_json_encode( $options ) ); } /** * Check if we should show the notice. * * @since 3.8.0 * @access public * @return bool Should show? */ public function should_show_notice() { if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { return false; } if ( is_network_admin() ) { return false; } if ( ! current_user_can( 'manage_options' ) ) { return false; } $current_screen = get_current_screen(); if ( empty( $current_screen ) ) { return false; } if ( ! $this->has_conflicting_plugins() ) { return false; } return true; } /** * Filter conflicting plugins. * It will appear at wp-admin/plugins.php?optimole_conflicts * * @since 3.8.0 * @access public * @param array $plugins List of plugins. * @return array */ public function filter_conflicting_plugins( $plugins ) { if ( ! isset( $_GET['optimole_conflicts'] ) ) { return $plugins; } $allowed_plugins = $this->get_conflicting_plugins( true ); $filtered_plugins = []; foreach ( $plugins as $plugin_file => $plugin_data ) { if ( in_array( $plugin_file, $allowed_plugins, true ) ) { $filtered_plugins[ $plugin_file ] = $plugin_data; } } return $filtered_plugins; } /** * Update the option value using AJAX * * @since 3.8.0 * @access public */ public function dismiss_notice() { if ( ! isset( $_POST['nonce'] ) ) { $response = [ 'success' => false, 'message' => 'Missing nonce or value.', ]; wp_send_json( $response ); } $nonce = sanitize_text_field( $_POST['nonce'] ); if ( ! wp_verify_nonce( $nonce, 'optml_dismiss_conflict_notice' ) ) { $response = [ 'success' => false, 'message' => 'Invalid nonce.', ]; wp_send_json( $response ); } $this->dismiss_conflicting_plugins(); $response = [ 'success' => true, ]; wp_send_json( $response ); } } conflicts/divi.php 0000644 00000004211 14720403740 0010173 0 ustar 00 <?php /** * Class Optml_Divi * * An example of a conflict. */ class Optml_Divi extends Optml_Abstract_Conflict { /** * Optml_Divi constructor. */ public function __construct() { $this->priority = 2; $this->severity = self::SEVERITY_HIGH; parent::__construct(); } /** * Set the message property * * @since 2.2.6 * @access public */ public function define_message() { $this->message = sprintf( /* translators: 1 is the start of the bold tag, 2 is ending bold tag, 3 is new line tag, 4 is anchor tag start, 5 is ending anchor tag */ __( 'It seems your are using %1$sDivi%2$s right now. %3$s In order for Optimole to replace the images in your Divi pages, you will need to go to %4$sDivi -> Theme Options -> Builder -> Advanced -> Static CSS File Generations%5$s and click on Clear for the images to be processed. ', 'optimole-wp' ), '<b>', '</b>', '<br/>', '<a target="_blank" href="' . admin_url( 'admin.php?page=et_divi_options' ) . '">', '</a>' ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.2.6 * @access public */ public function is_conflict_valid() { $show_message = true; if ( is_plugin_active( 'divi-builder/divi-builder.php' ) ) { if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/divi-builder/divi-builder.php' ); if ( version_compare( $plugin_data['Version'], '4.10.8', '<' ) ) { $show_message = true; } } $theme = wp_get_theme(); // Divi, no child theme. if ( strcmp( $theme->get( 'Name' ), 'Divi' ) === 0 && $theme->parent() === false && version_compare( $theme->get( 'Version' ), '4.10.8', '<' ) ) { $show_message = true; } // Child theme, parent divi. if ( $theme->parent() !== false && strcmp( $theme->parent()->get( 'Name' ), 'Divi' ) === 0 && version_compare( $theme->parent()->get( 'Version' ), '4.10.8', '<' ) ) { $show_message = true; } if ( ! function_exists( 'et_get_option' ) ) { return false; } if ( 'off' === et_get_option( 'et_pb_static_css_file', 'off' ) ) { return false; } return $show_message; } } conflicts/jetpack_photon.php 0000644 00000002620 14720403740 0012252 0 ustar 00 <?php /** * Class Optml_Jetpack_Photon * * An example of a conflict. */ class Optml_Jetpack_Photon extends Optml_Abstract_Conflict { /** * Optml_Jetpack_Photon constructor. */ public function __construct() { $this->priority = 2; $this->severity = self::SEVERITY_HIGH; parent::__construct(); } /** * Set the message property * * @since 2.0.6 * @access public */ public function define_message() { $this->message = sprintf( /* translators: 1 is the start of the bold tag, 2 is ending bold tag, 3 is new line tag, 4 is anchor tag start, 5 is ending anchor tag, 6 is start of bold tag, 7 is ending bold tag */ __( 'It seems your are using %1$sJetpack%2$s with site accelerator option enabled for images. %3$s To avoid any possible conflicts with Optimole replacement mechanism, you can go to %4$sJetpack -> Perfomance%5$s and turn off the site accelerator option for %6$simages%7$s', 'optimole-wp' ), '<b>', '</b>', '<br/>', '<a target="_blank" href="' . admin_url( 'admin.php?page=jetpack#/performance' ) . '">', '</a>', '<b>', '</b>' ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.0.6 * @access public */ public function is_conflict_valid() { if ( ! is_plugin_active( 'jetpack/jetpack.php' ) ) { return false; } if ( ! class_exists( 'Jetpack', false ) ) { return false; } return Jetpack::is_module_active( 'photon' ); } } conflicts/abstract_conflict.php 0000644 00000005355 14720403740 0012736 0 ustar 00 <?php /** * The Abstract class inherited by all conflicts. * * @package \Optimole\Inc\Conflicts * @author Optimole <friends@optimole.com> */ /** * Class Optml_Abstract_Conflict * * @since 2.0.6 */ abstract class Optml_Abstract_Conflict { /** * Constant for low severity. * * @since 2.0.6 * @const string SEVERITY_LOW */ const SEVERITY_LOW = 'low'; /** * Constant for medium severity. * * @since 2.0.6 * @const string SEVERITY_MEDIUM */ const SEVERITY_MEDIUM = 'medium'; /** * Constant for high severity. * * @since 2.0.6 * @const string SEVERITY_HIGH */ const SEVERITY_HIGH = 'high'; /** * The type of the conflict if required. * * @since 2.0.6 * @access protected * @var string $type */ protected $type = 'base_conflict'; /** * Level of conflict severity. * * @since 2.0.6 * @access protected * @var string $severity */ protected $severity = self::SEVERITY_LOW; /** * Message of the conflict. * * @since 2.0.6 * @access protected * @var string */ protected $message = ''; /** * Priority of conflict for same level of severity conflicts. * * @since 2.0.6 * @access protected * @var int */ protected $priority = 1; /** * Optml_Abstract_Conflict constructor. */ public function __construct() { $this->define_message(); } /** * Set the message property * * @since 2.0.6 * @access public */ abstract public function define_message(); /** * Checks if conflict is active. * * @param array $dismissed_conflicts A list of dismissed conflicts. Passed by the manager. * * @return bool * @since 2.0.6 * @access public */ public function is_active( $dismissed_conflicts = [] ) { $conflict_id = $this->get_id(); if ( isset( $dismissed_conflicts[ $conflict_id ] ) && $dismissed_conflicts[ $conflict_id ] === 'true' ) { return false; } return $this->is_conflict_valid(); } /** * Get the id for the conflict. * * @param int $length Optional. A length for the generated ID. * * @return bool|string * @since 2.0.6 * @access public */ public function get_id( $length = 8 ) { $hash = sha1( strtolower( get_called_class() ) . $this->type . $this->severity . $this->priority ); return substr( $hash, 0, $length ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.0.6 * @access public */ abstract public function is_conflict_valid(); /** * Get the conflict information. * * @return array * @since 2.0.6 * @access public */ public function get_conflict() { return [ 'id' => $this->get_id(), 'type' => $this->type, 'priority' => $this->priority, 'severity' => $this->severity, 'message' => $this->message, ]; } } conflicts/conflict_manager.php 0000644 00000006720 14720403740 0012542 0 ustar 00 <?php /** * The Conflict Manager class, orchestrates conflicts. * * @package \Optimole\Inc\Conflicts * @author Optimole <friends@optimole.com> */ /** * Class Optml_Conflict_Manager * * @since 2.0.6 */ class Optml_Conflict_Manager { /** * List of conflicts to watch. * * @since 2.0.6 * @access protected * @var array $watched_conflicts */ protected $watched_conflicts = []; /** * List of conflicts dismissed by user. * * @since 2.0.6 * @access protected * @var array $dismissed_conflicts */ protected $dismissed_conflicts = []; /** * Optml_Conflict_Manager constructor. * * @since 2.0.6 * @access public * @param array $register_conflicts A list of conflicts to be registered. */ public function __construct( $register_conflicts = [] ) { $this->dismissed_conflicts = get_option( 'optml_dismissed_conflicts', [] ); if ( ! empty( $register_conflicts ) ) { foreach ( $register_conflicts as $conflict_to_watch ) { $this->watch( $conflict_to_watch ); } } } /** * Add a conflict to the watched conflicts. * * @since 2.0.6 * @access public * @param string $conflict A conflict class name. */ public function watch( $conflict ) { if ( is_subclass_of( new $conflict(), 'Optml_Abstract_Conflict' ) ) { array_push( $this->watched_conflicts, new $conflict() ); } } /** * Dismiss conflict. * * @since 2.0.6 * @access public * @param string $id The conflict ID. * * @return bool */ public function dismiss_conflict( $id ) { $this->dismissed_conflicts[ $id ] = 'true'; return update_option( 'optml_dismissed_conflicts', $this->dismissed_conflicts ); } /** * Get the conflict list. * * @since 2.0.6 * @access public * @return array */ public function get_conflict_list() { $conflict_list = []; if ( empty( $this->watched_conflicts ) ) { return $conflict_list; } /** * An instance of Optml_Abstract_Conflict * * @var Optml_Abstract_Conflict $conflict */ foreach ( $this->watched_conflicts as $conflict ) { if ( $conflict->is_active( $this->dismissed_conflicts ) ) { array_push( $conflict_list, $conflict->get_conflict() ); } } // Sort conflicts by severity and priority. usort( $conflict_list, function ( $item1, $item2 ) { if ( ! isset( $item1['severity'] ) ) { return -1; } if ( ! isset( $item2['severity'] ) ) { return -1; } $severity_map = [ 'high' => 0, 'medium' => 1, 'low' => 1, ]; if ( $severity_map[ $item1['severity'] ] === $severity_map[ $item2['severity'] ] ) { if ( ! isset( $item1['priority'] ) ) { return 0; } if ( ! isset( $item2['priority'] ) ) { return 0; } if ( $item1['priority'] === $item2['priority'] ) { return 0; } return $item1['priority'] < $item2['priority'] ? -1 : +1; } return $severity_map[ $item1['severity'] ] < $severity_map[ $item2['severity'] ] ? -1 : 1; } ); return $conflict_list; } /** * Get the total count for active conflicts. * * @since 2.0.6 * @access public * @return int */ public function get_conflict_count() { if ( empty( $this->watched_conflicts ) ) { return 0; } $count = 0; /** * An instance of Optml_Abstract_Conflict * * @var Optml_Abstract_Conflict $conflict */ foreach ( $this->watched_conflicts as $conflict ) { if ( $conflict->is_active( $this->dismissed_conflicts ) ) { ++$count; } } return $count; } } conflicts/jetpack_lazyload.php 0000644 00000003005 14720403740 0012560 0 ustar 00 <?php /** * Class Optml_Jetpack_Lazyload * * An example of a conflict. */ class Optml_Jetpack_Lazyload extends Optml_Abstract_Conflict { /** * Optml_Jetpack_Lazyload constructor. */ public function __construct() { $this->severity = self::SEVERITY_MEDIUM; parent::__construct(); } /** * Set the message property * * @since 2.0.6 * @access public */ public function define_message() { $this->message = sprintf( /* translators: 1 is the start of the bold tag, 2 is ending bold tag, 3 is new line tag, 4 is anchor tag start, 5 is ending anchor tag */__( 'It seems your are using %1$sJetpack%2$s with Lazy loading option ON. %3$s Optimole already provides a lazy loading mechanism by it\'s own which might conflict with this. If you would like to further use Optimole lazy loading feature, you can turn that off from %4$sJetpack -> Perfomance%5$s page. ', 'optimole-wp' ), '<b>', '</b>', '<br/>', '<a target="_blank" href="' . admin_url( 'admin.php?page=jetpack#/performance' ) . '">', '</a>' ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.0.6 * @access public */ public function is_conflict_valid() { if ( ! is_plugin_active( 'jetpack/jetpack.php' ) ) { return false; } if ( ! class_exists( 'Jetpack', false ) ) { return false; } if ( ! class_exists( 'Jetpack', false ) ) { return false; } if ( ! Optml_Main::instance()->admin->settings->use_lazyload() ) { return false; } return Jetpack::is_module_active( 'lazy-images' ); } } conflicts/wprocket.php 0000644 00000002562 14720403740 0011105 0 ustar 00 <?php /** * Class Optml_Wprocket * * An example of a conflict. */ class Optml_Wprocket extends Optml_Abstract_Conflict { /** * Optml_Wprocket constructor. */ public function __construct() { $this->severity = self::SEVERITY_MEDIUM; parent::__construct(); } /** * Set the message property * * @since 2.0.6 * @access public */ public function define_message() { $this->message = sprintf( /* translators: 1 is the start of the bold tag, 2 is ending bold tag, 3 is new line tag, 4 is anchor tag start, 5 is ending anchor tag */__( 'It seems your are using %1$sWP Rocket%2$s with Lazy loading for images option active. %3$s Optimole already provides a lazy loading mechanism by it\'s own which might conflict with this. If you would like to further use Optimole lazy loading feature, you can turn that off from %4$sSettings -> WP Rocket -> Media%5$s page.', 'optimole-wp' ), '<b>', '</b>', '<br/>', '<a target="_blank" href="' . admin_url( 'options-general.php?page=wprocket#media' ) . '">', '</a>' ); } /** * Determine if conflict is applicable. * * @return bool * @since 2.0.6 * @access public */ public function is_conflict_valid() { if ( ! is_plugin_active( 'wp-rocket/wp-rocket.php' ) ) { return false; } if ( ! function_exists( 'get_rocket_option' ) ) { return false; } return get_rocket_option( 'lazyload', false ); } } app_replacer.php 0000644 00000044356 14720403740 0007727 0 ustar 00 <?php use Optimole\Sdk\Optimole; use Optimole\Sdk\ValueObject\Position; /** * Class Optml_App_Replacer * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ abstract class Optml_App_Replacer { use Optml_Dam_Offload_Utils; /** * Filters used for lazyload. * * @var array Lazyload filters. */ protected static $filters = null; /** * Holds an array of image sizes. * * @var array */ protected static $image_sizes = []; /** * Holds width/height to crop array based on possible image sizes. * * @var array */ protected static $size_to_crop = []; /** * Holds possible src attributes. * * @var array */ protected static $possible_src_attributes = null; /** * Holds possible tag replace flags where we should ignore our optimization. * * @var array */ protected static $ignore_tag_strings = null; /** * Holds possible lazyload flags where we should ignore our lazyload. * * @var array */ protected static $ignore_lazyload_strings = null; /** * Holds flags that should ignore the data-opt-tag format. * * @var array */ protected static $ignore_data_opt_attribute = null; /** * Settings handler. * * @var Optml_Settings $settings */ public $settings = null; /** * Cached offloading status. * * @var null|bool Offload status. */ public static $offload_enabled = null; /** * Defines if the dimensions should be limited when images are served. * * @var bool */ protected $limit_dimensions_enabled = false; /** * Defines which is the maximum width accepted when images are served. * * @var int */ protected $limit_width = 1920; /** * Defines which is the maximum height accepted when images are served. * * @var int */ protected $limit_height = 1080; /** * Defines if css minification should be used. * * @var int */ protected $is_css_minify_on = 1; /** * Defines if js minification should be used. * * @var int */ protected $is_js_minify_on = 0; /** * A cached version of `wp_upload_dir` * * @var null|array */ protected $upload_resource = null; /** * Possible domain sources to optimize. * * @var array Domains. */ protected $possible_sources = []; /** * Possible custom sizes definitions. * * @var array Custom sizes definitions. */ private static $custom_size_buffer = []; /** * Whitelisted domains sources to optimize from, according to optimole service. * * @var array Domains. */ protected $allowed_sources = []; /** * Holds site mapping array, * if there is already a cdn and we want to fetch the images from there * and not from he original site. * * @var array Site mappings. */ protected $site_mappings = []; /** * Whether the site is whitelisted or not. Used when signing the urls. * * @var bool Domains. */ protected $is_allowed_site = false; /** * Holds the most recent value for the cache buster, updated to hold only for images if active_cache_buster_assets is defined. * * @var string Cache Buster global value if active_cache_buster_assets is undefined else the value for images. */ protected $active_cache_buster = ''; /** * Holds possible ignored by class urls. * * @var array */ protected static $ignored_url_map = []; /** * Holds the most recent value for the cache buster for assets. * * @var string Cache Buster value. */ protected $active_cache_buster_assets = ''; /** * Returns possible src attributes. * * @return array */ public static function possible_src_attributes() { if ( null !== self::$possible_src_attributes ) { return self::$possible_src_attributes; } self::$possible_src_attributes = apply_filters( 'optml_possible_src_attributes', [] ); return self::$possible_src_attributes; } /** * Returns possible src attributes. * * @return array */ public static function possible_lazyload_flags() { if ( null !== self::$ignore_lazyload_strings ) { return self::$ignore_lazyload_strings; } self::$possible_src_attributes = apply_filters( 'optml_possible_lazyload_flags', [ 'skip-lazy', 'data-skip-lazy' ] ); return array_merge( self::$possible_src_attributes, [ '<noscript' ] ); } /** * Returns possible tag replacement flags. * * @return array */ public static function possible_tag_flags() { if ( null !== self::$ignore_tag_strings ) { return self::$ignore_tag_strings; } self::$ignore_tag_strings = apply_filters( 'optml_skip_optimizations_css_classes', [ 'skip-optimization' ] ); return self::$ignore_tag_strings; } /** * Returns possible data-opt-src ignore flags attributes. * * @return array */ public static function possible_data_ignore_flags() { if ( null !== self::$ignore_data_opt_attribute ) { return self::$ignore_data_opt_attribute; } self::$ignore_data_opt_attribute = apply_filters( 'optml_ignore_data_opt_flag', [] ); return self::$ignore_data_opt_attribute; } /** * Size to crop maping. * * @return array Size mapping. */ protected static function size_to_crop() { if ( ! empty( self::$size_to_crop ) ) { return self::$size_to_crop; } foreach ( self::image_sizes() as $size_data ) { if ( isset( self::$size_to_crop[ $size_data['width'] . $size_data['height'] ] ) && isset( $size_data['enlarge'] ) ) { continue; } self::$size_to_crop[ $size_data['width'] . $size_data['height'] ] = isset( $size_data['enlarge'] ) ? [ 'crop' => $size_data['crop'], 'enlarge' => true, ] : $size_data['crop']; } return self::$size_to_crop; } /** * Set possible custom size. * * @param int $width Width value. * @param int $height Height Value. * @param bool|array $crop Croping. */ public static function add_size( $width = null, $height = null, $crop = null ) { if ( is_null( $width ) || is_null( $height ) ) { return; } self::$custom_size_buffer[ 'cmole' . $width . $height ] = [ 'width' => (int) $width, 'height' => (int) $height, 'enlarge' => true, 'crop' => $crop, ]; } /** * Returns the array of image sizes since `get_intermediate_image_sizes` and image metadata doesn't include the * custom image sizes in a reliable way. * * @return array * @global $wp_additional_image_sizes */ public static function image_sizes() { if ( ! empty( self::$image_sizes ) ) { return self::$image_sizes; } global $_wp_additional_image_sizes; // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes $images = [ 'thumb' => [ 'width' => intval( get_option( 'thumbnail_size_w' ) ), 'height' => intval( get_option( 'thumbnail_size_h' ) ), 'crop' => (bool) get_option( 'thumbnail_crop', false ), ], 'medium' => [ 'width' => intval( get_option( 'medium_size_w' ) ), 'height' => intval( get_option( 'medium_size_h' ) ), 'crop' => false, ], 'large' => [ 'width' => intval( get_option( 'large_size_w' ) ), 'height' => intval( get_option( 'large_size_h' ) ), 'crop' => false, ], 'full' => [ 'width' => null, 'height' => null, 'crop' => false, ], ]; // Compatibility mapping as found in wp-includes/media.php $images['thumbnail'] = $images['thumb']; // Update class variable, merging in $_wp_additional_image_sizes if any are set if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) { self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes ); } else { self::$image_sizes = $images; } self::$image_sizes = array_merge( self::$image_sizes, self::$custom_size_buffer ); self::$image_sizes = array_map( function ( $value ) { $value['crop'] = isset( $value['crop'] ) ? ( is_array( $value['crop'] ) ? $value['crop'] : (bool) $value['crop'] ) : false; return $value; }, self::$image_sizes ); return self::$image_sizes; } /** * The initialize method. */ public function init() { $this->settings = new Optml_Settings(); $this->set_properties(); self::$filters = $this->settings->get_filters(); add_filter( 'optml_should_avif_ext', function ( $should_avif, $ext ) { return $ext !== 'svg'; }, 10, 2 ); add_filter( 'optml_possible_lazyload_flags', function ( $strings = [] ) { foreach ( self::$filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_CLASS ] as $rule_flag => $status ) { $strings[] = $rule_flag; } return $strings; }, 10 ); add_filter( 'optml_skip_optimizations_css_classes', function ( $strings = [] ) { foreach ( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_CLASS ] as $rule_flag => $status ) { $strings[] = $rule_flag; } return $strings; }, 10 ); if ( self::$offload_enabled === null ) { self::$offload_enabled = ( new Optml_Settings() )->is_offload_enabled(); } } /** * Set the cdn url based on the current connected user. */ public function set_properties() { $upload_data = wp_upload_dir(); $this->upload_resource = [ 'url' => str_replace( [ 'https://', 'http://' ], '', $upload_data['baseurl'] ), 'directory' => $upload_data['basedir'], ]; $this->upload_resource['url_length'] = strlen( $this->upload_resource['url'] ); $content_url = content_url(); if ( strpos( $content_url, '/' ) === 0 ) { $content_url = get_site_url() . $content_url; } $content_parts = parse_url( $content_url ); $this->upload_resource['content_path'] = '/'; $this->upload_resource['content_folder'] = '/'; if ( isset( $content_parts['path'] ) ) { $this->upload_resource['content_path'] = $content_parts['path']; $this->upload_resource['content_folder'] = ltrim( $content_parts['path'], '/' ); } $this->upload_resource['content_folder_length'] = strlen( $this->upload_resource['content_folder'] ); $this->upload_resource['content_host'] = $content_parts['scheme'] . '://' . $content_parts['host']; $service_data = $this->settings->get( 'service_data' ); $domain = ''; if ( isset( $service_data['is_cname_assigned'] ) && $service_data['is_cname_assigned'] === 'yes' && ! empty( $service_data['domain'] ) ) { $domain = $service_data['domain']; } Optml_Config::init( [ 'key' => $service_data['cdn_key'], 'secret' => $service_data['cdn_secret'], 'domain' => $domain, 'api_key' => $this->settings->get( 'api_key' ), ] ); if ( defined( 'OPTML_SITE_MIRROR' ) && constant( 'OPTML_SITE_MIRROR' ) ) { $this->site_mappings[ rtrim( get_home_url(), '/' ) ] = rtrim( constant( 'OPTML_SITE_MIRROR' ), '/' ); } $this->possible_sources = $this->extract_domain_from_urls( array_merge( [ get_home_url(), ], array_values( $this->site_mappings ), array_keys( $this->site_mappings ) ) ); $this->allowed_sources = $this->extract_domain_from_urls( $service_data['whitelist'] ); $this->active_cache_buster = $this->settings->get( 'cache_buster_images' ); $this->active_cache_buster_assets = $this->settings->get( 'cache_buster_assets' ); if ( empty( $this->active_cache_buster ) ) { $this->active_cache_buster = $this->settings->get( 'cache_buster' ); } if ( empty( $this->active_cache_buster_assets ) ) { $this->active_cache_buster_assets = $this->settings->get( 'cache_buster' ); } // Allways allow Photon urls. $this->allowed_sources['i0.wp.com'] = true; $this->allowed_sources['i1.wp.com'] = true; $this->allowed_sources['i2.wp.com'] = true; $this->is_allowed_site = count( array_diff_key( $this->possible_sources, $this->allowed_sources ) ) > 0; $this->limit_dimensions_enabled = $this->settings->get( 'limit_dimensions' ) === 'enabled'; if ( $this->limit_dimensions_enabled ) { $this->limit_height = $this->settings->get( 'limit_height' ); $this->limit_width = $this->settings->get( 'limit_width' ); } $this->is_css_minify_on = ( $this->settings->get( 'css_minify' ) === 'enabled' ) ? 1 : 0; $this->is_js_minify_on = ( $this->settings->get( 'js_minify' ) === 'enabled' ) ? 1 : 0; add_filter( 'optml_strip_image_size_from_url', [ $this, 'strip_image_size_from_url' ], 10 ); add_filter( 'image_resize_dimensions', [ __CLASS__, 'listen_to_sizes' ], 999999, 6 ); } /** * Method to expose upload resource property. * * @return null|array */ public function get_upload_resource() { return $this->upload_resource; } /** * List to resizes and save the crop for later re-use. * * @param null $value Original resize. * @param int $orig_w Original W. * @param int $orig_h Original H. * @param int $dest_w Output W. * @param int $dest_h Output H. * @param mixed $crop Cropping behaviour. * * @return mixed Original value. */ public static function listen_to_sizes( $value, $orig_w, $orig_h, $dest_w, $dest_h, $crop ) { self::add_size( $dest_w, $dest_h, $crop ); return $value; } /** * Extract domains and use them as keys for fast processing. * * @param array $urls Input urls. * * @return array Array of domains as keys. */ protected function extract_domain_from_urls( $urls = [] ) { if ( ! is_array( $urls ) ) { return []; } $urls = array_map( function ( $value ) { $parts = parse_url( $value ); $domain = ''; if ( isset( $parts['host'] ) ) { $domain = $parts['host']; } elseif ( isset( $parts['path'] ) ) { $domain = $parts['path']; } return $domain; }, $urls ); $urls = array_filter( $urls ); $urls = array_unique( $urls ); $urls = array_fill_keys( $urls, true ); // build www versions of urls, just in case we need them for validation. foreach ( $urls as $domain => $status ) { if ( ! ( substr( $domain, 0, 4 ) === 'www.' ) ) { $urls[ 'www.' . $domain ] = true; } } return $urls; } /** * Check if we can replace the url. * * @param string $url Url to change. * * @return bool Either we can replace this url or not. */ public function can_replace_url( $url ) { if ( ! is_string( $url ) ) { return false; // @codeCoverageIgnore } if ( $this->url_has_dam_flag( $url ) ) { return true; } $url_parts = parse_url( $url ); if ( ! isset( $url_parts['host'] ) ) { return false; } if ( false === ( isset( $this->possible_sources[ $url_parts['host'] ] ) || isset( $this->allowed_sources[ $url_parts['host'] ] ) ) ) { return false; } if ( false === Optml_Filters::should_do_image( $url, self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) { return false; } if ( ! empty( self::$ignored_url_map ) && isset( self::$ignored_url_map[ crc32( $url ) ] ) ) { return false; } return true; } /** * Checks if the file is a image size and return the full url. * * @param string $url The image URL. * * @return string **/ public function strip_image_size_from_url( $url ) { if ( apply_filters( 'optml_should_skip_strip_image_size', false, $url ) === true ) { return $url; } if ( preg_match( '#(-\d+x\d+(?:_c)?|(@2x))\.(' . implode( '|', array_keys( Optml_Config::$image_extensions ) ) . '){1}$#i', $url, $src_parts ) ) { $stripped_url = str_replace( $src_parts[1], '', $url ); if ( $this->settings->is_offload_enabled() ) { // Treat offloaded attachments differently. $id = $this->attachment_url_to_post_id( $stripped_url ); if ( $id !== 0 && $this->is_new_offloaded_attachment( $id ) ) { return $stripped_url; } } // Extracts the file path to the image minus the base url $file_path = substr( $stripped_url, strpos( $stripped_url, $this->upload_resource['url'] ) + $this->upload_resource['url_length'] ); if ( file_exists( $this->upload_resource['directory'] . $file_path ) ) { return $stripped_url; } } return $url; } /** * Try to determine height and width from strings WP appends to resized image filenames. * * @param string $src The image URL. * * @return array An array consisting of width and height. */ protected function parse_dimensions_from_filename( $src ) { $width_height_string = []; $extensions = array_keys( Optml_Config::$image_extensions ); if ( preg_match( '#-(\d+)x(\d+)(:?_c)?\.(?:' . implode( '|', $extensions ) . '){1}$#i', $src, $width_height_string ) ) { $width = (int) $width_height_string[1]; $height = (int) $width_height_string[2]; $crop = ( isset( $width_height_string[3] ) && $width_height_string[3] === '_c' ); if ( $width && $height ) { return [ $width, $height, $crop ]; } } return [ false, false, false ]; } /** * Get the optimized urls for the wp media modal. * * @param string $url Original url. * @param string $table_id The cloud id of the image. * @param int|string $width Image width. * @param int|string $height Image height. * @param array $resize Optml crop array. * @return string The optimized url. */ public function get_media_optimized_url( $url, $table_id, $width = 'auto', $height = 'auto', $resize = [] ) { return str_replace( $url, Optml_Media_Offload::KEYS['not_processed_flag'] . 'media_cloud' . '/' . Optml_Media_Offload::KEYS['uploaded_flag'] . $table_id . '/' . $url, $this->get_optimized_image_url( $url, $width, $height, $resize ) ); } /** * Test that the url has the dam flag. * * @param string $url The image URL to check. * * @return bool */ public function url_has_dam_flag( $url ) { return strpos( $url, Optml_Dam::URL_DAM_FLAG ) !== false; } /** * Get the optimized image url for the image url. * * @param string $url The image URL. * @param mixed $width The image width. * @param mixed $height The image height. * @param array $resize The resize properties. * * @return string */ protected function get_optimized_image_url( $url, $width, $height, $resize = [] ) { $width = is_int( $width ) ? $width : 'auto'; $height = is_int( $height ) ? $height : 'auto'; $optimized_image = Optimole::image( $url, $this->settings->get( 'cache_buster' ) ) ->width( $width ) ->height( $height ); if ( is_array( $resize ) && ! empty( $resize['type'] ) ) { $optimized_image->resize( $resize['type'], $resize['gravity'] ?? Position::CENTER, $resize['enlarge'] ?? false ); } $optimized_image->quality( $this->settings->to_accepted_quality( $this->settings->get_quality() ) ); return $optimized_image->getUrl(); } } lazyload_replacer.php 0000644 00000050536 14720403740 0010763 0 ustar 00 <?php /** * The class handles the img tag replacements for lazyload. * * @package \Optml\Inc * @author Optimole <friends@optimole.com> */ final class Optml_Lazyload_Replacer extends Optml_App_Replacer { use Optml_Normalizer; use Optml_Validator; const IFRAME_PLACEHOLDER_CLASS = ' iframe[data-opt-src]:not([data-opt-lazy-loaded]) { background-color: #ffffff; background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2240%22%20height%3D%2240%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22-webkit-transform-origin%3A50%25%2050%25%3B-webkit-animation%3Aspin%201.5s%20linear%20infinite%3B-webkit-backface-visibility%3Ahidden%3Banimation%3Aspin%201.5s%20linear%20infinite%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cdefs%3E%3Cstyle%3E%3C%21%5BCDATA%5B%40-webkit-keyframes%20spin%7Bfrom%7B-webkit-transform%3Arotate%280deg%29%7Dto%7B-webkit-transform%3Arotate%28-359deg%29%7D%7D%40keyframes%20spin%7Bfrom%7Btransform%3Arotate%280deg%29%7Dto%7Btransform%3Arotate%28-359deg%29%7D%7D%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22outer%22%3E%3Cpath%20d%3D%22M20%200a3.994%203.994%200%20110%207.988A3.994%203.994%200%200120%200z%22%2F%3E%3Cpath%20d%3D%22M5.858%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23d2d2d2%22%2F%3E%3Cpath%20d%3D%22M20%2032.012A3.994%203.994%200%201120%2040a3.994%203.994%200%20010-7.988z%22%20fill%3D%22%23828282%22%2F%3E%3Cpath%20d%3D%22M28.494%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23656565%22%2F%3E%3Cpath%20d%3D%22M3.994%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%23bbb%22%2F%3E%3Cpath%20d%3D%22M5.858%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23a4a4a4%22%2F%3E%3Cpath%20d%3D%22M36.006%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%234a4a4a%22%2F%3E%3Cpath%20d%3D%22M28.494%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23323232%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); background-repeat: no-repeat; background-position: 50% 50%; } video[data-opt-src]:not([data-opt-lazy-loaded]) { background-color: #ffffff; background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2240%22%20height%3D%2240%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22-webkit-transform-origin%3A50%25%2050%25%3B-webkit-animation%3Aspin%201.5s%20linear%20infinite%3B-webkit-backface-visibility%3Ahidden%3Banimation%3Aspin%201.5s%20linear%20infinite%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cdefs%3E%3Cstyle%3E%3C%21%5BCDATA%5B%40-webkit-keyframes%20spin%7Bfrom%7B-webkit-transform%3Arotate%280deg%29%7Dto%7B-webkit-transform%3Arotate%28-359deg%29%7D%7D%40keyframes%20spin%7Bfrom%7Btransform%3Arotate%280deg%29%7Dto%7Btransform%3Arotate%28-359deg%29%7D%7D%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22outer%22%3E%3Cpath%20d%3D%22M20%200a3.994%203.994%200%20110%207.988A3.994%203.994%200%200120%200z%22%2F%3E%3Cpath%20d%3D%22M5.858%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23d2d2d2%22%2F%3E%3Cpath%20d%3D%22M20%2032.012A3.994%203.994%200%201120%2040a3.994%203.994%200%20010-7.988z%22%20fill%3D%22%23828282%22%2F%3E%3Cpath%20d%3D%22M28.494%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23656565%22%2F%3E%3Cpath%20d%3D%22M3.994%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%23bbb%22%2F%3E%3Cpath%20d%3D%22M5.858%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23a4a4a4%22%2F%3E%3Cpath%20d%3D%22M36.006%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%234a4a4a%22%2F%3E%3Cpath%20d%3D%22M28.494%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23323232%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); background-repeat: no-repeat; background-position: 50% 50%; }'; const IFRAME_PLACEHOLDER_STYLE = '<style type="text/css">' . self::IFRAME_PLACEHOLDER_CLASS . '</style>'; const IFRAME_TEMP_COMMENT = '/** optmliframelazyloadplaceholder */'; const SVG_PLACEHOLDER = 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20#width#%20#height#%22%20width%3D%22#width#%22%20height%3D%22#height#%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22#width#%22%20height%3D%22#height#%22%20fill%3D%22#fill#%22%2F%3E%3C%2Fsvg%3E'; /** * If frame lazyload is present on page. * * @var bool Whether or not at least one iframe has been lazyloaded. */ private static $found_iframe = false; /** * Cached object instance. * * @var Optml_Lazyload_Replacer */ protected static $instance = null; /** * Holds the number of images to skip from lazyload. * * @var integer The number image tags to skip. */ private static $skip_lazyload_images = null; /** * Holds classes for listening to lazyload on background. * * @var array Lazyload background classes. */ private static $lazyload_background_classes = null; /** * Selectors used for background lazyload. * * @var array Lazyload background CSS selectors. */ private static $background_lazyload_selectors = null; /** * Holds flags which remove noscript tag bundle causing issues on render, i.e slider plugins. * * @var array Noscript flags. */ private static $ignore_no_script_flags = null; /** * Holds possible iframe lazyload flags where we should ignore our lazyload. * * @var array */ protected static $iframe_lazyload_flags = null; /** * Holds classes responsabile for watching lazyload behaviour. * * @var array Lazyload classes. */ private static $lazyload_watcher_classes = null; /** * Should we use the generic placeholder? * * @var bool Lazyload placeholder flag. */ private static $is_lazyload_placeholder = false; /** * Class instance method. * * @codeCoverageIgnore * @static * @return Optml_Lazyload_Replacer * @since 1.0.0 * @access public */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] ); } return self::$instance; } /** * Return lazyload selectors for background images. * * @return array Lazyload selectors. */ public static function get_background_lazyload_selectors() { if ( null !== self::$background_lazyload_selectors ) { return self::$background_lazyload_selectors; } if ( self::instance()->settings->get( 'bg_replacer' ) === 'disabled' ) { self::$background_lazyload_selectors = []; return self::$background_lazyload_selectors; } $default_watchers = [ '.elementor-section[data-settings*="background_background"]', '.elementor-section > .elementor-background-overlay', '[class*="wp-block-cover"][style*="background-image"]', '[class*="wp-block-group"][style*="background-image"]', ]; $saved_watchers = self::instance()->settings->get_watchers(); $saved_watchers = str_replace( [ "\n", "\r" ], ',', $saved_watchers ); $saved_watchers = explode( ',', $saved_watchers ); $all_watchers = array_merge( $default_watchers, $saved_watchers ); $all_watchers = apply_filters( 'optml_lazyload_bg_selectors', $all_watchers ); $all_watchers = array_filter( $all_watchers, function ( $value ) { return ! empty( $value ) && strlen( $value ) >= 2; } ); self::$background_lazyload_selectors = $all_watchers; return self::$background_lazyload_selectors; } /** * Returns background classes for lazyload. * * @return array */ public static function get_lazyload_bg_classes() { if ( null !== self::$lazyload_background_classes ) { return self::$lazyload_background_classes; } self::$lazyload_background_classes = apply_filters( 'optml_lazyload_bg_classes', [] ); return self::$lazyload_background_classes; } /** * Returns the number of images to skip from lazyload. * * @return integer */ public static function get_skip_lazyload_limit() { if ( self::$skip_lazyload_images !== null ) { return self::$skip_lazyload_images; } self::$skip_lazyload_images = apply_filters( 'optml_lazyload_images_skip', self::instance()->settings->get( 'skip_lazyload_images' ) ); return self::$skip_lazyload_images; } /** * Returns classes for lazyload additional watch. * * @return array */ public static function get_watcher_lz_classes() { if ( null !== self::$lazyload_watcher_classes ) { return self::$lazyload_watcher_classes; } self::$lazyload_watcher_classes = apply_filters( 'optml_watcher_lz_classes', [] ); return self::$lazyload_watcher_classes; } /** * The initialize method. */ public function init() { parent::init(); if ( ! $this->settings->use_lazyload() ) { return; } $filters = $this->settings->get_filters(); if ( ! Optml_Filters::should_do_page( $filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_URL ], $filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_URL_MATCH ] ) ) { return; } self::$is_lazyload_placeholder = self::$instance->settings->get( 'lazyload_placeholder' ) === 'enabled'; add_filter( 'optml_tag_replace', [ $this, 'lazyload_tag_replace' ], 2, 6 ); add_filter( 'optml_video_replace', [ $this, 'lazyload_video_replace' ], 2, 1 ); } /** * Check if there are lazyloaded iframes. * * @return bool Whether an iframe was lazyloaded on the page or not. */ public static function found_iframe() { return self::$found_iframe; } /** * Check if the img tag should be lazyloaded early. * * @param string $image_tag The image tag. * @return bool Whether the image tag should be lazyloaded early or not. */ public function should_lazyload_early( $image_tag ) { $flags = apply_filters( 'optml_lazyload_early_flags', [] ); foreach ( $flags as $flag ) { if ( strpos( $image_tag, $flag ) !== false ) { return true; } } return false; } /** * Replaces the tags with lazyload tags. * * @param string $new_tag The new tag. * @param string $original_url The original URL. * @param string $new_url The optimized URL. * @param array $optml_args Options passed for URL optimization. * @param bool $is_slashed If the url needs slashes. * @param string $full_tag Full tag, wrapper included. * * @return string */ public function lazyload_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) { if ( ! $this->can_lazyload_for( $original_url, $full_tag ) ) { return Optml_Tag_Replacer::instance()->regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed ); } if ( self::instance()->settings->get( 'native_lazyload' ) === 'enabled' ) { if ( strpos( $new_tag, 'loading=' ) === false ) { $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img loading=\"lazy\"' : '<img loading="lazy"', $new_tag ); } return $new_tag; } $should_ignore_rescale = ! $this->is_valid_mimetype_from_url( $original_url, [ 'gif' => true, 'svg' => true ] ); if ( ! self::$is_lazyload_placeholder && ! $should_ignore_rescale ) { $optml_args['quality'] = 'eco'; $optml_args['resize'] = []; $low_url = apply_filters( 'optml_content_url', $original_url, $optml_args ); $low_url = $is_slashed ? addcslashes( $low_url, '/' ) : $low_url; } else { $low_url = $this->get_svg_for( isset( $optml_args['width'] ) ? $optml_args['width'] : '100%', isset( $optml_args['height'] ) ? $optml_args['height'] : '100%', ( $should_ignore_rescale ? null : $original_url ) ); } $opt_format = ''; if ( $this->should_add_data_tag( $full_tag ) ) { $opt_format = ' data-opt-src="%s" '; $custom_class = ''; if ( $should_ignore_rescale ) { $custom_class .= 'optimole-lazy-only '; } if ( $this->should_lazyload_early( $full_tag ) ) { $custom_class .= 'optimole-load-early'; } if ( ! empty( $custom_class ) ) { if ( strpos( $new_tag, 'class=' ) === false ) { $opt_format .= ' class="' . $custom_class . '" '; } else { $new_tag = str_replace( ( $is_slashed ? 'class=\"' : 'class="' ), ( $is_slashed ? 'class=\"' . $custom_class . ' ' : 'class="' . $custom_class . ' ' ), $new_tag ); } } $opt_format = $is_slashed ? addslashes( $opt_format ) : $opt_format; } $new_url = $is_slashed ? addcslashes( $new_url, '/' ) : $new_url; $opt_src = sprintf( $opt_format, $new_url ); $no_script_tag = str_replace( $original_url, $new_url, $new_tag ); $new_tag = preg_replace( [ '/((?:\s|\'|"){1,}src(?>=|"|\'|\s|\\\\)*)' . preg_quote( $original_url, '/' ) . '/m', '/<img/im', ], [ "$1$low_url", '<img' . $opt_src, ], $new_tag, 1 ); // We remove srcset and sizes attributes since our lazyload does not need them. $pattern = '/\s+(?:srcset|sizes)=("[^"]*"|\'[^\']*\'|\\\\"[^\\\\"]*\\\\")/i'; $new_tag = preg_replace( $pattern, '', $new_tag ); // We keep this for srcset lazyload compatibility that might break our mechanism. $new_tag = str_replace( 'srcset=', 'old-srcset=', $new_tag ); if ( ! $this->should_add_noscript( $new_tag ) ) { return $new_tag; } return $new_tag . '<noscript>' . $no_script_tag . '</noscript>'; } /** * Replaces video embeds with lazyload embeds. * * @param string $content Html page content. * * @return string */ public function lazyload_video_replace( $content ) { $video_tags = []; $iframes = []; $videos = []; preg_match_all( '#(?:<noscript\s*>\s*)?<iframe(.*?)></iframe>(?:\s*</noscript\s*>)?#is', $content, $iframes ); preg_match_all( '#(?:<noscript\s*>\s*)?<video(.*?)>.*?</video>(?:\s*</noscript\s*>)?#is', $content, $videos ); $video_tags = array_merge( $iframes[0], $videos[0] ); $search = []; $replace = []; foreach ( $video_tags as $video_tag ) { if ( ! $this->should_lazyload_iframe( $video_tag ) ) { continue; } if ( preg_match( "/ data-opt-src=['\"]/is", $video_tag ) ) { continue; } $should_add_noscript = true; $original_tag = $video_tag; // replace the src and add the data-opt-src attribute if ( strpos( $video_tag, 'iframe' ) !== false ) { $video_tag = preg_replace( '/iframe(.*?)src=/is', 'iframe$1 src="about:blank" data-opt-src=', $video_tag ); } elseif ( strpos( $video_tag, 'video' ) !== false ) { if ( strpos( $video_tag, 'source' ) !== false ) { if ( strpos( $video_tag, 'preload' ) === false ) { $video_tag = preg_replace( '/video(.*?)>/is', 'video$1 preload="metadata">', $video_tag, 1 ); $should_add_noscript = false; } else { continue; } } else { $video_tag = preg_replace( '/video(.*?)src=/is', 'video$1 data-opt-src=', $video_tag ); } } if ( $this->should_add_noscript( $video_tag ) && $should_add_noscript ) { $video_tag .= '<noscript>' . $original_tag . '</noscript>'; } array_push( $search, $original_tag ); array_push( $replace, $video_tag ); self::$found_iframe = true; } $search = array_unique( $search ); $replace = array_unique( $replace ); $content = str_replace( $search, $replace, $content ); return $content; } /** * Check if the lazyload is allowed for this url. * * @param string $url Url. * @param string $tag Html tag. * * @return bool We can lazyload? */ public function can_lazyload_for( $url, $tag = '' ) { foreach ( self::possible_lazyload_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { return false; } } if ( false === Optml_Filters::should_do_image( $url, self::$filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_FILENAME ] ) ) { return false; } $url = strtok( $url, '?' ); $type = wp_check_filetype( basename( $url ), Optml_Config::$image_extensions ); if ( empty( $type['ext'] ) ) { return false; } if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_EXT ], $type['ext'] ) ) { return false; } if ( defined( 'OPTML_DISABLE_PNG_LAZYLOAD' ) && OPTML_DISABLE_PNG_LAZYLOAD ) { return $type['ext'] !== 'png'; } if ( Optml_Tag_Replacer::$lazyload_skipped_images < self::get_skip_lazyload_limit() ) { return false; } return true; } /** * Check if we should add the data-opt-tag. * * @param string $tag Html tag. * * @return bool Should add? */ public function should_add_data_tag( $tag ) { foreach ( self::possible_data_ignore_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { return false; } } return true; } /** * Get SVG markup with specific width/height. * * @param int|string $width Markup Width. * @param int|string $height Markup Height. * @param string|null $url Original URL. * * @return string SVG code. */ public function get_svg_for( $width, $height, $url = null ) { if ( $url !== null && ! is_numeric( $width ) ) { $url = strtok( $url, '?' ); $key = crc32( $url ); $sizes = wp_cache_get( $key, 'optml_sources' ); if ( $sizes === false ) { $filepath = substr( $url, strpos( $url, $this->upload_resource['content_folder'] ) + $this->upload_resource['content_folder_length'] ); $filepath = WP_CONTENT_DIR . $filepath; if ( is_file( $filepath ) ) { $sizes = getimagesize( $filepath ); wp_cache_add( $key, [ $sizes[0], $sizes[1] ], 'optml_sources', DAY_IN_SECONDS ); } } list( $width, $height ) = $sizes; } $width = ! is_numeric( $width ) ? '100%' : $width; $height = ! is_numeric( $height ) ? '100%' : $height; $fill = $this->settings->get( 'placeholder_color' ); if ( empty( $fill ) ) { $fill = 'transparent'; } return str_replace( [ '#width#', '#height#', '#fill#' ], [ $width, $height, urlencode( $fill ), ], self::SVG_PLACEHOLDER ); } /** * Check if we should add the noscript tag. * * @param string $tag Html tag. * * @return bool Should add? */ public function should_add_noscript( $tag ) { if ( $this->settings->get( 'no_script' ) === 'disabled' ) { return false; } foreach ( self::get_ignore_noscript_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { return false; } } return true; } /** * Check if we should lazyload iframe. * * @param string $tag Html tag. * * @return bool Should add? */ public function should_lazyload_iframe( $tag ) { foreach ( self::get_iframe_lazyload_flags() as $banned_string ) { if ( strpos( $tag, $banned_string ) !== false ) { return false; } } return true; } /** * Returns flags for ignoring noscript tag additional watch. * * @return array */ public static function get_ignore_noscript_flags() { if ( null !== self::$ignore_no_script_flags ) { return self::$ignore_no_script_flags; } self::$ignore_no_script_flags = apply_filters( 'optml_ignore_noscript_on', [] ); return self::$ignore_no_script_flags; } /** * Returns possible lazyload flags for iframes. * * @return array */ public static function get_iframe_lazyload_flags() { if ( null !== self::$iframe_lazyload_flags ) { return self::$iframe_lazyload_flags; } self::$iframe_lazyload_flags = self::possible_lazyload_flags(); self::$iframe_lazyload_flags = array_merge( self::$iframe_lazyload_flags, apply_filters( 'optml_iframe_lazyload_flags', [ 'gform_ajax_frame', '<noscript', 'recaptcha', '-src' ] ) ); return self::$iframe_lazyload_flags; } /** * Throw error on object clone * * The whole idea of the singleton design pattern is that there is a single * object therefore, we don't want the object to be cloned. * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } /** * Disable unserializing of the class * * @codeCoverageIgnore * @access public * @return void * @since 1.0.0 */ public function __wakeup() { // Unserializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); } } admin.php 0000644 00000273051 14720403740 0006356 0 ustar 00 <?php /** * Admin class. * * Author: Andrei Baicus <andrei@themeisle.com> * Created on: 19/07/2018 * * @soundtrack Somewhere Else - Marillion * @package \Optimole\Inc * @author Optimole <friends@optimole.com> */ use enshrined\svgSanitize\Sanitizer; /** * Class Optml_Admin */ class Optml_Admin { use Optml_Normalizer; const IMAGE_DATA_COLLECTED = 'optml_image_data_collected'; const IMAGE_DATA_COLLECTED_BATCH = 100; const BF_PROMO_DISMISS_KEY = 'optml_bf24_notice_dismiss'; /** * Hold the settings object. * * @var Optml_Settings Settings object. */ public $settings; /** * Hold the plugin conflict object. * * @var Optml_Conflicting_Plugins Settings object. */ public $conflicting_plugins; const NEW_USER_DEFAULTS_UPDATED = 'optml_defaults_updated'; const OLD_USER_ENABLED_LD = 'optml_enabled_limit_dimensions'; const OLD_USER_ENABLED_CL = 'optml_enabled_cloud_sites'; /** * Optml_Admin constructor. */ public function __construct() { $this->settings = new Optml_Settings(); $this->conflicting_plugins = new Optml_Conflicting_Plugins(); add_filter( 'plugin_action_links_' . plugin_basename( OPTML_BASEFILE ), [ $this, 'add_action_links' ] ); add_action( 'admin_menu', [ $this, 'add_dashboard_page' ] ); add_action( 'admin_menu', [ $this, 'add_settings_subpage' ], 99 ); add_action( 'admin_head', [ $this, 'menu_icon_style' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MIN ); add_action( 'admin_notices', [ $this, 'add_notice' ] ); add_action( 'admin_notices', [ $this, 'add_notice_upgrade' ] ); add_action( 'admin_notices', [ $this, 'add_notice_conflicts' ] ); add_action( 'optml_daily_sync', [ $this, 'daily_sync' ] ); add_action( 'admin_init', [ $this, 'redirect_old_dashboard' ] ); if ( $this->settings->is_connected() ) { add_action( 'init', [ $this, 'check_domain_change' ] ); add_action( 'optml_pull_image_data_init', [ $this, 'pull_image_data_init' ] ); add_action( 'optml_pull_image_data', [ $this, 'pull_image_data' ] ); // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); if ( $below_6_0_0 ) { add_filter( 'wp_insert_attachment_data', [ $this, 'legacy_detect_image_title_changes' ], 10, 3 ); } else { add_filter( 'wp_insert_attachment_data', [ $this, 'detect_image_title_changes' ], 10, 4 ); } add_action( 'updated_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 ); add_action( 'added_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 ); add_action( 'init', [ $this, 'schedule_data_enhance_cron' ] ); } add_action( 'init', [ $this, 'update_default_settings' ] ); add_action( 'init', [ $this, 'update_limit_dimensions' ] ); add_action( 'init', [ $this, 'update_cloud_sites_default' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect' ] ); add_action( 'admin_init', [ $this, 'init_no_script' ] ); if ( ! is_admin() && $this->settings->is_connected() && ! wp_next_scheduled( 'optml_daily_sync' ) ) { wp_schedule_event( time() + 10, 'daily', 'optml_daily_sync', [] ); } add_action( 'optml_after_setup', [ $this, 'register_public_actions' ], 999999 ); if ( ! function_exists( 'is_wpcom_vip' ) ) { add_filter( 'upload_mimes', [ $this, 'allow_svg', ] ); // phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.upload_mimes add_filter( 'wp_handle_upload_prefilter', [ $this, 'check_svg_and_sanitize' ] ); } } /** * Check if the file is an SVG, if so handle appropriately * * @param array $file An array of data for a single file. * * @return mixed */ public function check_svg_and_sanitize( $file ) { // Ensure we have a proper file path before processing. if ( ! isset( $file['tmp_name'] ) ) { return $file; } $file_name = isset( $file['name'] ) ? $file['name'] : ''; $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file_name ); $type = ! empty( $wp_filetype['type'] ) ? $wp_filetype['type'] : ''; if ( 'image/svg+xml' === $type ) { if ( ! current_user_can( 'upload_files' ) ) { $file['error'] = 'Invalid'; return $file; } if ( ! $this->sanitize_svg( $file['tmp_name'] ) ) { $file['error'] = 'Invalid'; } } return $file; } /** * Sanitize the SVG * * @param string $file Temp file path. * * @return bool|int */ protected function sanitize_svg( $file ) { // We can ignore the phpcs warning here as we're reading and writing to the Temp file. $dirty = file_get_contents( $file ); // phpcs:ignore // Is the SVG gzipped? If so we try and decode the string. $is_zipped = $this->is_gzipped( $dirty ); if ( $is_zipped && ( ! function_exists( 'gzdecode' ) || ! function_exists( 'gzencode' ) ) ) { return false; } if ( $is_zipped ) { $dirty = gzdecode( $dirty ); // If decoding fails, bail as we're not secure. if ( false === $dirty ) { return false; } } $sanitizer = new Sanitizer(); $clean = $sanitizer->sanitize( $dirty ); if ( false === $clean ) { return false; } // If we were gzipped, we need to re-zip. if ( $is_zipped ) { $clean = gzencode( $clean ); } // We can ignore the phpcs warning here as we're reading and writing to the Temp file. file_put_contents( $file, $clean ); // phpcs:ignore return true; } /** * Check if the contents are gzipped * * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format * * @param string $contents Content to check. * * @return bool */ protected function is_gzipped( $contents ) { // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found if ( function_exists( 'mb_strpos' ) ) { return 0 === mb_strpos( $contents, "\x1f" . "\x8b" . "\x08" ); } else { return 0 === strpos( $contents, "\x1f" . "\x8b" . "\x08" ); } // phpcs:enable } /** * Schedules the hourly cron that starts the querying for images alt/title attributes * * @uses action: init */ public function schedule_data_enhance_cron() { if ( ! wp_next_scheduled( 'optml_pull_image_data_init' ) ) { wp_schedule_event( time(), 'hourly', 'optml_pull_image_data_init' ); } } /** * Query the database for images and extract the alt/title to send them to the API * * @uses action: optml_pull_image_data */ public function pull_image_data() { // Get all image attachments that are not processed $args = [ 'post_type' => 'attachment', 'post_mime_type' => 'image', 'posts_per_page' => self::IMAGE_DATA_COLLECTED_BATCH, 'meta_query' => [ 'relation' => 'AND', [ 'key' => self::IMAGE_DATA_COLLECTED, 'compare' => 'NOT EXISTS', ], ], ]; $attachments = get_posts( $args ); $image_data = []; foreach ( $attachments as $attachment ) { // Get image URL, alt, and title $image_url = wp_get_attachment_url( $attachment->ID ); $image_alt = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ); $image_title = $attachment->post_title; $image_data[ $image_url ] = []; if ( ! empty( $image_alt ) ) { $image_data[ $image_url ]['alt'] = $image_alt; } if ( ! empty( $image_title ) ) { $image_data[ $image_url ]['title'] = $image_title; } if ( empty( $image_data[ $image_url ] ) ) { unset( $image_data[ $image_url ] ); } // Mark the image as processed update_post_meta( $attachment->ID, self::IMAGE_DATA_COLLECTED, 'yes' ); } if ( ! empty( $image_data ) ) { $api = new Optml_Api(); $api->call_data_enrich_api( $image_data ); } if ( ! empty( $attachments ) ) { wp_schedule_single_event( time() + 5, 'optml_pull_image_data' ); } } /** * Schedule the event to pull image alt/title * * @uses action: optml_pull_image_data_init */ public function pull_image_data_init() { if ( ! wp_next_scheduled( 'optml_pull_image_data' ) ) { wp_schedule_single_event( time() + 5, 'optml_pull_image_data' ); } } /** * Delete the processed meta from an image when the alt text is changed * * @uses action: updated_post_meta, added_post_meta */ public function detect_image_alt_change( $meta_id, $post_id, $meta_key, $meta_value ) { // Check if the updated metadata is for alt text and it's an attachment if ( $meta_key === '_wp_attachment_image_alt' && get_post_type( $post_id ) === 'attachment' ) { delete_post_meta( $post_id, self::IMAGE_DATA_COLLECTED ); } } /** * Delete the processed meta from an image when the title is changed * * @uses filter: wp_insert_attachment_data */ public function detect_image_title_changes( $data, $postarr, $unsanitized_postarr, $update ) { // Check if it's an attachment being updated if ( $data['post_type'] !== 'attachment' ) { return $data; } if ( ! isset( $postarr['ID'] ) ) { return $data; } if ( isset( $postarr['post_title'] ) && isset( $postarr['original_post_title'] ) && $postarr['post_title'] !== $postarr['original_post_title'] ) { delete_post_meta( $postarr['ID'], self::IMAGE_DATA_COLLECTED ); } return $data; } /** * Delete the processed meta from an image when the title is changed * * @uses filter: wp_insert_attachment_data */ public function legacy_detect_image_title_changes( $data, $postarr, $unsanitized_postarr ) { return $this->detect_image_title_changes( $data, $postarr, $unsanitized_postarr, true ); } /** * Init no_script setup value based on whether the user is connected or not. */ public function init_no_script() { if ( $this->settings->is_connected() ) { $raw_settings = $this->settings->get_raw_settings(); if ( ! isset( $raw_settings['no_script'] ) ) { $this->settings->update( 'no_script', 'enabled' ); } } } /** * Checks if domain has changed */ public function check_domain_change() { $previous_domain = get_option( 'optml_current_domain', 0 ); $site_url = $this->to_domain_hash( get_home_url() ); if ( $site_url !== $previous_domain ) { update_option( 'optml_current_domain', $site_url ); if ( $previous_domain !== 0 ) { $this->daily_sync(); } } } /** * Update the limit dimensions setting to enabled if the user is new. */ public function update_default_settings() { if ( get_option( self::NEW_USER_DEFAULTS_UPDATED ) === 'yes' ) { return; } if ( $this->settings->is_connected() ) { update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' ); return; } $this->settings->update( 'limit_dimensions', 'enabled' ); $this->settings->update( 'lazyload', 'enabled' ); $this->settings->update( 'best_format', 'disabled' ); $this->settings->update( 'skip_lazyload_images', '2' ); $this->settings->update( 'avif', 'disabled' ); update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' ); } /** * Enable limit dimensions for old users after removing the Resize option. * * @return void */ public function update_limit_dimensions() { if ( get_option( self::OLD_USER_ENABLED_LD ) === 'yes' ) { return; } // New users already have this enabled as we changed defaults. if ( ! $this->settings->is_connected() ) { return; } $this->settings->update( 'limit_dimensions', 'enabled' ); update_option( self::OLD_USER_ENABLED_LD, 'yes' ); } /** * Enable cloud sites and add the current site to the whitelist. * * @return void */ public function update_cloud_sites_default() { if ( get_option( self::OLD_USER_ENABLED_CL ) === 'yes' ) { return; } if ( ! $this->settings->is_connected() ) { return; } // This is already enabled. Don't alter it. if ( $this->settings->get( 'cloud_images' ) === 'enabled' ) { update_option( self::OLD_USER_ENABLED_CL, 'yes' ); return; } $this->settings->update( 'cloud_images', 'enabled' ); $this->settings->update( 'cloud_sites', $this->settings->get_cloud_sites_whitelist_default() ); update_option( self::OLD_USER_ENABLED_CL, 'yes' ); } /** * Adds Optimole tag to admin bar */ public function add_report_menu() { global $wp_admin_bar; $wp_admin_bar->add_node( [ 'id' => 'optml_report_script', 'href' => '#', 'title' => '<span class="ab-icon"></span>Optimole ' . __( 'debugger', 'optimole-wp' ), ] ); $wp_admin_bar->add_menu( [ 'id' => 'optml_status', 'title' => __( 'Troubleshoot this page', 'optimole-wp' ), 'parent' => 'optml_report_script', ] ); } /** * Adds Optimole css to admin bar */ public function print_report_css() { ?> <style type="text/css"> li#wp-admin-bar-optml_report_script > div :hover { cursor: pointer; color: #00b9eb !important; text-decoration: underline; } #wpadminbar #wp-admin-bar-optml_report_script .ab-icon:before { content: "\f227"; top: 3px; } /* The Modal (background) */ .optml-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 2147483641; /* Sit on top */ padding-top: 100px; /* Location of the box */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0, 0, 0); /* Fallback color */ background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ } /* Modal Content */ .optml-modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 80%; } /* The Close Button */ .optml-close { color: #aaaaaa; float: right; font-size: 28px; font-weight: bold; } .optml-modal-content ul { list-style: none; font-size: 80%; margin-top: 50px; } .optml-close:hover, .optml-close:focus { color: #000; text-decoration: none; cursor: pointer; } </style> <?php } /** * Register public actions. */ public function register_public_actions() { add_action( 'wp_head', [ $this, 'generator' ] ); add_filter( 'wp_resource_hints', [ $this, 'add_dns_prefetch' ], 10, 2 ); if ( Optml_Manager::should_ignore_image_tags() ) { return; } if ( ! is_admin() && $this->settings->get( 'report_script' ) === 'enabled' && current_user_can( 'manage_options' ) ) { add_action( 'wp_head', [ $this, 'print_report_css' ] ); add_action( 'wp_before_admin_bar_render', [ $this, 'add_report_menu' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'add_diagnosis_script' ] ); } if ( ! $this->settings->use_lazyload() || ( $this->settings->get( 'native_lazyload' ) === 'enabled' && $this->settings->get( 'video_lazyload' ) === 'disabled' && $this->settings->get( 'bg_replacer' ) === 'disabled' ) ) { return; } add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] ); add_action( 'wp_head', [ $this, 'inline_bootstrap_script' ] ); add_filter( 'optml_additional_html_classes', [ $this, 'add_no_js_class_to_html_tag' ], 10 ); } /** * Use filter to add additional class to html tag. * * @param array $classes The classes to be added. * * @return array */ public function add_no_js_class_to_html_tag( $classes ) { // we need the space padding since some plugins might not target correctly with js there own classes // this causes some issues if they concat directly, this way we can protect against that since no matter what // there will be an extra space padding so that classes can be easily identified return array_merge( $classes, [ ' optml_no_js ' ] ); } /** * Adds script for lazyload/js replacement. */ public function inline_bootstrap_script() { $domain = 'https://' . $this->settings->get_cdn_url() . '/js-lib'; if ( defined( 'OPTML_JS_CDN' ) && constant( 'OPTML_JS_CDN' ) ) { $domain = 'https://' . constant( 'OPTML_JS_CDN' ) . '/js-lib'; } $min = ! OPTML_DEBUG ? '.min' : ''; $bgclasses = Optml_Lazyload_Replacer::get_lazyload_bg_classes(); $watcher_classes = Optml_Lazyload_Replacer::get_watcher_lz_classes(); $lazyload_bg_selectors = Optml_Lazyload_Replacer::get_background_lazyload_selectors(); foreach ( $bgclasses as $key ) { $lazyload_bg_selectors[] = '.' . $key; } $lazyload_bg_selectors = empty( $lazyload_bg_selectors ) ? '' : sprintf( '%s', implode( ', ', (array) $lazyload_bg_selectors ) ); $bgclasses = empty( $bgclasses ) ? '' : sprintf( '"%s"', implode( '","', (array) $bgclasses ) ); $watcher_classes = empty( $watcher_classes ) ? '' : sprintf( '"%s"', implode( '","', (array) $watcher_classes ) ); $default_network = ( $this->settings->get( 'network_optimization' ) === 'enabled' ); $limit_dimensions = $this->settings->get( 'limit_dimensions' ) === 'enabled'; $limit_width = $limit_dimensions ? $this->settings->get( 'limit_width' ) : 0; $limit_height = $limit_dimensions ? $this->settings->get( 'limit_height' ) : 0; $retina_ready = $limit_dimensions || ! ( $this->settings->get( 'retina_images' ) === 'enabled' ); $scale_is_disabled = ( $this->settings->get( 'scale' ) === 'enabled' ); $native_lazy_enabled = ( $this->settings->get( 'native_lazyload' ) === 'enabled' ); $output = sprintf( ' <style type="text/css"> img[data-opt-src]:not([data-opt-lazy-loaded]) { transition: .2s filter linear, .2s opacity linear, .2s border-radius linear; -webkit-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear; -moz-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear; -o-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear; } img[data-opt-src]:not([data-opt-lazy-loaded]) { opacity: .75; -webkit-filter: blur(8px); -moz-filter: blur(8px); -o-filter: blur(8px); -ms-filter: blur(8px); filter: blur(8px); transform: scale(1.04); animation: 0.1s ease-in; -webkit-transform: translate3d(0, 0, 0); } %s </style> <script type="application/javascript"> document.documentElement.className = document.documentElement.className.replace(/\boptml_no_js\b/g, ""); (function(w, d){ var b = d.getElementsByTagName("head")[0]; var s = d.createElement("script"); var v = ("IntersectionObserver" in w && "isIntersecting" in w.IntersectionObserverEntry.prototype) ? "_no_poly" : ""; s.async = true; s.src = "%s/v2/latest/optimole_lib" + v + "%s.js"; b.appendChild(s); w.optimoleData = { lazyloadOnly: "optimole-lazy-only", backgroundReplaceClasses: [%s], nativeLazyload : %s, scalingDisabled: %s, watchClasses: [%s], backgroundLazySelectors: "%s", network_optimizations: %s, ignoreDpr: %s, quality: %d, maxWidth: %d, maxHeight: %d, } }(window, document)); </script>', Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT, esc_url( $domain ), $min, wp_strip_all_tags( $bgclasses ), $native_lazy_enabled ? 'true' : 'false', $scale_is_disabled ? 'true' : 'false', wp_strip_all_tags( $watcher_classes ), addcslashes( wp_strip_all_tags( $lazyload_bg_selectors ), '"' ), defined( 'OPTML_NETWORK_ON' ) && constant( 'OPTML_NETWORK_ON' ) ? ( OPTML_NETWORK_ON ? 'true' : 'false' ) : ( $default_network ? 'true' : 'false' ), $retina_ready ? 'true' : 'false', $this->settings->get_numeric_quality(), $limit_width, $limit_height ); echo $output; } /** * Adds script for lazyload/js replacement. */ public function add_diagnosis_script() { wp_enqueue_script( 'optml-report', OPTML_URL . 'assets/js/report_script.js' ); $ignored_domains = [ 'gravatar.com', 'instagram.com', 'fbcdn' ]; $report_script = [ 'optmlCdn' => $this->settings->get_cdn_url(), 'restUrl' => untrailingslashit( rest_url( OPTML_NAMESPACE . '/v1' ) ) . '/check_redirects', 'nonce' => wp_create_nonce( 'wp_rest' ), 'ignoredDomains' => $ignored_domains, 'wait' => __( 'We are checking the current page for any issues with optimized images ...', 'optimole-wp' ), 'description' => __( 'Optimole page analyzer', 'optimole-wp' ), ]; wp_localize_script( 'optml-report', 'reportScript', $report_script ); } /** * Add settings links in the plugin listing page. * * @param array $links Old plugin links. * * @return array Altered links. */ public function add_action_links( $links ) { if ( ! is_array( $links ) ) { return $links; } return array_merge( $links, [ '<a href="' . admin_url( 'admin.php?page=optimole' ) . '">' . __( 'Settings', 'optimole-wp' ) . '</a>', ] ); } /** * Check if we should show the notice. * * @return bool Should show? */ public function should_show_notice() { if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { return false; } if ( is_network_admin() ) { return false; } if ( ! current_user_can( 'manage_options' ) ) { return false; } if ( $this->settings->is_connected() ) { return false; } $current_screen = get_current_screen(); if ( empty( $current_screen ) ) { return false; } if ( ( get_option( 'optml_notice_optin', 'no' ) === 'yes' ) ) { return false; } return true; } /** * Show upgrade notice. */ public function add_notice_upgrade() { if ( ! $this->should_show_upgrade() ) { return; } ?> <div class="notice optml-notice-optin" style="background-color: #577BF9; color:white; border: none !important; display: flex;"> <div style="margin: 1% 2%;"> <img src='<?php echo OPTML_URL . 'assets/img/upgrade_icon.png'; ?>'> </div> <div style="margin-top: 0.7%;"> <p style="font-size: 16px !important;"> <?php printf( /* translators: 1 - opening strong tag, 2 - visits limit, 3 - closing strong tag, 4 - opening strong tag, 5 - closing strong tag, 6 - br tag */ __( '%1$sIt seems you are close to the %2$s visits limit with %3$sOptimole%4$s for this month.%5$s %6$s For a larger quota you may want to check the upgrade plans. If you exceed the quota we will need to deliver back your original, un-optimized images, which might decrease your site speed performance.', 'optimole-wp' ), '<strong>', number_format_i18n( 1000 ), '</strong>', '<strong>', '</strong>', '<br/><br/>' ); ?> </p> <p style="margin: 1.5% 0;"> <a href="<?php echo esc_url( tsdk_translate_link( 'https://optimole.com/pricing' ) ); ?>" target="_blank" style="border-radius: 4px;padding: 9px 10px;border: 2px solid #FFF;color: white;text-decoration: none;"><?php _e( 'Check upgrade plans', 'optimole-wp' ); ?> </a> <a style="padding: 2%; color: white;" href="<?php echo wp_nonce_url( add_query_arg( [ 'optml_hide_upg' => 'yes' ] ), 'hide_nonce', 'optml_nonce' ); ?>"><?php _e( 'I have already done this', 'optimole-wp' ); ?></a> </p> </div> </div> <?php } /** * Check if we should show the upgrade notice to users. * * @return bool Should we show it? */ public function should_show_upgrade() { $current_screen = get_current_screen(); if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || is_network_admin() || ! current_user_can( 'manage_options' ) || ! $this->settings->is_connected() || empty( $current_screen ) ) { return false; } if ( get_option( 'optml_notice_hide_upg', 'no' ) === 'yes' ) { return false; } if ( $current_screen->base !== 'upload' ) { return false; } $service_data = $this->settings->get( 'service_data' ); if ( ! isset( $service_data['plan'] ) ) { return false; } if ( $service_data['plan'] !== 'free' ) { return false; } $visitors_limit = isset( $service_data['visitors_limit'] ) ? (int) $service_data['visitors_limit'] : 0; $visitors_left = isset( $service_data['visitors_left'] ) ? (int) $service_data['visitors_left'] : 0; if ( $visitors_limit === 0 ) { return false; } if ( $visitors_left > 300 ) { return false; } return true; } /** * CSS styles for Notice. */ public static function notice_styles() { ?> <style> .optml-notice-optin:not(.has-dismiss) { background: url(" <?php echo esc_attr( OPTML_URL . '/assets/img/disconnected.svg' ); ?> ") #fff 100% 0 no-repeat; position: relative; padding: 0; } .optml-notice-optin.has-dismiss { position: relative; } .optml-notice-optin .content { background: rgba(255, 255, 255, 0.75); display: flex; align-items: center; padding: 20px; } .optml-notice-optin img { max-width: 100px; margin-right: 20px; display: none; } .optml-notice-optin .description { font-size: 14px; margin-bottom: 20px; color: #000; } .optml-notice-optin .actions { margin-top: auto; display: flex; gap: 20px; } @media screen and (min-width: 768px) { .optml-notice-optin img { display: block; } } </style> <?php } /** * JS for Notice. */ public static function notice_js( $action ) { ?> <script> jQuery(document).ready(function ($) { // AJAX request to update the option value $('.optml-notice-optin button.notice-dismiss').click(function (e) { e.preventDefault(); var notice = $(this).closest('.optml-notice-optin'); var nonce = '<?php echo esc_attr( wp_create_nonce( $action ) ); ?>'; $.ajax({ url: window.ajaxurl, type: 'POST', data: { action: '<?php echo esc_attr( $action ); ?>', nonce }, complete() { notice.remove(); } }); }); }); </script> <?php } /** * Adds opt in notice. */ public function add_notice() { if ( ! $this->should_show_notice() ) { return; } self::notice_styles(); ?> <div class="notice notice-info optml-notice-optin"> <div class="content"> <img src="<?php echo OPTML_URL . '/assets/img/logo.svg'; ?>" alt="<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>"/> <div> <p class="notice-title"> <?php echo esc_html__( 'Finish setting up!', 'optimole-wp' ); ?></p> <p class="description"> <?php printf( /* translators: 1 - opening strong tag, 2 - closing strong tag, 3 - opening strong tag, 4 - closing strong tag */ __( 'Welcome to %1$sOptimole%2$s, the easiest way to optimize your website images. Your users will enjoy a %3$sfaster%4$s website after you connect it with our service.', 'optimole-wp' ), '<strong>', '</strong>', '<strong>', '</strong>' ); ?> </p> <div class="actions"> <a href="<?php echo esc_url( admin_url( 'admin.php?page=optimole' ) ); ?>" class="button button-primary button-hero"><?php _e( 'Connect to OptiMole', 'optimole-wp' ); ?> </a> <a class="button button-secondary button-hero" href="<?php echo wp_nonce_url( add_query_arg( [ 'optml_hide_optin' => 'yes' ] ), 'hide_nonce', 'optml_nonce' ); ?>"><?php _e( 'I will do it later', 'optimole-wp' ); ?> </a> </div> </div> </div> </div> <?php } /** * Adds conflicts notice. */ public function add_notice_conflicts() { if ( $this->settings->is_connected() || ! $this->conflicting_plugins->should_show_notice() ) { return; } $plugins = $this->conflicting_plugins->get_conflicting_plugins(); $names = []; foreach ( $plugins as $plugin ) { $plugin_data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin ); $names[] = $plugin_data['Name']; } $names = implode( ', ', $names ); self::notice_styles(); self::notice_js( 'optml_dismiss_conflict_notice' ); ?> <div class="notice notice-info optml-notice-optin has-dismiss"> <div class="content"> <img src="<?php echo OPTML_URL . '/assets/img/logo.svg'; ?>" alt="<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>"/> <div> <p class="notice-title"> <strong><?php echo esc_html__( 'Oops... Multiple image optimization plugins active', 'optimole-wp' ); ?></strong> </p> <p class="description"> <?php printf( /* translators: 1 - plugin name, 2 - opening bold tag, 3 - closing bold tag */ __( 'We noticed multiple image optimization plugins active on your site, which may cause issues in Optimole. We recommend using only one image optimization plugin on your site for the best results. The following plugins may cause issues in Optimole: %2$s%1$s%3$s.', 'optimole-wp' ), $names, '<strong>', '</strong>' ); ?> </p> <div class="actions"> <a href="<?php echo esc_url( admin_url( 'plugins.php?optimole_conflicts' ) ); ?>" class="button button-primary button-hero"><?php _e( 'Manage Plugins', 'optimole-wp' ); ?> </a> </div> </div> </div> <button type="button" class="notice-dismiss"> <span class="screen-reader-text"><?php _e( 'Dismiss this notice.', 'optimole-wp' ); ?></span> </button> </div> <?php } /** * Add style classes for lazy loading background images. */ protected function get_background_lazy_css() { $watchers = Optml_Lazyload_Replacer::get_background_lazyload_selectors(); $css = []; foreach ( $watchers as $selector ) { $css[] = 'html ' . $selector . ':not(.optml-bg-lazyloaded)'; } if ( empty( $css ) ) { return ''; } $css = implode( ",\n", $css ) . ' { background-image: none !important; } '; return strip_tags( $css ); } /** * Enqueue frontend scripts. */ public function frontend_scripts() { $bg_css = $this->get_background_lazy_css(); wp_register_style( 'optm_lazyload_noscript_style', false ); wp_enqueue_style( 'optm_lazyload_noscript_style' ); wp_add_inline_style( 'optm_lazyload_noscript_style', "html.optml_no_js img[data-opt-src] { display: none !important; } \n " . $bg_css ); if ( $this->settings->use_lazyload() === true ) { wp_register_script( 'optml-print', false ); wp_enqueue_script( 'optml-print' ); $script = ' (function(w, d){ w.addEventListener("beforeprint", function(){ let images = d.getElementsByTagName( "img" ); for (let img of images) { if ( !img.dataset.optSrc) { continue; } img.src = img.dataset.optSrc; delete img.dataset.optSrc; } }); }(window, document)); '; wp_add_inline_script( 'optml-print', $script ); } } /** * Maybe redirect to dashboard page. */ public function maybe_redirect() { if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_optin'] ) && $_GET['optml_hide_optin'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) { update_option( 'optml_notice_optin', 'yes' ); } if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_upg'] ) && $_GET['optml_hide_upg'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) { update_option( 'optml_notice_hide_upg', 'yes' ); } if ( ! get_transient( 'optml_fresh_install' ) ) { return; } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } delete_transient( 'optml_fresh_install' ); if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) { return; } if ( $this->settings->is_connected() ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) ); exit; } /** * Output Generator tag. */ public function generator() { if ( ! $this->settings->is_connected() ) { return; } if ( ! $this->settings->is_enabled() ) { return; } echo '<meta name="generator" content="Optimole ' . esc_attr( OPTML_VERSION ) . '">'; } /** * Update daily the quota routine. */ public function daily_sync() { $api_key = $this->settings->get( 'api_key' ); $service_data = $this->settings->get( 'service_data' ); $application = ''; if ( isset( $service_data['cdn_key'] ) ) { $application = $service_data['cdn_key']; } if ( empty( $api_key ) ) { return; } $request = new Optml_Api(); $data = $request->get_user_data( $api_key, $application ); if ( $data === false || is_wp_error( $data ) ) { return; } if ( $data === 'disconnect' ) { $settings = new Optml_Settings(); $settings->reset(); return; } add_filter( 'optml_dont_trigger_settings_updated', '__return_true' ); $this->settings->update( 'service_data', $data ); if ( isset( $data['extra_visits'] ) ) { $this->settings->update_frontend_banner_from_remote( $data['extra_visits'] ); } remove_filter( 'optml_dont_trigger_settings_updated', '__return_true' ); } /** * Adds cdn url for prefetch. * * @param array $hints Hints array. * @param string $relation_type Type of relation. * * @return array Altered hints array. */ public function add_dns_prefetch( $hints, $relation_type ) { if ( 'dns-prefetch' !== $relation_type && 'preconnect' !== $relation_type ) { return $hints; } if ( ! $this->settings->is_connected() ) { return $hints; } if ( ! $this->settings->is_enabled() ) { return $hints; } $hints[] = sprintf( 'https://%s', $this->settings->get_cdn_url() ); return $hints; } /** * Add the dashboard page. */ public function add_dashboard_page() { if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) { return; } add_menu_page( 'Optimole', 'Optimole', 'manage_options', 'optimole', [ $this, 'render_dashboard_page' ], OPTML_URL . 'assets/img/logo.svg', 11 ); } /** * Add menu icon style. * * @return void */ public function menu_icon_style() { echo '<style>#toplevel_page_optimole img{ max-width:22px;padding-top:6px!important;opacity:.9!important;} #toplevel_page_optimole li.wp-first-item{ display:none }</style>'; } /** * Add the settings page. * * We do it with another priority as it goes above the cloud library otherwise. */ public function add_settings_subpage() { if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) { return; } if ( ! $this->settings->is_connected() ) { return; } add_submenu_page( 'optimole', __( 'Settings', 'optimole-wp' ), __( 'Settings', 'optimole-wp' ), 'manage_options', 'optimole#settings', [ $this, 'render_dashboard_page' ] ); } /** * Redirect old dashboard. * * @return void */ public function redirect_old_dashboard() { if ( ! is_admin() ) { return; } global $pagenow; if ( $pagenow !== 'upload.php' ) { return; } if ( ! isset( $_GET['page'] ) ) { return; } if ( $_GET['page'] !== 'optimole' ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) ); exit; } /** * Render dashboard page. */ public function render_dashboard_page() { if ( get_option( 'optml_notice_optin', 'no' ) !== 'yes' ) { update_option( 'optml_notice_optin', 'yes' ); } ?> <style> .notice:not(.optml-notice-optin) { display: none !important; } </style> <div id="optimole-app"></div> <?php } /** * Enqueue scripts needed for admin functionality. * * @codeCoverageIgnore */ public function enqueue() { $current_screen = get_current_screen(); if ( ! isset( $current_screen->id ) ) { return; } if ( $current_screen->id !== 'toplevel_page_optimole' ) { return; } $asset_file = include OPTML_PATH . 'assets/build/dashboard/index.asset.php'; wp_register_script( OPTML_NAMESPACE . '-admin', OPTML_URL . 'assets/build/dashboard/index.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_localize_script( OPTML_NAMESPACE . '-admin', 'optimoleDashboardApp', $this->localize_dashboard_app() ); wp_enqueue_script( OPTML_NAMESPACE . '-admin' ); wp_enqueue_style( OPTML_NAMESPACE . '-admin', OPTML_URL . 'assets/build/dashboard/style-index.css', [ 'wp-components', ], $asset_file['version'] ); } /** * Localize the dashboard app. * * @codeCoverageIgnore * @return array */ private function localize_dashboard_app() { global $wp_version; $is_offload_media_available = 'no'; if ( version_compare( $wp_version, '5.3', '>=' ) ) { $is_offload_media_available = 'yes'; } $api_key = $this->settings->get( 'api_key' ); $service_data = $this->settings->get( 'service_data' ); $user = get_userdata( get_current_user_id() ); $user_status = 'inactive'; $auto_connect = get_option( Optml_Settings::OPTML_USER_EMAIL, 'no' ); $available_apps = isset( $service_data['available_apps'] ) ? $service_data['available_apps'] : null; if ( isset( $service_data['cdn_key'] ) && $available_apps !== null ) { foreach ( $service_data['available_apps'] as $app ) { if ( isset( $app['key'] ) && $app['key'] === $service_data['cdn_key'] && isset( $app['status'] ) && $app['status'] === 'active' ) { $user_status = 'active'; } } } $routes = []; foreach ( Optml_Rest::$rest_routes as $route_type ) { foreach ( $route_type as $route => $details ) { $routes[ $route ] = OPTML_NAMESPACE . '/v1/' . $route; } } // Disable this for now, we need a better way to detect it. // $cron_disabled = apply_filters( 'optml_offload_wp_cron_disabled', defined( 'DISABLE_WP_CRON' ) && constant( 'DISABLE_WP_CRON' ) === true ); $language = get_user_locale(); $available_languages = [ 'de_DE' => 'de', 'de_DE_formal' => 'de', ]; $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en'; return [ 'strings' => $this->get_dashboard_strings(), 'assets_url' => OPTML_URL . 'assets/', 'dam_url' => 'admin.php?page=optimole-dam', 'connection_status' => empty( $service_data ) ? 'no' : 'yes', 'has_application' => isset( $service_data['app_count'] ) && $service_data['app_count'] >= 1 ? 'yes' : 'no', 'user_status' => $user_status, 'available_apps' => $available_apps, 'api_key' => $api_key, 'routes' => $routes, 'language' => $lang_code, 'nonce' => wp_create_nonce( 'wp_rest' ), 'user_data' => $service_data, 'remove_latest_images' => defined( 'OPTML_REMOVE_LATEST_IMAGES' ) && constant( 'OPTML_REMOVE_LATEST_IMAGES' ) ? ( OPTML_REMOVE_LATEST_IMAGES ? 'yes' : 'no' ) : 'no', 'current_user' => [ 'email' => $user->user_email, ], 'site_settings' => $this->settings->get_site_settings(), 'offload_limit' => $this->settings->get( 'offload_limit' ), 'home_url' => home_url(), 'optimoleHome' => tsdk_translate_link( 'https://optimole.com/' ), 'optimoleDashHome' => tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ), 'optimoleDashBilling' => tsdk_translate_link( 'https://dashboard.optimole.com/settings/billing', 'query' ), 'days_since_install' => round( ( time() - get_option( 'optimole_wp_install', 0 ) ) / DAY_IN_SECONDS ), 'is_offload_media_available' => $is_offload_media_available, 'auto_connect' => $auto_connect, 'cron_disabled' => false, // $cron_disabled && ! function_exists( 'as_schedule_single_action' ), 'submenu_links' => [ [ 'href' => 'admin.php?page=optimole#settings', 'text' => __( 'Settings', 'optimole-wp' ), 'hash' => '#settings', ], ], 'bf_notices' => $this->get_bf_notices(), ]; } /** * Get the black friday notices. * * @return array */ public function get_bf_notices() { $date = new DateTime(); $now = time(); $start = strtotime( '2024-11-25 00:00:00' ); $end = strtotime( '2024-12-03 23:59:59' ); if ( $now < $start || $now > $end ) { return []; } $notices = [ 'sidebar' => [ 'title' => sprintf( '<span class="text-promo-orange">%1$s:</span> %2$s', __( 'Private Sale', 'optimole-wp' ), __( '25 Nov - 03 Dec', 'optimole-wp' ) ), 'subtitle' => sprintf( /* translators: 1 is the promo code, 2 is the discount amount ('25 off') */ __( 'Use code %1$s for %2$s on yearly plans.', 'optimole-wp' ), '<span class="border-b border-0 border-white border-dashed text-promo-orange">BFCM2425</span>', '<span class="text-promo-orange uppercase">' . __( '25% off', 'optimole-wp' ) . '</span>' ), 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( 'https://optimole.com/pricing' ), 'bfcm24', 'sidebarnotice' ) ), ], ]; if ( get_option( self::BF_PROMO_DISMISS_KEY ) === 'yes' ) { return $notices; } $notices['banner'] = [ /* translators: number of days left */ 'urgency' => sprintf( __( 'Hurry up! only %s left', 'optimole-wp' ), human_time_diff( $end, $now ) ), /* translators: private sale */ 'title' => sprintf( __( 'Black Friday %s', 'optimole-wp' ), '<span class="text-promo-orange">' . __( 'private sale' ) . '</span>' ), 'subtitle' => sprintf( /* translators: 1 is the promo code, 2 is the discount amount ('25 off') */ __( 'Use coupon code %1$s for an instant %2$s on Optimole yearly plans', 'optimole-wp' ), '<span class="border-b border-0 border-white border-dashed text-promo-orange">BFCM2425</span>', '<span class="text-promo-orange">' . __( '25% discount', 'optimole-wp' ) . '</span>' ), 'cta_text' => __( 'Claim now', 'optimole-wp' ), 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( 'https://optimole.com/pricing' ), 'bfcm24', 'dismissiblenotice' ) ), 'dismiss_key' => self::BF_PROMO_DISMISS_KEY, ]; return $notices; } /** * Get all dashboard strings. * * @codeCoverageIgnore * @return array */ private function get_dashboard_strings() { return [ 'optimole' => 'Optimole', 'version' => OPTML_VERSION, 'terms_menu' => __( 'Terms', 'optimole-wp' ), 'privacy_menu' => __( 'Privacy', 'optimole-wp' ), 'testdrive_menu' => __( 'Test Optimole', 'optimole-wp' ), 'service_details' => __( 'Image optimization service', 'optimole-wp' ), 'connect_btn' => __( 'Connect to Optimole', 'optimole-wp' ), 'disconnect_btn' => __( 'Disconnect', 'optimole-wp' ), 'select' => __( 'Select', 'optimole-wp' ), 'your_domain' => __( 'your domain', 'optimole-wp' ), 'add_api' => __( 'Add your API Key', 'optimole-wp' ), 'your_api_key' => __( 'Your API Key', 'optimole-wp' ), 'looking_for_api_key' => __( 'LOOKING FOR YOUR API KEY?', 'optimole-wp' ), 'refresh_stats_cta' => __( 'Refresh Stats', 'optimole-wp' ), 'updating_stats_cta' => __( 'UPDATING STATS', 'optimole-wp' ), 'api_key_placeholder' => __( 'API Key', 'optimole-wp' ), 'account_needed_heading' => __( 'Sign-up for API key', 'optimole-wp' ), 'invalid_key' => __( 'Invalid API Key', 'optimole-wp' ), 'keep_connected' => __( 'Ok, keep me connected', 'optimole-wp' ), 'cloud_library' => __( 'Cloud Library', 'optimole-wp' ), 'image_storage' => __( 'Image Storage', 'optimole-wp' ), 'disconnect_title' => __( 'You are about to disconnect from the Optimole API', 'optimole-wp' ), 'disconnect_desc' => __( 'Please note that disconnecting your site from the Optimole API will impact your website performance. If you still want to disconnect click the button below.', 'optimole-wp' ), 'email_address_label' => __( 'Your email address', 'optimole-wp' ), 'steps_connect_api_title' => __( 'Connect your account', 'optimole-wp' ), 'register_btn' => __( 'Create & connect your account', 'optimole-wp' ), 'step_one_api_title' => __( 'Enter your API key.', 'optimole-wp' ), 'optml_dashboard' => sprintf( /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */ __( 'Get it from the %1$s Optimole Dashboard%2$s.', 'optimole-wp' ), '<a style="white-space:nowrap; text-decoration: underline !important;" href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ) ) . '" target="_blank"> ', '<span style="text-decoration:none; font-size:15px; margin-top:2px;" class="dashicons dashicons-external"></span></a>' ), 'steps_connect_api_desc' => sprintf( /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */ __( 'Copy the API Key you have received via email or you can get it from %1$s Optimole dashboard%2$s. If your account has multiple domains select the one you want to use. <br/>', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ) ) . '" target="_blank"> ', '</a>' ), 'api_exists' => __( 'I already have an API key.', 'optimole-wp' ), 'back_to_register' => __( 'Register account', 'optimole-wp' ), 'back_to_connect' => __( 'Go to previous step', 'optimole-wp' ), /* translators: 1 is starting anchor tag, 2 is ending anchor tag */ 'error_register' => sprintf( __( 'Error registering account. You can try again %1$shere%2$s', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/register', 'query' ) ) . '" target="_blank"> ', '</a>' ), 'invalid_email' => __( 'Please use a valid email address.', 'optimole-wp' ), 'connected' => __( 'CONNECTED', 'optimole-wp' ), 'connecting' => __( 'CONNECTING', 'optimole-wp' ), 'not_connected' => __( 'NOT CONNECTED', 'optimole-wp' ), 'usage' => __( 'Monthly Usage', 'optimole-wp' ), 'quota' => __( 'Monthly visits quota', 'optimole-wp' ), 'logged_in_as' => __( 'LOGGED IN AS', 'optimole-wp' ), 'private_cdn_url' => __( 'IMAGES DOMAIN', 'optimole-wp' ), 'existing_user' => __( 'Existing user?', 'optimole-wp' ), 'notification_message_register' => __( 'We sent you the API Key in the email. Add it below to connect to Optimole.', 'optimole-wp' ), 'premium_support' => __( 'Access our Premium Support', 'optimole-wp' ), 'account_needed_title' => sprintf( /* translators: 1 is the link to optimole.com */ __( 'In order to get access to free image optimization service you will need an API key from %s.', 'optimole-wp' ), ' <a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/register', 'query' ) ) . '" target="_blank">optimole.com</a>' ), 'account_needed_subtitle_1' => sprintf( /* translators: 1 is starting bold tag, 2 is ending bold tag, 3 is the starting bold tag, 4 is the limit number, 5 is ending bold tag, 6 is the starting anchor tag for the docs link on how we count visits, 7 is the ending anchor tag. */ __( 'You will get access to our %1$simage optimization service for FREE%2$s in the limit of %3$s%4$s%5$s %6$svisitors%7$s per month.', 'optimole-wp' ), '<strong>', '</strong>', number_format_i18n( 1000 ), '<strong>', '</strong>', '<a href="https://docs.optimole.com/article/1134-how-optimole-counts-the-number-of-visitors" target="_blank">', '</a>' ), 'account_needed_subtitle_3' => sprintf( /* translators: 1 is the starting anchor tag for the docs link, 2 is the ending anchor tag. */ __( 'Need help? %1$sGetting Started with Optimole%2$s', 'optimole-wp' ), '<a target="_blank" href="https://docs.optimole.com/article/1173-how-to-get-started-with-optimole-in-just-3-steps">', '</a>' ), 'account_needed_subtitle_2' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( 'Bonus, if you dont use a CDN, we got you covered, %1$swe will serve the images using CloudFront CDN%2$s from 450+ locations.', 'optimole-wp' ), '<strong>', '</strong>' ), 'account_needed_footer' => sprintf( /* translators: 1 is the number of users using Optimole */ __( 'Trusted by more than %1$s happy users', 'optimole-wp' ), number_format_i18n( 200000, 0 ) ), 'account_connecting_title' => __( 'Connecting to Optimole', 'optimole-wp' ), 'account_connecting_subtitle' => __( 'Sit tight while we connect you to the Dashboard', 'optimole-wp' ), 'notice_just_activated' => ! $this->settings->is_connected() ? sprintf( /* translators: 1 starting bold tag, 2 is the ending bold tag */ __( '%1$sImage optimisation is currently running.%2$s <br/> Your visitors will now view the best image for their device automatically, all served from the Optimole Cloud Service on the fly. You might see for the very first image request being redirected to the original URL while we do the optimization in the background. You can relax, we\'ll take it from here.', 'optimole-wp' ), '<strong>', '</strong>' ) : '', 'notice_api_not_working' => __( 'It seems there is an issue with your WordPress configuration and the core REST API functionality is not available. This is crucial as Optimole relies on this functionality in order to work.<br/> The root cause might be either a security plugin which blocks this feature or some faulty server configuration which constrain this WordPress feature.You can try to disable any of the security plugins that you use in order to see if the issue persists or ask the hosting company to further investigate.', 'optimole-wp' ), 'notice_disabled_account' => sprintf( /* translators: 1 anchor tag to pricing, 2 is the ending endin anchor tag, 3 is the bold tag start, 4 ending bold tag, 5 new line tag */ __( '%3$sYour account has been disabled due to exceeding quota.%4$s All images are being redirected to the original unoptimized URL. %5$sPlease %1$supgrade%2$s to re-activate the account.', 'optimole-wp' ), '<b><a href="' . esc_url( tsdk_translate_link( 'https://optimole.com/pricing' ) ) . '">', '</a></b>', '<b>', '</b>', '<br>' ), 'signup_terms' => sprintf( /* translators: 1 is starting anchor tag to terms, 2 is starting anchor tag to privacy link and 3 is ending anchor tag. */ __( 'By signing up, you agree to our %1$sTerms of Service %3$s and %2$sPrivacy Policy %3$s.', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://optimole.com/terms/' ) ) . '" target="_blank" >', '<a href="' . esc_url( tsdk_translate_link( 'https://optimole.com/privacy-policy/' ) ) . '" target="_blank">', '</a>' ), 'dashboard_menu_item' => __( 'Dashboard', 'optimole-wp' ), 'settings_menu_item' => __( 'Settings', 'optimole-wp' ), 'help_menu_item' => __( 'Help', 'optimole-wp' ), 'settings_exclusions_menu_item' => __( 'Exclusions', 'optimole-wp' ), 'settings_resize_menu_item' => __( 'Resize', 'optimole-wp' ), 'settings_compression_menu_item' => __( 'Compression', 'optimole-wp' ), 'advanced_settings_menu_item' => __( 'Advanced', 'optimole-wp' ), 'general_settings_menu_item' => __( 'General', 'optimole-wp' ), 'lazyload_settings_menu_item' => __( 'Lazyload', 'optimole-wp' ), 'watermarks_menu_item' => __( 'Watermark', 'optimole-wp' ), 'conflicts_menu_item' => __( 'Possible Issues', 'optimole-wp' ), 'conflicts' => [ 'title' => __( 'We might have some possible conflicts with the plugins that you use. In order to benefit from Optimole\'s full potential you will need to address this issues.', 'optimole-wp' ), 'message' => __( 'Details', 'optimole-wp' ), 'conflict_close' => __( 'I\'ve done this.', 'optimole-wp' ), 'no_conflicts_found' => __( 'No conflicts found. We are all peachy now. 🍑', 'optimole-wp' ), ], 'upgrade' => [ 'title' => __( 'Upgrade', 'optimole-wp' ), 'title_long' => __( 'Upgrade to Optimole Pro', 'optimole-wp' ), 'reason_1' => __( 'Priority & Live Chat support', 'optimole-wp' ), 'reason_2' => __( 'Extend visits limit', 'optimole-wp' ), 'reason_3' => __( 'Custom domain', 'optimole-wp' ), 'reason_4' => __( 'Site audit', 'optimole-wp' ), 'cta' => __( 'View plans', 'optimole-wp' ), ], 'neve' => [ 'is_active' => defined( 'NEVE_VERSION' ) ? 'yes' : 'no', 'byline' => __( 'Fast, perfomance built-in WordPress theme.', 'optimole-wp' ), 'reason_1' => __( 'Lightweight, 25kB in page-weight.', 'optimole-wp' ), 'reason_2' => __( '100+ Starter Sites available.', 'optimole-wp' ), 'reason_3' => __( 'AMP/Mobile ready.', 'optimole-wp' ), 'reason_4' => __( 'Lots of customizations options.', 'optimole-wp' ), 'reason_5' => __( 'Fully compatible with Optimole.', 'optimole-wp' ), ], 'metrics' => [ 'metricsTitle1' => __( 'Images optimized', 'optimole-wp' ), 'metricsSubtitle1' => __( 'Since plugin activation', 'optimole-wp' ), 'metricsTitle2' => __( 'Saved file size', 'optimole-wp' ), 'metricsSubtitle2' => __( 'For the latest 10 images', 'optimole-wp' ), 'metricsTitle3' => __( 'Average compression', 'optimole-wp' ), 'metricsSubtitle3' => __( 'During last month', 'optimole-wp' ), 'metricsTitle4' => __( 'Traffic', 'optimole-wp' ), 'metricsSubtitle4' => __( 'During last month', 'optimole-wp' ), ], 'options_strings' => [ 'best_format_title' => __( 'Automatic Best Image Format Selection', 'optimole-wp' ), 'best_format_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole picks the ideal format for your images, balancing quality and speed. It tests different formats, like AVIF and WebP, ensuring images look good and load quickly. %1$sLearn more%2$s.', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1942-best-format">', '</a>' ), 'add_filter' => __( 'Add filter', 'optimole-wp' ), 'add_site' => __( 'Add site', 'optimole-wp' ), 'admin_bar_desc' => __( 'Show in the WordPress admin bar the available quota from Optimole service.', 'optimole-wp' ), 'auto_q_title' => __( 'Auto', 'optimole-wp' ), 'cache_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Clears all Optimole’s cached resources (images, JS, CSS). Useful if you made changes to your images and don\'t see those applying on your site. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1941-clear-cached-resources">', '</a>' ), 'cache_title' => __( 'Clear Cached Resources', 'optimole-wp' ), 'clear_cache_notice' => __( 'Clearing cached resources will re-optimize the images and might affect the site performance for a few minutes.', 'optimole-wp' ), 'image_size_notice' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Use this option if you notice images are not cropped correctly after using Optimole. Add the affected image sizes here to automatically adjust and correct their cropping for optimal display. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1947-add-new-image-crop-size">', '</a>' ), 'clear_cache_images' => __( 'Clear cached images', 'optimole-wp' ), 'clear_cache_assets' => __( 'Clear cached CSS & JS', 'optimole-wp' ), 'connect_step_0' => __( 'Connecting your site to the Optimole service.', 'optimole-wp' ), 'connect_step_1' => __( 'Checking for possible conflicts.', 'optimole-wp' ), 'connect_step_2' => __( 'Inspecting the images from your site.', 'optimole-wp' ), 'connect_step_3' => __( 'All done, Optimole is currently optimizing your site.', 'optimole-wp' ), 'disabled' => __( 'Disabled', 'optimole-wp' ), 'enable_avif_title' => __( 'AVIF Image Support', 'optimole-wp' ), 'enable_avif_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to automatically convert images to the AVIF format on browsers that support it. This format provides quality images with reduced file sizes, and faster webpage loading. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1943-avif-conversion">', '</a>' ), 'enable_bg_lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to lazy-load images set as CSS backgrounds. If Optimole misses any, you can directly target specific CSS selectors to ensure all background images are optimized. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1169-how-to-enable-the-background-lazyload-feature-for-certain-background-images">', '</a>' ), 'enable_bg_lazyload_title' => __( 'CSS Background Lazy Load', 'optimole-wp' ), 'enable_video_lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'By default, lazy loading does not work for embedded videos and iframes. Enable this option to activate the lazy-load on these elements. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1952-lazy-loading-for-embedded-videos-and-iframes">', '</a>' ), 'enable_video_lazyload_title' => __( 'Lazy Loading for Embedded Videos and Iframes', 'optimole-wp' ), 'enable_noscript_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enables fallback images for browsers that can\'t handle JavaScript-based lazy loading or related features. Disabling it may resolve conflicts with other plugins or configurations and decrease HTML page size. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1959-noscript-tag">', '</a>' ), 'enable_noscript_title' => __( 'Noscript Tag', 'optimole-wp' ), 'enable_gif_replace_title' => __( 'GIF to Video Conversion', 'optimole-wp' ), 'enable_report_title' => __( 'Enable Error Diagnosis Tool', 'optimole-wp' ), 'enable_report_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Activates the Optimole debugging tool in the admin bar for reports on Optimole-related website issues using the built-in diagnostic feature. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1390-how-does-the-error-diagnosis-tool-work">', '</a>' ), 'enable_offload_media_title' => __( 'Store Your Images in Optimole Cloud', 'optimole-wp' ), 'enable_offload_media_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Free up space on your server by transferring your images to Optimole Cloud; you can transfer them back anytime. Once moved, the images will still be visible in the Media Library and can be used as before. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1967-store-your-images-in-optimole-cloud">', '</a>' ), 'enable_cloud_images_title' => __( 'Unified Image Access', 'optimole-wp' ), 'enable_cloud_images_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this setting to access all your Optimole images, including those from other websites connected to your Optimole account, directly on this site. They will be available for browsing in the Cloud Library tab. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1323-cloud-library-browsing">', '</a>' ), 'enable_image_replace' => __( 'Enable Optimole Image Handling', 'optimole-wp' ), 'enable_lazyload_placeholder_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to use a generic transparent placeholder instead of the blurry images during lazy loading. Enhance the visual experience by selecting a custom color for the placeholder. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1192-lazy-load-generic-placeholder">', '</a>' ), 'enable_lazyload_placeholder_title' => __( 'Lazy Load with Generic Placeholder', 'optimole-wp' ), 'enable_network_opt_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole will automatically reduce the image quality when it detects a slower network, making your images load faster on low-speed internet connections. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1945-network-based-optimizations">', '</a>' ), 'enable_network_opt_title' => __( 'Network-based Optimizations', 'optimole-wp' ), 'enable_resize_smart_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole automatically detects the most interesting or important part of your images. When pictures are resized or cropped, this feature ensures the focus stays on the most interesting part of the picture. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1871-how-to-use-the-smart-cropping-in-optimole">', '</a>' ), 'enable_resize_smart_title' => __( 'Smart Cropping', 'optimole-wp' ), 'enable_retina_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this feature to optimize your images for Retina displays. Retina-ready images are optimized to look sharp on screens with higher pixel density, offering viewers enhanced visual quality. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1391-what-is-a-retina-image">', '</a>' ), 'enable_retina_title' => __( 'Retina Quality', 'optimole-wp' ), 'enable_limit_dimensions_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Define the max width or height limits for images on your website. Larger images will be automatically adjusted to fit within these parameters while preserving their original aspect ratio. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1946-limit-image-sizes">', '</a>' ), 'enable_limit_dimensions_title' => __( 'Limit Image Sizes', 'optimole-wp' ), 'enable_limit_dimensions_notice' => __( 'When you enable this feature to define a max width or height for image resizing, please note that DPR (retina) images will be disabled. This is done to ensure consistency in image dimensions across your website. Although this may result in slightly lower image quality for high-resolution displays, it will help maintain uniform image sizes, improving your website\'s overall layout and potentially boosting performance.', 'optimole-wp' ), 'enable_badge_title' => __( 'Enable Optimole Badge', 'optimole-wp' ), 'enable_badge_description' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Get 20.000 more visits for free by enabling the Optimole badge on your websites. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1940-optimole-badge">', '</a>' ), 'image_sizes_title' => __( 'Your cropped image sizes', 'optimole-wp' ), 'enabled' => __( 'Enabled', 'optimole-wp' ), 'exclude_class_desc' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sImage tag%2$s contains class', 'optimole-wp' ), '<strong>', '</strong>' ), 'exclude_ext_desc' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sImage extension%2$s is', 'optimole-wp' ), '<strong>', '</strong>' ), 'exclude_filename_desc' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sImage filename%2$s contains', 'optimole-wp' ), '<strong>', '</strong>' ), 'exclude_desc_optimize' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Here you can define exceptions, in case you don\'t want some images to be optimised. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1191-exclude-from-optimizing-or-lazy-loading">', '</a>' ), 'exclude_title_lazyload' => __( 'Don\'t lazy-load images if', 'optimole-wp' ), 'exclude_desc_lazyload' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Define exceptions, in case you don\'t want the lazy-load to be active on certain images. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1191-exclude-from-optimizing-or-lazy-loading">', '</a>' ), 'exclude_title_optimize' => __( 'Don\'t optimize images if', 'optimole-wp' ), 'exclude_url_desc' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sPage url%2$s contains', 'optimole-wp' ), '<strong>', '</strong>' ), 'name' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sName: %2$s', 'optimole-wp' ), '<strong>', '</strong>' ), 'cropped' => __( 'cropped', 'optimole-wp' ), 'exclude_url_match_desc' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sPage url%2$s matches', 'optimole-wp' ), '<strong>', '</strong>' ), 'exclude_first' => __( 'Exclude first', 'optimole-wp' ), 'images' => __( 'images', 'optimole-wp' ), 'exclude_first_images_title' => __( 'Bypass Lazy Load for First Images', 'optimole-wp' ), 'exclude_first_images_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Indicate how many images at the top of each page should bypass lazy loading, ensuring they’re instantly visible. Enter 0 to not exclude any images from the lazy loading process. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1948-bypass-lazy-load-for-first-images">', '</a>' ), 'filter_class' => __( 'Image class', 'optimole-wp' ), 'filter_ext' => __( 'Image extension', 'optimole-wp' ), 'filter_filename' => __( 'Image filename', 'optimole-wp' ), 'filter_operator_contains' => __( 'contains', 'optimole-wp' ), 'filter_operator_matches' => __( 'matches', 'optimole-wp' ), 'filter_operator_is' => __( 'is', 'optimole-wp' ), 'filter_url' => __( 'Page URL', 'optimole-wp' ), 'filter_helper' => __( 'For homepage use `home` keyword.', 'optimole-wp' ), 'gif_replacer_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to automatically convert GIF images to Video files (MP4 and WebM). %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1171-gif-to-video-conversion">', '</a>' ), 'height_field' => __( 'Height', 'optimole-wp' ), 'add_image_size_button' => __( 'Add size', 'optimole-wp' ), 'add_image_size_desc' => __( 'Add New Image Crop Size', 'optimole-wp' ), 'here' => __( ' here.', 'optimole-wp' ), 'hide' => __( 'Hide', 'optimole-wp' ), 'high_q_title' => __( 'High', 'optimole-wp' ), 'image_1_label' => __( 'Original', 'optimole-wp' ), 'image_2_label' => __( 'Optimized', 'optimole-wp' ), 'lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Scales large images to fit their display space, ensuring your website runs fast. With lazy loading, images appear when needed while scrolling, making navigation smoother. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1939-scale-images-lazy-load">', '</a>' ), 'filter_length_error' => __( 'The filter should be at least 3 characters long.', 'optimole-wp' ), 'scale_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to allow Optimole to resize lazy-loaded images for optimal display on your screen. Keep it disabled to retain the original image size, though it may result in slower page loads. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1950-image-scaling">', '</a>' ), 'low_q_title' => __( 'Low', 'optimole-wp' ), 'medium_q_title' => __( 'Medium', 'optimole-wp' ), 'no_images_found' => __( 'You dont have any images in your Media Library. Add one and check how the Optimole will perform.', 'optimole-wp' ), 'native_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable to use the browser\'s built-in lazy loading feature. Enabling this will disable the auto scale feature, meaning images will not be automatically resized to fit the screen dimensions. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1949-browser-native-lazy-load">', '</a>' ), 'option_saved' => __( 'Option saved.', 'optimole-wp' ), 'ml_quality_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Optimole ML algorithms will predict the optimal image quality to get the smallest possible size with minimum perceived quality losses. When disabled, you can control the quality manually. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1016-what-is-the-difference-between-the-auto-high-medium-low-compression-levels">', '</a>' ), 'quality_desc' => __( 'Lower image quality might boost your loading speed by lowering the size. However, the low image quality may negatively impact the visual appearance of the images. Try experimenting with the setting, then click the View sample image link to see what option works best for you.', 'optimole-wp' ), 'quality_selected_value' => __( 'Selected value', 'optimole-wp' ), 'quality_slider_desc' => __( 'See one sample image which will help you choose the right quality of the compression.', 'optimole-wp' ), 'quality_title' => __( 'Auto Quality Powered by ML(Machine Learning)', 'optimole-wp' ), 'strip_meta_title' => __( 'Strip Image Metadata', 'optimole-wp' ), 'strip_meta_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Removes extra information from images, including EXIF and IPTC data (like camera settings and copyright info). This makes the pictures lighter and helps your website load faster. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1944-strip-image-metadata">', '</a>' ), 'replacer_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole will manage, optimize, and serve all the images on your website. If disabled, optimization, lazy loading, and other features will no longer be available. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1218-how-to-turn-off-image-replacement-in-optimoles-wordpress-plugin">', '</a>' ), 'sample_image_loading' => __( 'Loading a sample image.', 'optimole-wp' ), 'save_changes' => __( 'Save changes', 'optimole-wp' ), 'show' => __( 'Show', 'optimole-wp' ), 'selected_sites_title' => __( 'CURRENTLY SHOWING IMAGES FROM', 'optimole-wp' ), 'selected_sites_desc' => __( 'Site:', 'optimole-wp' ), 'selected_all_sites_desc' => __( 'Currently viewing images from all sites', 'optimole-wp' ), 'select_all_sites_desc' => __( 'View images from all sites', 'optimole-wp' ), 'select_site' => __( 'Select a website', 'optimole-wp' ), 'cloud_site_title' => __( 'Show images only from these sites:', 'optimole-wp' ), 'cloud_site_desc' => __( 'Browse images only from the specified websites. Otherwise, images from all websites will appear in the library.', 'optimole-wp' ), 'toggle_ab_item' => __( 'Admin bar status', 'optimole-wp' ), 'toggle_lazyload' => __( 'Scale Images & Lazy loading', 'optimole-wp' ), 'toggle_scale' => __( 'Image Scaling', 'optimole-wp' ), 'toggle_native' => __( 'Browser Native Lazy Load', 'optimole-wp' ), 'on_toggle' => __( 'On', 'optimole-wp' ), 'off_toggle' => __( 'Off', 'optimole-wp' ), 'view_sample_image' => __( 'View sample image', 'optimole-wp' ), 'watch_placeholder_lazyload' => __( 'Add each CSS selector on a new line or separated by comma(,)', 'optimole-wp' ), 'watch_desc_lazyload' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enter CSS selectors for any background images not covered by the default lazy loading. This ensures those images also benefit from the optimized loading process. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1169-how-to-enable-the-background-lazyload-feature-for-certain-background-images">', '</a>' ), 'watch_title_lazyload' => __( 'Extend CSS Background Lazy Loading', 'optimole-wp' ), 'width_field' => __( 'Width', 'optimole-wp' ), 'crop' => __( 'crop', 'optimole-wp' ), 'toggle_cdn' => __( 'Serve CSS & JS Through Optimole', 'optimole-wp' ), 'cdn_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole will optimize your CSS and JS files and, if they contain images, the images as well, then deliver them via the CDN for faster webpage loading. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1966-serve-css-js-through-optimole">', '</a>' ), 'enable_css_minify_title' => __( 'Minify CSS files', 'optimole-wp' ), 'css_minify_desc' => __( 'Once Optimole will serve your CSS files, it will also minify the files and serve them via CDN.', 'optimole-wp' ), 'enable_js_minify_title' => __( 'Minify JS files', 'optimole-wp' ), 'js_minify_desc' => __( 'Once Optimole will serve your JS files, it will also minify the files and serve them via CDN.', 'optimole-wp' ), 'sync_title' => __( 'Offload Existing Images', 'optimole-wp' ), 'rollback_title' => __( 'Restore Offloaded Images', 'optimole-wp' ), 'sync_desc' => __( 'Right now all the new images uploaded to your site are moved automatically to Optimole Cloud. In order to offload the existing ones, please click Sync images and wait for the process to finish. You can rollback anytime.', 'optimole-wp' ), 'rollback_desc' => __( 'Pull all the offloaded images to Optimole Cloud back to your server.', 'optimole-wp' ), 'sync_media' => __( 'Sync images', 'optimole-wp' ), 'rollback_media' => __( 'Rollback images', 'optimole-wp' ), 'sync_media_progress' => __( 'Moving your images to Optimole...', 'optimole-wp' ), 'estimated_time' => __( 'Estimated time remaining', 'optimole-wp' ), 'calculating_estimated_time' => __( 'We are currently calculating the estimated time for this job...', 'optimole-wp' ), 'images_processing' => __( 'We are currently processing your images in the background. Leaving the page won\'t stop the process.', 'optimole-wp' ), 'active_optimize_exclusions' => __( 'Active Optimizing Exclusions', 'optimole-wp' ), 'active_lazyload_exclusions' => __( 'Active Lazy-loading Exclusions', 'optimole-wp' ), 'minutes' => __( 'minutes', 'optimole-wp' ), 'stop' => __( 'Stop', 'optimole-wp' ), 'show_logs' => __( 'Show Logs', 'optimole-wp' ), 'hide_logs' => __( 'Hide Logs', 'optimole-wp' ), 'view_logs' => __( 'View Full Logs', 'optimole-wp' ), 'rollback_media_progress' => __( 'Moving images into your media library...', 'optimole-wp' ), 'rollback_media_error' => __( 'An unexpected error occured while pulling the offloaded back to your site', 'optimole-wp' ), 'rollback_media_error_desc' => __( 'You can try again to pull back the rest of the images.', 'optimole-wp' ), 'remove_notice' => __( 'Remove notice', 'optimole-wp' ), 'sync_media_error' => __( 'An unexpected error occured while offloading all existing images from your site to Optimole', 'optimole-wp' ), 'sync_media_link' => __( 'The selected images have been offloaded to our servers, you can check them', 'optimole-wp' ), 'rollback_media_link' => __( 'The selected images have been restored to your server, you can check them', 'optimole-wp' ), 'sync_media_error_desc' => __( 'You can try again to offload the rest of the images to Optimole.', 'optimole-wp' ), 'offload_disable_warning_title' => __( 'Important! Please read carefully', 'optimole-wp' ), 'offload_disable_warning_desc' => __( 'If you turn off this option, you will not be able to see the images in the Media Library without restoring the images first. Do you want to restore the images to your site upon turning off the option?', 'optimole-wp' ), 'offload_enable_info_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'You are not required to use the offload functionality for the plugin to work, use it if you want to save on hosting space. %1$s More details%2$s', 'optimole-wp' ), '<a style="white-space: nowrap;" target=”_blank” href="https://docs.optimole.com/article/1323-cloud-library-browsing">', '<span style="font-size:15px; margin-top:2px;" class="dashicons dashicons-external"></span></a>' ), 'offload_conflicts_part_1' => __( 'We have detected the following plugins that conflict with the offload features:', 'optimole-wp' ), 'offload_conflicts_part_2' => __( 'Please disable those plugins temporarily in order for Optimole to rollback the images to your site.', 'optimole-wp' ), 'offloading_success' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%s Your images are now stored in Optimole Cloud.', 'optimole-wp' ), '<strong>' . __( 'Transfer Complete.', 'optimole-wp' ) . '</strong>' ), 'rollback_success' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%s Your images have been restored to your website.', 'optimole-wp' ), '<strong>' . __( 'Transfer Complete.', 'optimole-wp' ) . '</strong>' ), 'offloading_radio_legend' => __( 'Where your images are stored', 'optimole-wp' ), 'offload_radio_option_rollback_title' => __( 'Optimole Cloud and your website', 'optimole-wp' ), 'offload_radio_option_rollback_desc' => __( 'Images are stored in both the local WordPress media library and Optimole Cloud.', 'optimole-wp' ), 'offload_radio_option_offload_title' => __( 'Optimole Cloud only', 'optimole-wp' ), 'offload_radio_option_offload_desc' => __( 'Images are stored only in Optimole Cloud, allowing you to save space on your server. When enabled, any new images you upload in the Media Library will be automatically transferred to Optimole Cloud.', 'optimole-wp' ), 'offload_limit_reached' => sprintf( /* translators: Current limit of offloaded images */ __( 'You have reached the maximum offloading limit of %s images. To increase the offload limit and for more information, contact our support.', 'optimole-wp' ), '<strong>#offload_limit#</strong>' ), 'select' => __( 'Please select one ...', 'optimole-wp' ), 'yes' => __( 'Restore images after disabling', 'optimole-wp' ), 'no' => __( 'Do not restore images after disabling', 'optimole-wp' ), 'lazyload_placeholder_color' => __( 'Placeholder Color', 'optimole-wp' ), 'clear' => __( 'Clear', 'optimole-wp' ), 'settings_saved' => __( 'Settings saved', 'optimole-wp' ), 'settings_saved_error' => __( 'Error saving settings. Please reload the page and try again.', 'optimole-wp' ), 'cache_cleared' => __( 'Cache cleared', 'optimole-wp' ), 'cache_cleared_error' => __( 'Error clearing cache. Please reload the page and try again.', 'optimole-wp' ), 'offloading_start_title' => __( 'Transfer your images to Optimole', 'optimole-wp' ), 'offloading_start_description' => __( 'This process will transfer and store your images in Optimole Cloud and may take a while, depending on the number of images.', 'optimole-wp' ), 'offloading_start_action' => __( 'Transfer to Optimole Cloud', 'optimole-wp' ), 'offloading_stop_title' => __( 'Are you sure?', 'optimole-wp' ), 'offloading_stop_description' => __( 'This will halt the ongoing process. To retrieve images transferred from the Optimole Cloud, use the Rollback option.', 'optimole-wp' ), 'offloading_stop_action' => __( 'Cancel the transfer to Optimole', 'optimole-wp' ), 'rollback_start_title' => __( 'Transfer back all images to your site', 'optimole-wp' ), 'rollback_start_description' => __( 'This process will transfer back all images from Optimole to your website and may take a while, depending on the number of images.', 'optimole-wp' ), 'rollback_start_action' => __( 'Transfer back from Optimole', 'optimole-wp' ), 'rollback_stop_title' => __( 'Are you sure?', 'optimole-wp' ), 'rollback_stop_description' => __( 'Canceling will halt the ongoing process, and any remaining images will stay in the Optimole Cloud. To transfer images to the Optimole Cloud, use the Offloading option.', 'optimole-wp' ), 'rollback_stop_action' => __( 'Cancel the transfer from Optimole', 'optimole-wp' ), ], 'help' => [ 'section_one_title' => __( 'Help and Support', 'optimole-wp' ), 'section_two_title' => __( 'Documentation', 'optimole-wp' ), 'section_two_sub' => __( 'Docs Page', 'optimole-wp' ), 'get_support_title' => __( 'Get Support', 'optimole-wp' ), 'get_support_desc' => __( 'Need help or got a question? Submit a ticket and we\'ll get back to you.', 'optimole-wp' ), 'get_support_cta' => __( 'Contact Support', 'optimole-wp' ), 'feat_request_title' => __( 'Have a feature request?', 'optimole-wp' ), 'feat_request_desc' => __( 'Help us improve Optimole by sharing feedback and ideas for new features.', 'optimole-wp' ), 'feat_request_cta' => __( 'Submit a Feature Request', 'optimole-wp' ), 'feedback_title' => __( 'Changelog', 'optimole-wp' ), 'feedback_desc' => __( 'Check our changelog to see latest fixes and features implemented.', 'optimole-wp' ), 'feedback_cta' => __( 'View Changelog', 'optimole-wp' ), 'account_title' => __( 'Account', 'optimole-wp' ), 'account_item_one' => __( 'How Optimole counts the visitors?', 'optimole-wp' ), 'account_item_two' => __( 'What happens if I exceed plan limits?', 'optimole-wp' ), 'account_item_three' => __( 'Visits based plan', 'optimole-wp' ), 'image_processing_title' => __( 'Image Processing', 'optimole-wp' ), 'image_processing_item_one' => __( 'Getting Started With Optimole', 'optimole-wp' ), 'image_processing_item_two' => __( 'How Optimole can serve WebP images', 'optimole-wp' ), 'image_processing_item_three' => __( 'Adding Watermarks to your images', 'optimole-wp' ), 'api_title' => __( 'API', 'optimole-wp' ), 'api_item_one' => __( 'Cloud Library Browsing', 'optimole-wp' ), 'api_item_two' => __( 'Exclude from Optimizing or Lazy Loading', 'optimole-wp' ), 'api_item_three' => __( 'Custom Integration', 'optimole-wp' ), ], 'watermarks' => [ 'image' => __( 'Image', 'optimole-wp' ), 'loading_remove_watermark' => __( 'Removing watermark resource ...', 'optimole-wp' ), 'max_allowed' => __( 'You are allowed to save maximum 5 images.', 'optimole-wp' ), 'list_header' => __( 'Possible watermarks', 'optimole-wp' ), 'settings_header' => __( 'Watermarks position settings', 'optimole-wp' ), 'no_images_found' => __( 'No images available for watermark. Please upload one.', 'optimole-wp' ), 'id' => __( 'ID', 'optimole-wp' ), 'name' => __( 'Name', 'optimole-wp' ), 'type' => __( 'Type', 'optimole-wp' ), 'action' => __( 'Action', 'optimole-wp' ), 'upload' => __( 'Upload', 'optimole-wp' ), 'add_desc' => __( 'Add new watermark', 'optimole-wp' ), 'wm_title' => __( 'Active watermark', 'optimole-wp' ), 'wm_desc' => __( 'The active watermark to use from the list of uploaded watermarks.', 'optimole-wp' ), 'opacity_field' => __( 'Opacity', 'optimole-wp' ), 'opacity_title' => __( 'Watermark opacity', 'optimole-wp' ), 'opacity_desc' => __( 'A value between 0 and 100 for the opacity level. If set to 0 it will disable the watermark.', 'optimole-wp' ), 'position_title' => __( 'Watermark position', 'optimole-wp' ), 'position_desc' => __( 'The place relative to the image where the watermark should be placed.', 'optimole-wp' ), 'pos_nowe_title' => __( 'North-West', 'optimole-wp' ), 'pos_no_title' => __( 'North', 'optimole-wp' ), 'pos_noea_title' => __( 'North-East', 'optimole-wp' ), 'pos_we_title' => __( 'West', 'optimole-wp' ), 'pos_ce_title' => __( 'Center', 'optimole-wp' ), 'pos_ea_title' => __( 'East', 'optimole-wp' ), 'pos_sowe_title' => __( 'South-West', 'optimole-wp' ), 'pos_so_title' => __( 'South', 'optimole-wp' ), 'pos_soea_title' => __( 'South-East', 'optimole-wp' ), 'offset_x_field' => __( 'Offset X', 'optimole-wp' ), 'offset_y_field' => __( 'Offset Y', 'optimole-wp' ), 'offset_title' => __( 'Watermark offset', 'optimole-wp' ), 'offset_desc' => __( 'Offset the watermark from set position on X and Y axis. Values can be positive or negative.', 'optimole-wp' ), 'scale_field' => __( 'Scale', 'optimole-wp' ), 'scale_title' => __( 'Watermark scale', 'optimole-wp' ), 'scale_desc' => __( 'A value between 0 and 300 for the scale of the watermark (100 is the original size and 300 is 3x the size) relative to the resulting image size. If set to 0 it will default to the original size.', 'optimole-wp' ), 'save_changes' => __( 'Save changes', 'optimole-wp' ), ], 'latest_images' => [ 'image' => __( 'Image', 'optimole-wp' ), 'no_images_found' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'We are currently optimizing your images. Meanwhile you can visit your %1$shomepage%2$s and check how our plugin performs.', 'optimole-wp' ), '<a href="' . esc_url( home_url() ) . '" target="_blank" >', '</a>' ), 'compression' => __( 'Optimization', 'optimole-wp' ), 'loading_latest_images' => __( 'Loading your optimized images...', 'optimole-wp' ), 'last' => __( 'Last', 'optimole-wp' ), 'saved' => __( 'Saved', 'optimole-wp' ), 'smaller' => __( 'smaller', 'optimole-wp' ), 'optimized_images' => __( 'optimized images', 'optimole-wp' ), 'same_size' => __( '🙉 We couldn\'t do better, this image is already optimized at maximum.', 'optimole-wp' ), 'small_optimization' => __( '😬 Not that much, just <strong>{ratio}</strong> smaller.', 'optimole-wp' ), 'medium_optimization' => __( '🤓 We are on the right track, <strong>{ratio}</strong> squeezed.', 'optimole-wp' ), 'big_optimization' => __( '❤️❤️❤️ Our moles just nailed it, this one is <strong>{ratio}</strong> smaller.', 'optimole-wp' ), ], 'csat' => [ 'title' => __( 'Your opinion matters', 'optimole-wp' ), 'close' => __( 'Close', 'optimole-wp' ), 'heading_one' => __( 'How easy did you find to get started using Optimole, on a scale of 1 to 5?', 'optimole-wp' ), 'heading_two' => __( 'Any specific feedback you would like to add?', 'optimole-wp' ), 'heading_three' => __( 'Thank you!', 'optimole-wp' ), 'low' => __( 'Very Poor', 'optimole-wp' ), 'high' => __( 'Excellent', 'optimole-wp' ), 'feedback_placeholder' => __( 'Add your feedback here (optional)', 'optimole-wp' ), 'skip' => __( 'Skip', 'optimole-wp' ), 'submit' => __( 'Submit', 'optimole-wp' ), 'thank_you' => __( 'Your input is highly appreciated and helps us shape a better experience in Optimole.', 'optimole-wp' ), ], 'cron_error' => sprintf( /* translators: 1 is code to disable cron, 2 value of the constant */ __( 'It seems that you have the %1$s constant defined as %2$s. The offloading process uses cron events to offload the images in the background. Please remove the constant from your wp-config.php file in order for the offloading process to work.', 'optimole-wp' ), '<code>DISABLE_WP_CRON</code>', '<code>true</code>' ), 'cancel' => __( 'Cancel', 'optimole-wp' ), ]; } /** * Allow SVG uploads * * @param array $mimes Supported mimes. * * @return array * @access public * @uses filter:upload_mimes */ public function allow_svg( $mimes ) { $mimes['svg'] = 'image/svg+xml'; return $mimes; } } media_offload.php 0000644 00000256660 14720403740 0010046 0 ustar 00 <?php /** * Optml_Media_Offload class. * * @package \Optimole\Inc * @author Optimole <friends@optimole.com> */ use Optimole\Sdk\Exception\InvalidArgumentException; use Optimole\Sdk\Exception\InvalidUploadApiResponseException; use Optimole\Sdk\Exception\RuntimeException; use Optimole\Sdk\Exception\UploadApiException; use Optimole\Sdk\Exception\UploadFailedException; use Optimole\Sdk\Exception\UploadLimitException; use Optimole\Sdk\Optimole; /** * Class Optml_Admin */ class Optml_Media_Offload extends Optml_App_Replacer { use Optml_Normalizer; use Optml_Dam_Offload_Utils; /** * Hold the settings object. * * @var Optml_Settings Settings object. */ public $settings; /** * Cached object instance. * * @var Optml_Media_Offload */ private static $instance = null; /** * Hold the logger object. * * @var Optml_Logger */ public $logger; const KEYS = [ 'uploaded_flag' => 'id:', 'not_processed_flag' => 'process:', ]; const META_KEYS = [ 'offloaded' => 'optimole_offload', 'offload_error' => 'optimole_offload_error', 'rollback_error' => 'optimole_rollback_error', ]; const OM_OFFLOADED_FLAG = 'om_image_offloaded'; const POST_OFFLOADED_FLAG = 'optimole_offload_post'; const POST_ROLLBACK_FLAG = 'optimole_rollback_post'; const RETRYABLE_META_COUNTER = '_optimole_retryable_errors'; /** * Flag used inside wp_get_attachment url filter. * * @var bool Whether or not to return the original url of the image. */ private static $return_original_url = false; /** * Flag used inside wp_get_attachment url filter. * * @var bool Whether or not to return the original url of the image. */ private static $offload_update_post = false; /** * Flag used inside wp_unique_filename filter. * * @var bool|string Whether to skip our custom deduplication. */ private static $current_file_deduplication = false; /** * Keeps the last deduplicated lower case value. * * @var bool|string Used to check if the current processed image was deduplicated. */ private static $last_deduplicated = false; /** * Checks if the plugin was installed before adding POST_OFFLOADED_FLAG. * * @var bool Used when applying the flags for the page query. */ private static $is_legacy_install = null; /** * Adds page meta query args * * @param string $action The action for which the args are needed. * @param array $args The initial args without the added meta_query args. * * @return array The args with the added meta_query args. */ public static function add_page_meta_query_args( $action, $args ) { if ( $action === 'offload_images' ) { $args['meta_query'] = [ 'relation' => 'AND', [ 'key' => self::POST_OFFLOADED_FLAG, 'compare' => 'NOT EXISTS', ], ]; } if ( $action === 'rollback_images' ) { $args['meta_query'] = [ 'relation' => 'AND', [ 'key' => self::POST_ROLLBACK_FLAG, 'compare' => 'NOT EXISTS', ], ]; if ( self::$is_legacy_install ) { $args['meta_query'][] = [ 'key' => self::POST_OFFLOADED_FLAG, 'value' => 'true', 'compare' => '=', ]; } } return $args; } /** * Get count of all images from db. * * @return int Number of all images. */ public static function number_of_all_images() { $total_images_by_mime = wp_count_attachments( 'image' ); return array_sum( (array) $total_images_by_mime ); } /** * Optml_Media_Offload constructor. */ public static function instance() { if ( null === self::$instance || self::is_phpunit_test() ) { self::$instance = new self(); self::$instance->settings = new Optml_Settings(); self::$instance->logger = Optml_Logger::instance(); if ( self::$instance->settings->is_connected() ) { self::$instance->init(); } if ( self::$instance->settings->is_offload_enabled() ) { add_filter( 'image_downsize', [ self::$instance, 'generate_filter_downsize_urls' ], 10, 3 ); add_filter( 'wp_generate_attachment_metadata', [ self::$instance, 'generate_image_meta' ], 10, 2 ); add_filter( 'wp_get_attachment_url', [ self::$instance, 'get_image_attachment_url' ], - 999, 2 ); add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_uploaded_images' ] ); self::$instance->add_new_actions(); add_action( 'delete_attachment', [ self::$instance, 'delete_attachment_hook' ], 10 ); add_filter( 'handle_bulk_actions-upload', [ self::$instance, 'bulk_action_handler' ], 10, 3 ); add_filter( 'bulk_actions-upload', [ self::$instance, 'register_bulk_media_actions' ] ); add_filter( 'media_row_actions', [ self::$instance, 'add_inline_media_action' ], 10, 2 ); add_filter( 'wp_calculate_image_srcset', [ self::$instance, 'calculate_image_srcset' ], 1, 5 ); add_action( 'post_updated', [ self::$instance, 'update_offload_meta' ], 10, 3 ); // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); if ( $below_6_0_0 ) { add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert_legacy' ], 10, 3 ); } else { add_filter( 'wp_insert_attachment_data', [ self::$instance, 'insert' ], 10, 4 ); } add_action( 'optml_start_processing_images', [ self::$instance, 'start_processing_images' ], 10, 5 ); add_action( 'optml_start_processing_images_by_id', [ self::$instance, 'start_processing_images_by_id', ], 10, 4 ); add_action( 'init', [ self::$instance, 'maybe_reschedule' ] ); if ( self::$is_legacy_install === null ) { self::$is_legacy_install = get_option( 'optimole_wp_install', 0 ) > 1677171600; } } } return self::$instance; } /** * Reschedule the transfer cron in case is missing or was lost. * * @return void */ public function maybe_reschedule() { // If this is in pending, we do nothing. if ( self::is_scheduled( 'optml_start_processing_images' ) ) { return; } // If there is no transfer in progress, we do nothing. if ( self::$instance->settings->get( 'transfer_status' ) === 'disabled' ) { return; } $transfer_type = self::$instance->settings->get( 'transfer_status' ); $in_progress = self::$instance->settings->get( 'rollback_images' === $transfer_type ? 'rollback_status' : 'offloading_status' ) !== 'disabled'; // We check if there is an in progress transfer. if ( ! $in_progress ) { return; } self::$instance->logger->add_log( $transfer_type, 'Cron missed, attempt to reschedule.' ); self::get_image_count( $transfer_type, false ); } /** * Function for `update_attached_file` filter-hook. * * @param string $file Path to the attached file to update. * @param int $attachment_id Attachment ID. * * @return string */ public function wp_update_attached_file_filter( $file, $attachment_id ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'called updated attached' ); } $info = pathinfo( $file ); $file_name = basename( $file ); $no_ext_file_name = basename( $file, '.' . $info['extension'] ); // if we have current deduplication set and it contains the filename that is updated // we replace the updated filename with the deduplicated filename if ( ! empty( self::$current_file_deduplication ) && stripos( self::$current_file_deduplication, $no_ext_file_name ) !== false ) { $file = str_replace( $file_name, self::$current_file_deduplication, $file ); // we need to store the filename we replaced to check when uploading the image if it was deduplicated self::$last_deduplicated = $file_name; self::$current_file_deduplication = false; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', self::$last_deduplicated ); } remove_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10 ); return $file; } /** * Function for `wp_insert_attachment_data` filter-hook. * Because we remove the images when new images are added the wp deduplication using the files will not work * To overcome this we hook the attachment data when it's added to the database and we use the post name (slug) which is unique against the database * For creating a unique quid by replacing the filename with the slug inside the existing guid * This will ensure the guid is unique and the next step will be to make sure the attached_file meta for the image is also unique * For this we will hook `update_attached_file` filter which is called after the data is inserted and there we will make sure we replace the filename * with the deduplicated one which we stored into `$current_file_deduplication` variable * * @param array $data An array of slashed, sanitized, and processed attachment post data. * @param array $postarr An array of slashed and sanitized attachment post data, but not processed. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post(). * @param bool $update Whether this is an existing attachment post being updated. * * @return array * @see self::insert_legacy() for backwards compatibility with older versions of WordPress < 6.0.0. */ public function insert( $data, $postarr, $unsanitized_postarr, $update ) { // the post name is unique against the database so not affected by removing the files // https://developer.wordpress.org/reference/functions/wp_unique_post_slug/ if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'data before' ); do_action( 'optml_log', $data ); } if ( empty( $data['guid'] ) ) { return $data; } $filename = wp_basename( $data['guid'] ); $ext = $this->get_ext( $filename ); // skip if the file is not an image if ( ! isset( Optml_Config::$all_extensions[ $ext ] ) && ! in_array( $ext, [ 'jpg', 'jpeg', 'jpe' ], true ) ) { return $data; } // on some instances (just unit tests) the post name has the extension appended like this : `image-1-jpg` // we remove that as it is redundant for the file name deduplication we are using it $sanitized_post_name = str_replace( '-' . $ext, '', $data['post_name'] ); // with the wp deduplication working the post_title is identical to the post_name // so when they are different it means we need to deduplicate using the post_name if ( ! empty( $data['post_name'] ) && $data['post_title'] !== $sanitized_post_name ) { // we append the extension to the post_name to create a filename // and use it to replace the filename in the guid $no_ext_filename = str_replace( '.' . $ext, '', $filename ); $no_ext_filename_sanitized = sanitize_title( $no_ext_filename ); // get the deduplication addition from the database post_name $diff = str_replace( strtolower( $no_ext_filename_sanitized ), '', $sanitized_post_name ); // create the deduplicated filename $to_replace_with = $no_ext_filename . $diff . '.' . $ext; $data['guid'] = str_replace( $filename, $to_replace_with, $data['guid'] ); // we store the deduplication to be used and add the filter for updating the attached_file meta self::$current_file_deduplication = $to_replace_with; add_filter( 'update_attached_file', [ self::$instance, 'wp_update_attached_file_filter' ], 10, 2 ); } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'data after' ); do_action( 'optml_log', $data ); } return $data; } /** * Wrapper for the `insert` method for WP versions < 6.0.0. * * @param array $data An array of slashed, sanitized, and processed attachment post data. * @param array $postarr An array of slashed and sanitized attachment post data, but not processed. * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed attachment post data as originally passed to wp_insert_post(). * * @return array */ public function insert_legacy( $data, $postarr, $unsanitized_postarr ) { return $this->insert( $data, $postarr, $unsanitized_postarr, false ); } /** * Update offload meta when the page is updated. * * @param int $post_ID Updated post id. * @param WP_Post $post_after Post before the update. * @param WP_Post $post_before Post after the update. * * @return void * @uses action:post_updated */ public function update_offload_meta( $post_ID, $post_after, $post_before ) { if ( self::$offload_update_post === true ) { return; } if ( get_post_type( $post_ID ) === 'attachment' ) { return; } // revisions are skipped inside the function no need to check them before delete_post_meta( $post_ID, self::POST_ROLLBACK_FLAG ); } /** * Get image size name from width and meta. * * @param array $sizes Image sizes . * @param integer $width Size width. * @param string $filename Image filename. * * @return null|string|array */ public static function get_image_size_from_width( $sizes, $width, $filename, $just_name = true ) { foreach ( $sizes as $name => $size ) { if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) { return $just_name ? $name : array_merge( $size, [ 'name' => $name ] ); } } return null; } /** * Replace image URLs in the srcset attributes. * * @param array $sources Array of image sources. * @param array $size_array Array of width and height values in pixels (in that order). * @param string $image_src The 'src' of the image. * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. * @param int $attachment_id Image attachment ID. * * @return array */ public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) { if ( ! is_array( $sources ) ) { return $sources; } if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) { if ( ! Optml_Media_Offload::is_uploaded_image( $image_src ) || ! isset( $image_meta['file'] ) || ! Optml_Media_Offload::is_uploaded_image( $image_meta['file'] ) ) { return $sources; } foreach ( $sources as $width => $source ) { $filename = wp_basename( $image_meta['file'] ); $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename ); $optimized_url = wp_get_attachment_image_src( $attachment_id, $size ); if ( false === $optimized_url ) { continue; } $sources[ $width ]['url'] = $optimized_url[0]; } return $sources; } if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) { return $sources; } $requested_width = $size_array[0]; $requested_height = $size_array[1]; if ( $requested_height < 1 || $requested_width < 1 ) { return $sources; } $requested_ratio = $requested_width / $requested_height; $image_sizes = $this->get_all_image_sizes(); $crop = false; // Loop through image sizes to make sure we're using the right cropping. foreach ( $image_sizes as $size_name => $args ) { if ( $args['width'] !== $requested_width && $args['height'] !== $requested_height ) { continue; } if ( isset( $args['crop'] ) ) { $crop = (bool) $args['crop']; } } foreach ( $sources as $width => $source ) { $filename = ( $image_meta['file'] ); $size = $this->get_image_size_from_width( $image_meta['sizes'], $width, $filename, false ); if ( $size === null || ! isset( $size['name'] ) ) { unset( $sources[ $width ] ); continue; } if ( ! isset( $image_sizes[ $size['name'] ] ) || (bool) $image_sizes[ $size['name'] ]['crop'] !== $crop ) { unset( $sources[ $width ] ); continue; } // Some image sizes might have 0 values for width or height. if ( $size['width'] < 1 || $size['height'] < 1 ) { unset( $sources[ $width ] ); continue; } $size_ratio = $size['width'] / $size['height']; // We need a srcset with the same aspect ratio. // Otherwise, we'll display different images on different devices. if ( $requested_ratio !== $size_ratio ) { unset( $sources[ $width ] ); continue; } $optimized_url = wp_get_attachment_image_src( $attachment_id, $size['name'] ); if ( false === $optimized_url ) { unset( $sources[ $width ] ); continue; } $sources[ $width ]['url'] = $optimized_url[0]; } // Add the requested size to the srcset. $sources[ $requested_width ] = [ 'url' => $image_src, 'descriptor' => 'w', 'value' => $requested_width, ]; return $sources; } /** * Check if the image is stored on our servers or not. * * @param string $src Image src or url. * * @return bool Whether image is upload or not. */ public static function is_not_processed_image( $src ) { return strpos( $src, self::KEYS['not_processed_flag'] ) !== false; } /** * Check if the image is stored on our servers or not. * * @param string $src Image src or url. * * @return bool Whether image is upload or not. */ public static function is_uploaded_image( $src ) { return strpos( $src, '/' . self::KEYS['uploaded_flag'] ) !== false; } /** * Get the attachment ID from the image tag. * * @param string $image Image tag. * * @return int|false */ public function get_id_from_tag( $image ) { $attachment_id = false; if ( preg_match( '#class=["|\']?[^"\']*(wp-image-|wp-video-)([\d]+)[^"\']*["|\']?#i', $image, $found ) ) { $attachment_id = intval( $found[2] ); } return $attachment_id; } /** * Get attachment id from url * * @param string $url The optimized url . * * @return false|mixed The attachment id . */ public static function get_attachment_id_from_url( $url ) { preg_match( '/\/' . Optml_Media_Offload::KEYS['not_processed_flag'] . '([^\/]*)\//', $url, $attachment_id ); return isset( $attachment_id[1] ) ? $attachment_id[1] : false; } /** * Get attachment id from local url * * @param string $url The url to look for. * * @return array The attachment id and the size from the url. */ public function get_local_attachement_id_from_url( $url ) { $size = 'full'; $found_size = $this->parse_dimensions_from_filename( $url ); $url = $this->add_schema( $url ); if ( $found_size[0] !== false && $found_size[1] !== false ) { $size = $found_size; } $url = $this->add_schema( $url ); $attachment_id = $this->attachment_url_to_post_id( $url ); return [ 'attachment_id' => $attachment_id, 'size' => $size ]; } /** * Filter out the urls that are saved to our servers when saving to the DB. * * @param array $data The post data array to save. * * @return array * @uses filter:wp_insert_post_data */ public function filter_uploaded_images( $data ) { $content = trim( wp_unslash( $data['post_content'] ) ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'content to update' ); do_action( 'optml_log', $content ); } $images = Optml_Manager::instance()->extract_urls_from_content( $content ); if ( ! isset( $images[0] ) ) { return $data; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'images to update' ); do_action( 'optml_log', $images ); } foreach ( $images as $url ) { $is_original_uploaded = self::is_uploaded_image( $url ); $attachment_id = false; $size = 'thumbnail'; if ( $is_original_uploaded ) { $found_size = $this->parse_dimension_from_optimized_url( $url ); if ( $found_size[0] !== 'auto' && $found_size[1] !== 'auto' ) { $size = $found_size; } $attachment_id = self::get_attachment_id_from_url( $url ); } else { $id_and_size = $this->get_local_attachement_id_from_url( $url ); $attachment_id = $id_and_size['attachment_id']; $size = $id_and_size['size']; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'image id and found size' ); do_action( 'optml_log', $attachment_id ); do_action( 'optml_log', $size ); } if ( false === $attachment_id || ! $this->is_legacy_offloaded_attachment( $attachment_id ) || ! wp_attachment_is_image( $attachment_id ) ) { continue; } $optimized_url = wp_get_attachment_image_src( $attachment_id, $size ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' image url to replace with ' ); do_action( 'optml_log', $optimized_url ); } if ( ! isset( $optimized_url[0] ) ) { continue; } if ( $is_original_uploaded === self::is_uploaded_image( $optimized_url[0] ) ) { continue; } $content = str_replace( $url, $optimized_url[0], $content ); } $data['post_content'] = wp_slash( $content ); return $data; } /** * Get all images that need to be updated from a post. * * @param string $post_content The content of the post. * @param string $job The job name. * * @return array An array containing the image ids. */ public function get_image_id_from_content( $post_content, $job ) { $content = trim( wp_unslash( $post_content ) ); $images = Optml_Manager::instance()->extract_urls_from_content( $content ); $found_images = []; if ( isset( $images[0] ) ) { foreach ( $images as $url ) { $is_original_uploaded = self::is_uploaded_image( $url ); $attachment_id = false; if ( $is_original_uploaded ) { if ( $job === 'rollback_images' ) { $attachment_id = self::get_attachment_id_from_url( $url ); } } else { if ( $job === 'offload_images' ) { $id_and_size = $this->get_local_attachement_id_from_url( $url ); $attachment_id = $id_and_size['attachment_id']; } } if ( false === $attachment_id || $attachment_id === 0 || ! wp_attachment_is_image( $attachment_id ) ) { continue; } $found_images[] = intval( $attachment_id ); } } return apply_filters( 'optml_content_images_to_update', $found_images, $content ); } /** * Get the posts ids and the images from them that need sync/rollback. * * @param int $page The current page from the query. * @param string $job The job name rollback_images/offload_images. * @param int $batch How many posts to query on a page. * @param array $page_in The pages that need to be updated. * * @return array An array containing the page of the query and an array containing the images for every post that need to be updated. */ public function update_content( $page, $job, $batch = 1, $page_in = [] ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' updating_content ' ); } $post_types = array_values( array_filter( get_post_types(), function ( $post_type ) { if ( $post_type === 'attachment' || $post_type === 'revision' ) { return false; } return true; } ) ); $query_args = apply_filters( 'optml_replacement_wp_query_args', [ 'post_type' => $post_types, 'post_status' => 'any', 'fields' => 'ids', 'posts_per_page' => $batch, 'update_post_meta_cache' => true, 'update_post_term_cache' => false, ] ); $query_args = self::add_page_meta_query_args( $job, $query_args ); if ( ! empty( $page_in ) ) { $query_args['post__in'] = $page_in; } $content = new \WP_Query( $query_args ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', $page ); } $images_to_update = []; if ( $content->have_posts() ) { while ( $content->have_posts() ) { $content->the_post(); $content_id = get_the_ID(); if ( get_post_type() !== 'attachment' ) { $ids = $this->get_image_id_from_content( get_post_field( 'post_content', $content_id ), $job ); if ( count( $ids ) > 0 ) { $images_to_update[ $content_id ] = $ids; $duplicated_pages = apply_filters( 'optml_offload_duplicated_images', [], $content_id ); if ( is_array( $duplicated_pages ) && ! empty( $duplicated_pages ) ) { foreach ( $duplicated_pages as $duplicated_id ) { $duplicated_ids = $this->get_image_id_from_content( get_post_field( 'post_content', $duplicated_id ), $job ); $images_to_update[ $duplicated_id ] = $duplicated_ids; } } } if ( $job === 'offload_images' ) { update_post_meta( $content_id, self::POST_OFFLOADED_FLAG, 'true' ); delete_post_meta( $content_id, self::POST_ROLLBACK_FLAG ); } if ( $job === 'rollback_images' ) { update_post_meta( $content_id, self::POST_ROLLBACK_FLAG, 'true' ); delete_post_meta( $content_id, self::POST_OFFLOADED_FLAG ); } } } ++$page; } $result['page'] = $page; $result['imagesToUpdate'] = $images_to_update; return $result; } /** * Add inline action to push to our servers. * * @param array $actions All actions. * @param \WP_Post $post The current post image object. * * @return array */ public function add_inline_media_action( $actions, $post ) { $meta = wp_get_attachment_metadata( $post->ID ); if ( ! isset( $meta['file'] ) ) { return $actions; } $file = $meta['file']; if ( wp_check_filetype( $file, Optml_Config::$all_extensions )['ext'] === false || ! current_user_can( 'delete_post', $post->ID ) ) { return $actions; } if ( ! self::is_uploaded_image( $file ) ) { $upload_action_url = add_query_arg( [ 'page' => 'optimole', 'optimole_action' => 'offload_images', '0' => $post->ID, ], 'admin.php' ); $actions['offload_images'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', $upload_action_url, esc_attr__( 'Offload to Optimole', 'optimole-wp' ), esc_html__( 'Offload to Optimole', 'optimole-wp' ) ); } if ( self::is_uploaded_image( $file ) ) { $rollback_action_url = add_query_arg( [ 'page' => 'optimole', 'optimole_action' => 'rollback_images', '0' => $post->ID, ], 'admin.php' ); $actions['rollback_images'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', $rollback_action_url, esc_attr__( 'Restore image to media library', 'optimole-wp' ), esc_html__( 'Restore image to media library', 'optimole-wp' ) ); } return $actions; } /** * Upload images to our servers and update inside pages. * * @param array $image_ids The id of the attachments for the selected images. * * @return int The number of successfully processed images. */ public function upload_and_update_existing_images( $image_ids ) { $success_up = 0; if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' images to upload ' ); do_action( 'optml_log', $image_ids ); } foreach ( $image_ids as $id ) { if ( self::is_uploaded_image( wp_get_attachment_metadata( $id )['file'] ) ) { // if this meta flag below failed at the initial update but the file meta above is updated it will cause an infinite query loop update_post_meta( $id, self::META_KEYS['offloaded'], 'true' ); update_post_meta( $id, self::OM_OFFLOADED_FLAG, true ); ++$success_up; continue; } $meta = $this->generate_image_meta( wp_get_attachment_metadata( $id ), $id ); if ( isset( $meta['file'] ) && self::is_uploaded_image( $meta['file'] ) ) { ++$success_up; wp_update_attachment_metadata( $id, $meta ); } } if ( $success_up > 0 ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' call post update, succesful images: ' ); do_action( 'optml_log', $success_up ); } } return $success_up; } /** * Return the original url of an image attachment. * * @param integer $post_id Image attachment id. * * @return string|bool The original url of the image. */ public static function get_original_url( $post_id ) { self::$return_original_url = true; $original_url = wp_get_attachment_url( $post_id ); self::$return_original_url = false; return $original_url; } /** * Bring images back to media library and update inside pages. * * @param array $image_ids The id of the attachments for the selected images. * * @return int The number of successfully processed images. */ public function rollback_and_update_images( $image_ids ) { $success_back = 0; if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' images to rollback ' ); do_action( 'optml_log', $image_ids ); } foreach ( $image_ids as $id ) { // Skip DAM attachment filtering. if ( $this->is_dam_imported_image( $id ) ) { continue; } $current_meta = wp_get_attachment_metadata( $id ); if ( ! isset( $current_meta['file'] ) || ! self::is_uploaded_image( $current_meta['file'] ) ) { delete_post_meta( $id, self::META_KEYS['offloaded'] ); delete_post_meta( $id, self::OM_OFFLOADED_FLAG ); ++$success_back; continue; } // Account for scaled images. $source_file = isset( $current_meta['original_image'] ) ? $current_meta['original_image'] : $current_meta['file']; // @phpstan-ignore-line - this exists for scaled images. $filename = pathinfo( $source_file, PATHINFO_BASENAME ); $image_id = preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $current_meta['file'], $matches ) ? $matches[1] : null; if ( null === $image_id ) { continue; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' image cloud id ' ); do_action( 'optml_log', $image_id ); } $image_url = Optimole::offload()->getImageUrl( $image_id ); if ( null === $image_url ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' error get url' ); } self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error getting URL.' ); continue; } if ( ! function_exists( 'download_url' ) ) { include_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! function_exists( 'download_url' ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } $timeout_seconds = 60; $temp_file = download_url( $image_url, $timeout_seconds ); if ( is_wp_error( $temp_file ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' download_url error ' ); } self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has error downloading URL.' ); continue; } $extension = $this->get_ext( $filename ); if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' image has invalid extension' ); do_action( 'optml_log', $extension ); } update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has invalid extension.' ); continue; } $type = Optml_Config::$image_extensions [ $extension ]; $file = [ 'name' => $filename, 'type' => $type, 'tmp_name' => $temp_file, 'error' => 0, 'size' => filesize( $temp_file ), ]; $overrides = [ // do not expect the default form data from normal uploads 'test_form' => false, // Setting this to false lets WordPress allow empty files, not recommended. 'test_size' => true, // A properly uploaded file will pass this test. There should be no reason to override this one. 'test_upload' => true, ]; if ( ! function_exists( 'wp_handle_sideload' ) ) { include_once ABSPATH . '/wp-admin/includes/file.php'; } if ( ! function_exists( 'wp_handle_sideload' ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } // Move the temporary file into the uploads directory. $results = wp_handle_sideload( $file, $overrides, get_the_date( 'Y/m', $id ) ); if ( ! empty( $results['error'] ) ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' wp_handle_sideload error' ); } update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' faced wp_handle_sideload error.' ); continue; } if ( ! function_exists( 'wp_create_image_subsizes' ) ) { include_once ABSPATH . '/wp-admin/includes/image.php'; } if ( ! function_exists( 'wp_create_image_subsizes' ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } $original_meta = wp_create_image_subsizes( $results['file'], $id ); if ( $type === 'image/svg+xml' ) { if ( ! function_exists( 'wp_get_attachment_metadata' ) || ! function_exists( 'wp_update_attachment_metadata' ) ) { include_once ABSPATH . '/wp-admin/includes/post.php'; } if ( ! function_exists( 'wp_get_attachment_metadata' ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } $meta = wp_get_attachment_metadata( $id ); if ( ! isset( $meta['file'] ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } $meta['file'] = $results['file']; wp_update_attachment_metadata( $id, $meta ); } if ( ! function_exists( 'update_attached_file' ) ) { include_once ABSPATH . '/wp-admin/includes/post.php'; } if ( ! function_exists( 'update_attached_file' ) ) { update_post_meta( $id, self::META_KEYS['rollback_error'], 'true' ); continue; } update_attached_file( $id, $results['file'] ); $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $id ); if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) { foreach ( $duplicated_images as $duplicated_id ) { $duplicated_meta = wp_get_attachment_metadata( $duplicated_id ); if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) { $duplicated_meta['file'] = $results['file']; if ( isset( $meta ) ) { foreach ( $meta['sizes'] as $key => $value ) { if ( isset( $original_meta['sizes'][ $key ]['file'] ) ) { $duplicated_meta['sizes'][ $key ]['file'] = $original_meta['sizes'][ $key ]['file']; } } } wp_update_attachment_metadata( $duplicated_id, $duplicated_meta ); delete_post_meta( $duplicated_id, self::META_KEYS['offloaded'] ); delete_post_meta( $duplicated_id, self::OM_OFFLOADED_FLAG ); } } } ++$success_back; self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_ROLLBACK, 'Image ID: ' . $id . ' has been rolled back.' ); $original_url = self::get_original_url( $id ); if ( $original_url === false ) { continue; } $this->delete_attachment_from_server( $original_url, $id, $image_id ); } if ( $success_back > 0 ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', ' call update post, success rollback' ); do_action( 'optml_log', $success_back ); } } return $success_back; } /** * Handle the bulk actions. * * @param string $redirect The current url from the media library. * @param string $doaction The current action selected. * @param array $image_ids The id of the attachments for the selected images. * * @return string The url with the correspondent query args for the executed actions. */ public function bulk_action_handler( $redirect, $doaction, $image_ids ) { if ( empty( $image_ids ) ) { return $redirect; } $image_ids = array_slice( $image_ids, 0, 20, true ); $redirect = 'admin.php'; $redirect = add_query_arg( 'optimole_action', $doaction, $redirect ); $redirect = add_query_arg( 'page', 'optimole', $redirect ); $redirect = add_query_arg( $image_ids, $redirect ); return $redirect; } /** * Register the bulk media actions. * * @param array $bulk_array The existing actions array. * * @return array The array with the appended actions. */ public function register_bulk_media_actions( $bulk_array ) { $bulk_array['offload_images'] = __( 'Push Image to Optimole', 'optimole-wp' ); $bulk_array['rollback_images'] = __( 'Restore image to media library', 'optimole-wp' ); return $bulk_array; } /** * Send delete request to our servers and update the meta. * * @param string $original_url Original url of the image. * @param integer $post_id Image id inside db. * @param string $image_id Our cloud id for the image. */ public function delete_attachment_from_server( $original_url, $post_id, $image_id ) { Optimole::offload()->deleteImage( $image_id ); delete_post_meta( $post_id, self::META_KEYS['offloaded'] ); delete_post_meta( $post_id, self::OM_OFFLOADED_FLAG ); } /** * Delete an image from our servers after it is removed from media. * * @param int $post_id The deleted post id. */ public function delete_attachment_hook( $post_id ) { $file = wp_get_attachment_metadata( $post_id ); if ( $file === false ) { return; } // Skip if the image was imported from cloud library. if ( $this->is_dam_imported_image( $post_id ) ) { return; } if ( ! $this->is_new_offloaded_attachment( $post_id ) && ! $this->is_legacy_offloaded_attachment( $post_id ) ) { return; } $file = $file['file']; if ( self::is_uploaded_image( $file ) || $this->is_new_offloaded_attachment( $post_id ) ) { $original_url = self::get_original_url( $post_id ); if ( $original_url === false ) { return; } $table_id = []; preg_match( '/\/' . self::KEYS['uploaded_flag'] . '([^\/]*)\//', $file, $table_id ); if ( ! isset( $table_id[1] ) ) { return; } $this->delete_attachment_from_server( $original_url, $post_id, $table_id[1] ); } } /** * Get optimized URL for an attachment image if it is uploaded to our servers. * * @param string $url The current url. * @param int $attachment_id The attachment image id. * * @return string Optimole cdn URL. * @uses filter:wp_get_attachment_url */ public function get_image_attachment_url( $url, $attachment_id ) { if ( self::$return_original_url === true ) { return $url; } if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) { $meta = wp_get_attachment_metadata( $attachment_id ); if ( ! isset( $meta['file'] ) ) { return $url; } // Skip DAM attachment filtering. if ( $this->is_dam_imported_image( $attachment_id ) ) { return $url; } $file = $meta['file']; if ( self::is_uploaded_image( $file ) ) { return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $attachment_id . $file, $this->get_optimized_image_url( $url, 'auto', 'auto' ) ); } else { // this is for the users that already offloaded the images before the other fixes $local_file = get_attached_file( $attachment_id ); if ( ! file_exists( $local_file ) ) { $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id ); if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) { foreach ( $duplicated_images as $id ) { if ( ! empty( $id ) ) { $duplicated_meta = wp_get_attachment_metadata( $id ); if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) { return str_replace( '/' . $url, '/' . self::KEYS['not_processed_flag'] . $id . $duplicated_meta['file'], $this->get_optimized_image_url( $url, 'auto', 'auto' ) ); } } } } } } return $url; } if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) { return $url; } return $this->get_new_offloaded_attachment_url( $url, $attachment_id ); } /** * Filter the requested image url. * * @param bool|array $image The previous image value (null). * @param int $attachment_id The ID of the attachment. * @param string|array $size Requested size of image. Image size name, or array of width and height values (in that order). * * @return bool|array The image sizes and optimized url. * @uses filter:image_downsize */ public function generate_filter_downsize_urls( $image, $attachment_id, $size ) { if ( $this->is_dam_imported_image( $attachment_id ) ) { return $image; } if ( $this->is_legacy_offloaded_attachment( $attachment_id ) ) { if ( self::$return_original_url === true ) { return $image; } $sizes2crop = self::size_to_crop(); if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) { return $image; } $data = image_get_intermediate_size( $attachment_id, $size ); if ( false === $data || ! self::is_uploaded_image( $data['url'] ) ) { return $image; } $resize = apply_filters( 'optml_default_crop', [] ); if ( isset( $sizes2crop[ $data['width'] . $data['height'] ] ) ) { $resize = $this->to_optml_crop( $sizes2crop[ $data['width'] . $data['height'] ] ); } $id_filename = []; preg_match( '/\/(' . self::KEYS['not_processed_flag'] . '.*)/', $data['url'], $id_filename ); if ( ! isset( $id_filename[1] ) ) { return $image; } $url = self::get_original_url( $attachment_id ); return [ str_replace( $url, $id_filename[1], $this->get_optimized_image_url( $url, $data['width'], $data['height'], $resize ) ), $data['width'], $data['height'], true, ]; } if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) { return $image; } return $this->alter_attachment_image_src( $image, $attachment_id, $size, false ); } /** * Get image extension. * * @param string $path Image path. * * @return string */ private function get_ext( $path ) { return pathinfo( $path, PATHINFO_EXTENSION ); } /** * Mark an image as having a retryable error. * * @param int $attachment_id The attachment ID. * @param string $reason The reason for the error. */ public static function mark_retryable_error( $attachment_id, $reason ) { static $allowed_retries = 5; $retries = get_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, true ); $retries = empty( $retries ) ? 0 : (int) $retries; if ( $retries >= $allowed_retries ) { self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Reached the maximum number of retries.' ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); return; } self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' ' . $reason . '. Marked for retry, retries done: ' . $retries ); update_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER, ( $retries + 1 ) ); } /** * Update image meta with optimized cdn path. * * @param array $meta Meta information of the image. * @param int $attachment_id The image attachment ID. * * @return array * @uses filter:wp_generate_attachment_metadata */ public function generate_image_meta( $meta, $attachment_id ) { if ( $this->is_dam_imported_image( $attachment_id ) ) { return $meta; } if ( self::$instance->settings->is_offload_limit_reached() ) { return $meta; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'called generate meta' ); } // No meta, or image was already uploaded. if ( ! isset( $meta['file'] ) || ! isset( $meta['width'] ) || ! isset( $meta['height'] ) || self::is_uploaded_image( $meta['file'] ) ) { do_action( 'optml_log', 'invalid meta' ); do_action( 'optml_log', $meta ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid meta.' ); return $meta; } // Skip images based on filters. if ( false === Optml_Filters::should_do_image( $meta['file'], self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_FILENAME ] ) ) { do_action( 'optml_log', 'optimization filter' ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); return $meta; } $original_url = self::get_original_url( $attachment_id ); // Could not find original URL. if ( $original_url === false ) { do_action( 'optml_log', 'error getting original url' ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid original url.' ); return $meta; } // We should strip the `-scaled` from the URL to not generate inconsistencies with automatically scaled images. $original_url = $this->maybe_strip_scaled( $original_url ); $local_file = $this->maybe_strip_scaled( get_attached_file( $attachment_id ) ); $extension = $this->get_ext( $local_file ); $content_type = Optml_Config::$image_extensions [ $extension ]; $temp = explode( '/', $local_file ); $file_name = end( $temp ); $no_ext_filename = str_replace( '.' . $extension, '', $file_name ); $original_name = $file_name; if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'file before replace' ); do_action( 'optml_log', $local_file ); } // check if the current filename is the last deduplicated filename if ( ! empty( self::$last_deduplicated ) && strpos( $no_ext_filename, str_replace( '.' . $extension, '', self::$last_deduplicated ) ) !== false ) { // replace the file with the original before deduplication to get the path where the image is uploaded $local_file = str_replace( $file_name, self::$last_deduplicated, $local_file ); $original_name = self::$last_deduplicated; self::$last_deduplicated = false; } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'file after replace' ); do_action( 'optml_log', $local_file ); } if ( ! file_exists( $local_file ) ) { update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); do_action( 'optml_log', 'missing file' ); do_action( 'optml_log', $local_file ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has missing file.' ); return $meta; } if ( ! isset( Optml_Config::$image_extensions [ $extension ] ) ) { update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); do_action( 'optml_log', 'invalid extension' ); do_action( 'optml_log', $extension ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid extension.' ); return $meta; } if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_EXT ], $extension ) ) { do_action( 'optml_log', 'extension filter' ); do_action( 'optml_log', $extension ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); return $meta; } $offload_manager = Optimole::offload(); $offload_usage = $offload_manager->getUsage(); $current_run = self::get_process_meta(); $remaining = isset( $current_run['remaining'] ) ? absint( $current_run['remaining'] ) : 0; if ( $remaining + $offload_usage->getCurrent() >= $offload_usage->getLimit() ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'limit exceeded' ); do_action( 'optml_log', $offload_usage ); } self::$instance->settings->update( 'offload_limit_reached', 'enabled' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: offloading images would exceed limit.' ); return $meta; } try { $image_id = $offload_manager->uploadImage( $local_file, $original_url ); if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'image id' ); do_action( 'optml_log', $image_id ); } // We clear the retry counter if we reach this point. delete_post_meta( $attachment_id, self::RETRYABLE_META_COUNTER ); } catch ( InvalidArgumentException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'invalid argument exception' ); do_action( 'optml_log', $exception ); } update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' file is missing or unreadable.' ); return $meta; } catch ( InvalidUploadApiResponseException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'missing table id or upload url' ); do_action( 'optml_log', $exception ); } update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has invalid table id or upload url.' ); return $meta; } catch ( UploadFailedException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'upload error' ); do_action( 'optml_log', $exception ); } update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has upload error.' ); return $meta; } catch ( UploadLimitException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'limit exceeded' ); do_action( 'optml_log', $exception ); } self::$instance->settings->update( 'offload_limit', $exception->getUsage()->getLimit() ); self::$instance->settings->update( 'offload_limit_reached', 'enabled' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Offload stopped: upload limit exceeded' ); return $meta; } catch ( UploadApiException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'upload api error' ); do_action( 'optml_log', $exception ); } self::mark_retryable_error( $attachment_id, 'Image ID: ' . $attachment_id . ' has an error from upload api:' . $exception->getMessage() ); return $meta; } catch ( RuntimeException $exception ) { if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'runtime exception' ); do_action( 'optml_log', $exception ); } update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has an issue.' ); return $meta; } $url_to_append = $original_url; $url_parts = parse_url( $original_url ); if ( isset( $url_parts['scheme'] ) && isset( $url_parts['host'] ) ) { $url_to_append = $url_parts['scheme'] . '://' . $url_parts['host'] . '/' . $file_name; } $optimized_url = $this->get_media_optimized_url( $url_to_append, $image_id ); if ( ( new Optml_Api() )->check_optimized_url( $optimized_url ) === false ) { do_action( 'optml_log', 'optimization error' ); do_action( 'optml_log', $optimized_url ); Optimole::offload()->deleteImage( $image_id ); update_post_meta( $attachment_id, self::META_KEYS['offload_error'], 'true' ); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has optimization error.' ); return $meta; } @unlink( $local_file ); update_post_meta( $attachment_id, self::META_KEYS['offloaded'], 'true' ); update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true ); $meta['file'] = '/' . self::KEYS['uploaded_flag'] . $image_id . '/' . $url_to_append; if ( isset( $meta['sizes'] ) ) { foreach ( $meta['sizes'] as $key => $value ) { $generated_image_size_path = str_replace( $original_name, $meta['sizes'][ $key ]['file'], $local_file ); file_exists( $generated_image_size_path ) && unlink( $generated_image_size_path ); $meta['sizes'][ $key ]['file'] = $file_name; } } // This is needed for scaled images. // Otherwise, `-scaled` images will be left behind. if ( isset( $meta['original_image'] ) ) { $ext = $this->get_ext( $local_file ); $scaled_path = str_replace( '.' . $ext, '-scaled.' . $ext, $local_file ); file_exists( $scaled_path ) && unlink( $scaled_path ); } $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id ); if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) { foreach ( $duplicated_images as $duplicated_id ) { $duplicated_meta = wp_get_attachment_metadata( $duplicated_id ); if ( isset( $duplicated_meta['file'] ) && ! self::is_uploaded_image( $duplicated_meta['file'] ) ) { $duplicated_meta['file'] = $meta['file']; if ( $duplicated_meta['sizes'] ) { foreach ( $meta['sizes'] as $key => $value ) { $duplicated_meta['sizes'][ $key ]['file'] = $file_name; } } wp_update_attachment_metadata( $duplicated_id, $duplicated_meta ); update_post_meta( $duplicated_id, self::META_KEYS['offloaded'], 'true' ); update_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true ); } } } if ( OPTML_DEBUG_MEDIA ) { do_action( 'optml_log', 'success offload' ); } self::decrement_process_meta_remaining(); self::$instance->logger->add_log( Optml_Logger::LOG_TYPE_OFFLOAD, 'Image ID: ' . $attachment_id . ' has been offloaded.' ); $attachment_page_id = wp_get_post_parent_id( $attachment_id ); if ( $attachment_page_id !== false && $attachment_page_id !== 0 ) { self::$offload_update_post = true; update_post_meta( $attachment_page_id, self::POST_OFFLOADED_FLAG, 'true' ); self::$offload_update_post = false; } return $meta; } /** * Get the args for wp query according to the scope. * * @param int $batch Number of images to get. * @param string $action The action for which to get the images. * * @return array|false The query options array or false if not passed a valid action. */ public static function get_images_or_pages_query_args( $batch, $action, $get_images = false ) { $args = [ 'posts_per_page' => $batch, 'fields' => 'ids', 'ignore_sticky_posts' => false, 'no_found_rows' => true, ]; if ( $get_images === true ) { $args['post_type'] = 'attachment'; $args['post_mime_type'] = 'image'; $args['post_status'] = 'inherit'; // Offload args. if ( $action === 'offload_images' ) { $args['meta_query'] = [ 'relation' => 'AND', [ 'key' => self::META_KEYS['offloaded'], 'compare' => 'NOT EXISTS', ], [ 'key' => self::META_KEYS['offload_error'], 'compare' => 'NOT EXISTS', ], ]; return $args; } // Rollback args. $args['meta_query'] = [ 'relation' => 'AND', [ 'key' => self::META_KEYS['offloaded'], 'value' => 'true', 'compare' => '=', ], [ 'key' => self::META_KEYS['rollback_error'], 'compare' => 'NOT EXISTS', ], [ 'key' => Optml_Dam::OM_DAM_IMPORTED_FLAG, 'compare' => 'NOT EXISTS', ], ]; return $args; } $args = self::add_page_meta_query_args( $action, $args ); $post_types = array_filter( get_post_types(), function ( $post_type ) { if ( $post_type === 'attachment' || $post_type === 'revision' ) { return false; } return true; } ); $args ['post_type'] = array_values( $post_types ); return $args; } /** * Query the database and upload images to our servers. * * @param int $batch Number of images to process in a batch. * * @return array Number of found images and number of successfully processed images. */ public function upload_images( $batch, $images = [] ) { self::$instance->settings->update( 'offload_limit_reached', 'disabled' ); if ( empty( $images ) || $images === 'none' ) { $args = self::get_images_or_pages_query_args( $batch, 'offload_images', true ); $attachments = new \WP_Query( $args ); $ids = $attachments->get_posts(); } else { $ids = array_slice( $images, 0, $batch ); } $result = [ 'found_images' => count( $ids ) ]; $result['success_offload'] = $this->upload_and_update_existing_images( $ids ); return $result; } /** * Query the database and bring back image to media library. * * @param int $batch Number of images to process in a batch. * * @return array Number of found images and number of successfully processed images. */ public function rollback_images( $batch, $images = [] ) { if ( empty( $images ) || $images === 'none' ) { $args = self::get_images_or_pages_query_args( $batch, 'rollback_images', true ); $attachments = new \WP_Query( $args ); $ids = $attachments->get_posts(); } else { $ids = array_slice( $images, 0, $batch ); } $result = [ 'found_images' => count( $ids ) ]; $result['success_rollback'] = $this->rollback_and_update_images( $ids ); return $result; } /** * Update the post with the given id, the images will be updated by the filters we use. * * @param int $post_id The post id to update. * * @return bool Whether the update was succesful or not. */ public function update_page( $post_id ) { self::$offload_update_post = true; $post_update = wp_update_post( [ 'ID' => $post_id ] ); self::$offload_update_post = false; if ( $post_update === 0 ) { return false; } do_action( 'optml_updated_post', $post_id ); return true; } /** * Calculate the number of images in media library and the number of posts/pages. * * @param string $action The actions for which to get the number of images. * * @return int Number of images. */ public static function number_of_images_and_pages( $action ) { $images_args = self::get_images_or_pages_query_args( - 1, $action, true ); $images = new \WP_Query( $images_args ); // With the new mechanism, when offloading images, we don't need to address pages anymore. // Bail early with the number of images. if ( $action === 'offload_images' ) { return $images->post_count; } $pages_args = self::get_images_or_pages_query_args( - 1, $action ); $pages = new \WP_Query( $pages_args ); return $pages->post_count + $images->post_count; } /** * Calculate the number of images in media library and the number of posts/pages by IDs. * * @param string $action The actions for which to get the number of images. * * @return int Number of images. */ public static function number_of_images_by_ids( $action, $ids ) { $args = self::get_images_or_pages_query_args( - 1, $action, true ); $args['post__in'] = $ids; $images = new \WP_Query( $args ); return $images->post_count; } /** * Get pages that contain images by IDs. * * @param string $action The actions for which to get the number of images. * @param array $images Image IDs. * @param int $batch Batch count. * @param int $page Page number. */ public static function get_posts_by_image_ids( $action, $images = [], $batch = 10, $page = 1 ) { if ( empty( $images ) ) { return []; } $transient_key = 'optml_images_' . md5( serialize( $images ) ); $transient = get_transient( $transient_key ); if ( false !== $transient ) { return array_slice( $transient, ( $page - 1 ) * $batch, $batch ); } global $wpdb; $image_urls = array_map( function ( $image_id ) { $meta = wp_get_attachment_metadata( $image_id ); $extension = Optml_Media_Offload::instance()->get_ext( $meta['file'] ); return str_replace( '.' . $extension, '', $meta['file'] ); }, $images ); // Sanitize the image URLs for use in the SQL query. $urls = array_map( 'esc_url_raw', $image_urls ); // Initialize an empty string to hold the query. $query = ''; // Iterate through the array and add each URL to the query. foreach ( $urls as $index => $url ) { // If it's the first item, we don't need to add OR to the beginning. if ( $index === 0 ) { $query .= $wpdb->prepare( 'post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' ); } else { $query .= $wpdb->prepare( ' OR post_content LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' ); } } // Get all the posts IDs by using LIMIT and offset in a loop. $ids = []; $offset = 0; $limit = $batch; while ( true ) { $posts = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE $query LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $limit, $offset ) ); if ( empty( $posts ) ) { break; } $ids = array_merge( $ids, $posts ); $offset += $limit; } set_transient( $transient_key, $ids, HOUR_IN_SECONDS ); return array_slice( $ids, ( $page - 1 ) * $batch, $batch ); } /** * Record process meta, * * @param int $count The number of images to process. * * @return void */ public static function record_process_meta( $count ) { $meta = get_option( 'optml_process_meta', [] ); $meta['count'] = $count; $meta['remaining'] = $count; $meta['start_time'] = time(); update_option( 'optml_process_meta', $meta ); } /** * Update the process meta count. * * @return void */ public static function decrement_process_meta_remaining() { $meta = get_option( 'optml_process_meta', [] ); if ( ! isset( $meta['remaining'] ) ) { return; } $meta['remaining'] = $meta['remaining'] - 1; update_option( 'optml_process_meta', $meta ); } /** * Get process meta, * * @return array */ public static function get_process_meta() { $res = []; $meta = get_option( 'optml_process_meta', [] ); $res['time_passed'] = isset( $meta['start_time'] ) ? ( time() - $meta['start_time'] ) / 60 : 0; $res['count'] = isset( $meta['count'] ) ? $meta['count'] : 0; $res['remaining'] = isset( $meta['remaining'] ) ? $meta['remaining'] : $res['count']; return $res; } /** * Calculate the number of images in media library and the number of posts/pages. * * @param string $action The actions for which to get the number of images. * @param bool $refresh Whether to refresh the cron or not. * @param array $images The images to process. * * @return array Image count and Cron status. */ public static function get_image_count( $action, $refresh, $images = [] ) { $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status'; $count = 0; $step = 0; $batch = apply_filters( 'optimole_offload_batch', 20 ); // Reduce this to smaller if we have memory issues during testing. if ( empty( $images ) ) { $count = Optml_Media_Offload::number_of_images_and_pages( $action ); } else { $count = Optml_Media_Offload::number_of_images_by_ids( $action, $images ); } $possible_batch = ceil( $count / 10 ); if ( $possible_batch < $batch ) { $batch = $possible_batch; } // If batch is less than 10, set it to 10. if ( $batch < 10 ) { $batch = 10; } $in_progress = self::$instance->settings->get( $option ) !== 'disabled'; if ( $count === 0 ) { $in_progress = false; } $type = 'offload_images' === $action ? 'offload' : 'rollback'; // We save the status if this ia multi step transfer. if ( empty( $images ) ) { self::$instance->settings->update( 'transfer_status', $action ); } if ( false === $refresh ) { // We check also the alternative action to avoid doing both in the same time and disable the running one. $in_progress_b = self::$instance->settings->get( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status' ) !== 'disabled'; // We do this only if there is a mass action in progress, not individual ones. if ( $in_progress_b && empty( $images ) ) { // We stop the oposite action from going any further. self::$instance->settings->update( 'rollback_images' === $action ? 'offloading_status' : 'rollback_status', 'disabled' ); } self::$instance->settings->update( 'offload_limit_reached', 'disabled' ); self::record_process_meta( $count ); self::$instance->settings->update( $option, $in_progress ? 'enabled' : 'disabled' ); self::$instance->logger->add_log( $type, Optml_Logger::LOG_SEPARATOR ); self::$instance->logger->add_log( $type, 'Started with a total count of ' . intval( $count ) . '.' ); if ( $in_progress !== true ) { return [ 'count' => $count, 'status' => $in_progress, 'action' => $type, ]; } if ( empty( $images ) ) { $total = ceil( $count / $batch ); self::schedule_action( time(), 'optml_start_processing_images', [ $action, $batch, 1, $total, $step, ] ); } else { self::schedule_action( time(), 'optml_start_processing_images_by_id', [ $action, $batch, 1, $images, ] ); } } $response = [ 'count' => $count, 'action' => $type, ]; if ( $type === 'offload' ) { $offload_limit_reached = self::$instance->settings->is_offload_limit_reached(); if ( $offload_limit_reached ) { $in_progress = false; self::$instance->settings->update( $option, 'disabled' ); } $response['reached_limit'] = self::$instance->settings->is_offload_limit_reached(); $response['offload_limit'] = self::$instance->settings->get( 'offload_limit' ); } $response['status'] = $in_progress; return $response; } /** * Schedule an action. * * @param int $time The time to schedule the action. * @param string $hook The hook to schedule. * @param array $args The arguments to pass to the hook. * * @return mixed */ public static function schedule_action( $time, $hook, $args ) { // We use AS if available to avoid issues with WP Cron. if ( function_exists( 'as_schedule_single_action' ) ) { return as_schedule_single_action( $time, $hook, $args ); } else { return wp_schedule_single_event( $time, $hook, $args ); } } /** * Check if an action hook is scheduled. * * @param string $hook The hook to check. * * @return bool */ public static function is_scheduled( $hook ) { if ( function_exists( 'as_has_scheduled_action' ) ) { return as_has_scheduled_action( $hook ); } elseif ( function_exists( 'as_next_scheduled_action' ) ) { // For older versions of AS. return as_next_scheduled_action( $hook ) !== false; } else { return wp_next_scheduled( $hook ) !== false; } } /** * Start Processing Images by IDs * * @param string $action The action for which to get the number of images. * @param int $batch The batch of images to process. * @param int $page The page of images to process. * @param array $image_ids The images to process. * * @return void */ public function start_processing_images_by_id( $action, $batch, $page, $image_ids = [] ) { $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status'; $type = 'offload_images' === $action ? Optml_Logger::LOG_TYPE_OFFLOAD : Optml_Logger::LOG_TYPE_ROLLBACK; if ( self::$instance->settings->get( $option ) === 'disabled' ) { return; } set_time_limit( 0 ); // Only use the legacy offloaded attachments to query the pages that need to be updated. // We can be confident that these IDs are already marked as offloaded. $legacy_offloaded = array_filter( $image_ids, function ( $id ) { return ! $this->is_new_offloaded_attachment( $id ); } ); // On the new mechanism, we don't update posts anymore when offloading. $page_in = $action === 'offload_images' ? [] : Optml_Media_Offload::get_posts_by_image_ids( $action, $legacy_offloaded, $batch, $page ); if ( empty( $image_ids ) && empty( $page_in ) && empty( $legacy_offloaded ) ) { $meta = self::get_process_meta(); self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' ); self::$instance->settings->update( $option, 'disabled' ); return; } try { // This will be 0 in the case of offloading now. if ( $action === 'rollback_images' && 0 !== count( $page_in ) ) { $to_update = Optml_Media_Offload::instance()->update_content( $page, $action, $batch, $page_in ); if ( isset( $to_update['page'] ) ) { if ( isset( $to_update['imagesToUpdate'] ) && count( $to_update['imagesToUpdate'] ) ) { foreach ( $to_update['imagesToUpdate'] as $post_id => $images ) { if ( ! empty( $image_ids ) ) { $images = array_intersect( $images, $image_ids ); } if ( empty( $images ) ) { continue; } Optml_Media_Offload::instance()->rollback_and_update_images( $images ); Optml_Media_Offload::instance()->update_page( $post_id ); } } } $page = $page + 1; } else { // From $image_ids get the number as per $batch and save it in $page_in and update $images with the remaining images. $images = array_slice( $image_ids, 0, $batch ); $image_ids = array_slice( $image_ids, $batch ); $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch, $images ) : Optml_Media_Offload::instance()->upload_images( $batch, $images ); } self::schedule_action( time(), 'optml_start_processing_images_by_id', [ $action, $batch, $page, $image_ids, ] ); } catch ( Exception $e ) { // Reschedule the cron to run again after a delay. Sometimes memory limit is exhausted. $delay_in_seconds = 10; self::$instance->logger->add_log( $type, $e->getMessage() ); self::schedule_action( time() + $delay_in_seconds, 'optml_start_processing_images_by_id', [ $action, $batch, $page, $image_ids, ] ); } } /** * Start Processing Images * * @param string $action The action for which to get the number of images. * @param int $batch The batch of images to process. * @param int $page The page of images to process. * @param int $total The total number of pages. * @param int $step The current step. * * @return void */ public function start_processing_images( $action, $batch, $page, $total, $step ) { $option = 'offload_images' === $action ? 'offloading_status' : 'rollback_status'; $type = 'offload_images' === $action ? 'offload' : 'rollback'; if ( self::$instance->settings->get( $option ) === 'disabled' ) { return; } if ( $step > $total || 0 === $total ) { $meta = self::get_process_meta(); self::$instance->logger->add_log( $type, 'Process finished with ' . $meta['count'] . ' items in ' . $meta['time_passed'] . ' minutes.' ); self::$instance->settings->update( $option, 'disabled' ); self::$instance->settings->update( 'show_offload_finish_notice', $type ); return; } set_time_limit( 0 ); try { $posts_to_update = $action === 'offload_images' ? [] : Optml_Media_Offload::instance()->update_content( $page, $action, $batch ); // Kept for backward compatibility with old offloading mechanism where pages were modified. if ( $action === 'rollback_images' && isset( $posts_to_update['page'] ) && $posts_to_update['page'] > $page ) { $page = $posts_to_update['page']; if ( isset( $posts_to_update['imagesToUpdate'] ) && count( $posts_to_update['imagesToUpdate'] ) ) { foreach ( $posts_to_update['imagesToUpdate'] as $post_id => $images ) { Optml_Media_Offload::instance()->rollback_and_update_images( $images ); Optml_Media_Offload::instance()->update_page( $post_id ); } } } else { $action === 'rollback_images' ? Optml_Media_Offload::instance()->rollback_images( $batch ) : Optml_Media_Offload::instance()->upload_images( $batch ); } $step = $step + 1; self::schedule_action( time(), 'optml_start_processing_images', [ $action, $batch, $page, $total, $step, ] ); } catch ( Exception $e ) { // Reschedule the cron to run again after a delay. Sometimes memory limit is exausted. $delay_in_seconds = 10; self::$instance->logger->add_log( $type, $e->getMessage() ); self::schedule_action( time() + $delay_in_seconds, 'optml_start_processing_images', [ $action, $batch, $page, $total, $step, ] ); } } /** * Alter attachment image src for offloaded images. * * @param array|false $image { * Array of image data. * * @type string $0 Image source URL. * @type int $1 Image width in pixels. * @type int $2 Image height in pixels. * @type bool $3 Whether the image is a resized image. * } * * @param int $attachment_id attachment id. * @param string|int[] $size image size. * @param bool $icon Whether the image should be treated as an icon. * * @return array $image. */ public function alter_attachment_image_src( $image, $attachment_id, $size, $icon ) { if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) { return $image; } $url = get_post( $attachment_id ); $url = $url->guid; $metadata = wp_get_attachment_metadata( $attachment_id ); // Use the original size if the requested size is full. if ( $size === 'full' || $this->is_attachment_edit_page( $attachment_id ) ) { $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, [ 'width' => $metadata['width'], 'height' => $metadata['height'], 'attachment_id' => $attachment_id, ] ); return [ $image_url, $metadata['width'], $metadata['height'], false, ]; } if ( wp_attachment_is( 'video', $attachment_id ) && doing_action( 'wp_insert_post_data' ) ) { return $image; } $sizes = $this->size_to_dimension( $size, $metadata ); $image_url = $this->get_new_offloaded_attachment_url( $url, $attachment_id, [ 'width' => $sizes['width'], 'height' => $sizes['height'], 'resize' => $sizes['resize'] ?? [], 'attachment_id' => $attachment_id, ] ); return [ $image_url, $sizes['width'], $sizes['height'], $size === 'full', ]; } /** * Needed for image sizes inside the editor. * * @param array $response Array of prepared attachment data. @see wp_prepare_attachment_for_js(). * @param WP_Post $attachment Attachment object. * @param array|false $meta Array of attachment meta data, or false if there is none. * * @return array */ public function alter_attachment_for_js( $response, $attachment, $meta ) { if ( ! $this->is_new_offloaded_attachment( $attachment->ID ) ) { return $response; } $meta = []; if ( isset( $response['width'] ) ) { $meta['width'] = $response['width']; } if ( isset( $response['height'] ) ) { $meta['height'] = $response['height']; } $sizes = Optml_App_Replacer::image_sizes(); foreach ( $sizes as $size => $args ) { if ( isset( $response['sizes'][ $size ] ) ) { continue; } $args = $this->size_to_dimension( $size, $meta ); $response['sizes'][ $size ] = array_merge( $args, [ 'url' => $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $args ), 'orientation' => ( $args['height'] > $args['width'] ) ? 'portrait' : 'landscape', ] ); } $response['url'] = $this->get_new_offloaded_attachment_url( $response['url'], $attachment->ID, $meta ); return $response; } /** * Alter attachment metadata. * * @param array $metadata The attachment metadata. * @param int $id The attachment ID. * * @return array */ public function alter_attachment_metadata( $metadata, $id ) { if ( ! $this->is_new_offloaded_attachment( $id ) ) { return $metadata; } return $this->get_altered_metadata_for_remote_images( $metadata, $id ); } /** * Get offloaded image attachment URL for new offloads. * * @param string $url The initial attachment URL. * @param int $attachment_id The attachment ID. * @param array $args The additional arguments. * - width: The width of the image. * - height: The height of the image. * - crop: Whether to crop the image. * * @return string */ private function get_new_offloaded_attachment_url( $url, $attachment_id, $args = [] ) { $process_flag = self::KEYS['not_processed_flag'] . $attachment_id; // Image might have already passed through this filter. if ( strpos( $url, $process_flag ) !== false ) { return $url; } $meta = wp_get_attachment_metadata( $attachment_id ); if ( ! isset( $meta['file'] ) ) { return $url; } $default_args = [ 'width' => 'auto', 'height' => 'auto', 'resize' => apply_filters( 'optml_default_crop', [] ), ]; $args = wp_parse_args( $args, $default_args ); // If this is not cropped, we constrain the dimensions to the original image. if ( empty( $args['resize'] ) && ! in_array( 'auto', [ $args['width'], $args['height'] ], true ) ) { $dimensions = wp_constrain_dimensions( $meta['width'], $meta['height'], $args['width'], $args['height'] ); $args['width'] = $dimensions[0]; $args['height'] = $dimensions[1]; } $file = $meta['file']; if ( self::is_uploaded_image( $file ) ) { $optimized_url = $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] ); return strpos( $optimized_url, $process_flag ) === false ? str_replace( '/' . ltrim( $file, '/' ), '/' . $process_flag . $file, $optimized_url ) : $optimized_url; } else { // this is for the users that already offloaded the images before the other fixes $local_file = get_attached_file( $attachment_id ); if ( ! file_exists( $local_file ) ) { $duplicated_images = apply_filters( 'optml_offload_duplicated_images', [], $attachment_id ); if ( is_array( $duplicated_images ) && ! empty( $duplicated_images ) ) { foreach ( $duplicated_images as $id ) { if ( ! empty( $id ) ) { $duplicated_meta = wp_get_attachment_metadata( $id ); if ( isset( $duplicated_meta['file'] ) && self::is_uploaded_image( $duplicated_meta['file'] ) ) { return $this->get_optimized_image_url( $this->get_offloaded_attachment_url( $attachment_id, $url ), $args['width'], $args['height'], $args['resize'] ); } } } } } } return $url; } /** * Replace the URLs in the editor content with the offloaded ones. * * @param string $content The incoming content. * * @return string */ public function replace_urls_in_editor_content( $content ) { $raw_extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content ); if ( empty( $raw_extracted ) ) { return $content; } $to_replace = []; foreach ( $raw_extracted as $url ) { $attachment = $this->get_local_attachement_id_from_url( $url ); // No local attachment. if ( $attachment['attachment_id'] === 0 ) { if ( $this->can_replace_url( $url ) ) { $to_replace[ $url ] = $this->get_optimized_image_url( $url, 'auto', 'auto' ); } continue; } $attachment_id = $attachment['attachment_id']; // Not offloaded. if ( ! $this->is_new_offloaded_attachment( $attachment_id ) ) { continue; } // Get W/H from url. $size = $this->parse_dimensions_from_filename( $url ); $width = $size[0] !== false ? $size[0] : 'auto'; $height = $size[1] !== false ? $size[1] : 'auto'; // Handle resize. $sizes2crop = self::size_to_crop(); $resize = apply_filters( 'optml_default_crop', [] ); $sizes = image_get_intermediate_size( $attachment_id, $size ); if ( false !== $sizes ) { if ( isset( $sizes2crop[ $width . $height ] ) ) { $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] ); } } // Build the optimized URL. $optimized_url = $this->get_optimized_image_url( self::KEYS['not_processed_flag'] . $attachment_id . '/' . ltrim( $this->get_offloaded_attachment_url( $attachment_id, $url ), '/' ), $width, $height, $resize ); // Drop any image size from the URL. $optimized_url = str_replace( '-' . $width . 'x' . $height, '', $optimized_url ); $to_replace[ $url ] = $optimized_url; } return str_replace( array_keys( $to_replace ), array_values( $to_replace ), $content ); } /** * Replaces the post content URLs to use Offloaded ones on editor fetch. * * @param \WP_REST_Response $response The response object. * @param \WP_Post $post The post object. * @param \WP_REST_Request $request The request object. * * @return \WP_REST_Response */ public function pre_filter_rest_content( \WP_REST_Response $response, \WP_Post $post, \WP_REST_Request $request ) { $context = $request->get_param( 'context' ); if ( $context !== 'edit' ) { return $response; } $data = $response->get_data(); // Actually replace all URLs. $data['content']['raw'] = $this->replace_urls_in_editor_content( $data['content']['raw'] ); $response->set_data( $data ); return $response; } /** * Legacy function to be used for WordPress versions under 6.0.0. * * @param array $post_data Slashed, sanitized, processed post data. * @param array $postarr Slashed sanitized post data. * @param array $unsanitized_postarr Un-sanitized post data. * * @return array */ public function legacy_filter_saved_data( $post_data, $postarr, $unsanitized_postarr ) { return $this->filter_saved_data( $post_data, $postarr, $unsanitized_postarr, true ); } /** * Filter post content to use local attachments when saving offloaded images. * * @param array $post_data Slashed, sanitized, processed post data. * @param array $postarr Slashed sanitized post data. * @param array $unsanitized_postarr Un-sanitized post data. * @param bool $update Whether this is an existing post being updated or not. * * @return array */ public function filter_saved_data( $post_data, $postarr, $unsanitized_postarr, $update ) { if ( $postarr['post_status'] === 'trash' ) { return $post_data; } $content = $post_data['post_content']; $extracted = Optml_Main::instance()->manager->extract_urls_from_content( $content ); $replace = []; foreach ( $extracted as $idx => $url ) { $id = self::get_attachment_id_from_url( $url ); if ( $id === false ) { continue; } $id = (int) $id; if ( $this->is_legacy_offloaded_attachment( $id ) ) { continue; } $original = self::get_original_url( $id ); if ( $original === false ) { continue; } $replace[ $url ] = $original; $size = $this->parse_dimension_from_optimized_url( $url ); if ( $size[0] === 'auto' || $size[1] === 'auto' ) { continue; } $extension = $this->get_ext( $url ); $metadata = wp_get_attachment_metadata( $id ); // Is this the full URL. if ( $metadata['width'] === (int) $size[0] && $metadata['height'] === (int) $size[1] ) { continue; } $size_crop_map = self::size_to_crop(); $crop = false; if ( isset( $size_crop_map[ $size[0] . $size[1] ] ) ) { $crop = $size_crop_map[ $size[0] . $size[1] ]; } if ( $crop ) { $width = $size[0]; $height = $size[1]; } else { // In case of an image size, we need to calculate the new dimensions for the proper file path. $constrained = wp_constrain_dimensions( $metadata['width'], $metadata['height'], $size[0], $size[1] ); $width = $constrained[0]; $height = $constrained[1]; } $replace[ $url ] = $this->maybe_strip_scaled( $replace[ $url ] ); $suffix = sprintf( '-%sx%s.%s', $width, $height, $extension ); $replace[ $url ] = str_replace( '.' . $extension, $suffix, $replace[ $url ] ); } $post_data['post_content'] = str_replace( array_keys( $replace ), array_values( $replace ), $content ); return $post_data; } /** * Alter the image size for the image widget. * * @param string $html the attachment image HTML string. * @param array $settings Control settings. * @param string $image_size_key Optional. Settings key for image size. * Default is `image`. * @param string $image_key Optional. Settings key for image. Default * is null. If not defined uses image size key * as the image key. * * @return string */ public function alter_elementor_image_size( $html, $settings, $image_size_key, $image_key ) { if ( ! isset( $settings['image'] ) ) { return $html; } $image = $settings['image']; if ( ! isset( $image['id'] ) ) { return $html; } if ( ! $this->is_new_offloaded_attachment( $image['id'] ) ) { return $html; } if ( ! isset( $settings['image_size'] ) ) { return $html; } if ( $settings['image_size'] === 'custom' ) { if ( ! isset( $settings['image_custom_dimension'] ) ) { return $html; } $custom_dimensions = $settings['image_custom_dimension']; if ( ! isset( $custom_dimensions['width'] ) || ! isset( $custom_dimensions['height'] ) ) { return $html; } $new_args = [ 'width' => $custom_dimensions['width'], 'height' => $custom_dimensions['height'], 'resize' => $this->to_optml_crop( true ), ]; $new_url = $this->get_new_offloaded_attachment_url( $image['url'], $image['id'], $new_args ); return str_replace( $image['url'], $new_url, $html ); } return $html; } /** * Adds new actions for new offloads. * * @return void */ public function add_new_actions() { add_filter( 'wp_prepare_attachment_for_js', [ self::$instance, 'alter_attachment_for_js' ], 999, 3 ); add_filter( 'wp_get_attachment_metadata', [ self::$instance, 'alter_attachment_metadata' ], 10, 2 ); add_filter( 'wp_get_attachment_image_src', [ self::$instance, 'alter_attachment_image_src' ], 10, 4 ); // Needed for rendering beaver builder css properly. add_filter( 'fl_builder_render_css', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 ); // Filter saved data on insert to use local attachments. // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); if ( $below_6_0_0 ) { add_filter( 'wp_insert_post_data', [ self::$instance, 'legacy_filter_saved_data' ], 10, 3 ); } else { add_filter( 'wp_insert_post_data', [ self::$instance, 'filter_saved_data' ], 10, 4 ); } // Filter loaded data in the editors to use local attachments. add_filter( 'content_edit_pre', [ self::$instance, 'replace_urls_in_editor_content' ], 10, 1 ); add_action( 'init', function () { $types = get_post_types_by_support( 'editor' ); foreach ( $types as $type ) { $post_type = get_post_type_object( $type ); if ( property_exists( $post_type, 'show_in_rest' ) && true === $post_type->show_in_rest ) { add_filter( 'rest_prepare_' . $type, [ self::$instance, 'pre_filter_rest_content' ], 10, 3 ); } } }, PHP_INT_MAX ); add_filter( 'get_attached_file', [ $this, 'alter_attached_file_response' ], 10, 2 ); add_filter( 'elementor/image_size/get_attachment_image_html', [ $this, 'alter_elementor_image_size', ], 10, 4 ); } /** * Elementor checks if the file exists before requesting a specific image size. * * Needed because otherwise there won't be any width/height on the `img` tags, breaking lazyload. * * Also needed because some * * @param string $file The file path. * @param int $id The attachment ID. * * @return bool|string */ public function alter_attached_file_response( $file, $id ) { if ( ! $this->is_new_offloaded_attachment( $id ) ) { return $file; } $metadata = wp_get_attachment_metadata( $id ); if ( isset( $metadata['file'] ) ) { $uploads = wp_get_upload_dir(); return $uploads['basedir'] . '/' . $metadata['file']; } return true; } /** * Maybe strip the `-scaled` from the URL. * * @param string $url The url. * * @return string */ public function maybe_strip_scaled( $url ) { $ext = $this->get_ext( $url ); return str_replace( '-scaled.' . $ext, '.' . $ext, $url ); } /** * Is it a PHPUnit test run. * * @return bool */ public static function is_phpunit_test() { return defined( 'OPTML_PHPUNIT_TESTING' ) && OPTML_PHPUNIT_TESTING === true; } /** * Get offloaded image attachment URL based on the given attachment ID and URL. * * @param mixed $attachment_id The attachment ID. * @param string $url The attachment URL. * * @return string */ private function get_offloaded_attachment_url( $attachment_id, $url ) { if ( ! $this->settings->is_offload_enabled() || ! is_numeric( $attachment_id ) ) { return $url; } elseif ( empty( $attachment_id ) && strpos( $url, self::KEYS['not_processed_flag'] ) !== false ) { $attachment_id = (int) self::get_attachment_id_from_url( $url ); } elseif ( empty( $attachment_id ) ) { $attachment_id = $this->attachment_url_to_post_id( $url ); } if ( $attachment_id > 0 || ! empty( get_post_meta( $attachment_id, self::OM_OFFLOADED_FLAG, true ) ) ) { $url = wp_get_attachment_metadata( $attachment_id )['file']; } return $url; } } compatibilities/wp_bakery.php 0000644 00000002601 14720403740 0012421 0 ustar 00 <?php /** * Class Optml_wp_bakery * * @reason Adding images from wpbakery pages to offload. */ class Optml_wp_bakery extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'js_composer/js_composer.php' ) && Optml_Main::instance()->admin->settings->get( 'offload_media' ) === 'enabled'; } /** * Register integration details. */ public function register() { add_filter( 'optml_content_images_to_update', [ $this, 'add_images_from_wp_bakery_to_offload' ], PHP_INT_MAX, 2 ); } /** * Ads the product pages to the list of posts parents when querying images for offload. * * @param array $found_images The found images from the default workflow. * @param string $content Post content to look for images. * * @return array The images array with the specific bakery images. */ public function add_images_from_wp_bakery_to_offload( $found_images, $content ) { $regex = '/image="(\d+)"/Uu'; preg_match_all( $regex, $content, $images_to_append ); return array_merge( $found_images, $images_to_append[1] ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/metaslider.php 0000644 00000004056 14720403740 0012575 0 ustar 00 <?php /** * Class Optml_metaslider. * * @reason Metaslider behaves strange when the noscript tag is present near the image tag. */ class Optml_metaslider extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'ml-slider/ml-slider.php' ) || is_plugin_active( 'ml-slider-pro/ml-slider-pro.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_ignore_noscript_on', [ $this, 'add_noscript_flags' ], PHP_INT_MAX, 1 ); add_filter( 'optml_possible_lazyload_flags', [ $this, 'add_ignore_lazyload' ], PHP_INT_MAX, 1 ); add_filter( 'optml_watcher_lz_classes', [ $this, 'add_watcher_class' ], 10 ); add_filter( 'metaslider_coin_slider_image_attributes', [ $this, 'setup_listner' ], PHP_INT_MAX, 1 ); add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.coin-slider > .coin-slider > a'; $all_watchers[] = '.coin-slider > .coin-slider'; return $all_watchers; } ); } /** * Disable lazyload on coinslider type. * * @param array $attributes Old attributes. * * @return mixed Slider attributes. */ public function setup_listner( $attributes ) { $attributes['class'] .= 'no-optml-lazyload'; return $attributes; } /** * Add ignore lazyload flag. * * @param array $flags Old flags. * * @return array New flags. */ public function add_ignore_lazyload( $flags = [] ) { $flags[] = 'no-optml-lazyload'; return $flags; } /** * Add nive slider watcher class. * * @param array $classes Old watcher. * * @return array New watchers. */ public function add_watcher_class( $classes ) { $classes[] = 'nivo-main-image'; return $classes; } /** * Return metaslider flags. * * @param array $flags Old flags. * * @return array New flags. */ public function add_noscript_flags( $flags = [] ) { $flags[] = 'slide-'; return $flags; } } compatibilities/w3_total_cache.php 0000644 00000002027 14720403740 0013317 0 ustar 00 <?php /** * Class Optml_w3_total_cache. * * @reason Cache_w3_total_cache stores the content of the page before Optimole starts replacing url's */ class Optml_w3_total_cache extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'w3-total-cache/w3-total-cache.php' ); } /** * Register integration details. */ public function register() { if ( Optml_Main::instance()->admin->settings->get( 'cdn' ) === 'enabled' ) { add_filter( 'w3tc_minify_processed', [ Optml_Main::instance()->manager, 'replace_content' ], 10 ); } add_action( 'optml_settings_updated', function () { if ( function_exists( 'w3tc_flush_all' ) ) { w3tc_flush_all(); } } ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/sg_optimizer.php 0000644 00000001327 14720403740 0013155 0 ustar 00 <?php /** * Class Optml_sg_optimizer. * * @reason Sg_optimizer has ob_start on init */ class Optml_sg_optimizer extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return Optml_Main::instance()->admin->settings->get( 'cdn' ) === 'enabled' && is_plugin_active( 'sg-cachepress/sg-cachepress.php' ); } /** * Register integration details. */ public function register() { add_action( 'init', [ Optml_Main::instance()->manager, 'process_template_redirect_content', ], defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN ); } } compatibilities/smart_search_woocommerce.php 0000644 00000002321 14720403740 0015507 0 ustar 00 <?php /** * Class Optml_smart_search_woocommerce. * * @reason Smart search by searchanise stores the content of the image urls before they are processed. */ class Optml_smart_search_woocommerce extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'smart-search-for-woocommerce/woocommerce-searchanise.php' ); } /** * Register integration details. */ public function register() { add_filter( 'se_get_product_image_post', [ $this, 'filter_image_url' ], 1, 3 ); } /** * Process image url before is send to the server. * * @param string $image_url The original image url. * @param int $image_id The image id . * @param int $size The image size. * @return string */ public function filter_image_url( $image_url, $image_id, $size ) { Optml_Url_Replacer::instance()->init(); return apply_filters( 'optml_content_url', $image_url ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/master_slider.php 0000644 00000001743 14720403740 0013301 0 ustar 00 <?php /** * Class Optml_master_slider. * * @reason Added classes to watch for background lazyload, * we can only hook the css before storing in db ('masterslider_get_all_custom_css'), but we can not reliably regenerate it when settings are changing */ class Optml_master_slider extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'master-slider/master-slider.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.master-slider'; return $all_watchers; } ); add_filter( 'optml_dont_replace_url', function ( $old, $url = null ) { if ( strpos( $url, 'blank.gif' ) !== false ) { return true; } return $old; }, 10, 2 ); } } compatibilities/cache_enabler.php 0000644 00000001776 14720403740 0013205 0 ustar 00 <?php /** * Class Optml_cache_enabler. * * @reason Cache_enabler stores the content of the page before Optimole starts replacing url's */ class Optml_cache_enabler extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'cache-enabler/cache-enabler.php' ); } /** * Register integration details. */ public function register() { // www.keycdn.com/support/wordpress-cache-enabler-plugin#hooks add_filter( 'cache_enabler_page_contents_before_store', [ Optml_Main::instance()->manager, 'replace_content' ], PHP_INT_MAX, 1 ); add_action( 'optml_settings_updated', function () { do_action( 'cache_enabler_clear_site_cache' ); } ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/divi_builder.php 0000644 00000004450 14720403740 0013103 0 ustar 00 <?php /** * Class Optml_divi_builder * * @reason Adding selectors for background lazyload */ class Optml_divi_builder extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { return ( strcmp( wp_get_theme(), 'Divi' ) === 0 || is_plugin_active( 'divi-builder/divi-builder.php' ) ); } /** * Register integration details. */ public function register() { add_action( 'et_core_static_file_created', [ $this, 'optimize_divi_static_files' ] ); add_action( 'optml_settings_updated', [ $this, 'clear_divi_static_files' ] ); add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.et_pb_slides > .et_pb_slide'; $all_watchers[] = '.et_parallax_bg'; $all_watchers[] = '.et_pb_video_overlay'; $all_watchers[] = '.et_pb_module:not([class*="et_pb_blog"])'; $all_watchers[] = '.et_pb_row'; $all_watchers[] = '.et_pb_section.et_pb_section_1'; $all_watchers[] = '.et_pb_with_background'; return $all_watchers; } ); } /** * Replace image urls upon divi's static css files creation * * @param ET_Core_PageResource $page_resource ET_Core_PageResource object. * @return void */ public function optimize_divi_static_files( $page_resource ) { if ( class_exists( 'ET_Core_PageResource' ) && null !== ET_Core_PageResource::$wpfs ) { if ( isset( $page_resource->path ) ) { $data = $page_resource->get_data( 'file' ); if ( ! empty( $data ) ) { $data = Optml_Main::instance()->manager->replace_content( $data ); ET_Core_PageResource::$wpfs->put_contents( $page_resource->path, $data, 0644 ); } } } } /** * Clear divi static files when plugin settings are changed * * @return void */ public function clear_divi_static_files() { if ( class_exists( 'ET_Core_PageResource' ) && method_exists( ET_Core_PageResource::class, 'remove_static_resources' ) ) { // same call as in et_core_page_resource_auto_clear but that function is not declared at this point ET_Core_PageResource::remove_static_resources( 'all', 'all' ); } } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/give_wp.php 0000644 00000002551 14720403740 0012102 0 ustar 00 <?php /** * Class Optml_elementor_builder * * @reason Adding selectors for background lazyload */ class Optml_give_wp extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return Optml_Main::instance()->admin->settings->use_lazyload() && is_plugin_active( 'give/give.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_should_ignore_image_tags', [ $this, 'check_givewp_page' ], 10 ); if ( Optml_Main::instance()->admin->settings->get( 'video_lazyload' ) === 'enabled' ) { add_filter( 'optml_iframe_lazyload_flags', [ $this, 'add_ignore_lazyload_iframe' ] ); } } /** * Add ignore page lazyload flag. * * @param bool $old_value Previous returned value. * * @return bool If we should lazyload the page. */ public function check_givewp_page( $old_value ) { if ( array_key_exists( 'giveDonationFormInIframe', $_GET ) && $_GET['giveDonationFormInIframe'] === '1' ) { return true; } return $old_value; } /** * Add ignore lazyload iframe flag. * * @param array $flags Old flags. * * @return array New flags. */ public function add_ignore_lazyload_iframe( $flags = [] ) { $flags[] = 'give-embed-form'; return $flags; } } compatibilities/jet_elements.php 0000644 00000001431 14720403740 0013114 0 ustar 00 <?php /** * Class Optml_jet_elements. * * @reason Disable lazyload on jetelements slider. */ class Optml_jet_elements extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'jet-elements/jet-elements.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_possible_lazyload_flags', [ $this, 'add_ignore_lazyload' ], PHP_INT_MAX, 1 ); } /** * Add ignore lazyload flag. * * @param array $flags Old flags. * * @return array New flags. */ public function add_ignore_lazyload( $flags = [] ) { $flags[] = 'sp-image'; return $flags; } } compatibilities/envira.php 0000644 00000004005 14720403740 0011722 0 ustar 00 <?php use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty; use Optimole\Sdk\ValueObject\Position; /** * Class Optml_shortcode_ultimate. * * @reason The gallery output contains a different src attribute used for lazyload * which prevented optimole to parse the tag. */ class Optml_envira extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return ( is_plugin_active( 'envira-gallery-lite/envira-gallery-lite.php' ) || is_plugin_active( 'envira-gallery/envira-gallery.php' ) ); } /** * Register integration details. */ public function register() { add_filter( 'optml_possible_lazyload_flags', [ $this, 'add_lazyflag' ], 10 ); add_filter( 'optml_parse_resize_from_tag', [ $this, 'check_resize_tag' ], 10, 2 ); add_filter( 'envira_gallery_image_src', [ $this, 'revert_src' ], 10 ); } /** * Revert the optimole url to the original state in * order to allow to be parsed by the image tag parser. * * @param string $image Image url. * * @return string Original url. */ public function revert_src( $image ) { $pos = strpos( $image, '/http' ); if ( $pos !== false ) { return ltrim( substr( $image, $pos ), '/' ); } return $image; } /** * Alter default resize for image tag parsing. * * @param array $old_resize Old array, if any. * @param string $tag Image tag. * * @return array Resize conf. */ public function check_resize_tag( $old_resize, $tag ) { if ( preg_match( '/(_c)\.(?:' . implode( '|', array_keys( Optml_Config::$image_extensions ) ) . ')/i', $tag, $match ) ) { return [ 'type' => ResizeTypeProperty::FILL, 'gravity' => Position::CENTER, ]; } return []; } /** * Add envira lazyload flag. * * @param array $strings Old strings. * * @return array New flags. */ public function add_lazyflag( $strings = [] ) { $strings[] = 'envira-gallery-image'; return $strings; } } compatibilities/otter_blocks.php 0000644 00000002201 14720403740 0013124 0 ustar 00 <?php /** * Class Optml_otter_blocks. * * @reason Adding selectors for background lazyload */ class Optml_otter_blocks extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'otter-blocks/otter-blocks.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.o-flip-front'; $all_watchers[] = '.o-flip-back'; $all_watchers[] = '.wp-block-themeisle-blocks-advanced-columns'; $all_watchers[] = '.wp-block-themeisle-blocks-advanced-columns-overlay'; $all_watchers[] = '.wp-block-themeisle-blocks-advanced-column'; $all_watchers[] = '.wp-block-themeisle-blocks-advanced-column-overlay'; return $all_watchers; } ); // Replace the image URL with the optimized one in Otter-generated CSS. add_filter( 'otter_apply_dynamic_image', [ Optml_Main::instance()->manager->url_replacer, 'build_url' ], 99 ); } } compatibilities/elementor_builder_late.php 0000644 00000002652 14720403740 0015151 0 ustar 00 <?php /** * Class Optml_elementor_builder_late * * @reason Adding action for elementor meta replacement */ class Optml_elementor_builder_late extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'elementor/elementor.php' ); } /** * Register integration details. */ public function register() { add_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 ); } /** * Replace urls in post meta values. * * @param mixed $metadata Metadata. * @param int $object_id Post id. * @param string $meta_key Meta key. * @param bool $single Is single. * * @return mixed Altered meta. */ public function replace_meta( $metadata, $object_id, $meta_key, $single ) { $meta_needed = '_elementor_data'; if ( ! empty( $meta_key ) && $meta_needed === $meta_key ) { remove_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX ); $current_meta = get_post_meta( $object_id, $meta_needed, $single ); add_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 ); if ( ! is_string( $current_meta ) ) { return $metadata; } return Optml_Main::instance()->manager->replace_content( $current_meta ); } // Return original if the check does not pass return $metadata; } } compatibilities/translate_press.php 0000644 00000001275 14720403740 0013655 0 ustar 00 <?php /** * Class Optml_translate_press. * * @reason Optml_translate_page conflict on buffer start need to hook earlier */ class Optml_translate_press extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'translatepress-multilingual/index.php' ); } /** * Register integration details. */ public function register() { add_action( 'init', [ Optml_Main::instance()->manager, 'process_template_redirect_content', ], defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN ); } } compatibilities/foogallery.php 0000644 00000001712 14720403740 0012603 0 ustar 00 <?php /** * Class Optml_shortcode_ultimate. * * @reason The gallery output contains a different src attribute used for lazyload * which prevented optimole to parse the tag. */ class Optml_foogallery extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'foogallery/foogallery.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_possible_src_attributes', [ $this, 'add_lazysrc' ], 10 ); add_filter( 'optml_possible_lazyload_flags', [ $this, 'add_lazysrc' ], 10 ); } /** * Add foogallery lazysrc attribute. * * @param array $attributes Old src attributes. * * @return array New src attributes. */ public function add_lazysrc( $attributes = [] ) { $attributes[] = 'data-src-fg'; return $attributes; } } compatibilities/essential_grid.php 0000644 00000001513 14720403740 0013433 0 ustar 00 <?php /** * Class Optml_metaslider. * * @reason Metaslider behaves strange when the noscript tag is present near the image tag. */ class Optml_essential_grid extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'essential-grid/essential-grid.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_lazyload_bg_classes', [ $this, 'add_bg_class' ], PHP_INT_MAX, 1 ); } /** * Adds essential grid listener class. * * @param array $classes Old classes. * * @return array New classes. */ public function add_bg_class( $classes ) { $classes[] = 'esg-media-poster'; return $classes; } } compatibilities/swift_performance.php 0000644 00000001327 14720403740 0014157 0 ustar 00 <?php /** * Class Optml_swift_performance. * * @reason Cache_swift_performance stores the content of the page before Optimole starts replacing url's */ class Optml_swift_performance extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return Optml_Main::instance()->admin->settings->get( 'cdn' ) === 'enabled' && is_plugin_active( 'swift-performance-lite/performance.php' ); } /** * Register integration details. */ public function register() { add_filter( 'swift_performance_buffer', [ Optml_Main::instance()->manager, 'replace_content' ], 10 ); } } compatibilities/elementor_builder.php 0000644 00000006631 14720403740 0014145 0 ustar 00 <?php /** * Class Optml_elementor_builder * * @reason Adding selectors for background lazyload */ class Optml_elementor_builder extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'elementor/elementor.php' ); } /** * Register integration details. */ public function register() { add_action( 'elementor/frontend/after_enqueue_styles', [ $this, 'add_src' ], PHP_INT_MIN, 1 ); add_filter( 'elementor/frontend/builder_content/before_enqueue_css_file', [ $this, 'add_src_filter' ], PHP_INT_MIN, 1 ); add_filter( 'elementor/frontend/builder_content/before_print_css', [ $this, 'remove_src_filter' ], PHP_INT_MIN, 1 ); add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.elementor-widget-container'; $all_watchers[] = '.elementor-background-slideshow__slide__image'; return $all_watchers; } ); add_action( 'optml_settings_updated', function () { if ( did_action( 'elementor/loaded' ) ) { if ( class_exists( '\Elementor\Plugin' ) ) { \Elementor\Plugin::instance()->files_manager->clear_cache(); } } } ); } /** * Remove filter for the image src after the css is saved by elementor. * * @param bool $with_css Flag used to determine if the css will be inline or not. Not used. * @return bool * @uses filter:elementor/frontend/builder_content/before_print_css */ public function remove_src_filter( $with_css ) { remove_filter( 'wp_get_attachment_image_src', [ $this, 'optimize_src' ], PHP_INT_MAX ); return $with_css; } /** * Add filter for the image src after the css is enqueued. * * @param object $css Elementor css info. Not used. * @return object * @uses filter:elementor/frontend/builder_content/before_enqueue_css_file */ public function add_src_filter( $css ) { // check if the filter was added to avoid duplication if ( has_filter( 'wp_get_attachment_image_src', [ $this, 'optimize_src' ] ) ) { return $css; } add_filter( 'wp_get_attachment_image_src', [ $this, 'optimize_src' ], PHP_INT_MAX, 4 ); return $css; } /** * Add action to add the filter for the image src. * * @return void */ public function add_src() { if ( ! has_filter( 'wp_get_attachment_image_src', [ $this, 'optimize_src' ] ) ) { add_filter( 'wp_get_attachment_image_src', [ $this, 'optimize_src' ], PHP_INT_MAX, 4 ); } } /** * Optimize the image src when it is requested in elementor. * * @param array $image Image data. * @param int $attachment_id Attachment id. * @param string|int[] $size Image size. * @param bool $icon Whether to use icon or not. * @return array * @uses filter:wp_get_attachment_image_src */ public function optimize_src( $image, $attachment_id, $size, $icon ) { if ( ! isset( $image[0] ) ) { return $image; } // We can't run the replacer before the setup is done, otherwise it will throw errors. if ( ! did_action( 'optml_replacer_setup' ) ) { return $image; } $image[0] = Optml_Main::instance()->manager->url_replacer->build_url( $image[0] ); return $image; } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/beaver_builder.php 0000644 00000002032 14720403740 0013406 0 ustar 00 <?php /** * Class Optml_beaver_builder. * * @reason Beaver builder offload the CSS in a separate file, which we access and process the image urls. */ class Optml_beaver_builder extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'bb-plugin/fl-builder.php' ) || is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.fl-col-content'; $all_watchers[] = '.fl-row-bg-photo > .fl-row-content-wrap'; return $all_watchers; } ); add_filter( 'fl_builder_render_css', [ Optml_Main::instance()->manager, 'replace_content' ], PHP_INT_MAX, 1 ); add_filter( 'fl_builder_render_js', [ Optml_Main::instance()->manager, 'replace_content' ], PHP_INT_MAX, 1 ); } } compatibilities/spectra.php 0000644 00000002714 14720403740 0012104 0 ustar 00 <?php /** * Class Optml_spectra * * @reason Optimizing Block images when saved to a CSS file. */ class Optml_spectra extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'ultimate-addons-for-gutenberg/ultimate-addons-for-gutenberg.php' ) && 'enabled' === get_option( '_uagb_allow_file_generation', 'enabled' ); } /** * Register integration details. */ public function register() { add_filter( 'uagb_block_attributes_for_css_and_js', [ $this, 'optimize_src' ], PHP_INT_MAX, 2 ); } /** * Optimize the src attribute. * * @param array $attrs Array of attributes. * @param string $name Name of the block. * * @return array */ public function optimize_src( $attrs, $name ) { $attrs = $this->iterate_array( $attrs ); return $attrs; } /** * Iterate through the array and replace the url. * * @param array $attrs Array of attributes. * @return array */ public function iterate_array( $attrs ) { foreach ( $attrs as $key => $value ) { if ( is_array( $value ) ) { $attrs[ $key ] = $this->iterate_array( $value ); } if ( is_string( $value ) && preg_match( '/(http|https):\/\/.*\.(?:png|jpg|jpeg|gif|webp)/i', $value ) ) { $attrs[ $key ] = Optml_Main::instance()->manager->url_replacer->build_url( $value ); } } return $attrs; } } compatibilities/thrive.php 0000644 00000003041 14720403740 0011736 0 ustar 00 <?php /** * Class Optml_thrive. * * @reason @reason Adding selectors for background lazyload */ class Optml_thrive extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'thrive-visual-editor/thrive-visual-editor.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_lazyload_bg_selectors', function ( $all_watchers ) { $all_watchers[] = '.tve-content-box-background'; $all_watchers[] = '.tve-page-section-out'; $all_watchers[] = '.thrv_text_element'; return $all_watchers; } ); if ( Optml_Main::instance()->admin->settings->get( 'offload_media' ) === 'enabled' ) { add_action( 'optml_updated_post', [ $this, 'update_trive_postmeta' ], 1, 1 ); } } /** * Update tve_updated_post meta with the correct status for images: offloaded/rollback. * * @param int $post_id The post id to be updated. */ public function update_trive_postmeta( $post_id ) { $post_meta = tve_get_post_meta( $post_id, 'tve_updated_post' ); $content = Optml_Media_Offload::instance()->filter_uploaded_images( [ 'post_content' => $post_meta ] ); tve_update_post_meta( $post_id, 'tve_updated_post', $content['post_content'] ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/facetwp.php 0000644 00000004363 14720403740 0012076 0 ustar 00 <?php /** * Class Optml_facetwp * * @reason Adding filter to force replacement on api request. */ class Optml_facetwp extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'facetwp/index.php' ); } /** * Register integration details. */ public function register() { add_filter( 'facetwp_ajax_response', [ $this, 'api_replacement_filter' ], 1, 2 ); add_action( 'facetwp_inject_template', [ $this, 'api_replacement_action' ], 1 ); } /** * Add filter to replace images from api requests. * * Modifies template for a facetwp template. * NOTE: $output needs to be json decoded before modification and re encoded before returning. * * @param string $output The output before optimization. * @param array $data More data about the request. * @return string The output with the optimized images. */ public function api_replacement_filter( $output, $data ) { do_action( 'optml_replacer_setup' ); $output = json_decode( $output ); if ( isset( $output->template ) ) { $output->template = Optml_Main::instance()->manager->replace_content( $output->template ); } // Ignore invalid UTF-8 characters in PHP 7.2+ if ( version_compare( phpversion(), '7.2', '<' ) ) { $output = json_encode( $output ); } else { $output = json_encode( $output, JSON_INVALID_UTF8_IGNORE ); } $output = Optml_Main::instance()->manager->replace_content( $output ); return $output; } /** * Add action to replace images from facetwp api requests. * * Modifies template for a non-facetwp template. * * @param array $output The output before optimization. * @return void */ public function api_replacement_action( $output ) { do_action( 'optml_replacer_setup' ); if ( ! isset( $output['template'] ) || ! function_exists( 'FWP' ) ) { return; } $output['template'] = Optml_Main::instance()->manager->replace_content( $output['template'] ); FWP()->request->output = $output; } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/wp_rest_cache.php 0000644 00000002157 14720403740 0013252 0 ustar 00 <?php /** * Class Optml_wp_rest_cache. * * @reason Wp rest cache stores the api response before Optimole starts replacing urls */ class Optml_wp_rest_cache extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'wp-rest-cache/wp-rest-cache.php' ); } /** * Register integration details. */ public function register() { add_filter( 'rest_pre_echo_response', [ $this, 'api_optimization' ], 10, 3 ); } /** * Replace the urls in the api response. * * @param array $result An array containing the result before our optimization. * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. * @return array The decoded result with the replaced urls. */ public function api_optimization( $result, $server, $request ) { $result = json_decode( Optml_Main::instance()->manager->process_urls_from_content( json_encode( $result ) ) ); return $result; } } compatibilities/pinterest.php 0000644 00000006071 14720403740 0012460 0 ustar 00 <?php /** * Class Pinterest. * * @reason Pinterest plugins picks eco images to share */ class Optml_pinterest extends Optml_compatibility { /** * String with all css selectors to target * * @var string */ private $selectors; /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $load = false; $selectors_array = []; if ( $this->isShareaholic() ) { $selectors_array[] = 'li.shareaholic-share-button[data-service=\"pinterest\"]'; $load = true; } if ( $this->isSassySocialShare() ) { $selectors_array[] = '.heateorSssSharing.heateorSssPinterestBackground'; $load = true; } $this->selectors = implode( ', ', array_filter( $selectors_array ) ); return $load; } /** * Register integration details. */ public function register() { add_action( 'wp_enqueue_scripts', function () { wp_register_script( 'optml-pinterest', false ); wp_enqueue_script( 'optml-pinterest' ); $script = sprintf( ' (function(w, d){ w.addEventListener("load", function() { const addCustomEventListener = function (selector, event, handler) { let rootElement = document.querySelector(\'body\'); rootElement.addEventListener(event, function (evt) { var targetElement = evt.target; while (targetElement != null) { if (targetElement.matches(selector)) { handler(evt); return; } targetElement = targetElement.parentElement; } }, true ); }; addCustomEventListener(\'%s\',\'mouseover\',function(){ let images = d.getElementsByTagName( "img" ); for ( let i = 0; i < images.length; i++ ) { if ( "optSrc" in images[i].dataset ) { images[i].src = images[i].dataset.optSrc ; } } }); }); }(window, document)); ', $this->selectors ); wp_add_inline_script( 'optml-pinterest', $script ); } ); } /** Check if plugin is active. @return bool */ private function isShareaholic() { if ( ! is_plugin_active( 'shareaholic/shareaholic.php' ) ) { return false; } return true; } /** * Check if plugin is active. * * @return bool */ private function isSassySocialShare() { if ( ! is_plugin_active( 'sassy-social-share/sassy-social-share.php' ) ) { return false; } $ss_options = get_option( 'heateor_sss' ); $ss_bars = [ 'vertical_re_providers', 'horizontal_re_providers' ]; if ( ! is_array( $ss_options ) ) { return false; } foreach ( $ss_bars as $key => $bar ) { if ( ! isset( $ss_options[ $bar ] ) ) { continue; } foreach ( $ss_options[ $bar ] as $index => $value ) { if ( isset( $value ) && is_string( $value ) ) { if ( strpos( $value, 'pinterest' ) !== false ) { return true; } } } } return false; } } compatibilities/yith_quick_view.php 0000644 00000001613 14720403740 0013643 0 ustar 00 <?php /** * Class Optml_yith_quick_view. * * @reason We need to turn on the image replacement on quick view ajax response, to do this we hook the product image filter. */ class Optml_yith_quick_view extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'yith-woocommerce-quick-view/init.php' ); } /** * Register integration details. */ public function register() { Optml_Url_Replacer::instance()->init(); add_filter( 'woocommerce_single_product_image_thumbnail_html', [ Optml_Main::instance()->manager, 'replace_content' ] ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/shortcode_ultimate.php 0000644 00000004065 14720403740 0014342 0 ustar 00 <?php use Optimole\Sdk\Resource\ImageProperty\ResizeTypeProperty; use Optimole\Sdk\ValueObject\Position; /** * Class Optml_shortcode_ultimate. * * @reason Shortcode Ultimate uses a strange image resizing feature * which has by default the hard cropping on. As we are following the WordPress * image size defaults, we need to change the default cropping * for shortcode's output. */ class Optml_shortcode_ultimate extends Optml_compatibility { /** * Tags where we subscribe the compatibility. * * @var array Allowed tags. */ private $allowed_tags = [ 'su_slider' => true, 'su_carousel' => true, 'su_custom_gallery' => true, ]; /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'shortcodes-ultimate/shortcodes-ultimate.php' ); } /** * Register integration details. */ public function register() { add_filter( 'do_shortcode_tag', [ $this, 'alter_shortcode_output' ], 10, 3 ); } /** * Alter shortcode output by replacing the image urls. * * @param string $output Previous shortcode output. * @param string $tag Shortcode tag. * @param array $attr Shortcode attrs. * * @return mixed New output. */ public function alter_shortcode_output( $output, $tag, $attr ) { if ( ! isset( $this->allowed_tags[ $tag ] ) ) { return $output; } add_filter( 'optml_default_crop', [ $this, 'change_default_crop' ] ); add_filter( 'optml_parse_resize_from_tag', [ $this, 'change_default_crop' ] ); $output = Optml_Main::instance()->manager->process_images_from_content( $output ); remove_filter( 'optml_default_crop', [ $this, 'change_default_crop' ] ); remove_filter( 'optml_parse_resize_from_tag', [ $this, 'change_default_crop' ] ); return $output; } /** * Change default crop. * * @return array New default cropping. */ public function change_default_crop() { return [ 'type' => ResizeTypeProperty::FILL, 'gravity' => Position::CENTER, ]; } } compatibilities/wpml.php 0000644 00000005633 14720403740 0011425 0 ustar 00 <?php /** * Class Optml_wpml * * @reason Wpml duplicates everything so we need to offload/update every image/page attachment. */ class Optml_wpml extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) && Optml_Main::instance()->admin->settings->get( 'offload_media' ) === 'enabled'; } /** * Register integration details. */ public function register() { add_filter( 'optml_offload_duplicated_images', [ $this, 'wpml_get_duplicates' ], PHP_INT_MAX, 2 ); add_filter( 'optml_ensure_source_attachment_id', [ $this, 'get_source_attachment' ], PHP_INT_MAX ); } /** * Ads the duplicated pages/images when offloading. * * @param array $duplicated_attachments The duplicated attachments array. * @param string | int $attachment_id The attachment id that is first offloaded. * * @return array The images array with the specific bakery images. */ public function wpml_get_duplicates( $duplicated_attachments, $attachment_id ) { // Get the TRID (Translation ID) from element. REF: https://wpml.org/wpml-hook/wpml_element_trid/ $trid = apply_filters( 'wpml_element_trid', null, $attachment_id, 'post_attachment' ); // Get all translations (elements with the same TRID). REF: https://wpml.org/wpml-hook/wpml_get_element_translations/ $translations = apply_filters( 'wpml_get_element_translations', null, $trid, 'post_attachment' ); foreach ( $translations as $translation ) { if ( isset( $translation->element_id ) ) { $duplicated_attachments[] = $translation->element_id; } } return $duplicated_attachments; } /** * Get the source attachment ID for the given attachment ID that might be a duplicate. * * @param int $attachment_id the attachment ID. * * @return int */ public function get_source_attachment( $attachment_id ) { if ( $this->is_offload_source( $attachment_id ) ) { return $attachment_id; } $duplicates = $this->wpml_get_duplicates( [], $attachment_id ); foreach ( $duplicates as $duplicate_id ) { if ( $this->is_offload_source( $duplicate_id ) ) { return (int) $duplicate_id; } } return $attachment_id; } /** * Checks if the given attachment is the source of the initial offload. * On some instances, it seems that WPML meta is faulty. * We are sure that the main attachment that has been offloaded has the proper attachment_meta. * * @param int $id The attachment ID. * * @return bool */ private function is_offload_source( $id ) { return ! empty( get_post_meta( (int) $id, Optml_Media_Offload::META_KEYS['offloaded'], true ) ); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return true; } } compatibilities/woocommerce.php 0000644 00000003614 14720403740 0012762 0 ustar 00 <?php /** * Class Optml_woocommerce * * @reason Adding flags for ignoring the lazyloaded tags. */ class Optml_woocommerce extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'woocommerce/woocommerce.php' ); } /** * Register integration details. */ public function register() { if ( Optml_Main::instance()->admin->settings->use_lazyload() ) { add_filter( 'optml_lazyload_early_flags', [ $this, 'add_lazyload_early_flag' ], PHP_INT_MAX, 1 ); } } /** * Add ignore lazyload flag. * * @param array $flags Old flags. * * @return array New flags. */ public function add_lazyload_early_flag( $flags = [] ) { $flags[] = 'data-large_image'; return $flags; } /** * Ads the product pages to the list of posts parents when querying images for offload. * * @param array $parents Default post parents. * * @return array New post parents that include product pages. */ public function add_product_pages_to_image_query( $parents = [ 0 ] ) { $paged = 1; $query_args = [ 'post_type' => 'product', 'fields' => 'ids', 'posts_per_page' => 150, 'post_status' => 'publish', 'paged' => $paged, ]; $products = new \WP_Query( $query_args ); $ids = $products->get_posts(); while ( ! empty( $ids ) ) { ++$paged; $parents = array_merge( $parents, $ids ); $query_args['paged'] = $paged; $products = new \WP_Query( $query_args ); $ids = $products->get_posts(); } return $parents; } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { if ( Optml_Main::instance()->admin->settings->get( 'offload_media' ) === 'enabled' ) { return true; } return false; } } compatibilities/wp_fastest_cache.php 0000644 00000001354 14720403740 0013744 0 ustar 00 <?php /** * Class Optml_wp_fastest_cache. * * @reason Wp fastest cache stores the content of the page before Optimole starts replacing url's for the minified css/js files */ class Optml_wp_fastest_cache extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return Optml_Main::instance()->admin->settings->get( 'cdn' ) === 'enabled' && is_plugin_active( 'wp-fastest-cache/wpFastestCache.php' ); } /** * Register integration details. */ public function register() { add_filter( 'wpfc_buffer_callback_filter', [ Optml_Main::instance()->manager, 'replace_content' ], 10 ); } } compatibilities/compatibility.php 0000644 00000001303 14720403740 0013305 0 ustar 00 <?php /** * Class Optml_compatibility. */ abstract class Optml_compatibility { /** * Register compatibility actions/filters. */ abstract public function register(); /** * Should we load the compatibility? * * @return bool Compatiblity */ abstract public function should_load(); /** * Will the compatibility be loaded? * * @return bool */ final public function will_load() { if ( ! Optml_Main::instance()->admin->settings->is_connected() ) { return false; } return $this->should_load(); } /** * Should we early load the compatibility? * * @return bool Whether to load the compatibility or not. */ public function should_load_early() { return false; } } compatibilities/revslider.php 0000644 00000002766 14720403740 0012451 0 ustar 00 <?php /** * Class Optml_revslider. * * @reason The slider output dont needs the data-opt-src and uses a background lazyload approach. */ class Optml_revslider extends Optml_compatibility { /** * Should we load the integration logic. * * @return bool Should we load. */ public function should_load() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; return is_plugin_active( 'revslider/revslider.php' ); } /** * Register integration details. */ public function register() { add_filter( 'optml_possible_lazyload_flags', [ $this, 'add_lazyflag' ], 10 ); add_filter( 'optml_ignore_data_opt_flag', [ $this, 'add_data_ignore' ], 10 ); add_filter( 'optml_lazyload_bg_classes', [ $this, 'add_bg_class' ], 10 ); } /** * Add Slider Revolution lazyload flag. * * @param array $strings Old strings. * * @return array New flags. */ public function add_lazyflag( $strings = [] ) { $strings[] = 'rev-slidebg'; $strings[] = 'rs-lazyload'; return $strings; } /** * Add classes for lazyload on background. * * @param array $classes Old classes. * * @return array New classes. */ public function add_bg_class( $classes = [] ) { $classes[] = 'tp-bgimg'; return $classes; } /** * Adds flag that should ignore applying the data-opt-src * * @param array $flags Flag that should ignore. * * @return array New flags. */ public function add_data_ignore( $flags = [] ) { $flags[] = 'rev-slidebg'; $flags[] = 'rs-lazyload'; return $flags; } } API.php 0000755 00000043346 14720416042 0005703 0 ustar 00 <?php /** * API class. * * @package Codeinwp/HyveLite */ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\Main; use ThemeIsle\HyveLite\BaseAPI; use ThemeIsle\HyveLite\Cosine_Similarity; /** * API class. */ class API extends BaseAPI { /** * Constructor. */ public function __construct() { parent::__construct(); $this->register_route(); } /** * Register hooks and actions. * * @return void */ private function register_route() { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * Register REST API route * * @return void */ public function register_routes() { $namespace = $this->get_endpoint(); $routes = array( 'settings' => array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_settings' ), ), array( 'methods' => \WP_REST_Server::CREATABLE, 'args' => array( 'data' => array( 'required' => true, 'type' => 'object', 'validate_callback' => function ( $param ) { return is_array( $param ); }, ), ), 'callback' => array( $this, 'update_settings' ), ), ), 'data' => array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => array( 'offset' => array( 'required' => false, 'type' => 'integer', 'default' => 0, ), 'type' => array( 'required' => false, 'type' => 'string', 'default' => 'any', ), 'search' => array( 'required' => false, 'type' => 'string', ), 'status' => array( 'required' => false, 'type' => 'string', ), ), 'callback' => array( $this, 'get_data' ), ), array( 'methods' => \WP_REST_Server::CREATABLE, 'args' => array( 'action' => array( 'required' => false, 'type' => 'string', ), 'data' => array( 'required' => true, 'type' => 'object', ), ), 'callback' => array( $this, 'add_data' ), ), array( 'methods' => \WP_REST_Server::DELETABLE, 'args' => array( 'id' => array( 'required' => true, 'type' => 'integer', ), ), 'callback' => array( $this, 'delete_data' ), ), ), 'threads' => array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => array( 'offset' => array( 'required' => false, 'type' => 'integer', 'default' => 0, ), ), 'callback' => array( $this, 'get_threads' ), ), ), 'chat' => array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => array( 'run_id' => array( 'required' => true, 'type' => 'string', ), 'thread_id' => array( 'required' => true, 'type' => 'string', ), 'record_id' => array( 'required' => true, 'type' => array( 'string', 'integer', ), ), 'message' => array( 'required' => false, 'type' => 'string', ), ), 'callback' => array( $this, 'get_chat' ), 'permission_callback' => function ( $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return wp_verify_nonce( $nonce, 'wp_rest' ); }, ), array( 'methods' => \WP_REST_Server::CREATABLE, 'args' => array( 'message' => array( 'required' => true, 'type' => 'string', ), 'thread_id' => array( 'required' => false, 'type' => 'string', ), 'record_id' => array( 'required' => false, 'type' => array( 'string', 'integer', ), ), ), 'callback' => array( $this, 'send_chat' ), 'permission_callback' => function ( $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return wp_verify_nonce( $nonce, 'wp_rest' ); }, ), ), ); foreach ( $routes as $route => $args ) { foreach ( $args as $key => $arg ) { if ( ! isset( $args[ $key ]['permission_callback'] ) ) { $args[ $key ]['permission_callback'] = function () { return current_user_can( 'manage_options' ); }; } } register_rest_route( $namespace, '/' . $route, $args ); } } /** * Get settings. * * @return \WP_REST_Response */ public function get_settings() { $settings = Main::get_settings(); return rest_ensure_response( $settings ); } /** * Update settings. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function update_settings( $request ) { $data = $request->get_param( 'data' ); $settings = Main::get_settings(); $updated = array(); foreach ( $data as $key => $datum ) { if ( ! array_key_exists( $key, $settings ) || $settings[ $key ] === $datum ) { continue; } $updated[ $key ] = $datum; } if ( empty( $updated ) ) { return rest_ensure_response( array( 'error' => __( 'No settings to update.', 'hyve-lite' ) ) ); } $validation = apply_filters( 'hyve_settings_validation', array( 'api_key' => function ( $value ) { return is_string( $value ); }, 'chat_enabled' => function ( $value ) { return is_bool( $value ); }, 'welcome_message' => function ( $value ) { return is_string( $value ); }, 'default_message' => function ( $value ) { return is_string( $value ); }, 'temperature' => function ( $value ) { return is_numeric( $value ); }, 'top_p' => function ( $value ) { return is_numeric( $value ); }, 'moderation_threshold' => function ( $value ) { return is_array( $value ) && array_reduce( $value, function ( $carry, $item ) { return $carry && is_int( $item ); }, true ); }, ) ); foreach ( $updated as $key => $value ) { if ( ! $validation[ $key ]( $value ) ) { return rest_ensure_response( array( // translators: %s: option key. 'error' => sprintf( __( 'Invalid value: %s', 'hyve-lite' ), $key ), ) ); } } foreach ( $updated as $key => $value ) { $settings[ $key ] = $value; if ( 'api_key' === $key && ! empty( $value ) ) { $openai = new OpenAI( $value ); $valid_api = $openai->setup_assistant(); if ( is_wp_error( $valid_api ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $valid_api ) ) ); } $settings['assistant_id'] = $valid_api; } } update_option( 'hyve_settings', $settings ); return rest_ensure_response( __( 'Settings updated.', 'hyve-lite' ) ); } /** * Get data. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function get_data( $request ) { $args = array( 'post_type' => $request->get_param( 'type' ), 'post_status' => 'publish', 'posts_per_page' => 20, 'fields' => 'ids', 'offset' => $request->get_param( 'offset' ), 'meta_query' => array( array( 'key' => '_hyve_added', 'compare' => 'NOT EXISTS', ), array( 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', ), ), ); $search = $request->get_param( 'search' ); if ( ! empty( $search ) ) { $args['s'] = $search; } $status = $request->get_param( 'status' ); if ( 'included' === $status ) { $args['meta_query'] = array( 'relation' => 'AND', array( 'key' => '_hyve_added', 'value' => '1', 'compare' => '=', ), array( 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', ), ); } if ( 'pending' === $status ) { $args['meta_query'] = array( 'relation' => 'AND', array( 'key' => '_hyve_needs_update', 'value' => '1', 'compare' => '=', ), array( 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', ), ); } if ( 'moderation' === $status ) { $args['meta_query'] = array( array( 'key' => '_hyve_moderation_failed', 'value' => '1', 'compare' => '=', ), ); } $query = new \WP_Query( $args ); $posts_data = array(); if ( $query->have_posts() ) { foreach ( $query->posts as $post_id ) { $post_data = array( 'ID' => $post_id, 'title' => get_the_title( $post_id ), 'content' => apply_filters( 'the_content', get_post_field( 'post_content', $post_id ) ), ); if ( 'moderation' === $status ) { $review = get_post_meta( $post_id, '_hyve_moderation_review', true ); if ( ! is_array( $review ) || empty( $review ) ) { $review = array(); } $post_data['review'] = $review; } $posts_data[] = $post_data; } } $posts = array( 'posts' => $posts_data, 'more' => $query->found_posts > 20, 'totalChunks' => $this->table->get_count(), ); return rest_ensure_response( $posts ); } /** * Add data. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function add_data( $request ) { $data = $request->get_param( 'data' ); $content = apply_filters( 'the_content', get_post_field( 'post_content', $data['post_id'] ) ); $chunks = str_split( $content, 2000 ); $moderation = $this->moderate( $chunks, $data['post_id'] ); if ( is_wp_error( $moderation ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $moderation ) ) ); } if ( true !== $moderation && 'override' !== $request->get_param( 'action' ) ) { update_post_meta( $data['post_id'], '_hyve_moderation_failed', 1 ); update_post_meta( $data['post_id'], '_hyve_moderation_review', $moderation ); return rest_ensure_response( array( 'error' => __( 'The content failed moderation policies.', 'hyve-lite' ), 'code' => 'content_failed_moderation', 'review' => $moderation, ) ); } if ( 'update' === $request->get_param( 'action' ) ) { $this->table->delete_by_post_id( $data['post_id'] ); delete_post_meta( $data['post_id'], '_hyve_needs_update' ); } $this->table->insert( $data ); update_post_meta( $data['post_id'], '_hyve_added', 1 ); delete_post_meta( $data['post_id'], '_hyve_moderation_failed' ); delete_post_meta( $data['post_id'], '_hyve_moderation_review' ); $this->table->process_posts(); return rest_ensure_response( true ); } /** * Delete data. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function delete_data( $request ) { $id = $request->get_param( 'id' ); $this->table->delete_by_post_id( $id ); delete_post_meta( $id, '_hyve_added' ); delete_post_meta( $id, '_hyve_needs_update' ); delete_post_meta( $id, '_hyve_moderation_failed' ); delete_post_meta( $id, '_hyve_moderation_review' ); return rest_ensure_response( true ); } /** * Get threads. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function get_threads( $request ) { $pages = apply_filters( 'hyve_threads_per_page', 3 ); $args = array( 'post_type' => 'hyve_threads', 'post_status' => 'publish', 'posts_per_page' => $pages, 'fields' => 'ids', 'offset' => $request->get_param( 'offset' ), ); $query = new \WP_Query( $args ); $posts_data = array(); if ( $query->have_posts() ) { foreach ( $query->posts as $post_id ) { $post_data = array( 'ID' => $post_id, 'title' => get_the_title( $post_id ), 'date' => get_the_date( 'd/m/Y g:i A', $post_id ), 'thread' => get_post_meta( $post_id, '_hyve_thread_data', true ), 'thread_id' => get_post_meta( $post_id, '_hyve_thread_id', true ), ); $posts_data[] = $post_data; } } $posts = array( 'posts' => $posts_data, 'more' => $query->found_posts > $pages, ); return rest_ensure_response( $posts ); } /** * Get chat. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function get_chat( $request ) { $run_id = $request->get_param( 'run_id' ); $thread_id = $request->get_param( 'thread_id' ); $query = $request->get_param( 'message' ); $record_id = $request->get_param( 'record_id' ); $openai = new OpenAI(); $status = $openai->get_status( $run_id, $thread_id ); if ( is_wp_error( $status ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $status ) ) ); } if ( 'completed' !== $status ) { return rest_ensure_response( array( 'status' => $status ) ); } $messages = $openai->get_messages( $thread_id ); if ( is_wp_error( $messages ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $messages ) ) ); } $messages = array_filter( $messages, function ( $message ) use ( $run_id ) { return $message->run_id === $run_id; } ); $message = reset( $messages )->content[0]->text->value; $message = json_decode( $message, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { return rest_ensure_response( array( 'error' => __( 'No messages found.', 'hyve-lite' ) ) ); } $settings = Main::get_settings(); $response = ( isset( $message['success'] ) && true === $message['success'] && isset( $message['response'] ) ) ? $message['response'] : $settings['default_message']; do_action( 'hyve_chat_response', $run_id, $thread_id, $query, $record_id, $message, $response ); return rest_ensure_response( array( 'status' => $status, 'success' => isset( $message['success'] ) ? $message['success'] : false, 'message' => $response, ) ); } /** * Send chat. * * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response */ public function send_chat( $request ) { $message = $request->get_param( 'message' ); $record_id = $request->get_param( 'record_id' ); $moderation = $this->moderate( $message ); if ( true !== $moderation ) { return rest_ensure_response( array( 'error' => __( 'Message was flagged.', 'hyve-lite' ) ) ); } $openai = new OpenAI(); $message_vector = $openai->create_embeddings( $message ); $message_vector = reset( $message_vector ); $message_vector = $message_vector->embedding; if ( is_wp_error( $message_vector ) ) { return rest_ensure_response( array( 'error' => __( 'No embeddings found.', 'hyve-lite' ) ) ); } $hash = md5( strtolower( $message ) ); set_transient( 'hyve_message_' . $hash, $message_vector, MINUTE_IN_SECONDS ); $posts = $this->table->get_by_status( 'processed' ); $embeddings_with_cosine_distance_sorted = array_map( function ( $row ) use ( $message_vector ) { $embeddings = json_decode( $row->embeddings, true ); if ( ! is_array( $embeddings ) ) { return array( 'post_id' => $row->post_id, 'distance' => 0, 'token_count' => $row->token_count, 'post_title' => $row->post_title, 'post_content' => $row->post_content, ); } $distance = Cosine_Similarity::calculate( $message_vector, $embeddings ); return array( 'post_id' => $row->post_id, 'distance' => $distance, 'token_count' => $row->token_count, 'post_title' => $row->post_title, 'post_content' => $row->post_content, ); }, $posts ); usort( $embeddings_with_cosine_distance_sorted, function ( $a, $b ) { if ( $a['distance'] < $b['distance'] ) { return 1; } elseif ( $a['distance'] > $b['distance'] ) { return -1; } else { return 0; } } ); $embeddings_with_cosine_distance_sorted = array_filter( $embeddings_with_cosine_distance_sorted, function ( $row ) { return $row['distance'] > 0.4; } ); $max_tokens_length = 2000; $curr_tokens_length = 0; $article_context = ''; foreach ( $embeddings_with_cosine_distance_sorted as $row ) { $curr_tokens_length += $row['token_count']; if ( $curr_tokens_length < $max_tokens_length ) { $article_context .= "\n ===START POST=== " . $row['post_title'] . ' - ' . $row['post_content'] . ' ===END POST==='; } } if ( $request->get_param( 'thread_id' ) ) { $thread_id = $request->get_param( 'thread_id' ); } else { $thread_id = $openai->create_thread(); } if ( is_wp_error( $thread_id ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $thread_id ) ) ); } $query_run = $openai->create_run( array( array( 'role' => 'user', 'content' => 'START QUESTION: ' . $message . ' :END QUESTION', ), array( 'role' => 'user', 'content' => 'START CONTEXT: ' . $article_context . ' :END CONTEXT', ), ), $thread_id ); if ( is_wp_error( $query_run ) ) { if ( strpos( $this->get_error_message( $query_run ), 'No thread found with id' ) !== false ) { $thread_id = $openai->create_thread(); if ( is_wp_error( $thread_id ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $thread_id ) ) ); } $query_run = $openai->create_run( array( array( 'role' => 'user', 'content' => 'Question: ' . $message, ), array( 'role' => 'user', 'content' => 'Context: ' . $article_context, ), ), $thread_id ); if ( is_wp_error( $query_run ) ) { return rest_ensure_response( array( 'error' => $this->get_error_message( $query_run ) ) ); } } } $record_id = apply_filters( 'hyve_chat_request', $thread_id, $record_id, $message ); return rest_ensure_response( array( 'thread_id' => $thread_id, 'query_run' => $query_run, 'record_id' => $record_id ? $record_id : null, ) ); } } Block.php 0000755 00000001464 14720416042 0006317 0 ustar 00 <?php /** * Block class. * * @package Codeinwp/HyveLite */ namespace ThemeIsle\HyveLite; /** * Block class. */ class Block { /** * Constructor. */ public function __construct() { add_action( 'init', array( $this, 'register_block' ) ); add_shortcode( 'hyve', array( $this, 'render_shortcode' ) ); } /** * Register block. * * @return void */ public function register_block() { register_block_type( HYVE_LITE_PATH . '/build/block' ); } /** * Render shortcode. * * @param array $atts Shortcode attributes. * * @return string */ public function render_shortcode( $atts ) { if ( isset( $atts['floating'] ) && 'true' === $atts['floating'] ) { return do_blocks( '<!-- wp:hyve/chat {"variant":"floating"} /-->' ); } return do_blocks( '<!-- wp:hyve/chat /-->' ); } } Threads.php 0000755 00000012756 14720416042 0006665 0 ustar 00 <?php /** * Post Tpe Class. * * @package Codeinwp\HyveLite */ namespace ThemeIsle\HyveLite; /** * Class Threads */ class Threads { /** * Constructor. */ public function __construct() { add_action( 'init', array( $this, 'register' ) ); add_action( 'hyve_chat_response', array( $this, 'record_message' ), 10, 6 ); add_filter( 'hyve_chat_request', array( $this, 'record_thread' ), 10, 3 ); } /** * Register the post types. * * @return void */ public function register() { $labels = array( 'name' => _x( 'Threads', 'post type general name', 'hyve-lite' ), 'singular_name' => _x( 'Thread', 'post type singular name', 'hyve-lite' ), 'menu_name' => _x( 'Threads', 'admin menu', 'hyve-lite' ), 'name_admin_bar' => _x( 'Thread', 'add new on admin bar', 'hyve-lite' ), 'add_new' => _x( 'Add New', 'Thread', 'hyve-lite' ), 'add_new_item' => __( 'Add New Thread', 'hyve-lite' ), 'new_item' => __( 'New Thread', 'hyve-lite' ), 'edit_item' => __( 'Edit Thread', 'hyve-lite' ), 'view_item' => __( 'View Thread', 'hyve-lite' ), 'all_items' => __( 'All Threads', 'hyve-lite' ), 'search_items' => __( 'Search Threads', 'hyve-lite' ), 'parent_item_colon' => __( 'Parent Thread:', 'hyve-lite' ), 'not_found' => __( 'No Threads found.', 'hyve-lite' ), 'not_found_in_trash' => __( 'No Threads found in Trash.', 'hyve-lite' ), ); $args = array( 'labels' => $labels, 'description' => __( 'Threads.', 'hyve-lite' ), 'public' => false, 'publicly_queryable' => false, 'show_ui' => false, 'show_in_menu' => false, 'query_var' => false, 'rewrite' => array( 'slug' => 'threads' ), 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'show_in_rest' => false, 'supports' => array( 'title', 'editor', 'custom-fields', 'comments' ), ); register_post_type( 'hyve_threads', $args ); } /** * Record the message. * * @param string $run_id Run ID. * @param string $thread_id Thread ID. * @param string $query Query. * @param string|int $record_id Record ID. * @param array $message Message. * @param string $response Response. * * @return void */ public function record_message( $run_id, $thread_id, $query, $record_id, $message, $response ) { if ( ! $record_id ) { return; } self::add_message( $record_id, array( 'thread_id' => $thread_id, 'sender' => 'bot', 'message' => $response, ) ); } /** * Record the thread. * * @param string $thread_id Thread ID. * @param string|int $record_id Record ID. * @param string $message Message. * * @return int */ public function record_thread( $thread_id, $record_id, $message ) { if ( $record_id ) { $record_id = self::add_message( $record_id, array( 'thread_id' => $thread_id, 'sender' => 'user', 'message' => $message, ) ); } else { $record_id = self::create_thread( $message, array( 'thread_id' => $thread_id, 'sender' => 'user', 'message' => $message, ) ); } return $record_id; } /** * Create a new thread. * * @param string $title The title of the thread. * @param array $data The data of the thread. * * @return int */ public static function create_thread( $title, $data ) { $post_id = wp_insert_post( array( 'post_title' => $title, 'post_content' => '', 'post_status' => 'publish', 'post_type' => 'hyve_threads', ) ); $thread_data = array( array( 'time' => time(), 'sender' => $data['sender'], 'message' => $data['message'], ), ); update_post_meta( $post_id, '_hyve_thread_data', $thread_data ); update_post_meta( $post_id, '_hyve_thread_count', 1 ); update_post_meta( $post_id, '_hyve_thread_id', $data['thread_id'] ); return $post_id; } /** * Add a new message to a thread. * * @param int $post_id The ID of the thread. * @param array $data The data of the message. * * @return int */ public static function add_message( $post_id, $data ) { $thread_id = get_post_meta( $post_id, '_hyve_thread_id', true ); if ( $thread_id !== $data['thread_id'] ) { return self::create_thread( $data['message'], $data ); } $thread_data = get_post_meta( $post_id, '_hyve_thread_data', true ); $thread_data[] = array( 'time' => time(), 'sender' => $data['sender'], 'message' => $data['message'], ); update_post_meta( $post_id, '_hyve_thread_data', $thread_data ); update_post_meta( $post_id, '_hyve_thread_count', count( $thread_data ) ); return $post_id; } /** * Get Thread Count. * * @return int */ public static function get_thread_count() { $threads = wp_count_posts( 'hyve_threads' ); return $threads->publish; } /** * Get Messages Count. * * @return int */ public static function get_messages_count() { $messages = get_transient( 'hyve_messages_count' ); if ( ! $messages ) { global $wpdb; $messages = $wpdb->get_var( $wpdb->prepare( "SELECT SUM( CAST( meta_value AS UNSIGNED ) ) FROM {$wpdb->postmeta} WHERE meta_key = %s", '_hyve_thread_count' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( ! $messages ) { $messages = 0; } set_transient( 'hyve_messages_count', $messages, HOUR_IN_SECONDS ); } return $messages; } } Cosine_Similarity.php 0000755 00000003100 14720416042 0010700 0 ustar 00 <?php /** * Cosine Similarity Calculator. * * @package Codeinwp\HyveLite */ namespace ThemeIsle\HyveLite; /** * Class Cosine_Similarity */ class Cosine_Similarity { /** * Calculates the dot product of two vectors. * * @param array $vector_a First vector. * @param array $vector_b Second vector. * @return float The dot product of the two vectors. */ private static function dot_product( array $vector_a, array $vector_b ): float { $sum = 0.0; foreach ( $vector_a as $key => $value ) { if ( isset( $vector_b[ $key ] ) ) { $sum += $value * $vector_b[ $key ]; } } return $sum; } /** * Calculates the magnitude (length) of a vector. * * @param array $vector The vector to calculate the magnitude of. * @return float The magnitude of the vector. */ private static function magnitude( array $vector ): float { $sum = 0.0; foreach ( $vector as $component ) { $sum += pow( $component, 2 ); } return sqrt( $sum ); } /** * Calculates the cosine similarity between two vectors. * * @param array $vector_a First vector. * @param array $vector_b Second vector. * @return float The cosine similarity between the two vectors. */ public static function calculate( array $vector_a, array $vector_b ): float { $dot_product = self::dot_product( $vector_a, $vector_b ); $magnitude_a = self::magnitude( $vector_a ); $magnitude_b = self::magnitude( $vector_b ); // To prevent division by zero. if ( 0.0 === $magnitude_a || 0.0 === $magnitude_b ) { return 0.0; } return $dot_product / ( $magnitude_a * $magnitude_b ); } } Main.php 0000755 00000015411 14720416042 0006146 0 ustar 00 <?php /** * Plugin Class. * * @package Codeinwp\HyveLite */ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\DB_Table; use ThemeIsle\HyveLite\Block; use ThemeIsle\HyveLite\Cosine_Similarity; use ThemeIsle\HyveLite\Threads; use ThemeIsle\HyveLite\API; /** * Class Main */ class Main { /** * Instace of DB_Table class. * * @since 1.2.0 * @var DB_Table */ public $table; /** * Instace of API class. * * @since 1.2.0 * @var API */ public $api; /** * Main constructor. */ public function __construct() { $this->table = new DB_Table(); $this->api = new API(); new Block(); new Threads(); add_action( 'admin_menu', array( $this, 'register_menu_page' ) ); add_action( 'save_post', array( $this, 'update_meta' ) ); add_action( 'delete_post', array( $this, 'delete_post' ) ); $settings = self::get_settings(); if ( is_array( $settings ) && isset( $settings['api_key'] ) && isset( $settings['assistant_id'] ) && ! empty( $settings['api_key'] ) && ! empty( $settings['assistant_id'] ) ) { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); } } /** * Register menu page. * * @since 1.2.0 * * @return void */ public function register_menu_page() { $page_hook_suffix = add_menu_page( __( 'Hyve', 'hyve-lite' ), __( 'Hyve', 'hyve-lite' ), 'manage_options', 'hyve', array( $this, 'menu_page' ), 'dashicons-format-chat', 99 ); add_action( "admin_print_scripts-$page_hook_suffix", array( $this, 'enqueue_options_assets' ) ); } /** * Menu page. * * @since 1.2.0 * * @return void */ public function menu_page() { ?> <div id="hyve-options"></div> <?php } /** * Load assets for option page. * * @since 1.2.0 * * @return void */ public function enqueue_options_assets() { $asset_file = include HYVE_LITE_PATH . '/build/backend/index.asset.php'; wp_enqueue_style( 'hyve-styles', HYVE_LITE_URL . 'build/backend/style-index.css', array( 'wp-components' ), $asset_file['version'] ); wp_enqueue_script( 'hyve-lite-scripts', HYVE_LITE_URL . 'build/backend/index.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_set_script_translations( 'hyve-lite-scripts', 'hyve-lite' ); $post_types = get_post_types( array( 'public' => true ), 'objects' ); $post_types_for_js = array(); foreach ( $post_types as $post_type ) { $post_types_for_js[] = array( 'label' => $post_type->labels->name, 'value' => $post_type->name, ); } $settings = self::get_settings(); wp_localize_script( 'hyve-lite-scripts', 'hyve', apply_filters( 'hyve_options_data', array( 'api' => $this->api->get_endpoint(), 'postTypes' => $post_types_for_js, 'hasAPIKey' => isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ), 'chunksLimit' => apply_filters( 'hyve_chunks_limit', 500 ), 'assets' => array( 'images' => HYVE_LITE_URL . 'assets/images/', ), 'stats' => array( 'threads' => Threads::get_thread_count(), 'messages' => Threads::get_messages_count(), 'totalChunks' => $this->table->get_count(), ), 'docs' => 'https://docs.themeisle.com/article/2009-hyve-documentation', 'pro' => 'https://themeisle.com/plugins/hyve/', ) ) ); } /** * Get Default Settings. * * @since 1.1.0 * * @return array */ public static function get_default_settings() { return apply_filters( 'hyve_default_settings', array( 'api_key' => '', 'chat_enabled' => true, 'welcome_message' => __( 'Hello! How can I help you today?', 'hyve-lite' ), 'default_message' => __( 'Sorry, I\'m not able to help with that.', 'hyve-lite' ), 'temperature' => 1, 'top_p' => 1, 'moderation_threshold' => array( 'sexual' => 80, 'hate' => 70, 'harassment' => 70, 'self-harm' => 50, 'sexual/minors' => 50, 'hate/threatening' => 60, 'violence/graphic' => 80, 'self-harm/intent' => 50, 'self-harm/instructions' => 50, 'harassment/threatening' => 60, 'violence' => 70, ), ) ); } /** * Get Settings. * * @since 1.1.0 * * @return array */ public static function get_settings() { $settings = get_option( 'hyve_settings', array() ); return wp_parse_args( $settings, self::get_default_settings() ); } /** * Enqueue assets. * * @since 1.2.0 * * @return void */ public function enqueue_assets() { $asset_file = include HYVE_LITE_PATH . '/build/frontend/frontend.asset.php'; wp_register_style( 'hyve-styles', HYVE_LITE_URL . 'build/frontend/style-index.css', array(), $asset_file['version'] ); wp_register_script( 'hyve-lite-scripts', HYVE_LITE_URL . 'build/frontend/frontend.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_set_script_translations( 'hyve-lite-scripts', 'hyve-lite' ); $settings = self::get_settings(); wp_localize_script( 'hyve-lite-scripts', 'hyve', apply_filters( 'hyve_frontend_data', array( 'api' => $this->api->get_endpoint(), 'audio' => array( 'click' => HYVE_LITE_URL . 'assets/audio/click.mp3', 'ping' => HYVE_LITE_URL . 'assets/audio/ping.mp3', ), 'welcome' => $settings['welcome_message'] ?? '', 'isEnabled' => $settings['chat_enabled'], ) ) ); if ( ! isset( $settings['chat_enabled'] ) || false === $settings['chat_enabled'] ) { return; } wp_enqueue_style( 'hyve-styles' ); wp_enqueue_script( 'hyve-lite-scripts' ); $has_pro = apply_filters( 'product_hyve_license_status', false ); if ( $has_pro ) { return; } wp_add_inline_script( 'hyve-lite-scripts', 'document.addEventListener("DOMContentLoaded", function() { const c = document.createElement("div"); c.className = "hyve-credits"; c.innerHTML = "<a href=\"https://themeisle.com/plugins/hyve/\" target=\"_blank\">Powered by Hyve</a>"; document.querySelector( ".hyve-input-box" ).before( c ); });' ); } /** * Update meta. * * @param int $post_id Post ID. * * @since 1.2.0 * * @return void */ public function update_meta( $post_id ) { $added = get_post_meta( $post_id, '_hyve_added', true ); if ( ! $added ) { return; } update_post_meta( $post_id, '_hyve_needs_update', 1 ); delete_post_meta( $post_id, '_hyve_moderation_failed' ); delete_post_meta( $post_id, '_hyve_moderation_review' ); } /** * Delete post. * * @param int $post_id Post ID. * * @since 1.2.0 * * @return void */ public function delete_post( $post_id ) { $this->table->delete_by_post_id( $post_id ); } } BaseAPI.php 0000755 00000006106 14720416042 0006467 0 ustar 00 <?php /** * BaseAPI class. * * @package Codeinwp/HyveLite */ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\Main; use ThemeIsle\HyveLite\DB_Table; use ThemeIsle\HyveLite\OpenAI; /** * BaseAPI class. */ class BaseAPI { /** * API namespace. * * @var string */ private $namespace = 'hyve'; /** * API version. * * @var string */ private $version = 'v1'; /** * Instance of DB_Table class. * * @var object */ protected $table; /** * Error messages. * * @var array */ private $errors = array(); /** * Constructor. */ public function __construct() { $this->table = new DB_Table(); $this->errors = array( 'invalid_api_key' => __( 'Incorrect API key provided.', 'hyve-lite' ), 'missing_scope' => __( ' You have insufficient permissions for this operation.', 'hyve-lite' ), ); } /** * Get Error Message. * * @param \WP_Error $error Error. * * @return string */ public function get_error_message( $error ) { if ( isset( $this->errors[ $error->get_error_code() ] ) ) { return $this->errors[ $error->get_error_code() ]; } return $error->get_error_message(); } /** * Get endpoint. * * @return string */ public function get_endpoint() { return $this->namespace . '/' . $this->version; } /** * Moderate data. * * @param array|string $chunks Data to moderate. * @param int $id Post ID. * * @return true|array|\WP_Error */ public function moderate( $chunks, $id = null ) { if ( $id ) { $moderated = get_transient( 'hyve_moderate_post_' . $id ); if ( false !== $moderated ) { return is_array( $moderated ) ? $moderated : true; } } $openai = new OpenAI(); $results = array(); $return = true; $settings = Main::get_settings(); $moderation_threshold = $settings['moderation_threshold']; if ( ! is_array( $chunks ) ) { $chunks = array( $chunks ); } foreach ( $chunks as $chunk ) { $moderation = $openai->moderate( $chunk ); if ( is_wp_error( $moderation ) ) { return $moderation; } if ( true !== $moderation && is_object( $moderation ) ) { $results[] = $moderation; } } if ( ! empty( $results ) ) { $flagged = array(); foreach ( $results as $result ) { $categories = $result->categories; foreach ( $categories as $category => $flag ) { if ( ! $flag ) { continue; } if ( ! isset( $moderation_threshold[ $category ] ) || $result->category_scores->$category < ( $moderation_threshold[ $category ] / 100 ) ) { continue; } if ( ! isset( $flagged[ $category ] ) ) { $flagged[ $category ] = $result->category_scores->$category; continue; } if ( $result->category_scores->$category > $flagged[ $category ] ) { $flagged[ $category ] = $result->category_scores->$category; } } } if ( empty( $flagged ) ) { $return = true; } else { $return = $flagged; } } if ( $id ) { set_transient( 'hyve_moderate_post_' . $id, $return, MINUTE_IN_SECONDS ); } return $return; } } DB_Table.php 0000755 00000020061 14720416042 0006653 0 ustar 00 <?php /** * Database Table Class. * * @package Codeinwp\HyveLite */ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\OpenAI; /** * Class DB_Table */ class DB_Table { /** * The name of our database table. * * @since 1.2.0 * @var string */ public $table_name; /** * The version of our database table. * * @since 1.2.0 * @var string */ public $version = '1.0.0'; /** * Cache prefix. * * @since 1.2.0 * @var string */ const CACHE_PREFIX = 'hyve-'; /** * DB_Table constructor. * * @since 1.2.0 */ public function __construct() { global $wpdb; $this->table_name = $wpdb->prefix . 'hyve'; if ( ! wp_next_scheduled( 'hyve_process_posts' ) ) { wp_schedule_event( time(), 'daily', 'hyve_process_posts' ); } add_action( 'hyve_process_posts', array( $this, 'process_posts' ) ); if ( ! $this->table_exists() || version_compare( $this->version, get_option( $this->table_name . '_db_version' ), '>' ) ) { $this->create_table(); } } /** * Create the table. * * @since 1.2.0 */ public function create_table() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $sql = 'CREATE TABLE ' . $this->table_name . ' ( id bigint(20) NOT NULL AUTO_INCREMENT, date datetime NOT NULL, modified datetime NOT NULL, post_id mediumtext NOT NULL, post_title mediumtext NOT NULL, post_content longtext NOT NULL, embeddings longtext NOT NULL, token_count int(11) NOT NULL DEFAULT 0, post_status VARCHAR(255) NOT NULL DEFAULT "scheduled", PRIMARY KEY (id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;'; dbDelta( $sql ); update_option( $this->table_name . '_db_version', $this->version ); } /** * Check if the table exists. * * @since 1.2.0 * * @return bool */ public function table_exists() { global $wpdb; $table = sanitize_text_field( $this->table_name ); return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table; } /** * Get columns and formats. * * @since 1.2.0 * * @return array */ public function get_columns() { return array( 'date' => '%s', 'modified' => '%s', 'post_id' => '%s', 'post_title' => '%s', 'post_content' => '%s', 'embeddings' => '%s', 'token_count' => '%d', 'post_status' => '%s', ); } /** * Get default column values. * * @since 1.2.0 * * @return array */ public function get_column_defaults() { return array( 'date' => gmdate( 'Y-m-d H:i:s' ), 'modified' => gmdate( 'Y-m-d H:i:s' ), 'post_id' => '', 'post_title' => '', 'post_content' => '', 'embeddings' => '', 'token_count' => 0, 'post_status' => 'scheduled', ); } /** * Insert a new row. * * @since 1.2.0 * * @param array $data The data to insert. * * @return int */ public function insert( $data ) { global $wpdb; $column_formats = $this->get_columns(); $column_defaults = $this->get_column_defaults(); $data = wp_parse_args( $data, $column_defaults ); $data = array_intersect_key( $data, $column_formats ); $wpdb->insert( $this->table_name, $data, $column_formats ); $this->delete_cache( 'entries' ); $this->delete_cache( 'entries_count' ); return $wpdb->insert_id; } /** * Update a row. * * @since 1.2.0 * * @param int $id The row ID. * @param array $data The data to update. * * @return int */ public function update( $id, $data ) { global $wpdb; $column_formats = $this->get_columns(); $column_defaults = $this->get_column_defaults(); $data = array_intersect_key( $data, $column_formats ); $wpdb->update( $this->table_name, $data, array( 'id' => $id ), $column_formats, array( '%d' ) ); $this->delete_cache( 'entry_' . $id ); $this->delete_cache( 'entries_processed' ); return $wpdb->rows_affected; } /** * Delete rows by post ID. * * @since 1.2.0 * * @param int $post_id The post ID. * * @return int */ public function delete_by_post_id( $post_id ) { global $wpdb; $wpdb->delete( $this->table_name, array( 'post_id' => $post_id ), array( '%d' ) ); $this->delete_cache( 'entry_post_' . $post_id ); $this->delete_cache( 'entries' ); $this->delete_cache( 'entries_processed' ); $this->delete_cache( 'entries_count' ); return $wpdb->rows_affected; } /** * Get all rows by status. * * @since 1.2.0 * * @param string $status The status. * @param int $limit The limit. * * @return array */ public function get_by_status( $status, $limit = 500 ) { global $wpdb; $cache = $this->get_cache( 'entries_' . $status ); if ( is_array( $cache ) && false !== $cache ) { return $cache; } $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE post_status = %s LIMIT %d', $this->table_name, $status, $limit ) ); if ( 'scheduled' !== $status ) { $this->set_cache( 'entries_' . $status, $results ); } return $results; } /** * Process posts. * * @since 1.2.0 * * @return void */ public function process_posts() { $posts = $this->get_by_status( 'scheduled' ); foreach ( $posts as $post ) { $id = $post->id; $content = $post->post_content; $openai = new OpenAI(); $embeddings = $openai->create_embeddings( $content ); $embeddings = reset( $embeddings ); $embeddings = $embeddings->embedding; if ( is_wp_error( $embeddings ) || ! $embeddings ) { continue; } $embeddings = wp_json_encode( $embeddings ); $this->update( $id, array( 'embeddings' => $embeddings, 'post_status' => 'processed', ) ); } } /** * Get Total Rows Count. * * @since 1.2.0 * * @return int */ public function get_count() { $cache = $this->get_cache( 'entries_count' ); if ( false !== $cache ) { return $cache; } global $wpdb; $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $this->table_name ) ); $this->set_cache( 'entries_count', $count ); return $count; } /** * Return cache. * * @since 1.2.0 * * @param string $key The cache key. * * @return mixed */ private function get_cache( $key ) { $key = $this->get_cache_key( $key ); if ( $this->get_cache_key( 'entries_processed' ) === $key ) { $total = get_transient( $key . '_total' ); if ( false === $total ) { return false; } $entries = array(); for ( $i = 0; $i < $total; $i++ ) { $chunk_key = $key . '_' . $i; $chunk = get_transient( $chunk_key ); if ( false === $chunk ) { return false; } $entries = array_merge( $entries, $chunk ); } return $entries; } return get_transient( $key ); } /** * Set cache. * * @since 1.2.0 * * @param string $key The cache key. * @param mixed $value The cache value. * @param int $expiration The expiration time. * * @return bool */ private function set_cache( $key, $value, $expiration = DAY_IN_SECONDS ) { $key = $this->get_cache_key( $key ); if ( $this->get_cache_key( 'entries_processed' ) === $key ) { $chunks = array_chunk( $value, 50 ); $total = count( $chunks ); foreach ( $chunks as $index => $chunk ) { $chunk_key = $key . '_' . $index; set_transient( $chunk_key, $chunk, $expiration ); } set_transient( $key . '_total', $total, $expiration ); return true; } return set_transient( $key, $value, $expiration ); } /** * Delete cache. * * @since 1.2.0 * * @param string $key The cache key. * * @return bool */ private function delete_cache( $key ) { $key = $this->get_cache_key( $key ); if ( $this->get_cache_key( 'entries_processed' ) === $key ) { $total = get_transient( $key . '_total' ); if ( false === $total ) { return true; } for ( $i = 0; $i < $total; $i++ ) { $chunk_key = $key . '_' . $i; delete_transient( $chunk_key ); } delete_transient( $key . '_total' ); return true; } return delete_transient( $key ); } /** * Return cache key. * * @since 1.2.0 * * @param string $key The cache key. * * @return string */ private function get_cache_key( $key ) { return self::CACHE_PREFIX . $key; } } OpenAI.php 0000755 00000034706 14720416042 0006405 0 ustar 00 <?php /** * OpenAI class. * * @package Codeinwp/HyveLite */ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\Main; /** * OpenAI class. */ class OpenAI { /** * Base URL. * * @var string */ private static $base_url = 'https://api.openai.com/v1/'; /** * Prompt Version. * * @var string */ private $prompt_version = '1.1.0'; /** * API Key. * * @var string */ private $api_key; /** * Assistant ID. * * @var string */ private $assistant_id; /** * Constructor. * * @param string $api_key API Key. */ public function __construct( $api_key = '' ) { $settings = Main::get_settings(); $this->api_key = ! empty( $api_key ) ? $api_key : ( isset( $settings['api_key'] ) ? $settings['api_key'] : '' ); $this->assistant_id = isset( $settings['assistant_id'] ) ? $settings['assistant_id'] : ''; if ( $this->assistant_id && version_compare( $this->prompt_version, get_option( 'hyve_prompt_version', '1.0.0' ), '>' ) ) { $this->update_assistant(); } } /** * Setup Assistant. * * @return string|\WP_Error */ public function setup_assistant() { $assistant = $this->retrieve_assistant(); if ( is_wp_error( $assistant ) ) { return $assistant; } if ( ! $assistant ) { return $this->create_assistant(); } return $assistant; } /** * Create Assistant. * * @return string|\WP_Error */ public function create_assistant() { $response = $this->request( 'assistants', array( 'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"response\": {\r\n \"type\": \"string\",\r\n \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n },\r\n \"success\": {\r\n \"type\": \"boolean\",\r\n \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n }\r\n },\r\n \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}", 'name' => 'Chatbot by Hyve', 'model' => 'gpt-3.5-turbo-0125', ) ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->id ) ) { $this->assistant_id = $response->id; return $response->id; } return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the assistant.', 'hyve-lite' ) ); } /** * Update Assistant. * * @return bool|\WP_Error */ public function update_assistant() { $assistant = $this->retrieve_assistant(); $settings = Main::get_settings(); $assistant_id = ''; if ( is_wp_error( $assistant ) ) { return $assistant; } if ( ! $assistant ) { $assistant_id = $this->create_assistant(); if ( is_wp_error( $assistant_id ) ) { return $assistant_id; } } else { $response = $this->request( 'assistants/' . $this->assistant_id, array( 'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"response\": {\r\n \"type\": \"string\",\r\n \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n },\r\n \"success\": {\r\n \"type\": \"boolean\",\r\n \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n }\r\n },\r\n \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}", ) ); if ( is_wp_error( $response ) ) { return $response; } if ( ! isset( $response->id ) ) { return false; } $this->assistant_id = $response->id; $assistant_id = $response->id; } $settings['assistant_id'] = $assistant_id; update_option( 'hyve_settings', $settings ); update_option( 'hyve_prompt_version', $this->prompt_version ); return true; } /** * Retrieve Assistant. * * @return string|\WP_Error|false */ public function retrieve_assistant() { if ( ! $this->assistant_id ) { return false; } $response = $this->request( 'assistants/' . $this->assistant_id ); if ( is_wp_error( $response ) ) { if ( strpos( $response->get_error_message(), 'No assistant found' ) !== false ) { return false; } return $response; } if ( isset( $response->id ) ) { return $response->id; } return false; } /** * Create Embeddings. * * @param string|array $content Content. * @param string $model Model. * * @return mixed */ public function create_embeddings( $content, $model = 'text-embedding-3-small' ) { $response = $this->request( 'embeddings', array( 'input' => $content, 'model' => $model, ) ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->data ) ) { return $response->data; } return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the embeddings.', 'hyve-lite' ) ); } /** * Create a Thread. * * @param array $params Parameters. * * @return string|\WP_Error */ public function create_thread( $params = array() ) { $response = $this->request( 'threads', $params ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->id ) ) { return $response->id; } return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the thread.', 'hyve-lite' ) ); } /** * Send Message. * * @param string $message Message. * @param string $thread Thread. * @param string $role Role. * * @return true|\WP_Error */ public function send_message( $message, $thread, $role = 'assistant' ) { $response = $this->request( 'threads/' . $thread . '/messages', array( 'role' => $role, 'content' => $message, ) ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->id ) ) { return true; } return new \WP_Error( 'unknown_error', __( 'An error occurred while sending the message.', 'hyve-lite' ) ); } /** * Create a run * * @param array $messages Messages. * @param string $thread Thread. * * @return string|\WP_Error */ public function create_run( $messages, $thread ) { $settings = Main::get_settings(); $response = $this->request( 'threads/' . $thread . '/runs', array( 'assistant_id' => $this->assistant_id, 'additional_messages' => $messages, 'temperature' => $settings['temperature'], 'top_p' => $settings['top_p'], 'response_format' => array( 'type' => 'json_object', ), ) ); if ( is_wp_error( $response ) ) { return $response; } if ( ! isset( $response->id ) || ( isset( $response->status ) && 'queued' !== $response->status ) ) { return new \WP_Error( 'unknown_error', __( 'An error occurred while creating the run.', 'hyve-lite' ) ); } return $response->id; } /** * Get Run Status. * * @param string $run_id Run ID. * @param string $thread Thread. * * @return string|\WP_Error */ public function get_status( $run_id, $thread ) { $response = $this->request( 'threads/' . $thread . '/runs/' . $run_id, array(), 'GET' ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->status ) ) { return $response->status; } return new \WP_Error( 'unknown_error', __( 'An error occurred while getting the run status.', 'hyve-lite' ) ); } /** * Get Thread Messages. * * @param string $thread Thread. * * @return mixed */ public function get_messages( $thread ) { $response = $this->request( 'threads/' . $thread . '/messages', array(), 'GET' ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->data ) ) { return $response->data; } return new \WP_Error( 'unknown_error', __( 'An error occurred while getting the messages.', 'hyve-lite' ) ); } /** * Create Moderation Request. * * @param string $message Message. * * @return true|object|\WP_Error */ public function moderate( $message ) { $response = $this->request( 'moderations', array( 'input' => $message, ) ); if ( is_wp_error( $response ) ) { return $response; } if ( isset( $response->results ) ) { $result = reset( $response->results ); if ( isset( $result->flagged ) && $result->flagged ) { return $result; } } return true; } /** * Create Request. * * @param string $endpoint Endpoint. * @param array $params Parameters. * @param string $method Method. * * @return mixed */ private function request( $endpoint, $params = array(), $method = 'POST' ) { if ( ! $this->api_key ) { return (object) array( 'error' => true, 'message' => 'API key is missing.', ); } $body = wp_json_encode( $params ); $response = ''; if ( 'POST' === $method ) { $response = wp_remote_post( self::$base_url . $endpoint, array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->api_key, 'OpenAI-Beta' => 'assistants=v2', ), 'body' => $body, 'method' => 'POST', 'data_format' => 'body', ) ); } if ( 'GET' === $method ) { $url = self::$base_url . $endpoint; $args = array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->api_key, 'OpenAI-Beta' => 'assistants=v2', ), ); if ( function_exists( 'vip_safe_wp_remote_get' ) ) { $response = vip_safe_wp_remote_get( $url, '', 3, 1, 20, $args ); } else { $response = wp_remote_get( $url, $args ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get } } if ( is_wp_error( $response ) ) { return $response; } else { $body = wp_remote_retrieve_body( $response ); $body = json_decode( $body ); if ( isset( $body->error ) ) { if ( isset( $body->error->message ) ) { return new \WP_Error( isset( $body->error->code ) ? $body->error->code : 'unknown_error', $body->error->message ); } return new \WP_Error( 'unknown_error', __( 'An error occurred while processing the request.', 'hyve-lite' ) ); } return $body; } } } plugins-integration.php 0000755 00000004026 14720425051 0011264 0 ustar 00 <?php $action_filter_loader = new WPML_Action_Filter_Loader(); $action_filter_loader->load( array( 'WPML_Compatibility_Factory', ) ); add_action( 'plugins_loaded', 'wpml_plugins_integration_setup', 10 ); /** * Loads compatibility classes for active plugins. */ function wpml_plugins_integration_setup() { global $sitepress, $wpdb; $factories_to_load = []; // bbPress integration. if ( class_exists( 'bbPress' ) ) { $wpml_bbpress_api = new WPML_BBPress_API(); $wpml_bbpress_filters = new WPML_BBPress_Filters( $wpml_bbpress_api ); $wpml_bbpress_filters->add_hooks(); } // NextGen Gallery. if ( defined( 'NEXTGEN_GALLERY_PLUGIN_VERSION' ) ) { // Todo: do not include files: move to autoloaded classes. require_once WPML_PLUGIN_PATH . '/inc/plugin-integration-nextgen.php'; } if ( class_exists( 'GoogleSitemapGeneratorLoader' ) ) { $wpml_google_sitemap_generator = new WPML_Google_Sitemap_Generator( $wpdb, $sitepress ); $wpml_google_sitemap_generator->init_hooks(); } if ( class_exists( 'Tiny_Plugin' ) ) { $factories_to_load[] = 'WPML_Compatibility_Tiny_Compress_Images_Factory'; } // phpcs:disable WordPress.NamingConventions.ValidVariableName global $DISQUSVERSION; if ( $DISQUSVERSION ) { $factories_to_load[] = 'WPML_Compatibility_Disqus_Factory'; } // phpcs:enable if ( defined( 'GOOGLESITEKIT_VERSION' ) ) { $factories_to_load[] = \WPML\Compatibility\GoogleSiteKit\Hooks::class; } $action_filter_loader = new WPML_Action_Filter_Loader(); $action_filter_loader->load( $factories_to_load ); } add_action( 'after_setup_theme', 'wpml_themes_integration_setup' ); /** * Loads compatibility classes for active themes. */ function wpml_themes_integration_setup() { $actions = []; if ( function_exists( 'twentyseventeen_panel_count' ) && ! function_exists( 'twentyseventeen_translate_panel_id' ) ) { $wpml_twentyseventeen = new WPML_Compatibility_2017(); $wpml_twentyseventeen->init_hooks(); } $action_filter_loader = new WPML_Action_Filter_Loader(); $action_filter_loader->load( $actions ); } taxonomy-term-translation/wpml-term-translation.class.php 0000755 00000024413 14720425051 0020027 0 ustar 00 <?php use WPML\FP\Fns; use WPML\LIB\WP\Cache; /** * @since 3.2 * * Class WPML_Term_Translation * * Provides APIs for translating taxonomy terms * * @package wpml-core * @subpackage taxonomy-term-translation */ class WPML_Term_Translation extends WPML_Element_Translation { const CACHE_MAX_WARMUP_COUNT = 200; const CACHE_EXPIRE = 0; const CACHE_GROUP = 'wpml_term_translation'; /** @var int */ protected $cache_max_warmup_count = self::CACHE_MAX_WARMUP_COUNT; /** * @return int */ public function get_cache_max_warmup_count() { return $this->cache_max_warmup_count; } /** * @param int $cache_max_warmup_count * * @return void */ public function set_cache_max_warmup_count( $cache_max_warmup_count ) { $this->cache_max_warmup_count = $cache_max_warmup_count; } /** * @return void */ public function add_hooks() { // wp_delete_term: Removes a term from the database. add_action( 'delete_term', array( $this, 'invalidate_cache' ) ); // wp_set_object_terms: Creates term and taxonomy relationships. add_action( 'set_object_terms', array( $this, 'invalidate_cache' ) ); // wp_insert_term: Adds a new term to the database. // wp_update_term: Updates term based on arguments provided. add_action( 'saved_term', array( $this, 'invalidate_cache' ) ); // wp_remove_object_terms: Removes term(s) associated with a given object. add_action( 'deleted_term_relationships', array( $this, 'invalidate_cache' ) ); // clean_object_term_cache: Removes the taxonomy relationship to terms from the cache. add_action( 'clean_object_term_cache', array( $this, 'invalidate_cache' ) ); // clean_term_cache: Removes all of the term IDs from the cache. // Covers: // wp_delete_term, // wp_insert_term, // wp_update_term, // wp_update_term_count_now (Updates the amount of terms in taxonomy), // _split_shared_term (Creates a new term for a term_taxonomy item that currently shares its term // with another term_taxonomy). add_action( 'clean_term_cache', array( $this, 'invalidate_cache' ) ); } /** * @return null|string */ private function get_cache_expire() { return self::CACHE_EXPIRE; } /** * @return void */ public function invalidate_cache() { Cache::flushGroup( self::CACHE_GROUP ); } /** * @param int $term_id * * @return null|string */ public function lang_code_by_termid( $term_id ) { return $this->get_element_lang_code( (int) $this->adjust_ttid_for_term_id( $term_id ) ); } /** * @param array $data */ private function set_data_to_cache( array $data ) { $expire = $this->get_cache_expire(); foreach ( $data as $row ) { Cache::set( self::CACHE_GROUP, 'ttid_by_termid_' . $row['term_id'], $expire, (int) $row['element_id'] ); Cache::set( self::CACHE_GROUP, 'ttid_by_termidtaxonomy_' . $row['term_id'] . $row['taxonomy'], $expire, (int) $row['element_id'] ); Cache::set( self::CACHE_GROUP, 'termid_by_ttid_' . $row['element_id'], $expire, (int) $row['term_id'] ); } } /** * We need to fetch items one by one only if items count where term_taxonomy_Id != term_id > CACHE_MAX_WARMUP_COUNT. * Otherwise we should just return the original argument instead of fetching one by one. * Because if no result found in the cache - we already have selected all possible rows and there is no sence to subquery for the same data with extra cond. * * @param int $count * * @return boolean */ private function is_cache_type_select_all_items_in_one_query( $count ) { return $count <= $this->get_cache_max_warmup_count(); } /** * Converts term_id into term_taxonomy_id * * @param string|int $term_id * * @return string|int */ public function adjust_ttid_for_term_id( $term_id ) { $count = $this->maybe_warm_term_id_cache(); if ( 0 === $count ) { return $term_id; } $key_prefix = 'ttid_by_termid_'; $key = $key_prefix . $term_id; $get_ttid_or_fallback_to_term_id = function() use ( $key_prefix, $key, $term_id, $count ) { if ( $this->is_cache_type_select_all_items_in_one_query( $count ) ) { return $term_id; } $data = $this->get_maybe_warm_term_id_cache_data( ' AND tax.term_id = %d', array( $term_id ) ); if ( count( $data ) ) { $this->set_data_to_cache( $data ); } else { // This will prevent from extra sql queries when function is called with the same param. Cache::set( self::CACHE_GROUP, $key_prefix . $term_id, $this->get_cache_expire(), $term_id ); } return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $term_id ); }; return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $get_ttid_or_fallback_to_term_id ); } /** * Converts term_taxonomy_id into term_id. * * @param string|int $ttid * * @return string|int */ public function adjust_term_id_for_ttid( $ttid ) { $count = $this->maybe_warm_term_id_cache(); if ( 0 === $count ) { return $ttid; } $key_prefix = 'termid_by_ttid_'; $key = $key_prefix . $ttid; $get_term_id_or_fallback_to_ttid = function() use ( $key_prefix, $key, $ttid, $count ) { if ( $this->is_cache_type_select_all_items_in_one_query( $count ) ) { return $ttid; } $data = $this->get_maybe_warm_term_id_cache_data( ' AND tax.term_taxonomy_id = %d', array( $ttid ) ); if ( count( $data ) ) { $this->set_data_to_cache( $data ); } else { // This will prevent from extra sql queries when function is called with the same param. Cache::set( self::CACHE_GROUP, $key_prefix . $ttid, $this->get_cache_expire(), $ttid ); } return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $ttid ); }; return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $get_term_id_or_fallback_to_ttid ); } /** * @param int $term_id * @param string $lang_code * @param bool|false $original_fallback if true will return the the input term_id in case no translation is found. * * @return null|int */ public function term_id_in( $term_id, $lang_code, $original_fallback = false ) { return $this->adjust_term_id_for_ttid( $this->element_id_in( (int) $this->adjust_ttid_for_term_id( $term_id ), $lang_code, $original_fallback ) ); } /** * @param string|int $term_id * @param string $taxonomy * * @return string|int|null */ public function trid_from_tax_and_id( $term_id, $taxonomy ) { $count = $this->maybe_warm_term_id_cache(); if ( 0 === $count ) { return $this->get_element_trid( $term_id ); } $key_prefix = 'ttid_by_termidtaxonomy_'; $key = $key_prefix . $term_id . $taxonomy; $get_term_id = function() use ( $key_prefix, $key, $term_id, $taxonomy, $count ) { if ( $this->is_cache_type_select_all_items_in_one_query( $count ) ) { return $term_id; } $data = $this->get_maybe_warm_term_id_cache_data( ' AND tax.term_id = %d AND tax.taxonomy = %s', array( $term_id, $taxonomy ) ); if ( count( $data ) ) { $this->set_data_to_cache( $data ); } else { // This will prevent from extra sql queries when function is called with the same params. Cache::set( self::CACHE_GROUP, $key_prefix . $term_id . $taxonomy, $this->get_cache_expire(), $term_id ); } return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $term_id ); }; $ttid = Cache::get( self::CACHE_GROUP, $key )->getOrElse( $get_term_id ); return $this->get_element_trid( $ttid ); } /** * Returns all post types to which a taxonomy is linked. * * @param string $taxonomy * * @return array * * @since 3.2.3 */ public function get_taxonomy_post_types( $taxonomy ) { return WPML_WP_Taxonomy::get_linked_post_types( $taxonomy ); } /** * @return string */ protected function get_element_join() { return " JOIN {$this->wpdb->term_taxonomy} tax ON wpml_translations.element_id = tax.term_taxonomy_id AND wpml_translations.element_type = CONCAT('tax_', tax.taxonomy) "; } /** * @param string $cols * * @return string */ protected function get_query_sql( $cols = 'wpml_translations.element_id, tax.term_id, tax.taxonomy' ) { $sql = ''; $sql .= "SELECT {$cols} FROM {$this->wpdb->prefix}icl_translations wpml_translations" . $this->get_element_join(); $sql .= " JOIN {$this->wpdb->terms} terms"; $sql .= ' ON terms.term_id = tax.term_id'; $sql .= ' WHERE tax.term_id != tax.term_taxonomy_id'; return $sql; } /** * @return string */ protected function get_type_prefix() { return 'tax_'; } /** * @return int */ private function maybe_warm_term_id_cache() { $key = 'items_count_for_cache'; $get_count = function() use ( $key ) { $sql = $this->get_query_sql( 'tax.term_id' ); // Query one more than the max warmup count. $sql = $sql . ' LIMIT 0, ' . ( $this->get_cache_max_warmup_count() + 1 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $data = $this->wpdb->get_results( $sql, ARRAY_A ); $count = is_array( $data ) ? count( $data ) : 0; Cache::set( self::CACHE_GROUP, $key, $this->get_cache_expire(), $count ); if ( $count > 0 && $count <= $this->get_cache_max_warmup_count() ) { $data = $this->get_maybe_warm_term_id_cache_data(); $this->set_data_to_cache( $data ); } return $count; }; return Cache::get( self::CACHE_GROUP, $key )->getOrElse( $get_count ); } /** * @param string $where_sql * @param array $where_sql_params * * @return array $data */ private function get_maybe_warm_term_id_cache_data( $where_sql = '', $where_sql_params = array() ) { $sql = $this->get_query_sql(); if ( count( $where_sql_params ) > 0 ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $sql = $this->wpdb->prepare( $sql . $where_sql, $where_sql_params ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $data = $this->wpdb->get_results( $sql, ARRAY_A ); return $data; } /** * @param string $term * @param string $slug * @param string $taxonomy * @param string $lang_code * * @return string */ public function generate_unique_term_slug( $term, $slug, $taxonomy, $lang_code ) { if ( '' === trim( $slug ) ) { $slug = sanitize_title( $term ); } return WPML_Terms_Translations::term_unique_slug( $slug, $taxonomy, $lang_code ); } /** * @return self */ public static function getGlobalInstance() { global $wpml_term_translations, $wpdb; if ( ! isset( $wpml_term_translations ) ) { $wpml_term_translations = new WPML_Term_Translation( $wpdb ); } return $wpml_term_translations; } } taxonomy-term-translation/wpml-term-hierarchy-duplication.class.php 0000755 00000010651 14720425051 0021757 0 ustar 00 <?php /** * Class WPML_Term_Hierarchy_Duplication * * @package wpml-core * @subpackage taxonomy-term-translation */ class WPML_Term_Hierarchy_Duplication extends WPML_WPDB_And_SP_User { public function duplicates_require_sync( $post_ids, $duplicates_only = true ) { $taxonomies = $this->sitepress->get_translatable_taxonomies( true ); foreach ( $taxonomies as $key => $tax ) { if ( ! is_taxonomy_hierarchical( $tax ) ) { unset( $taxonomies[ $key ] ); } } if ( (bool) $post_ids === true ) { $need_sync_taxonomies = $duplicates_only === true ? $this->get_need_sync_new_dupl( $post_ids, $taxonomies ) : $this->get_need_sync_all_terms( $taxonomies, $post_ids ); } else { $need_sync_taxonomies = array(); } return array_values( array_unique( $need_sync_taxonomies ) ); } private function get_need_sync_new_dupl( $duplicated_ids, $taxonomies ) { $new_terms = $this->get_new_terms_just_duplicated( $duplicated_ids, $taxonomies ); $affected_taxonomies = array(); foreach ( $new_terms as $term ) { $affected_taxonomies[] = $term->taxonomy; } $affected_taxonomies = array_unique( $affected_taxonomies ); $hierarchy_sync_helper = wpml_get_hierarchy_sync_helper( 'term' ); $unsynced_terms = $hierarchy_sync_helper->get_unsynced_elements( $affected_taxonomies, $this->sitepress->get_default_language() ); foreach ( $new_terms as $key => $new_term ) { $sync = true; foreach ( $unsynced_terms as $term_unsynced ) { if ( $term_unsynced->translated_id == $new_term->term_taxonomy_id ) { $sync = false; break; } } if ( $sync === true ) { unset( $new_terms[ $key ] ); } } $need_sync_taxonomies = array(); foreach ( $new_terms as $term ) { $need_sync_taxonomies[] = $term->taxonomy; } return $need_sync_taxonomies; } private function get_need_sync_all_terms( $translated_taxonomies, $post_ids ) { $hierarchy_sync_helper = wpml_get_hierarchy_sync_helper( 'term' ); $post_ids_in = wpml_prepare_in( (array) $post_ids, '%d' ); $taxonomies_in = wpml_prepare_in( $translated_taxonomies ); $this->wpdb->get_col( "SELECT DISTINCT tt.taxonomy FROM {$this->wpdb->term_taxonomy} tt JOIN {$this->wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id IN ({$post_ids_in}) AND tt.taxonomy IN ({$taxonomies_in})" ); foreach ( $translated_taxonomies as $key => $tax ) { $unsynced_terms = $hierarchy_sync_helper->get_unsynced_elements( $tax, $this->sitepress->get_default_language() ); if ( (bool) $unsynced_terms === false ) { unset( $translated_taxonomies[ $key ] ); } } return $translated_taxonomies; } private function get_new_terms_just_duplicated( $duplicate_ids, $taxonomies ) { if ( (bool) $duplicate_ids === false || (bool) $taxonomies === false ) { return array(); } $duplicate_ids_in = wpml_prepare_in( $duplicate_ids, '%d' ); $taxonomies_in = wpml_prepare_in( $taxonomies ); $terms = $this->wpdb->get_results( "SELECT tt.term_taxonomy_id, tt.taxonomy FROM {$this->wpdb->term_taxonomy} tt JOIN {$this->wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id JOIN {$this->wpdb->postmeta} pm ON pm.post_id = tr.object_id JOIN {$this->wpdb->terms} t_duplicate ON t_duplicate.term_id = tt.term_id JOIN {$this->wpdb->terms} t_original ON t_original.name = t_duplicate.name JOIN {$this->wpdb->term_taxonomy} tt_master ON tt_master.term_id = t_original.term_id JOIN {$this->wpdb->term_relationships} tr_master ON tt_master.term_taxonomy_id = tr_master.term_taxonomy_id LEFT JOIN {$this->wpdb->term_relationships} tr_other ON tt.term_taxonomy_id = tr_other.term_taxonomy_id AND tr_other.object_id != tr.object_id AND tr_other.object_id NOT IN ({$duplicate_ids_in}) LEFT JOIN {$this->wpdb->postmeta} pm_other ON pm_other.post_id = tr_other.object_id AND NOT (pm_other.meta_key = '_icl_lang_duplicate_of' AND pm_other.meta_value IN ({$duplicate_ids_in})) WHERE pm.meta_key = '_icl_lang_duplicate_of' AND tr_other.object_id IS NULL AND pm_other.post_id IS NULL AND pm.meta_value IN ({$duplicate_ids_in}) AND tr_master.object_id IN ({$duplicate_ids_in}) AND tt.taxonomy IN ({$taxonomies_in})" ); return $terms; } } taxonomy-term-translation/nav-menu-translation/wpml-nav-menu-actions.class.php 0000755 00000015205 14720425051 0023771 0 ustar 00 <?php /** * Class WPML_Nav_Menu_Actions * * @package wpml-core * @subpackage taxonomy-term-translation */ class WPML_Nav_Menu_Actions extends WPML_Full_Translation_API { /** * @param SitePress $sitepress * @param wpdb $wpdb * @param WPML_Post_Translation $post_translations * @param WPML_Term_Translation $term_translations */ public function __construct( &$sitepress, &$wpdb, &$post_translations, &$term_translations ) { parent::__construct( $sitepress, $wpdb, $post_translations, $term_translations ); add_action( 'wp_delete_nav_menu', array( $this, 'wp_delete_nav_menu' ) ); add_action( 'wp_create_nav_menu', array( $this, 'wp_update_nav_menu' ), 10, 2 ); add_action( 'wp_update_nav_menu', array( $this, 'wp_update_nav_menu' ), 10, 2 ); add_action( 'wp_update_nav_menu_item', array( $this, 'wp_update_nav_menu_item' ), 10, 3 ); add_action( 'delete_post', array( $this, 'wp_delete_nav_menu_item' ) ); add_filter( 'pre_update_option_theme_mods_' . get_option( 'stylesheet' ), array( $this, 'pre_update_theme_mods_theme' ) ); if ( is_admin() ) { add_filter( 'theme_mod_nav_menu_locations', array( $this, 'theme_mod_nav_menu_locations' ) ); } } public function wp_delete_nav_menu( $id ) { $menu_id_tt = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT term_taxonomy_id FROM {$this->wpdb->term_taxonomy} WHERE term_id=%d AND taxonomy='nav_menu'", $id ) ); $update_args = array( 'element_id' => $menu_id_tt, 'element_type' => 'tax_nav_menu', 'context' => 'tax', ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'before_delete' ) ) ); $q = "DELETE FROM {$this->wpdb->prefix}icl_translations WHERE element_id=%d AND element_type='tax_nav_menu' LIMIT 1"; $q_prepared = $this->wpdb->prepare( $q, $menu_id_tt ); $this->wpdb->query( $q_prepared ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'after_delete' ) ) ); } function wp_update_nav_menu( $menu_id, $menu_data = null ) { if ( $menu_data ) { $trid = $this->get_trid_from_post_data(); $language_code = $this->get_save_lang( $menu_id ); $menu_id_tt = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT term_taxonomy_id FROM {$this->wpdb->term_taxonomy} WHERE term_id=%d AND taxonomy='nav_menu' LIMIT 1", $menu_id ) ); $this->term_translations->reload(); $this->sitepress->set_element_language_details( $menu_id_tt, 'tax_nav_menu', $trid, $language_code ); } } function wp_update_nav_menu_item( $menu_id, $menu_item_db_id, $args ) { $menu_lang = $this->term_translations->lang_code_by_termid( $menu_id ); $trid = $this->post_translations->get_element_trid( $menu_item_db_id ); if ( array_key_exists( 'menu-item-type', $args ) && ( $args['menu-item-type'] === 'post_type' || $args['menu-item-type'] === 'taxonomy' ) && array_key_exists( 'menu-item-object-id', $args ) && $menu_id > 0 ) { $language_code_item = $args['menu-item-type'] === 'post_type' ? $this->post_translations->get_element_lang_code( $args['menu-item-object-id'] ) : $this->term_translations->lang_code_by_termid( $args['menu-item-object-id'] ); $language_code_item = $language_code_item ? $language_code_item : $this->sitepress->get_current_language(); if ( $language_code_item !== $menu_lang ) { wp_remove_object_terms( (int) $menu_item_db_id, (int) $menu_id, 'nav_menu' ); } } $language_code = isset( $language_code_item ) && $language_code_item ? $language_code_item : ( $menu_lang ? $menu_lang : $this->sitepress->get_current_language() ); $this->sitepress->set_element_language_details( $menu_item_db_id, 'post_nav_menu_item', $trid, $language_code ); } public function wp_delete_nav_menu_item( $menu_item_id ) { $post = get_post( $menu_item_id ); if ( ! empty( $post->post_type ) && $post->post_type == 'nav_menu_item' ) { $update_args = array( 'element_id' => $menu_item_id, 'element_type' => 'post_nav_menu_item', 'context' => 'post', ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'before_delete' ) ) ); $q = "DELETE FROM {$this->wpdb->prefix}icl_translations WHERE element_id=%d AND element_type='post_nav_menu_item' LIMIT 1"; $q_prepared = $this->wpdb->prepare( $q, $menu_item_id ); $this->wpdb->query( $q_prepared ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'after_delete' ) ) ); } } public function pre_update_theme_mods_theme( $val ) { $default_language = $this->sitepress->get_default_language(); $current_language = $this->sitepress->get_current_language(); if ( isset( $val['nav_menu_locations'] ) && filter_input( INPUT_GET, 'action' ) === 'delete' && $current_language !== $default_language ) { $val['nav_menu_locations'] = get_theme_mod( 'nav_menu_locations' ); } if ( isset( $val['nav_menu_locations'] ) ) { foreach ( (array) $val['nav_menu_locations'] as $k => $v ) { if ( ! $v && $current_language !== $default_language ) { $tl = get_theme_mod( 'nav_menu_locations' ); if ( isset( $tl[ $k ] ) ) { $val['nav_menu_locations'][ $k ] = $tl[ $k ]; } } else { $val['nav_menu_locations'][ $k ] = icl_object_id( $val['nav_menu_locations'][ $k ], 'nav_menu', true, $default_language ); } } } return $val; } public function theme_mod_nav_menu_locations( $theme_locations ) { if ( is_admin() && (bool) $theme_locations === true ) { $current_lang = $this->sitepress->get_current_language(); foreach ( (array) $theme_locations as $location => $menu_id ) { $translated_menu_id = $this->term_translations->term_id_in( $menu_id, $current_lang ); if ( $translated_menu_id ) { $theme_locations[ $location ] = $translated_menu_id; } } } return $theme_locations; } private function get_save_lang( $menu_id ) { $language_code = isset( $_POST['icl_nav_menu_language'] ) ? $_POST['icl_nav_menu_language'] : $this->term_translations->lang_code_by_termid( $menu_id ); $language_code = $language_code ? $language_code : $this->sitepress->get_current_language(); return $language_code; } /** * @return bool|int|mixed|null|string */ private function get_trid_from_post_data() { $trid = null; if ( ! empty( $_POST['icl_translation_of'] ) && $_POST['icl_translation_of'] !== 'none' ) { $trid = $this->sitepress->get_element_trid( $_POST['icl_translation_of'], 'tax_nav_menu' ); return $trid; } elseif ( isset( $_POST['icl_nav_menu_trid'] ) ) { $trid = ( (int) $_POST['icl_nav_menu_trid'] ); return $trid; } return $trid; } } taxonomy-term-translation/wpml-term-filters.class.php 0000755 00000007054 14720425051 0017143 0 ustar 00 <?php /** * WPML_Term_Filters class file. * * @package WPML\Core * @subpackage taxonomy-term-translation */ /** * Class WPML_Term_Filters */ class WPML_Term_Filters extends WPML_WPDB_And_SP_User { /** * Init class. */ public function init() { $taxonomies = get_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $this->add_hooks_to_translated_taxonomy( $taxonomy ); } add_action( 'registered_taxonomy', [ $this, 'registered_taxonomy' ], 10, 3 ); } /** * @param string $taxonomy Taxonomy slug. * @param array|string $object_type Object type or array of object types. * @param array $taxonomy_object Array of taxonomy registration arguments. */ public function registered_taxonomy( $taxonomy, $object_type, $taxonomy_object ) { $this->add_hooks_to_translated_taxonomy( $taxonomy ); } /** * @param string $taxonomy Taxonomy slug. */ private function add_hooks_to_translated_taxonomy( $taxonomy ) { if ( is_taxonomy_translated( $taxonomy ) ) { add_filter( "pre_option_{$taxonomy}_children", [ $this, 'pre_option_tax_children' ], 10, 0 ); add_action( "create_{$taxonomy}", [ $this, 'update_tax_children_option' ], 10, 0 ); add_action( "edit_{$taxonomy}", [ $this, 'update_tax_children_option' ], 10, 0 ); } } public function update_tax_children_option( $taxonomy_input = false ) { global $wpml_language_resolution, $wp_taxonomies; $language_codes = $wpml_language_resolution->get_active_language_codes(); $language_codes[] = 'all'; $taxonomy = str_replace( array( 'create_', 'edit_' ), '', current_action() ); $taxonomy = isset( $wp_taxonomies[ $taxonomy ] ) ? $taxonomy : $taxonomy_input; foreach ( $language_codes as $lang ) { $tax_children = $this->get_tax_hier_array( $taxonomy, $lang ); $option_key = "{$taxonomy}_children_{$lang}"; update_option( $option_key, $tax_children ); } } public function pre_option_tax_children() { $taxonomy = str_replace( array( 'pre_option_', '_children' ), '', current_filter() ); $lang = $this->sitepress->get_current_language(); $option_key = "{$taxonomy}_children_{$lang}"; $tax_children = get_option( $option_key, false ); if ( $tax_children === false ) { $tax_children = $this->get_tax_hier_array( $taxonomy, $lang ); update_option( $option_key, $tax_children ); } return ! empty( $tax_children ) ? $tax_children : false; } /** * @param string $taxonomy * @param string $lang_code * * @return array */ public function get_tax_hier_array( $taxonomy, $lang_code ) { $hierarchy = array(); if ( $lang_code != 'all' ) { $terms = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT term_id, parent FROM {$this->wpdb->term_taxonomy} tt JOIN {$this->wpdb->prefix}icl_translations iclt ON tt.term_taxonomy_id = iclt.element_id WHERE tt.parent > 0 AND tt.taxonomy = %s AND iclt.language_code = %s AND iclt.element_type = %s ORDER BY term_id", $taxonomy, $lang_code, 'tax_' . $taxonomy ) ); } else { $terms = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT term_id, parent FROM {$this->wpdb->term_taxonomy} tt WHERE tt.parent > 0 AND tt.taxonomy = %s ORDER BY term_id", $taxonomy ) ); } foreach ( $terms as $term ) { if ( $term->parent > 0 ) { $hierarchy[ $term->parent ] = isset( $hierarchy[ $term->parent ] ) ? $hierarchy[ $term->parent ] : array(); $hierarchy[ $term->parent ][] = $term->term_id; } } return $hierarchy; } } taxonomy-term-translation/wpml-term-translation-utils.class.php 0000755 00000007207 14720425051 0021167 0 ustar 00 <?php use WPML\FP\Fns; class WPML_Term_Translation_Utils extends WPML_SP_User { /** * Duplicates all terms, that exist in the given target language, * from the original post to the translation in that language. * * @param int $original_post_id * @param string $lang */ function sync_terms( $original_post_id, $lang ) { $this->synchronize_terms( $original_post_id, $lang, false ); } /** * Duplicates all terms on the original post to its translation in the given target language. * Missing terms are created with the same name as their originals. * * @param int $original_post_id * @param string $lang */ function duplicate_terms( $original_post_id, $lang ) { $this->synchronize_terms( $original_post_id, $lang, true ); } /** * @param int $original_post_id * @param string $lang * @param bool $duplicate sets whether missing terms should be created by duplicating the original term */ private function synchronize_terms( $original_post_id, $lang, $duplicate ) { global $wpml_post_translations; $returnTrue = Fns::always( true ); add_filter( 'wpml_disable_term_adjust_id', $returnTrue ); $wpml_post_translations->reload(); $translated_post_id = $wpml_post_translations->element_id_in( $original_post_id, $lang ); if ( (bool) $translated_post_id === true ) { $taxonomies = get_post_taxonomies( $original_post_id ); foreach ( $taxonomies as $tax ) { $terms_on_original = wp_get_object_terms( $original_post_id, $tax ); if ( is_wp_error ( $terms_on_original ) ) { continue; } if ( ! $this->sitepress->is_translated_taxonomy( $tax ) ) { if ( $this->sitepress->get_setting( 'sync_post_taxonomies' ) ) { // Taxonomy is not translated so we can just copy from the original foreach ( $terms_on_original as $key => $term ) { $terms_on_original[ $key ] = $term->term_id; } wp_set_object_terms( $translated_post_id, $terms_on_original, $tax ); } } else { /** @var int[] $translated_terms translated term_ids */ $translated_terms = $this->get_translated_term_ids( $terms_on_original, $lang, $tax, $duplicate ); wp_set_object_terms( $translated_post_id, $translated_terms, $tax ); } } } remove_filter( 'wpml_disable_term_adjust_id', $returnTrue ); $post_type = get_post_type( $original_post_id ); $post_type && clean_object_term_cache( $original_post_id, $post_type ); } /** * @param object[] $terms * @param string $lang * @param string $taxonomy * @param bool $duplicate sets whether missing terms should be created by duplicating the original term * * @return array */ private function get_translated_term_ids( $terms, $lang, $taxonomy, $duplicate ) { /** @var WPML_Term_Translation $wpml_term_translations */ global $wpml_term_translations; $term_utils = new WPML_Terms_Translations(); $wpml_term_translations->reload(); $translated_terms = array(); foreach ( $terms as $orig_term ) { $translated_id = (int) $wpml_term_translations->term_id_in( $orig_term->term_id, $lang ); if ( ! $translated_id && $duplicate ) { $translation = $term_utils->create_automatic_translation( array( 'lang_code' => $lang, 'taxonomy' => $taxonomy, 'trid' => $wpml_term_translations->get_element_trid( $orig_term->term_taxonomy_id ), 'source_language' => $wpml_term_translations->get_element_lang_code( $orig_term->term_taxonomy_id ), ) ); $translated_id = isset( $translation['term_id'] ) ? $translation['term_id'] : false; } if ( $translated_id ) { $translated_terms[] = $translated_id; } } return $translated_terms; } } taxonomy-term-translation/wpml-term-translations.class.php 0000755 00000057364 14720425051 0020225 0 ustar 00 <?php require_once dirname( __FILE__ ) . '/wpml-update-term-action.class.php'; use WPML\FP\Lst; use WPML\FP\Fns; use WPML\FP\Obj; use WPML\FP\Logic; use function WPML\FP\pipe; /** * @since 3.1.8 * * Class WPML_Terms_Translations * * This class holds some basic functionality for translating taxonomy terms. * * @package wpml-core * @subpackage taxonomy-term-translation */ class WPML_Terms_Translations { /** * @param array<string|\WP_Term> $terms * @param string[]|string $taxonomies This is only used by the WP core AJAX call that fetches the preview * auto-complete for flat taxonomy term adding * * @return array<\WP_Term> * @deprecated since Version 3.1.8.3 */ public static function get_terms_filter( $terms, $taxonomies ) { global $wpdb, $sitepress; $lang = $sitepress->get_current_language(); foreach ( $taxonomies as $taxonomy ) { if ( $sitepress->is_translated_taxonomy( $taxonomy ) ) { $element_type = 'tax_' . $taxonomy; $query = $wpdb->prepare( "SELECT wptt.term_id FROM {$wpdb->prefix}icl_translations AS iclt JOIN {$wpdb->prefix}term_taxonomy AS wptt ON iclt.element_id = wptt.term_taxonomy_id WHERE language_code=%s AND element_type = %s", $lang, $element_type ); $element_ids_array = $wpdb->get_col( $query ); foreach ( $terms as $key => $term ) { if ( ! is_object( $term ) ) { $term = get_term_by( 'name', $term, $taxonomy ); } if ( $term && isset( $term->taxonomy ) && $term->taxonomy === $taxonomy && ! in_array( $term->term_id, $element_ids_array ) ) { unset( $terms[ $key ] ); } } } } return $terms; } /** * @param string $slug * @param string $taxonomy * @param string $lang * Creates a unique slug for a given term, using a scheme * encoding the language code in the slug. * * @return string */ public static function term_unique_slug( $slug, $taxonomy, $lang ) { global $sitepress; $default_language = $sitepress->get_default_language(); if ( $lang !== $default_language && self::term_slug_exists( $slug, $taxonomy ) ) { $slug .= '-' . $lang; } $i = 2; $suffix = '-' . $i; if ( self::term_slug_exists( $slug, $taxonomy ) ) { while ( self::term_slug_exists( $slug . $suffix, $taxonomy ) ) { $i ++; $suffix = '-' . $i; } $slug .= $suffix; } return $slug; } /** * @param string $slug * @param string $taxonomy * * @return bool */ private static function term_slug_exists( $slug, $taxonomy ) { global $wpdb; $existing_term_prepared_query = $wpdb->prepare( "SELECT t.term_id FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE t.slug = %s AND tt.taxonomy = %s LIMIT 1", $slug, $taxonomy ); $term_id = $wpdb->get_var( $existing_term_prepared_query ); return (bool) $term_id; } /** * This function provides an action hook only used by WCML. * It will be removed in the future and should not be implemented in new spots. * * @deprecated deprecated since version 3.1.8.3 * * @param string $taxonomy The identifier of the taxonomy the translation was just saved to. * @param array $translated_term The associative array holding term taxonomy id and term id, * as returned by wp_insert_term or wp_update_term. */ public static function icl_save_term_translation_action( $taxonomy, $translated_term ) { global $wpdb, $sitepress; if ( is_taxonomy_hierarchical( $taxonomy ) ) { $term_taxonomy_id = $translated_term['term_taxonomy_id']; $original_ttid = $sitepress->get_original_element_id( $term_taxonomy_id, 'tax_' . $taxonomy ); $original_tax_sql = "SELECT * FROM {$wpdb->term_taxonomy} WHERE taxonomy=%s AND term_taxonomy_id = %d"; $original_tax_prepared = $wpdb->prepare( $original_tax_sql, array( $taxonomy, $original_ttid ) ); $original_tax = $wpdb->get_row( $original_tax_prepared ); do_action( 'icl_save_term_translation', $original_tax, $translated_term ); } } /** * Prints a hidden div, containing the list of allowed terms for a post type in each language. * This is used to only display the correct categories and tags in the quick-edit fields of the post table. * * @param string $column_name * @param string|string[]|\WP_Post $post_type */ public static function quick_edit_terms_removal( $column_name, $post_type ) { /** @var SitePress $sitepress */ global $sitepress, $wpdb; if ( $column_name == 'icl_translations' ) { $taxonomies = array_filter( get_object_taxonomies( $post_type ), array( $sitepress, 'is_translated_taxonomy', ) ); $terms_by_language_and_taxonomy = array(); if ( ! empty( $taxonomies ) ) { $res = $wpdb->get_results( " SELECT language_code, taxonomy, term_id FROM {$wpdb->term_taxonomy} tt JOIN {$wpdb->prefix}icl_translations wpml_translations ON wpml_translations.element_id = tt.term_taxonomy_id AND wpml_translations.element_type = CONCAT('tax_', tt.taxonomy) WHERE tt.taxonomy IN (" . wpml_prepare_in( $taxonomies ) . ' )' ); } else { $res = array(); } foreach ( $res as $term ) { $lang = $term->language_code; $tax = $term->taxonomy; $terms_by_language_and_taxonomy[ $lang ] = isset( $terms_by_language_and_taxonomy[ $lang ] ) ? $terms_by_language_and_taxonomy[ $lang ] : array(); $terms_by_language_and_taxonomy[ $lang ][ $tax ] = isset( $terms_by_language_and_taxonomy[ $lang ][ $tax ] ) ? $terms_by_language_and_taxonomy[ $lang ][ $tax ] : array(); $terms_by_language_and_taxonomy[ $lang ][ $tax ][] = $term->term_id; } $terms_json = wp_json_encode( $terms_by_language_and_taxonomy ); echo $terms_json ? '<div id="icl-terms-by-lang" style="display: none;">' . wp_kses_post( $terms_json ) . '</div>' : ''; } } /** * Creates a new term from an argument array. * * @param array $args * @return array|bool * Returns either an array containing the term_id and term_taxonomy_id of the term resulting from this database * write or false on error. */ public static function create_new_term( $args ) { global $wpdb, $sitepress; /** @var string $taxonomy */ $taxonomy = false; /** @var string $lang_code */ $lang_code = false; /** * Sets whether translations of posts are to be updated by the newly created term, * should they be missing a translation still. * During debug actions designed to synchronise post and term languages this should not be set to true, * doing so introduces the possibility of removing terms from posts before switching * them with their translation in the correct language. * * @var bool */ $sync = false; extract( $args, EXTR_OVERWRITE ); require_once dirname( __FILE__ ) . '/wpml-update-term-action.class.php'; $new_term_action = new WPML_Update_Term_Action( $wpdb, $sitepress, $args ); $new_term = $new_term_action->execute(); if ( $sync && $new_term && $taxonomy && $lang_code ) { self::sync_taxonomy_terms_language( $taxonomy ); } return $new_term; } /** * @param array<mixed> $args * Creates an automatic translation of a term, the name of which is set as "original" . @ "lang_code" and the slug of which is set as "original_slug" . - . "lang_code". * * @return array|bool */ public function create_automatic_translation( $args ) { global $sitepress; $term = false; $lang_code = false; $taxonomy = ''; $original_id = false; $original_tax_id = false; $trid = false; $original_term = false; $update_translations = false; $source_language = null; extract( $args, EXTR_OVERWRITE ); if ( $trid && ! $original_id ) { $original_tax_id = $sitepress->get_original_element_id_by_trid( $trid ); $original_term = get_term_by( 'term_taxonomy_id', $original_tax_id, $taxonomy, OBJECT, 'no' ); } if ( $original_id && ! $original_tax_id ) { $original_term = get_term( $original_id, $taxonomy, OBJECT, 'no' ); if ( isset( $original_term['term_taxonomy_id'] ) ) { $original_tax_id = $original_term['term_taxonomy_id']; } } if ( ! $trid ) { $trid = $sitepress->get_element_trid( $original_tax_id, 'tax_' . $taxonomy ); } if ( ! $source_language ) { $source_language = $sitepress->get_source_language_by_trid( $trid ); } $existing_translations = $sitepress->get_element_translations( $trid, 'tax_' . $taxonomy ); if ( $lang_code && isset( $existing_translations[ $lang_code ] ) ) { $new_translated_term = false; } else { if ( ! $original_term ) { if ( $original_id ) { $original_term = get_term( $original_id, $taxonomy, OBJECT, 'no' ); } elseif ( $original_tax_id ) { $original_term = get_term_by( 'term_taxonomy_id', $original_tax_id, $taxonomy, OBJECT, 'no' ); } } $translated_slug = false; if ( ! $term && isset( $original_term->name ) ) { $term = $original_term->name; /** * @deprecated use 'wpml_duplicate_generic_string' instead, with the same arguments */ $term = apply_filters( 'icl_duplicate_generic_string', $term, $lang_code, array( 'context' => 'taxonomy', 'attribute' => $taxonomy, 'key' => $original_term->term_id, ) ); $term = apply_filters( 'wpml_duplicate_generic_string', $term, $lang_code, array( 'context' => 'taxonomy', 'attribute' => $taxonomy, 'key' => $original_term->term_id, ) ); } if ( isset( $original_term->slug ) ) { $translated_slug = $original_term->slug; /** * @deprecated use 'wpml_duplicate_generic_string' instead, with the same arguments */ $translated_slug = apply_filters( 'icl_duplicate_generic_string', $translated_slug, $lang_code, array( 'context' => 'taxonomy_slug', 'attribute' => $taxonomy, 'key' => $original_term->term_id, ) ); $translated_slug = apply_filters( 'wpml_duplicate_generic_string', $translated_slug, $lang_code, array( 'context' => 'taxonomy_slug', 'attribute' => $taxonomy, 'key' => $original_term->term_id, ) ); $translated_slug = is_string( $lang_code ) ? self::term_unique_slug( $translated_slug, $taxonomy, $lang_code ) : $translated_slug; } $new_translated_term = false; if ( $term ) { $new_term_args = array( 'term' => $term, 'slug' => $translated_slug, 'taxonomy' => $taxonomy, 'lang_code' => $lang_code, 'original_tax_id' => $original_tax_id, 'update_translations' => $update_translations, 'trid' => $trid, 'source_language' => $source_language, ); $new_translated_term = self::create_new_term( $new_term_args ); } } return $new_translated_term; } /** * @param string $taxonomy * * Sets all taxonomy terms to the correct language on each post, having at least one term from the taxonomy. */ public static function sync_taxonomy_terms_language( $taxonomy ) { $all_posts_in_taxonomy = get_posts( array( 'tax_query' => array( 'taxonomy' => $taxonomy ) ) ); foreach ( $all_posts_in_taxonomy as $post_in_taxonomy ) { self::sync_post_and_taxonomy_terms_language( $post_in_taxonomy->ID, $taxonomy ); } } /** * @param int $post_id * * Sets all taxonomy terms ot the correct language for a given post. */ public static function sync_post_terms_language( $post_id ) { $taxonomies = get_taxonomies(); foreach ( $taxonomies as $taxonomy ) { self::sync_post_and_taxonomy_terms_language( $post_id, $taxonomy ); } } /** * @param int $post_id * @param string $taxonomy * Synchronizes a posts taxonomy term's languages with the posts language for all translations of the post. */ public static function sync_post_and_taxonomy_terms_language( $post_id, $taxonomy ) { global $sitepress; $post = get_post( $post_id ); $post_type = $post->post_type; $post_trid = $sitepress->get_element_trid( $post_id, 'post_' . $post_type ); $post_translations = $sitepress->get_element_translations( $post_trid, 'post_' . $post_type ); $terms_from_original_post = wp_get_post_terms( $post_id, $taxonomy ); if ( is_wp_error ( $terms_from_original_post ) ) { return; } $is_original = true; if ( $sitepress->get_original_element_id( $post_id, 'post_' . $post_type ) != $post_id ) { $is_original = false; } foreach ( $post_translations as $post_language => $translated_post ) { $translated_post_id = $translated_post->element_id; if ( ! $translated_post_id ) { continue; } $terms_from_translated_post = wp_get_post_terms( $translated_post_id, $taxonomy ); if ( is_wp_error ( $terms_from_translated_post ) ) { continue; } if ( $is_original ) { $duplicates = $sitepress->get_duplicates( $post_id ); if ( in_array( $translated_post_id, $duplicates ) ) { $terms = array_merge( $terms_from_original_post, $terms_from_translated_post ); } else { $terms = $terms_from_translated_post; } } else { $terms = $terms_from_translated_post; } foreach ( (array) $terms as $term ) { $term_original_tax_id = $term->term_taxonomy_id; $original_term_language_object = $sitepress->get_element_language_details( $term_original_tax_id, 'tax_' . $term->taxonomy ); if ( $original_term_language_object && isset( $original_term_language_object->language_code ) ) { $original_term_language = $original_term_language_object->language_code; } else { $original_term_language = $post_language; } if ( $original_term_language != $post_language ) { $term_trid = $sitepress->get_element_trid( $term_original_tax_id, 'tax_' . $term->taxonomy ); $translated_terms = $sitepress->get_element_translations( $term_trid, 'tax_' . $term->taxonomy, false, false, true ); $term_id = $term->term_id; wp_remove_object_terms( $translated_post_id, (int) $term_id, $taxonomy ); if ( isset( $translated_terms[ $post_language ] ) ) { $term_in_correct_language = $translated_terms[ $post_language ]; wp_set_post_terms( $translated_post_id, array( (int) $term_in_correct_language->term_id ), $taxonomy, true ); } if ( isset( $term->term_taxonomy_id ) ) { wp_update_term_count( $term->term_taxonomy_id, $taxonomy ); } } wp_update_term_count( $term_original_tax_id, $taxonomy ); } } } /** * @param int $post_id Object ID. * @param array $terms An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ public static function set_object_terms_action( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { global $sitepress; // TODO: [WPML 3.2] We have a better way to check if the post is an external type (e.g. Package). if ( get_post( $post_id ) ) { self::set_tags_in_proper_language( $post_id, $tt_ids, $taxonomy, $old_tt_ids ); if ( $sitepress->get_setting( 'sync_post_taxonomies' ) ) { $term_actions_helper = $sitepress->get_term_actions_helper(); $term_actions_helper->added_term_relationships( $post_id ); } } } /** * @param int $post_id Object ID. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param array $old_tt_ids Old array of term taxonomy IDs. * @param bool $isBulkEdit */ private static function set_tags_in_proper_language( $post_id, $tt_ids, $taxonomy, $old_tt_ids ) { $isEditAction = isset( $_POST['action'] ) && ( 'editpost' === $_POST['action'] || 'inline-save' === $_POST['action'] ); $isBulkEdit = isset( $_REQUEST['bulk_edit'] ); if ( $isEditAction || $isBulkEdit ) { $tt_ids = array_map( 'intval', $tt_ids ); $tt_ids = array_diff( $tt_ids, $old_tt_ids ); self::quick_edited_post_terms( $post_id, $taxonomy, $tt_ids ); } } /** * @param int $post_id * @param string $taxonomy * @param array $changed_ttids * * Running this function will remove certain issues arising out of bulk adding of terms to posts of various languages. * This case can result in situations in which the WP Core functionality adds a term to a post, before the language assignment * operations of WPML are triggered. This leads to states in which terms can be assigned to a post even though their language * differs from that of the post. * This function behaves between hierarchical and flat taxonomies. Hierarchical terms from the wrong taxonomy are simply removed * from the post. Flat terms are added with the same name but in the correct language. * For flat terms this implies either the use of the existing term or the creation of a new one. * This function uses wpdb queries instead of the WordPress API, it is therefore save to be run out of * any language setting. */ public static function quick_edited_post_terms( $post_id, $taxonomy, $newlyAddedTermIds ) { global $wpdb, $sitepress, $wpml_post_translations; $postLang = $wpml_post_translations->get_element_lang_code( $post_id ) ?: Obj::prop( 'icl_post_language', $_POST ); if ( ! $sitepress->is_translated_taxonomy( $taxonomy ) || ! ( $postLang ) ) { return; } $sql = "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE language_code = %s AND element_type = %s"; $termIdsInPostLang = $wpdb->get_col( $wpdb->prepare( $sql, $postLang, 'tax_' . $taxonomy ) ); $termIdsInPostLang = Fns::map( \WPML\FP\Cast::toInt(), $termIdsInPostLang ); $newlyCreatedTermIds = []; $isInPostLang = Lst::includes( Fns::__, $termIdsInPostLang ); if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { $getTermInPostLang = Obj::prop( 'idInPostLang' ); $createTermIfNotExists = Logic::ifElse( $getTermInPostLang, Fns::identity(), self::createTerm( $taxonomy, $postLang ) ); $updateOrDeleteTermInPost = Logic::ifElse( $getTermInPostLang, self::updatePostTaxonomy( $post_id ), pipe( Obj::prop( 'id' ), self::deletePostTaxonomy( $post_id ) ) ); $newlyCreatedTermIds = \wpml_collect( $newlyAddedTermIds ) ->reject( $isInPostLang ) ->map( self::appendTermName() ) ->map( self::appendTermIdCounterpartInPostLang( $termIdsInPostLang, $taxonomy ) ) ->map( $createTermIfNotExists ) ->map( $updateOrDeleteTermInPost ) ->all(); } else { \wpml_collect( $newlyAddedTermIds )->reject( $isInPostLang )->each( self::deletePostTaxonomy( $post_id ) ); } // Update term counts manually here, since using sql, will not trigger the updating of term counts automatically. wp_update_term_count( array_merge( $newlyAddedTermIds, $newlyCreatedTermIds ), $taxonomy ); } /** * @param int $postId * * @return Closure */ private static function deletePostTaxonomy( $postId ) { return function ( $termId ) use ( $postId ) { global $wpdb; $wpdb->delete( $wpdb->term_relationships, [ 'object_id' => $postId, 'term_taxonomy_id' => $termId, ] ); }; } /** * @return Closure */ private static function appendTermName() { return function ( $termId ) { global $wpdb; $sql = "SELECT t.name FROM {$wpdb->terms} AS t JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id WHERE tt.term_taxonomy_id=%d"; $termName = $wpdb->get_var( $wpdb->prepare( $sql, $termId ) ); return [ 'id' => $termId, 'name' => $termName ]; }; } /** * @param array $termIdsInPostLang * @param string $taxonomy * * @return Closure */ private static function appendTermIdCounterpartInPostLang( $termIdsInPostLang, $taxonomy ) { return function ( $termData ) use ( $termIdsInPostLang, $taxonomy ) { global $wpdb; $idInPostLang = false; if ( count( $termIdsInPostLang ) ) { $in = wpml_prepare_in( $termIdsInPostLang, '%d' ); $sql = " SELECT tt.term_taxonomy_id FROM {$wpdb->terms} AS t JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id WHERE t.name=%s AND tt.taxonomy=%s AND tt.term_taxonomy_id IN ({$in}) "; $idInPostLang = $wpdb->get_var( $wpdb->prepare( $sql, $termData['name'], $taxonomy ) ); } return Obj::assoc( 'idInPostLang', $idInPostLang, $termData ); }; } /** * @param string $taxonomy * @param string $postLang * * @return Closure */ private static function createTerm( $taxonomy, $postLang ) { return function ( $termData ) use ( $taxonomy, $postLang ) { global $sitepress; $idInCorrectId = false; $newTerm = wp_insert_term( $termData['name'], $taxonomy, [ 'slug' => self::term_unique_slug( sanitize_title( $termData['name'] ), $taxonomy, $postLang ) ] ); if ( isset( $newTerm['term_taxonomy_id'] ) ) { $idInCorrectId = $newTerm['term_taxonomy_id']; $trid = $sitepress->get_element_trid( $termData['id'], 'tax_' . $taxonomy ); $sitepress->set_element_language_details( $idInCorrectId, 'tax_' . $taxonomy, $trid, $postLang ); } return Obj::assoc( 'idInPostLang', $idInCorrectId, $termData ); }; } /** * @param int $postId * * @return Closure */ private static function updatePostTaxonomy( $postId ) { return function ( $termData ) use ( $postId ) { global $wpdb; $wpdb->update( $wpdb->term_relationships, [ 'term_taxonomy_id' => $termData['idInPostLang'] ], [ 'object_id' => $postId, 'term_taxonomy_id' => $termData['id'], ] ); }; } /** * Returns an array of all terms, that have a language suffix on them. * This is used by troubleshooting functionality. * * @return array */ public static function get_all_terms_with_language_suffix() { global $wpdb; $lang_codes = $wpdb->get_col( "SELECT code FROM {$wpdb->prefix}icl_languages" ); /* Build the expression to find all potential candidates for renaming. * These must have the part "<space>@lang_code<space>" in them. */ $where_parts = array(); foreach ( $lang_codes as $key => $code ) { $where_parts[ $key ] = "t.name LIKE '" . '% @' . esc_sql( $code ) . "%'"; } $where = '(' . join( ' OR ', $where_parts ) . ')'; $terms_with_suffix = $wpdb->get_results( "SELECT t.name, t.term_id, tt.taxonomy FROM {$wpdb->terms} AS t JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id WHERE {$where}" ); $terms = array(); foreach ( $terms_with_suffix as $term ) { if ( $term->name == WPML_Troubleshooting_Terms_Menu::strip_language_suffix( $term->name ) ) { continue; } $term_id = $term->term_id; $term_taxonomy_label = $term->taxonomy; $taxonomy = get_taxonomy( $term->taxonomy ); if ( $taxonomy && isset( $taxonomy->labels ) && isset( $taxonomy->labels->name ) ) { $term_taxonomy_label = $taxonomy->labels->name; } if ( isset( $terms[ $term_id ] ) && isset( $terms[ $term_id ]['taxonomies'] ) ) { if ( ! in_array( $term_taxonomy_label, $terms[ $term_id ]['taxonomies'] ) ) { $terms[ $term_id ]['taxonomies'][] = $term_taxonomy_label; } } else { $terms[ $term_id ] = array( 'name' => $term->name, 'taxonomies' => array( $term_taxonomy_label ), ); } } return $terms; } } taxonomy-term-translation/wpml-update-term-action.class.php 0000755 00000020114 14720425051 0020220 0 ustar 00 <?php /** * Class WPML_Update_Term_Action * * This class holds the functionality for creating or editing a taxonomy term. * * @package wpml-core * @subpackage taxonomy-term-translation */ class WPML_Update_Term_Action extends WPML_WPDB_And_SP_User { /** * TRUE if this object represents valid data for the update or creation of a term, false otherwise. * * @var bool */ private $is_valid = true; /** * TRUE if this object represents term update action, false if it represents a term creation action. * * @var bool */ private $is_update; /** * Argument array containing arguments in a format that can and is used as input to \wp_update_term or * \wp_insert_term * * @var array */ private $wp_new_term_args = array(); /** * The taxonomy in which this action takes place. * * @var string */ private $taxonomy; /** * Trid value in the icl_translations table to which this action is to be written. * * @var int */ private $trid; /** * Language of the term that is to result from this action. * * @var string */ private $lang_code; /** * Source language of the term that is to result from this action. * * @var string|null */ private $source_lang_code = null; /** * Array holding translations of the term created by this object prior to it's creation. * * @var array */ private $existing_translations = array(); /** * The term id of the term to be updated or resulting from this action. * * @var int */ private $term_id; /** * This only gets set for update actions. In this case the new slug has to be compared with the old slug, * to decide whether any slug name sanitation has to happen. * * @var string */ private $old_slug; /** * @param wpdb $wpdb * @param SitePress $sitepress * @param array $args */ public function __construct( &$wpdb, &$sitepress, $args ) { parent::__construct( $wpdb, $sitepress ); /** * Actual name of the term. Same as the name input argument to \wp_update_term or \wp_insert_term * * @var string|bool */ $term = false; $slug = ''; $taxonomy = ''; /** @var string $lang_code */ $lang_code = ''; $trid = null; /** @var int|bool $original_tax_id */ $original_tax_id = false; /** * Taxonomy_term_id of the parent element * * @var int */ $parent = 0; $description = false; $term_group = false; $source_language = null; extract( $args, EXTR_OVERWRITE ); // We cannot create a term unless we at least know its name if ( (string) $term !== '' && $taxonomy ) { $this->wp_new_term_args['name'] = $term; $this->taxonomy = $taxonomy; } else { $this->is_valid = false; return; } if ( $parent ) { $this->wp_new_term_args['parent'] = $parent; } if ( $description ) { $this->wp_new_term_args['description'] = $description; } if ( $term_group ) { $this->wp_new_term_args['term_group'] = $term_group; } $this->wp_new_term_args['term_group'] = $term_group; $this->is_valid = $this->set_language_information( $trid, $original_tax_id, $lang_code, $source_language ); $this->set_action_type(); if ( ! $this->is_update || ( $this->is_update && $slug != $this->old_slug && ! empty( $slug ) ) ) { if ( trim( $slug ) == '' ) { $slug = sanitize_title( $term ); } $slug = WPML_Terms_Translations::term_unique_slug( $slug, $taxonomy, $lang_code ); $this->wp_new_term_args['slug'] = $slug; } } /** * Writes the term update or creation action saved in this object to the database. * * @return array|false * Returns either an array containing the term_id and term_taxonomy_id of the term resulting from this database * write or false on error. */ public function execute() { global $sitepress; $switch_lang = new WPML_Temporary_Switch_Language( $sitepress, $this->lang_code ); remove_action( 'create_term', array( $sitepress, 'create_term' ), 1 ); remove_action( 'edit_term', array( $sitepress, 'create_term' ), 1 ); add_action( 'create_term', array( $this, 'add_term_language_action' ), 1, 3 ); $new_term = false; if ( $this->is_valid ) { if ( $this->is_update && $this->term_id ) { $new_term = wp_update_term( $this->term_id, $this->taxonomy, $this->wp_new_term_args ); } else { $new_term = wp_insert_term( $this->wp_new_term_args['name'], $this->taxonomy, $this->wp_new_term_args ); } } add_action( 'create_term', array( $sitepress, 'create_term' ), 1, 3 ); add_action( 'edit_term', array( $sitepress, 'create_term' ), 1, 3 ); remove_action( 'create_term', array( $this, 'add_term_language_action' ), 1 ); if ( ! is_array( $new_term ) ) { $new_term = false; } unset( $switch_lang ); return $new_term; } /** * This action is to be hooked to the WP create_term and edit_term hooks. * It sets the correct language information after a term is saved. * * @param int|string $term_id * @param int|string $term_taxonomy_id * @param string $taxonomy */ public function add_term_language_action( $term_id, $term_taxonomy_id, $taxonomy ) { if ( $this->is_valid && ! $this->is_update && $this->taxonomy == $taxonomy ) { $this->sitepress->set_element_language_details( $term_taxonomy_id, 'tax_' . $taxonomy, $this->trid, $this->lang_code, $this->source_lang_code ); } } /** * Sets the language variables for this object. * * @param bool|int $trid * @param bool|int $original_tax_id * @param string $lang_code * @param bool|string $source_language * @return bool True if the given language parameters allowed for determining valid language information, false * otherwise. */ private function set_language_information( $trid, $original_tax_id, $lang_code, $source_language ) { if ( ! $lang_code || ! $this->sitepress->is_active_language( $lang_code ) ) { return false; } else { $this->lang_code = $lang_code; } if ( ! $trid && $original_tax_id ) { $trid = $this->sitepress->get_element_trid( $original_tax_id, 'tax_' . $this->taxonomy ); } if ( $trid ) { $this->trid = $trid; $this->existing_translations = $this->sitepress->get_element_translations( $trid, 'tax_' . $this->taxonomy ); foreach ( $this->existing_translations as $lang => $translation ) { if ( $original_tax_id && isset( $translation->element_id ) && $translation->element_id == $original_tax_id && isset( $translation->language_code ) && $translation->language_code ) { $this->source_lang_code = $translation->language_code; break; } elseif ( isset( $translation->language_code ) && $translation->language_code && ! $translation->source_language_code ) { $this->source_lang_code = $translation->language_code; } } } return true; } /** * Sets the action type of this object. * In case of this action being an update the is_update flag is set true. * Also the term_id of the existing term is saved in $this->term_id. */ private function set_action_type() { if ( ! $this->trid ) { $this->is_update = false; } elseif ( isset( $this->existing_translations[ $this->lang_code ] ) ) { $existing_db_entry = $this->existing_translations[ $this->lang_code ]; if ( isset( $existing_db_entry->element_id ) && $existing_db_entry->element_id ) { // Term update actions need information about the term_id, not the term_taxonomy_id saved in the element_id column of icl_translations. /** @var \stdClass $term */ $term = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT t.term_id, t.slug FROM {$this->wpdb->terms} AS t JOIN {$this->wpdb->term_taxonomy} AS tt ON t.term_id=tt.term_id WHERE term_taxonomy_id=%d", $existing_db_entry->element_id ) ); if ( $term->term_id && $term->slug ) { $this->is_update = true; $this->term_id = $term->term_id; $this->old_slug = $term->slug; } else { $this->is_update = false; } } else { $this->sitepress->delete_element_translation( $this->trid, 'tax_' . $this->taxonomy, $this->lang_code ); $this->is_update = false; } } else { $this->is_update = false; } } } taxonomy-term-translation/wpml-frontend-tax-filters.class.php 0000755 00000006324 14720425051 0020604 0 ustar 00 <?php class WPML_Frontend_Tax_Filters { public function __construct() { add_filter( 'taxonomy_template', array( $this, 'slug_template' ) ); add_filter( 'category_template', array( $this, 'slug_template' ) ); add_filter( 'tag_template', array( $this, 'slug_template' ) ); } /** * Adjust template (taxonomy-)$taxonomy-$term.php for translated term slugs and IDs * * @since 3.1 * * @param string $template * * @return string The template filename if found. */ function slug_template( $template ) { global $sitepress; $term = $this->get_queried_tax_term(); if ( $term === false || ! is_taxonomy_translated( $term->taxonomy ) ) { return $template; } $templates = array(); $has_filter = remove_filter( 'get_term', array( $sitepress, 'get_term_adjust_id' ), 1 ); $current_language = $sitepress->get_current_language(); $default_language = $sitepress->get_default_language(); $templates = $this->add_term_templates( $term, $current_language, $templates ); $templates = $this->add_original_term_templates( $term, $default_language, $current_language, $templates ); if ( ! in_array( $term->taxonomy, array( 'category', 'post_tag' ), true ) ) { $templates[] = 'taxonomy-' . $current_language . '.php'; $templates[] = 'taxonomy.php'; } if ( $has_filter ) { add_filter( 'get_term', array( $sitepress, 'get_term_adjust_id' ), 1, 1 ); } $new_template = locate_template( array_unique( $templates ) ); $template = $new_template ? $new_template : $template; return $template; } private function get_template_prefix( $taxonomy ) { $prefix = in_array( $taxonomy, array( 'category', 'post_tag' ), true ) ? '' : 'taxonomy-'; $prefix .= $taxonomy == 'post_tag' ? 'tag' : $taxonomy; return $prefix; } private function add_term_templates( $term, $current_language, $templates ) { $prefix = $this->get_template_prefix( $term->taxonomy ); $templates[] = "$prefix-{$current_language}-{$term->slug}.php"; $templates[] = "$prefix-{$current_language}-{$term->term_id}.php"; $templates[] = "$prefix-{$current_language}.php"; $templates[] = "$prefix-{$term->slug}.php"; $templates[] = "$prefix-{$term->term_id}.php"; return $templates; } private function add_original_term_templates( $term, $default_language, $current_language, $templates ) { $taxonomy = $term->taxonomy; $prefix = $this->get_template_prefix( $taxonomy ); $original_term_id = icl_object_id( $term->term_id, $taxonomy, true, $default_language ); $original_term = get_term_by( 'id', $original_term_id, $taxonomy ); if ( $original_term ) { $templates[] = "$prefix-{$current_language}-{$original_term->slug}.php"; $templates[] = "$prefix-{$current_language}-{$original_term_id}.php"; $templates[] = "$prefix-{$original_term->slug}.php"; $templates[] = "$prefix-{$original_term_id}.php"; $templates[] = "$prefix-{$current_language}.php"; $templates[] = "$prefix.php"; } return $templates; } private function get_queried_tax_term() { global $wp_query; /** @var WP_Query $wp_query */ $term = $wp_query->get_queried_object(); $res = false; if ( (bool) $term === true && isset( $term->taxonomy ) && $term->taxonomy ) { $res = $term; } return $res; } } taxonomy-term-translation/wpml-term-hierarchy-sync.class.php 0000755 00000002343 14720425051 0020417 0 ustar 00 <?php class WPML_Term_Hierarchy_Sync extends WPML_Hierarchy_Sync { protected $element_id_column = 'term_taxonomy_id'; protected $parent_id_column = 'parent'; protected $parent_element_id_column = 'term_id'; protected $element_type_column = 'taxonomy'; protected $element_type_prefix = 'tax_'; /** * @param wpdb $wpdb */ public function __construct( &$wpdb ) { parent::__construct( $wpdb ); $this->elements_table = $wpdb->term_taxonomy; } public function is_need_sync( $taxonomy, $ref_lang = false ) { return (bool) $this->get_unsynced_elements( $taxonomy, $ref_lang ); } public function sync_element_hierarchy( $element_types, $ref_lang = false ) { /** @var WPML_Term_Filters $wpml_term_filters_general */ global $wpml_term_filters_general; parent::sync_element_hierarchy( $element_types, $ref_lang ); do_action( 'wpml_sync_term_hierarchy_done' ); $element_types = (array) $element_types; foreach ( $element_types as $taxonomy ) { $wpml_term_filters_general->update_tax_children_option( $taxonomy ); } } /** * @param string $element_type * * @return bool */ public function is_hierarchical( $element_type ) { return is_taxonomy_hierarchical( $element_type ); } } core/wpml-tm-element-translations.class.php 0000755 00000017672 14720425051 0015074 0 ustar 00 <?php class WPML_TM_Element_Translations extends WPML_TM_Record_User { /** @var int[] $trid_cache */ private $trid_cache; /** @var int[] $job_id_cache */ private $job_id_cache; /** @var int[] $job_id_cache */ private $translation_status_cache; /** @var array $translation_review_status_cache */ private $translation_review_status_cache; /** @var bool[] $update_status_cache */ private $update_status_cache; /** @var string[] $element_type_prefix_cache */ private $element_type_prefix_cache = array(); public function init_hooks() { add_action( 'wpml_cache_clear', array( $this, 'reload' ) ); add_filter( 'wpml_tm_translation_status', array( $this, 'get_translation_status_filter', ), 10, 2 ); } public function reload() { $this->trid_cache = array(); $this->job_id_cache = array(); $this->translation_status_cache = array(); $this->translation_review_status_cache = array(); $this->update_status_cache = array(); $this->element_type_prefix_cache = array(); } public function is_update_needed( $trid, $language_code ) { if ( isset( $this->update_status_cache[ $trid ][ $language_code ] ) ) { $needs_update = $this->update_status_cache[ $trid ][ $language_code ]; } else { $this->init_job_id( $trid, $language_code ); $needs_update = isset( $this->update_status_cache[ $trid ][ $language_code ] ) ? $this->update_status_cache[ $trid ][ $language_code ] : 0; } return (bool) $needs_update; } /** * @param int $trid * @param string $language_code * * @return string */ public function get_element_type_prefix( $trid, $language_code ) { if ( $trid && $language_code && ! isset( $this->element_type_prefix_cache[ $trid ] ) ) { $this->init_job_id( $trid, $language_code ); } return $trid && array_key_exists( $trid, $this->element_type_prefix_cache ) ? $this->element_type_prefix_cache[ $trid ] : ''; } public function get_translation_status_filter( $empty, $args ) { $trid = $args['trid']; $language_code = $args['language_code']; return $this->get_translation_status( $trid, $language_code ); } /** * @param int $trid * @param string $language_code * * @return int */ public function get_translation_status( $trid, $language_code ) { if ( isset( $this->translation_status_cache[ $trid ][ $language_code ] ) ) { $status = $this->translation_status_cache[ $trid ][ $language_code ]; } else { $this->init_job_id( $trid, $language_code ); $status = isset( $this->translation_status_cache[ $trid ][ $language_code ] ) ? $this->translation_status_cache[ $trid ][ $language_code ] : 0; } return (int) $status; } /** * @param int $trid * @param string $language_code * * @return string|null */ public function get_translation_review_status( $trid, $language_code ) { if ( ! isset( $this->translation_review_status_cache[ $trid ][ $language_code ] ) ) { $this->init_job_id( $trid, $language_code ); } return isset( $this->translation_review_status_cache[ $trid ][ $language_code ] ) ? $this->translation_review_status_cache[ $trid ][ $language_code ] : null; } public function init_job_id( $trid, $target_lang_code ) { global $wpdb, $wpml_language_resolution; if ( ! isset( $this->job_id_cache[ $trid ][ $target_lang_code ] ) ) { $jobs = $wpdb->get_results( $wpdb->prepare( $this->sql_select_for_statuses( 't.trid = %d' ), $trid ) ); $active_langs = $wpml_language_resolution->get_active_language_codes(); foreach ( $active_langs as $lang_code ) { $this->cache_job_in_lang( $jobs, $lang_code, $trid ); } } } public function init_jobs( $trids ) { if ( ! is_array( $trids ) || empty( $trids ) ) { return; } global $wpdb, $wpml_language_resolution; $trids = array_map( 'absint', $trids ); $trids = array_unique( $trids ); $list_of_trids = implode( ',', $trids ); $jobs = $wpdb->get_results( $this->sql_select_for_statuses( "t.trid IN ($list_of_trids)" ) ); $active_langs = $wpml_language_resolution->get_active_language_codes(); $jobs_per_trid = []; foreach( $jobs as $job ) { if ( ! array_key_exists( $job->trid, $jobs_per_trid ) ) { $jobs_per_trid[ $job->trid ] = []; } $jobs_per_trid[ $job->trid ][] = $job; } foreach ( $jobs_per_trid as $trid => $trid_jobs ) { foreach ( $active_langs as $lang_code ) { $this->cache_job_in_lang( $trid_jobs, $lang_code, $trid ); } } } /** * @param string $where Required: "SELECT...WHERE $where". * * @return string */ private function sql_select_for_statuses( $where ) { global $wpdb; return "SELECT t.trid, tj.job_id, ts.status, ts.review_status, ts.needs_update, t.language_code, SUBSTRING_INDEX(t.element_type, '_', 1) AS element_type_prefix FROM {$wpdb->prefix}icl_translate_job tj JOIN {$wpdb->prefix}icl_translation_status ts ON tj.rid = ts.rid JOIN {$wpdb->prefix}icl_translations t ON ts.translation_id = t.translation_id WHERE $where"; } /** * @param object[] $jobs * @param string $lang * @param int|string $trid * * @return false|object */ private function cache_job_in_lang( $jobs, $lang, $trid ) { $res = false; foreach ( $jobs as $job ) { if ( $job->language_code === $lang ) { $res = $job; break; } } if ( (bool) $res === true ) { $job_id = $res->job_id; $status = $res->status; $review_status = $res->review_status; $needs_update = (bool) $res->needs_update; $element_type_prefix = $res->element_type_prefix; } else { $job_id = - 1; $status = 0; $review_status = null; $needs_update = false; $element_type_prefix = $this->fallback_type_prefix( $trid ); } $this->cache_job( (int) $trid, $lang, $job_id, $status, $review_status, $needs_update, $element_type_prefix ); return $res; } private function fallback_type_prefix( $trid ) { global $wpdb; if ( isset( $this->element_type_prefix_cache[ $trid ] ) && (bool) $this->element_type_prefix_cache[ $trid ] === true ) { $prefix = $this->element_type_prefix_cache[ $trid ]; } elseif ( (bool) $this->tm_records->get_post_translations()->get_element_translations( null, $trid ) ) { $prefix = 'post'; } else { $prefix = $wpdb->get_var( $wpdb->prepare( "SELECT SUBSTRING_INDEX(element_type, '_', 1) FROM {$wpdb->prefix}icl_translations WHERE trid = %d LIMIT 1", $trid ) ); } return $prefix; } /** * @param int $trid * @param string $language_code * @param int $job_id * @param int $status * @param ?string $review_status * @param bool $needs_update * @param string $element_type_prefix */ private function cache_job( $trid, $language_code, $job_id, $status, $review_status, $needs_update, $element_type_prefix ) { if ( (bool) $job_id === true && (bool) $trid === true && (bool) $language_code === true ) { $this->maybe_init_trid_cache( $trid ); $this->job_id_cache[ $trid ][ $language_code ] = $job_id; $this->translation_status_cache[ $trid ][ $language_code ] = $status; $this->translation_review_status_cache[ $trid ][ $language_code ] = $review_status; $this->update_status_cache[ $trid ][ $language_code ] = $needs_update; $this->element_type_prefix_cache[ $trid ] = isset( $this->element_type_prefix_cache[ $trid ] ) && (bool) $this->element_type_prefix_cache[ $trid ] === true ? $this->element_type_prefix_cache[ $trid ] : $element_type_prefix; } } private function maybe_init_trid_cache( $trid ) { foreach ( array( &$this->job_id_cache, &$this->trid_cache, &$this->translation_status_cache, &$this->translation_review_status_cache, &$this->update_status_cache, ) as $cache ) { $cache[ $trid ] = isset( $cache[ $trid ] ) ? $cache[ $trid ] : array(); } } } functions-load-tm.php 0000755 00000064117 14720425051 0010634 0 ustar 00 <?php use WPML\TM\Editor\ClassicEditorActions; use WPML\TM\Jobs\Query\CompositeQuery; use WPML\TM\Jobs\Query\LimitQueryHelper; use WPML\TM\Jobs\Query\OrderQueryHelper; use WPML\TM\Jobs\Query\PackageQuery; use WPML\TM\Jobs\Query\PostQuery; use WPML\TM\Jobs\Query\QueryBuilder; use WPML\TM\Jobs\Query\StringQuery; use WPML\TM\Jobs\Query\StringsBatchQuery; use WPML\FP\Obj; use function WPML\Container\make; use \WPML\Setup\Option as SetupOptions; use WPML\TM\ATE\TranslateEverything\TranslatableData\Calculate; if ( ! \WPML\Plugins::isTMActive() && ( ! wpml_is_setup_complete() || false !== SetupOptions::isTMAllowed() ) ) { /** * @return WPML_TM_Element_Translations */ function wpml_tm_load_element_translations() { global $wpml_tm_element_translations, $wpdb, $wpml_post_translations, $wpml_term_translations; if ( ! isset( $wpml_tm_element_translations ) && defined( 'WPML_TM_PATH' ) ) { require_once WPML_TM_PATH . '/inc/core/wpml-tm-element-translations.class.php'; $tm_records = new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); $wpml_tm_element_translations = new WPML_TM_Element_Translations( $tm_records ); $wpml_tm_element_translations->init_hooks(); } return $wpml_tm_element_translations; } function wpml_tm_load_status_display_filter() { global $wpml_tm_status_display_filter, $iclTranslationManagement, $sitepress, $wpdb; $blog_translators = wpml_tm_load_blog_translators(); $tm_api = new WPML_TM_API( $blog_translators, $iclTranslationManagement ); $tm_api->init_hooks(); if ( ! isset( $wpml_tm_status_display_filter ) ) { $status_helper = wpml_get_post_status_helper(); $job_factory = wpml_tm_load_job_factory(); $wpml_tm_status_display_filter = new WPML_TM_Translation_Status_Display( $wpdb, $sitepress, $status_helper, $job_factory, $tm_api ); } $wpml_tm_status_display_filter->init(); } /** * @depecated since WPML 4.5.0 * * @return \WPML_TM_Page_Builders_Hooks */ function wpml_tm_page_builders_hooks() { static $page_builder_hooks; if ( ! $page_builder_hooks ) { global $sitepress; $page_builder_hooks = new WPML_TM_Page_Builders_Hooks( null, $sitepress ); } return $page_builder_hooks; } /** * @return \WPML_Custom_XML_Factory */ function wpml_tm_custom_xml_factory() { static $tm_custom_xml_factory; if ( ! $tm_custom_xml_factory ) { $tm_custom_xml_factory = new WPML_Custom_XML_Factory(); } return $tm_custom_xml_factory; } /** * @return \WPML_Custom_XML_UI_Hooks */ function wpml_tm_custom_xml_ui_hooks() { static $tm_custom_xml_ui_hooks; if ( ! $tm_custom_xml_ui_hooks ) { global $sitepress; $factory = wpml_tm_custom_xml_factory(); if ( $factory ) { $tm_custom_xml_ui_hooks = new WPML_Custom_XML_UI_Hooks( $factory->create_resources( $sitepress->get_wp_api() ) ); } } return $tm_custom_xml_ui_hooks; } /** * @return \WPML_UI_Screen_Options_Factory */ function wpml_ui_screen_options_factory() { static $screen_options_factory; if ( ! $screen_options_factory ) { global $sitepress; $screen_options_factory = new WPML_UI_Screen_Options_Factory( $sitepress ); } return $screen_options_factory; } /** * @return \WPML_TM_Loader */ function wpml_tm_loader() { static $tm_loader; if ( ! $tm_loader ) { $tm_loader = new WPML_TM_Loader(); } return $tm_loader; } /** * @return \WPML_TP_Translator */ function wpml_tm_translator() { static $tm_translator; if ( ! $tm_translator ) { $tm_translator = new WPML_TP_Translator(); } return $tm_translator; } /** * It returns a single instance of \WPML_Translation_Management. * * @return \WPML_Translation_Management */ function wpml_translation_management() { global $WPML_Translation_Management; if ( ! $WPML_Translation_Management ) { global $sitepress; $WPML_Translation_Management = new WPML_Translation_Management( $sitepress, wpml_tm_loader(), wpml_load_core_tm(), wpml_tm_translator() ); } return $WPML_Translation_Management; } /** * @return \WPML_Translation_Basket */ function wpml_translation_basket() { static $translation_basket; if ( ! $translation_basket ) { global $wpdb; $translation_basket = new WPML_Translation_Basket( $wpdb ); } return $translation_basket; } /** * @return \WPML_TM_Translate_Independently */ function wpml_tm_translate_independently() { static $translate_independently; if ( ! $translate_independently ) { global $sitepress; $translate_independently = new WPML_TM_Translate_Independently( wpml_load_core_tm(), wpml_translation_basket(), $sitepress ); } return $translate_independently; } /** * @return WPML_Translation_Proxy_Basket_Networking */ function wpml_tm_load_basket_networking() { global $iclTranslationManagement, $wpdb; if ( ! defined( 'WPML_TM_PATH' ) ) { return null; } require_once WPML_TM_PATH . '/inc/translation-proxy/wpml-translationproxy-basket-networking.class.php'; $basket = new WPML_Translation_Basket( $wpdb ); return new WPML_Translation_Proxy_Basket_Networking( $basket, $iclTranslationManagement ); } /** * @return WPML_Translation_Proxy_Networking */ function wpml_tm_load_tp_networking() { global $wpml_tm_tp_networking; if ( ! isset( $wpml_tm_tp_networking ) ) { $tp_lock_factory = new WPML_TP_Lock_Factory(); $wpml_tm_tp_networking = new WPML_Translation_Proxy_Networking( new WP_Http(), $tp_lock_factory->create() ); } return $wpml_tm_tp_networking; } /** * @return WPML_TM_Blog_Translators */ function wpml_tm_load_blog_translators() { global $wpdb, $sitepress, $wpml_post_translations, $wpml_term_translations, $wpml_cache_factory; static $instance; if ( ! $instance ) { $tm_records = new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); $translator_records = new WPML_Translator_Records( $wpdb, new WPML_WP_User_Query_Factory(), wp_roles() ); $instance = new WPML_TM_Blog_Translators( $sitepress, $tm_records, $translator_records, $wpml_cache_factory ); } return $instance; } /** * @return WPML_TM_Translators_Dropdown */ function wpml_tm_get_translators_dropdown() { static $instance; if ( ! $instance ) { $instance = new WPML_TM_Translators_Dropdown( wpml_tm_load_blog_translators() ); } return $instance; } /** * @return WPML_TM_Mail_Notification */ function wpml_tm_init_mail_notifications() { global $wpml_tm_mailer, $sitepress, $wpdb, $iclTranslationManagement, $wp_api; if ( null === $wp_api ) { $wp_api = new WPML_WP_API(); } if ( is_admin() && defined( 'WPML_TM_PATH' ) ) { $blog_translators = wpml_tm_load_blog_translators(); $email_twig_factory = new WPML_TM_Email_Twig_Template_Factory(); $batch_report = new WPML_TM_Batch_Report( $blog_translators ); $batch_report_email_template = new WPML_TM_Email_Jobs_Summary_View( $email_twig_factory->create(), $blog_translators, $sitepress ); $batch_report_email_builder = new WPML_TM_Batch_Report_Email_Builder( $batch_report, $batch_report_email_template ); $batch_report_email_process = new WPML_TM_Batch_Report_Email_Process( $batch_report, $batch_report_email_builder ); $batch_report_hooks = new WPML_TM_Batch_Report_Hooks( $batch_report, $batch_report_email_process ); $batch_report_hooks->add_hooks(); $wpml_tm_unsent_jobs = new WPML_TM_Unsent_Jobs( $blog_translators, $sitepress ); $wpml_tm_unsent_jobs->add_hooks(); $wpml_tm_unsent_jobs_notice = new WPML_TM_Unsent_Jobs_Notice( $wp_api ); $wpml_tm_unsent_jobs_notice_hooks = new WPML_TM_Unsent_Jobs_Notice_Hooks( $wpml_tm_unsent_jobs_notice, $wp_api, WPML_Notices::DISMISSED_OPTION_KEY ); $wpml_tm_unsent_jobs_notice_hooks->add_hooks(); $user_jobs_notification_settings = new WPML_User_Jobs_Notification_Settings(); $user_jobs_notification_settings->add_hooks(); $email_twig_factory = new WPML_Twig_Template_Loader( array( WPML_TM_PATH . '/templates/user-profile/' ) ); $notification_template = new WPML_User_Jobs_Notification_Settings_Template( $email_twig_factory->get_template() ); $user_jobs_notification_settings_render = new WPML_User_Jobs_Notification_Settings_Render( $notification_template ); $user_jobs_notification_settings_render->add_hooks(); } if ( ! isset( $wpml_tm_mailer ) ) { $iclTranslationManagement = $iclTranslationManagement ? $iclTranslationManagement : wpml_load_core_tm(); if ( empty( $iclTranslationManagement->settings ) ) { $iclTranslationManagement->init(); } $settings = isset( $iclTranslationManagement->settings['notification'] ) ? $iclTranslationManagement->settings['notification'] : array(); $email_twig_factory = new WPML_TM_Email_Twig_Template_Factory(); $email_notification_view = new WPML_TM_Email_Notification_View( $email_twig_factory->create() ); $has_active_remote_service = TranslationProxy::is_current_service_active_and_authenticated(); $wpml_tm_mailer = new WPML_TM_Mail_Notification( $sitepress, $wpdb, wpml_tm_load_job_factory(), $email_notification_view, $settings, $has_active_remote_service ); } $wpml_tm_mailer->init(); return $wpml_tm_mailer; } /** * It returns a single instance of the class. * * @return WPML_Dashboard_Ajax */ function wpml_tm_load_tm_dashboard_ajax() { global $wpml_tm_dashboard_ajax, $sitepress; if ( ! isset( $wpml_tm_dashboard_ajax ) && defined( 'WPML_TM_PATH' ) ) { require_once WPML_TM_PATH . '/menu/dashboard/wpml-tm-dashboard-ajax.class.php'; $wpml_tm_dashboard_ajax = new WPML_Dashboard_Ajax( ); if ( defined( 'OTG_TRANSLATION_PROXY_URL' ) && defined( 'ICL_SITEPRESS_VERSION' ) ) { $wpml_tp_api = wpml_tm_get_tp_project_api(); $wpml_tp_api_ajax = new WPML_TP_Refresh_Language_Pairs( $wpml_tp_api ); $wpml_tp_api_ajax->add_hooks(); $sync_jobs_ajax_handler = new WPML_TP_Sync_Ajax_Handler( wpml_tm_get_tp_sync_jobs(), new WPML_TM_Last_Picked_Up( $sitepress ) ); $sync_jobs_ajax_handler->add_hooks(); } } return $wpml_tm_dashboard_ajax; } function wpml_tm_load_and_intialize_dashboard_ajax() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { if ( defined( 'DOING_AJAX' ) ) { wpml_tm_load_tm_dashboard_ajax(); } elseif ( defined( 'WPML_TM_FOLDER' ) && is_admin() && isset( $_GET['page'] ) && WPML_TM_FOLDER . '/menu/main.php' === $_GET['page'] && ( ! isset( $_GET['sm'] ) || $_GET['sm'] === 'dashboard' ) ) { $wpml_tm_dashboard_ajax = wpml_tm_load_tm_dashboard_ajax(); add_action( 'wpml_tm_scripts_enqueued', array( $wpml_tm_dashboard_ajax, 'enqueue_js' ) ); } } } add_action( 'plugins_loaded', 'wpml_tm_load_and_intialize_dashboard_ajax' ); /** * It returns a single instance of the class. * * @return WPML_Translation_Job_Factory */ function wpml_tm_load_job_factory() { global $wpml_translation_job_factory, $wpdb, $wpml_post_translations, $wpml_term_translations; if ( ! $wpml_translation_job_factory ) { $tm_records = new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); $wpml_translation_job_factory = new WPML_Translation_Job_Factory( $tm_records ); $wpml_translation_job_factory->init_hooks(); } return $wpml_translation_job_factory; } /** * It returns a single instance of the class. * * @return WPML_TM_XLIFF_Factory */ function wpml_tm_xliff_factory() { static $xliff_factory; if ( ! $xliff_factory ) { $xliff_factory = new WPML_TM_XLIFF_Factory(); } return $xliff_factory; } /** * It returns a single instance of the class. * * @return WPML_TM_XLIFF_Shortcodes */ function wpml_tm_xliff_shortcodes() { static $xliff_shortcodes; if ( ! $xliff_shortcodes ) { $xliff_shortcodes = new WPML_TM_XLIFF_Shortcodes(); } return $xliff_shortcodes; } /** * It returns an instance of the class. * * @return \WPML_TM_Old_Jobs_Editor */ function wpml_tm_load_old_jobs_editor() { static $instance; if ( ! $instance ) { $instance = new WPML_TM_Old_Jobs_Editor( wpml_tm_load_job_factory() ); } return $instance; } function tm_after_load() { global $wpml_tm_translation_status, $wpdb, $wpml_post_translations, $wpml_term_translations; if ( ! isset( $wpml_tm_translation_status ) && defined( 'WPML_TM_PATH' ) ) { require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy.class.php'; require_once WPML_TM_PATH . '/inc/ajax.php'; ( new ClassicEditorActions() )->addHooks(); wpml_tm_load_job_factory(); wpml_tm_init_mail_notifications(); wpml_tm_load_element_translations(); $wpml_tm_translation_status = make( WPML_TM_Translation_Status::class ); $wpml_tm_translation_status->init(); add_action( 'wpml_pre_status_icon_display', 'wpml_tm_load_status_display_filter' ); require_once WPML_TM_PATH . '/inc/wpml-private-actions-tm.php'; } } /** * It returns an instance of the class. * * @return WPML_TM_Records */ function wpml_tm_get_records() { global $wpdb, $wpml_post_translations, $wpml_term_translations; return new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); } /** * It returns an instance of the class. * * @return WPML_TM_Xliff_Frontend */ function setup_xliff_frontend() { global $xliff_frontend; $xliff_factory = new WPML_TM_XLIFF_Factory(); $xliff_frontend = $xliff_factory->create_frontend(); add_action( 'init', array( $xliff_frontend, 'init' ), $xliff_frontend->get_init_priority() ); return $xliff_frontend; } /** * It returns an instance of the class. * * @param int $job_id The ID of the job. * @param bool $applyTranslationMemoryForCompletedJobs * * @return WPML_TM_ATE_Models_Job_Create */ function wpml_tm_create_ATE_job_creation_model( $job_id, $applyTranslationMemoryForCompletedJobs = true ) { $job_factory = wpml_tm_load_job_factory(); $translation_job = $job_factory->get_translation_job( $job_id, false, 0, true ); $rid = \WPML\TM\API\Job\Map::fromJobId( $job_id ); $job = new WPML_TM_ATE_Models_Job_Create(); $job->id = $job_id; $job->source_id = $rid; $previousStatus = \WPML_TM_ICL_Translation_Status::makeByRid( $rid )->previous(); if ( $previousStatus->map( Obj::prop( 'status' ) )->getOrElse( null ) === (string) ICL_TM_ATE_CANCELLED ) { wpml_tm_load_job_factory()->update_job_data( $job_id, array( 'editor' => WPML_TM_Editors::ATE ) ); $job->existing_ate_id = make( \WPML\TM\ATE\JobRecords::class )->get_ate_job_id( $job_id ); } else { /** * We have to use the previous state because in this place the job has already changed its status from COMPLETED to IN PROGRESS. */ $apply_memory = (bool) $previousStatus->map( function ( $job ) use ( $applyTranslationMemoryForCompletedJobs ) { $result = (int) Obj::prop( 'status', $job ) === ICL_TM_COMPLETE && ! Obj::prop( 'needs_update', $job ) ? $applyTranslationMemoryForCompletedJobs : true; // I have to cast it to int because if I return bool FALSE, Maybe internal mechanism treats it as nullable and default value from `getOrElse` is returned instead of $result. return (int) $result; } )->getOrElse( $applyTranslationMemoryForCompletedJobs ); $job->source_language->code = $translation_job->get_source_language_code(); $job->source_language->name = $translation_job->get_source_language_code( true ); $job->target_language->code = $translation_job->get_language_code(); $job->target_language->name = $translation_job->get_language_code( true ); $job->deadline = strtotime( $translation_job->get_deadline_date() ); $job->apply_memory = $apply_memory; /* * wpmldev-1840 * * With wpmldev-1730 WPML estimates the credits, which the site will * require by fetching post content, title and excerpt. * In the future this estimation should happen on ATE, but for that * they need to get the WPML calculated chars per job to compare * with the real costs for the translation. * Once ATE is providing the calculation and does no longer need * the `wpml_chars_count` parameter, the following block * until "END" can be deleted. * * Also the property "wpml_chars_count" can be removed from * ./classes/ATE/models/class-wpml-tm-ate-models-job-create.php */ $calculate = new Calculate(); $fields = $translation_job->get_original_fields(); foreach ( $fields as $key => $value ) { if ( ! empty( $value ) && in_array( $key, [ 'title', 'body', 'excerpt' ], true ) ) { $job->wpml_chars_count += $calculate->chars( $value ); } } /* END */ $job->permalink = '#'; if ( 'Post' === $translation_job->get_type() ) { $job->permalink = get_permalink( $translation_job->get_original_element_id() ); } $job->notify_enabled = true; $job->notify_url = \WPML\TM\ATE\REST\PublicReceive::get_receive_ate_job_url( $job_id ); $job->site_identifier = wpml_get_site_id( WPML_TM_ATE::SITE_ID_SCOPE ); $encoded_xliff = base64_encode( wpml_tm_get_job_xliff( $job_id, $apply_memory ) ); $job->file->type = 'data:application/x-xliff;base64'; $job->file->name = $translation_job->get_title(); $job->file->content = $encoded_xliff; } return $job; } /** * It returns a single instance of the class. * * @param int $job_id The ID of the job. * @param bool $apply_memory * * @return string */ function wpml_tm_get_job_xliff( $job_id, $apply_memory = true ) { static $xliff_writer; if ( ! $xliff_writer ) { $job_factory = wpml_tm_load_job_factory(); $xliff_writer = new WPML_TM_Xliff_Writer( $job_factory ); } return $xliff_writer->generate_job_xliff( $job_id, $apply_memory ); } /** * It returns a single instance of the class. * * @return \WPML_Rest */ function wpml_tm_get_wpml_rest() { static $wpml_rest; if ( ! $wpml_rest ) { $http = new WP_Http(); $wpml_rest = new WPML_Rest( $http ); } return $wpml_rest; } /** * It returns a single instance of the class. * * @return \WPML_TP_API_Client */ function wpml_tm_get_tp_api_client() { static $client; if ( ! $client ) { $client = new WPML_TP_API_Client( OTG_TRANSLATION_PROXY_URL, new WP_Http(), new WPML_TP_Lock( new WPML_WP_API() ), new WPML_TP_HTTP_Request_Filter() ); } return $client; } /** * It returns a single instance of the class. * * @return \WPML_TP_Project */ function wpml_tm_get_tp_project() { static $project; if ( ! $project ) { global $sitepress; $translation_service = $sitepress->get_setting( 'translation_service' ); $translation_projects = $sitepress->get_setting( 'icl_translation_projects' ); $project = new WPML_TP_Project( $translation_service, $translation_projects ); } return $project; } /** * It returns a single instance of the class. * * @return \WPML_TP_Jobs_API */ function wpml_tm_get_tp_jobs_api() { static $api; if ( ! $api ) { $api = new WPML_TP_Jobs_API( wpml_tm_get_tp_api_client(), wpml_tm_get_tp_project(), new WPML_TM_Log() ); } return $api; } /** * It returns a single instance of the class. * * @return \WPML_TP_Project_API */ function wpml_tm_get_tp_project_api() { static $api; if ( ! $api ) { $api = new WPML_TP_Project_API( wpml_tm_get_tp_api_client(), wpml_tm_get_tp_project(), new WPML_TM_Log() ); } return $api; } /** * It returns a single instance of the class. * * @return \WPML_TP_XLIFF_API */ function wpml_tm_get_tp_xliff_api() { static $api; if ( ! $api ) { $api = new WPML_TP_XLIFF_API( wpml_tm_get_tp_api_client(), wpml_tm_get_tp_project(), new WPML_TM_Log(), new WPML_TP_Xliff_Parser( new \WPML_TM_Validate_HTML() ), wpml_tm_get_tp_jobs_api() ); } return $api; } /** * It returns a single instance of the class. * * @param bool $forceReload * @param bool $loadObsoleteStringQuery * @param bool $dontCache * * @return \WPML_TM_Jobs_Repository */ function wpml_tm_get_jobs_repository( $forceReload = false, $loadObsoleteStringQuery = true, $dontCache = false ) { static $repository; if ( ! $repository || $forceReload ) { global $wpdb; $limit_helper = new LimitQueryHelper(); $order_helper = new OrderQueryHelper(); $subqueries = array( new PostQuery( $wpdb, new QueryBuilder( $limit_helper, $order_helper ) ), ); if ( defined( 'WPML_ST_VERSION' ) && get_option( 'wpml-package-translation-db-updates-run' ) ) { $subqueries[] = new PackageQuery( $wpdb, new QueryBuilder( $limit_helper, $order_helper ) ); if ( $loadObsoleteStringQuery ) { $subqueries[] = new StringQuery( $wpdb, new QueryBuilder( $limit_helper, $order_helper ) ); } $subqueries[] = new StringsBatchQuery( $wpdb, new QueryBuilder( $limit_helper, $order_helper ) ); } $result = new WPML_TM_Jobs_Repository( $wpdb, new CompositeQuery( $subqueries, $limit_helper, $order_helper ), new WPML_TM_Job_Elements_Repository( $wpdb ) ); if ( $dontCache ) { return $result; } else { $repository = $result; } } return $repository; } function wpml_tm_reload_jobs_repository() { wpml_tm_get_jobs_repository( true ); } /** * Reloading Jobs Repository when WPML String Translation finishes setup. */ add_action( 'wpml_register_string_packages', 'wpml_tm_reload_jobs_repository' ); /** * It returns an instance of the class. * * @return WPML_TM_ATE_Job_Repository */ function wpml_tm_get_ate_jobs_repository() { static $instance; if ( ! $instance ) { return new WPML_TM_ATE_Job_Repository( wpml_tm_get_jobs_repository(), new \WPML\TM\ATE\Jobs() ); } return $instance; } /** * @return \WPML\TM\ATE\JobRecords */ function wpml_tm_get_ate_job_records() { global $wpdb; static $instance; if ( ! $instance ) { $instance = new WPML\TM\ATE\JobRecords( $wpdb ); } return $instance; } /** * It returns a single instance of the class. * * @return \WPML_TP_Sync_Jobs */ function wpml_tm_get_tp_sync_jobs() { static $sync_jobs; if ( ! $sync_jobs ) { global $wpdb, $sitepress; $sync_jobs = new WPML_TP_Sync_Jobs( new WPML_TM_Sync_Jobs_Status( wpml_tm_get_jobs_repository(), wpml_tm_get_tp_jobs_api() ), new WPML_TM_Sync_Jobs_Revision( wpml_tm_get_jobs_repository(), wpml_tm_get_tp_jobs_api() ), new WPML_TP_Sync_Update_Job( $wpdb, $sitepress ) ); } return $sync_jobs; } /** * It returns a single instance of the class. * * @return \WPML_TP_Translations_Repository */ function wpml_tm_get_tp_translations_repository() { static $repository; if ( ! $repository ) { $repository = new WPML_TP_Translations_Repository( wpml_tm_get_tp_xliff_api(), wpml_tm_get_jobs_repository() ); } return $repository; } /** * It returns a single instance of the class. * * @return \WPML_WP_User_Query_Factory */ function wpml_tm_get_wp_user_query_factory() { static $wp_user_query_factory; if ( ! $wp_user_query_factory ) { $wp_user_query_factory = new WPML_WP_User_Query_Factory(); } return $wp_user_query_factory; } /** * It returns a single instance of the class. * * @return \WPML_WP_User_Factory */ function wpml_tm_get_wp_user_factory() { static $wp_user_factory; if ( ! $wp_user_factory ) { $wp_user_factory = new WPML_WP_User_Factory(); } return $wp_user_factory; } /** * It returns a single instance of the class. * * @return \WPML_TM_Email_Twig_Template_Factory */ function wpml_tm_get_email_twig_template_factory() { static $email_twig_template_factory; if ( ! $email_twig_template_factory ) { $email_twig_template_factory = new WPML_TM_Email_Twig_Template_Factory(); } return $email_twig_template_factory; } /** * It returns a single instance of the class. * * @return \WPML_TM_AMS_ATE_Factories */ function wpml_tm_ams_ate_factories() { static $tm_ams_ate_factories; if ( ! $tm_ams_ate_factories ) { $tm_ams_ate_factories = new WPML_TM_AMS_ATE_Factories(); } return $tm_ams_ate_factories; } /** * @return string * @throws \Auryn\InjectionException */ function wpml_tm_get_ams_ate_console_url() { /** @var WPML_TM_Admin_Sections $admin_sections */ $admin_sections = WPML\Container\make( 'WPML_TM_Admin_Sections' ); return $admin_sections->get_item_url( WPML_TM_AMS_ATE_Console_Section::SLUG ); } /** * @param \WPML\TM\ATE\Log\Entry $entry * @param bool $avoidDuplication * * @throws \WPML\Auryn\InjectionException */ function wpml_tm_ate_ams_log( WPML\TM\ATE\Log\Entry $entry, $avoidDuplication = false ) { make( WPML\TM\ATE\Log\Storage::class )->add( $entry, $avoidDuplication ); } /** * @param \WPML\TM\ATE\Log\Entry $entry * * @throws \WPML\Auryn\InjectionException */ function wpml_tm_ate_ams_log_remove( WPML\TM\ATE\Log\Entry $entry ) { make( WPML\TM\ATE\Log\Storage::class )->remove( $entry ); } /** * @param string $original * @param string $translation * @param bool $finished_state * * @return WPML_TM_Translated_Field */ function wpml_tm_create_translated_field( $original, $translation, $finished_state ) { return new WPML_TM_Translated_Field( $original, $translation, $finished_state ); } /** * @param int $post_id * @param \WP_Post $post * @param bool $force_set_status */ function wpml_tm_save_post( $post_id, $post, $force_set_status = false ) { global $wpdb, $wpml_post_translations, $wpml_term_translations; if ( false === $force_set_status && get_post_meta( $post_id, '_icl_lang_duplicate_of', true ) ) { $force_set_status = ICL_TM_DUPLICATE; } $action_helper = new WPML_TM_Action_Helper(); $blog_translators = wpml_tm_load_blog_translators(); $tm_records = new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); $save_post_action = new WPML_TM_Post_Actions( $action_helper, $blog_translators, $tm_records ); if ( 'revision' === $post->post_type || 'auto-draft' === $post->post_status || isset( $_POST['autosave'] ) ) { return; } $save_post_action->save_post_actions( $post_id, $post, $force_set_status ); } add_action( 'wpml_tm_save_post', 'wpml_tm_save_post', 10, 3 ); } wp-nav-menus/class-wpml-nav-menu.php 0000755 00000076044 14720425051 0013436 0 ustar 00 <?php use WPML\API\Sanitize; class WPML_Nav_Menu { private $current_menu; private $current_lang; /** @var WPML_Term_Translation $term_translations */ protected $term_translations; /** @var WPML_Post_Translation $post_translations */ protected $post_translations; /** @var SitePress $sitepress */ protected $sitepress; /** @var wpdb $wpdb */ public $wpdb; /** @var WPML_Nav_Menu_Actions $nav_menu_actions */ public $nav_menu_actions; function __construct( SitePress $sitepress, wpdb $wpdb, WPML_Post_Translation $post_translations, WPML_Term_Translation $term_translations ) { $this->sitepress = $sitepress; $this->wpdb = $wpdb; $this->post_translations = $post_translations; $this->term_translations = $term_translations; $this->nav_menu_actions = new WPML_Nav_Menu_Actions( $sitepress, $wpdb, $post_translations, $term_translations ); } public function init_hooks() { if ( is_admin() ) { add_filter( 'option_nav_menu_options', array( $this, 'option_nav_menu_options' ) ); add_filter( 'wp_get_nav_menus', array( $this, 'wp_get_nav_menus_filter' ) ); } if ( $this->must_filter_menus() ) { add_filter( 'get_terms', array( $this, 'get_terms_filter' ), 1, 3 ); } add_action( 'init', array( $this, 'init' ) ); add_filter( 'wp_nav_menu_args', array( $this, 'wp_nav_menu_args_filter' ) ); add_filter( 'wp_nav_menu_items', array( $this, 'wp_nav_menu_items_filter' ) ); add_filter( 'nav_menu_meta_box_object', array( $this, '_enable_sitepress_query_filters' ), 11 ); } /** * @return bool */ private function must_filter_menus() { global $pagenow; return 'nav-menus.php' === $pagenow || 'widgets.php' === $pagenow || filter_input( INPUT_POST, 'action' ) === 'save-widget'; } function init() { /** @var WPML_Request $wpml_request_handler */ /** @var WPML_Language_Resolution $wpml_language_resolution */ global $sitepress, $sitepress_settings, $pagenow, $wpml_request_handler, $wpml_language_resolution; $this->adjust_current_language_if_required(); $default_language = $sitepress->get_default_language(); // add language controls for menus no option but javascript if ( $pagenow === 'nav-menus.php' ) { add_action( 'admin_footer', array( $this, 'nav_menu_language_controls' ), 10 ); wp_enqueue_script( 'wp_nav_menus', ICL_PLUGIN_URL . '/res/js/wp-nav-menus.js', array( 'jquery' ), ICL_SITEPRESS_VERSION, true ); wp_enqueue_style( 'wp_nav_menus_css', ICL_PLUGIN_URL . '/res/css/wp-nav-menus.css', array(), ICL_SITEPRESS_VERSION, 'all' ); // filter posts by language add_action( 'parse_query', array( $this, 'action_parse_query' ) ); } if ( is_admin() ) { $this->_set_menus_language(); $this->get_current_menu(); } if ( isset( $_POST['action'] ) && $_POST['action'] === 'menu-get-metabox' && (bool) ( $lang = $wpml_language_resolution->get_referrer_language_code() ) !== false ) { $sitepress->switch_lang( $lang ); } if ( isset( $this->current_menu['language'] ) && isset( $this->current_menu['id'] ) && $this->current_menu['id'] && $this->current_menu['language'] && $this->current_menu['language'] != $default_language && isset( $_GET['menu'] ) && empty( $_GET['lang'] ) ) { wp_redirect( admin_url( sprintf( 'nav-menus.php?menu=%d&lang=%s', $this->current_menu['id'], $this->current_menu['language'] ) ) ); } $this->current_lang = $wpml_request_handler->get_requested_lang(); if ( isset( $_POST['icl_wp_nav_menu_ajax'] ) ) { $this->ajax( $_POST ); } // for theme locations that are not translated into the current language // reflect status in the theme location navigation switcher add_action( 'admin_footer', array( $this, '_set_custom_status_in_theme_location_switcher' ) ); // filter menu by language when adjust ids is off // not on ajax calls if ( ! $sitepress_settings['auto_adjust_ids'] && ! defined( 'DOING_AJAX' ) ) { add_filter( 'get_term', array( $sitepress, 'get_term_adjust_id' ), 1, 1 ); } $this->setup_menu_item(); if ( $this->sitepress->get_wp_api()->is_core_page( 'menu-sync/menus-sync.php' ) ) { $this->setup_menu_synchronization(); } add_action( 'wp_ajax_icl_msync_confirm', array( $this, 'sync_menus_via_ajax' ) ); add_action( 'wp_ajax_wpml_get_links_for_menu_strings_translation', array( $this, 'get_links_for_menu_strings_translation_ajax' ) ); } function sync_menus_via_ajax() { if ( isset( $_POST['_icl_nonce_menu_sync'] ) && wp_verify_nonce( $_POST['_icl_nonce_menu_sync'], '_icl_nonce_menu_sync' ) ) { if ( ! session_id() ) { session_start(); } global $icl_menus_sync,$wpdb, $wpml_post_translations, $wpml_term_translations, $sitepress; include_once WPML_PLUGIN_PATH . '/inc/wp-nav-menus/menus-sync.php'; $icl_menus_sync = new ICLMenusSync( $sitepress, $wpdb, $wpml_post_translations, $wpml_term_translations ); $icl_menus_sync->init( isset( $_SESSION['wpml_menu_sync_menu'] ) ? $_SESSION['wpml_menu_sync_menu'] : null ); $results = $icl_menus_sync->do_sync( $_POST['sync'] ); $_SESSION['wpml_menu_sync_menu'] = $results; wp_send_json_success( true ); } else { wp_send_json_error( false ); } } public function get_links_for_menu_strings_translation_ajax() { global $icl_menus_sync, $wpml_post_translations, $wpml_term_translations; $nonce = isset( $_GET['_nonce'] ) ? sanitize_text_field( $_GET['_nonce'] ) : ''; if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( esc_html__( 'Unauthorized', 'sitepress' ), 401 ); return; } if ( ! wp_verify_nonce( $nonce, 'wpml_get_links_for_menu_strings_translation' ) ) { wp_send_json_error( esc_html__( 'Invalid request!', 'sitepress' ), 400 ); return; } include_once WPML_PLUGIN_PATH . '/inc/wp-nav-menus/menus-sync.php'; $icl_menus_sync = new ICLMenusSync( $this->sitepress, $this->wpdb, $wpml_post_translations, $wpml_term_translations ); wp_send_json_success( $icl_menus_sync->get_links_for_menu_strings_translation() ); } /** * @param string $menu_id */ function admin_menu_setup( $menu_id ) { if ( 'WPML' !== $menu_id ) { return; } $menu = array(); $menu['order'] = 700; $menu['page_title'] = __( 'WP Menus Sync', 'sitepress' ); $menu['menu_title'] = __( 'WP Menus Sync', 'sitepress' ); $menu['capability'] = 'wpml_manage_wp_menus_sync'; $menu['menu_slug'] = WPML_PLUGIN_FOLDER . '/menu/menu-sync/menus-sync.php'; do_action( 'wpml_admin_menu_register_item', $menu ); } /** * * Associates menus without language information with default language */ private function _set_menus_language() { global $wpdb, $sitepress; $default_language = $sitepress->get_default_language(); $untranslated_menus = $wpdb->get_col( " SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} tt LEFT JOIN {$wpdb->prefix}icl_translations i ON CONCAT('tax_', tt.taxonomy ) = i.element_type AND i.element_id = tt.term_taxonomy_id WHERE tt.taxonomy='nav_menu' AND i.language_code IS NULL" ); foreach ( (array) $untranslated_menus as $item ) { $sitepress->set_element_language_details( $item, 'tax_nav_menu', null, $default_language ); } $untranslated_menu_items = $wpdb->get_col( " SELECT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->prefix}icl_translations i ON CONCAT('post_', p.post_type ) = i.element_type AND i.element_id = p.ID WHERE p.post_type = 'nav_menu_item' AND i.language_code IS NULL" ); if ( ! empty( $untranslated_menu_items ) ) { foreach ( $untranslated_menu_items as $item ) { $sitepress->set_element_language_details( $item, 'post_nav_menu_item', null, $default_language, null, true, true ); } } } function ajax( $data ) { if ( $data['icl_wp_nav_menu_ajax'] == 'translation_of' ) { $trid = isset( $data['trid'] ) ? $data['trid'] : false; echo $this->render_translation_of( $data['lang'], $trid ); } exit; } function _get_menu_language( $menu_id ) { /** @var WPML_Term_Translation $wpml_term_translations */ global $wpml_term_translations; return $menu_id ? $wpml_term_translations->lang_code_by_termid( $menu_id ) : false; } /** * * Gets first menu in a specific language * used to override nav_menu_recently_edited when a different language is selected * * @param string $lang * @return int */ function _get_first_menu( $lang ) { global $wpdb; $menu_tt_id = $wpdb->get_var( "SELECT MIN(element_id) FROM {$wpdb->prefix}icl_translations WHERE element_type='tax_nav_menu' AND language_code='" . esc_sql( $lang ) . "'" ); return $menu_tt_id ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id=%d", $menu_tt_id ) ) : false; } function get_current_menu() { global $sitepress, $wpml_request_handler; $nav_menu_recently_edited = get_user_option( 'nav_menu_recently_edited' ); $nav_menu_recently_edited_lang = $this->_get_menu_language( $nav_menu_recently_edited ); $current_language = $sitepress->get_current_language(); $admin_language_cookie = $wpml_request_handler->get_cookie_lang(); if ( ! isset( $_REQUEST['menu'] ) && $nav_menu_recently_edited_lang != $current_language ) { // if no menu is specified and the language is set override nav_menu_recently_edited $nav_menu_selected_id = $this->_get_first_menu( $current_language ); if ( $nav_menu_selected_id ) { update_user_option( get_current_user_id(), 'nav_menu_recently_edited', $nav_menu_selected_id ); } else { $_REQUEST['menu'] = 0; } } elseif ( ! isset( $_REQUEST['menu'] ) && ! isset( $_GET['lang'] ) && ( empty( $nav_menu_recently_edited_lang ) || $nav_menu_recently_edited_lang != $admin_language_cookie ) && ( empty( $_POST['action'] ) || $_POST['action'] != 'update' ) ) { // if no menu is specified, no language is set, override nav_menu_recently_edited if its language is different than default $nav_menu_selected_id = $this->_get_first_menu( $current_language ); update_user_option( get_current_user_id(), 'nav_menu_recently_edited', $nav_menu_selected_id ); } elseif ( isset( $_REQUEST['menu'] ) ) { $nav_menu_selected_id = $_REQUEST['menu']; } else { $nav_menu_selected_id = $nav_menu_recently_edited; } $this->current_menu['id'] = $nav_menu_selected_id; if ( $this->current_menu['id'] ) { $this->_load_menu( $this->current_menu['id'] ); } else { $this->current_menu['trid'] = isset( $_GET['trid'] ) ? (int) $_GET['trid'] : null; if ( isset( $_POST['icl_nav_menu_language'] ) ) { $this->current_menu['language'] = $_POST['icl_nav_menu_language']; } elseif ( isset( $_GET['lang'] ) ) { $this->current_menu['language'] = (int) $_GET['lang']; } else { $this->current_menu['language'] = $admin_language_cookie; } $this->current_menu['translations'] = array(); } } /** * @param bool|int $menu_id * * @return array */ function _load_menu( $menu_id = false ) { $menu_id = $menu_id ? $menu_id : $this->current_menu['id']; $menu_term_object = get_term( $menu_id, 'nav_menu' ); if ( ! empty( $menu_term_object->term_taxonomy_id ) ) { $ttid = $menu_term_object->term_taxonomy_id; $current_menu = array( 'id' => $menu_id ); $current_menu['trid'] = $this->term_translations->get_element_trid( $ttid ); $current_menu['translations'] = $current_menu['trid'] ? $this->sitepress->get_element_translations( $current_menu['trid'], 'tax_nav_menu' ) : array(); $current_menu['language'] = $this->term_translations->lang_code_by_termid( $menu_id ); } $this->current_menu = ! empty( $current_menu['translations'] ) ? $current_menu : null; return $this->current_menu; } private function get_action_icon( $css_class, $label ) { return '<span class="' . $css_class . '" title="' . esc_attr( $label ) . '"></span>'; } function nav_menu_language_controls() { global $sitepress, $wpdb; $this->_load_menu(); $default_language = $sitepress->get_default_language(); $current_lang = isset( $this->current_menu['language'] ) ? $this->current_menu['language'] : $sitepress->get_current_language(); $langsel = '<br class="clear" />'; // show translations links if this is not a new element if ( isset( $this->current_menu['id'] ) && $this->current_menu['id'] ) { $langsel .= '<div class="icl_nav_menu_text" style="float:right;">'; $langsel .= __( 'Translations:', 'sitepress' ); foreach ( $sitepress->get_active_languages() as $lang ) { if ( ! isset( $this->current_menu['language'] ) || $lang['code'] == $this->current_menu['language'] ) { continue; } if ( isset( $this->current_menu['translations'][ $lang['code'] ] ) ) { $menu_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id=%d", $this->current_menu['translations'][ $lang['code'] ]->element_id ) ); $label = __( 'edit translation', 'sitepress' ); $tr_link = '<a style="text-decoration:none" title="' . esc_attr( $label ) . '" href="' . admin_url( 'nav-menus.php' ) . '?menu=' . $menu_id . '&lang=' . $lang['code'] . '">' . $this->get_action_icon( WPML_Post_Status_Display::ICON_TRANSLATION_EDIT, $label ) . $lang['display_name'] . '</a>'; } else { $label = __( 'add translation', 'sitepress' ); $tr_link = '<a style="text-decoration:none" title="' . esc_attr( $label ) . '" href="' . admin_url( 'nav-menus.php' ) . '?action=edit&menu=0&trid=' . $this->current_menu['trid'] . '&lang=' . $lang['code'] . '">' . $this->get_action_icon( WPML_Post_Status_Display::ICON_TRANSLATION_ADD, $label ) . esc_html( $lang['display_name'] ) . '</a>'; } $trs[] = $tr_link; } $langsel .= ' '; if ( isset( $trs ) ) { $langsel .= join( ', ', $trs ); } $langsel .= '</div><br />'; $langsel .= '<div class="icl_nav_menu_text" style="float:right; clear:right">'; $langsel .= '<div><a href="' . admin_url( 'admin.php?page=' . WPML_PLUGIN_FOLDER . '/menu/menu-sync/menus-sync.php' ) . '">' . __( 'Synchronize menus between languages.', 'sitepress' ) . '</a></div>'; $langsel .= '</div>'; } // show languages dropdown $langsel .= '<label class="menu-name-label howto"><span>' . __( 'Language', 'sitepress' ) . '</span>'; $langsel .= ' '; $langsel .= '<select name="icl_nav_menu_language" id="icl_menu_language">'; foreach ( $sitepress->get_active_languages() as $lang ) { if ( isset( $this->current_menu['translations'][ $lang['code'] ] ) && $this->current_menu['language'] != $lang['code'] ) { continue; } if ( isset( $this->current_menu['language'] ) && $this->current_menu['language'] ) { $selected = $lang['code'] == $this->current_menu['language'] ? ' selected="selected"' : ''; } else { $selected = $lang['code'] == $sitepress->get_current_language() ? ' selected="selected"' : ''; } $langsel .= '<option value="' . $lang['code'] . '"' . $selected . '>' . $lang['display_name'] . '</option>'; } $langsel .= '</select>'; $langsel .= '</label>'; if ( $current_lang !== $default_language ) { // show 'translation of' if this element is not in the default language and there are untranslated elements $langsel .= '<span id="icl_translation_of_wrap">'; $trid_current = ! empty( $this->current_menu['trid'] ) ? $this->current_menu['trid'] : ( isset( $_GET['trid'] ) ? $_GET['trid'] : 0 ); $langsel .= $this->render_translation_of( $current_lang, (int) $trid_current ); $langsel .= '</span>'; } $langsel .= '</span>'; // Add trid to form. if ( $this->current_menu && $this->current_menu['trid'] ) { $langsel .= '<input type="hidden" id="icl_nav_menu_trid" name="icl_nav_menu_trid" value="' . $this->current_menu['trid'] . '" />'; } $langsel .= ''; echo $this->render_button_language_switcher_settings(); ?> <script type="text/javascript"> jQuery(document).ready(function () { addLoadEvent(function () { var update_menu_form = jQuery('#update-nav-menu'); update_menu_form.find('.publishing-action:first').before('<?php echo addslashes_gpc( $langsel ); ?>'); jQuery('#side-sortables').before('<?php $this->languages_menu(); ?>'); <?php if ( $this->current_lang != $default_language ) : ?> jQuery('.nav-tabs .nav-tab').each(function () { jQuery(this).attr('href', jQuery(this).attr('href') + '&lang=<?php echo $this->current_lang; ?>'); }); var original_action = update_menu_form.attr('ACTION') ? update_menu_form.attr('ACTION') : ''; update_menu_form.attr('ACTION', original_action + '?lang=<?php echo $this->current_lang; ?>'); <?php endif; ?> WPML_core.wp_nav_align_inputs(); }); }); </script> <?php } function get_menus_without_translation( $lang, $trid = 0 ) { $res_query = " SELECT ts.element_id, ts.trid, t.name FROM {$this->wpdb->prefix}icl_translations ts JOIN {$this->wpdb->term_taxonomy} tx ON ts.element_id = tx.term_taxonomy_id JOIN {$this->wpdb->terms} t ON tx.term_id = t.term_id LEFT JOIN {$this->wpdb->prefix}icl_translations mo ON mo.trid = ts.trid AND mo.language_code = %s WHERE ts.element_type='tax_nav_menu' AND ts.language_code != %s AND ts.source_language_code IS NULL AND tx.taxonomy = 'nav_menu' AND ( mo.element_id IS NULL OR ts.trid = %d ) "; $res_query_prepared = $this->wpdb->prepare( $res_query, $lang, $lang, $trid ); $res = $this->wpdb->get_results( $res_query_prepared ); $menus = array(); foreach ( $res as $row ) { $menus[ $row->trid ] = $row; } return $menus; } private function render_translation_of( $lang, $trid = false ) { global $sitepress; $out = ''; if ( $sitepress->get_default_language() != $lang ) { $menus = $this->get_menus_without_translation( $lang, (int) $trid ); $disabled = empty( $this->current_menu['id'] ) && isset( $_GET['trid'] ) ? ' disabled="disabled"' : ''; $out .= '<label class="menu-name-label howto"><span>' . __( 'Translation of', 'sitepress' ) . '</span> '; $out .= '<select name="icl_translation_of" id="icl_menu_translation_of"' . $disabled . '>'; $out .= '<option value="none">--' . __( 'none', 'sitepress' ) . '--</option>'; foreach ( $menus as $mtrid => $m ) { if ( (int) $trid === (int) $mtrid ) { $selected = ' selected="selected"'; } else { $selected = ''; } $out .= '<option value="' . $m->element_id . '"' . $selected . '>' . $m->name . '</option>'; } $out .= '</select>'; $out .= '</label>'; if ( $disabled !== '' ) { $out .= '<input type="hidden" name="icl_nav_menu_trid" value="' . (int) $_GET['trid'] . '"/>'; } } return $out; } private function render_button_language_switcher_settings() { /* @var WPML_Language_Switcher $wpml_language_switcher */ global $wpml_language_switcher; $output = ''; $default_lang = $this->sitepress->get_default_language(); $default_lang_menu = isset( $this->current_menu['translations'][ $default_lang ] ) ? $this->current_menu['translations'][ $default_lang ] : null; if ( $default_lang_menu && isset( $default_lang_menu->element_id ) ) { $output = '<div id="wpml-ls-menu-management" style="display:none;">'; $output .= $wpml_language_switcher->get_button_to_edit_slot( 'menus', $default_lang_menu->element_id ); $output .= '</div>'; } return $output; } function get_menus_by_language() { global $wpdb, $sitepress; $langs = array(); $res_query = " SELECT lt.name AS language_name, l.code AS lang, COUNT(ts.translation_id) AS c FROM {$wpdb->prefix}icl_languages l JOIN {$wpdb->prefix}icl_languages_translations lt ON lt.language_code = l.code JOIN {$wpdb->prefix}icl_translations ts ON l.code = ts.language_code WHERE lt.display_language_code=%s AND l.active = 1 AND ts.element_type = 'tax_nav_menu' GROUP BY ts.language_code ORDER BY major DESC, english_name ASC "; $admin_language = $sitepress->get_admin_language(); $res_query_prepared = $wpdb->prepare( $res_query, $admin_language ); $res = $wpdb->get_results( $res_query_prepared ); foreach ( $res as $row ) { $langs[ $row->lang ] = $row; } return $langs; } function languages_menu( $echo = true ) { global $sitepress; $langs = $this->get_menus_by_language(); // include empty languages foreach ( $sitepress->get_active_languages() as $lang ) { if ( ! isset( $langs[ $lang['code'] ] ) ) { $langs[ $lang['code'] ] = new stdClass(); $langs[ $lang['code'] ]->language_name = $lang['display_name']; $langs[ $lang['code'] ]->lang = $lang['code']; } } $url = admin_url( 'nav-menus.php' ); $ls = array(); foreach ( $langs as $l ) { $class = $l->lang == $this->current_lang ? ' class="current"' : ''; $url_suffix = '?lang=' . $l->lang; $count_string = isset( $l->c ) && $l->c > 0 ? ' (' . $l->c . ')' : ''; $ls[] = '<a href="' . $url . $url_suffix . '"' . $class . '>' . esc_html( $l->language_name ) . $count_string . '</a>'; } $ls_string = '<div class="icl_lang_menu icl_nav_menu_text">'; $ls_string .= join( ' | ', $ls ); $ls_string .= '</div>'; if ( $echo ) { echo $ls_string; } return $ls_string; } function get_terms_filter( $terms, $taxonomies, $args ) { global $wpdb, $sitepress, $pagenow; // deal with the case of not translated taxonomies // we'll assume that it's called as just a single item if ( ! $sitepress->is_translated_taxonomy( $taxonomies[0] ) && 'nav_menu' !== $taxonomies[0] ) { return $terms; } // special case for determining list of menus for updating auto-add option if ( 'nav-menus.php' === $pagenow && array_key_exists( 'fields', $args ) && array_key_exists( 'action', $_POST ) && 'nav_menu' === $taxonomies[0] && 'ids' === $args['fields'] && 'update' === $_POST['action'] ) { return $terms; } if ( ! empty( $terms ) ) { $txs = array(); foreach ( $taxonomies as $t ) { $txs[] = 'tax_' . $t; } $el_types = wpml_prepare_in( $txs ); // get all term_taxonomy_id's $tt = array(); foreach ( $terms as $t ) { if ( is_object( $t ) ) { $tt[] = $t->term_taxonomy_id; } else { if ( is_numeric( $t ) ) { $tt[] = $t; } } } // filter the ones in the current language $ftt = array(); if ( ! empty( $tt ) ) { $ftt = $wpdb->get_col( $wpdb->prepare( " SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE element_type IN ({$el_types}) AND element_id IN (" . wpml_prepare_in( $tt, '%d' ) . ') AND language_code=%s', $this->current_lang ) ); } foreach ( $terms as $k => $v ) { if ( isset( $v->term_taxonomy_id ) && ! in_array( $v->term_taxonomy_id, $ftt ) ) { unset( $terms[ $k ] ); } } } return array_values( $terms ); } /** * Filter posts by language. * * @param \WP_Query $q * * @return \WP_Query */ public function parse_query( $q ) { if ( ! array_key_exists( 'post_type', $q->query_vars ) ) { return $q; } if ( 'nav_menu_item' === $q->query_vars['post_type'] ) { return $q; } // Not filtering custom posts that are not translated. if ( $this->sitepress->is_translated_post_type( $q->query_vars['post_type'] ) ) { $q->query_vars['suppress_filters'] = 0; } return $q; } /** * @param \WP_Query $q * * @return void */ public function action_parse_query( $q ) { $this->parse_query( $q ); } /** * @param mixed $val * * @return mixed */ function option_nav_menu_options( $val ) { global $wpdb, $sitepress; // special case of getting menus with auto-add only in a specific language $debug_backtrace = $sitepress->get_backtrace( 5 ); // Ignore objects and limit to first 5 stack frames, since 4 is the highest index we use if ( isset( $debug_backtrace[4] ) && $debug_backtrace[4]['function'] === '_wp_auto_add_pages_to_menu' && ! empty( $val['auto_add'] ) ) { $post_lang = Sanitize::stringProp( 'icl_post_language', $_POST ); $post_lang = ! $post_lang && isset( $_POST['lang'] ) ? Sanitize::string( $_POST['lang'] ) : $post_lang; $post_lang = ! $post_lang && $this->is_duplication_mode() ? $sitepress->get_current_language() : $post_lang; if ( $post_lang ) { $val['auto_add'] = $wpdb->get_col( $wpdb->prepare( " SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE element_type = 'tax_nav_menu' AND element_id IN ( " . wpml_prepare_in( $val['auto_add'], '%d' ) . ' ) AND language_code = %s', $post_lang ) ); } } return $val; } /** * @return bool */ private function is_duplication_mode() { return isset( $_POST['langs'] ); } function wp_nav_menu_args_filter( $args ) { if ( ! $args['menu'] ) { $locations = get_nav_menu_locations(); if ( isset( $args['theme_location'] ) && isset( $locations[ $args['theme_location'] ] ) ) { $args['menu'] = self::convert_nav_menu_id( $locations[ $args['theme_location'] ] ); } }; if ( ! $args['menu'] ) { remove_filter( 'theme_mod_nav_menu_locations', array( $this->nav_menu_actions, 'theme_mod_nav_menu_locations' ) ); $locations = get_nav_menu_locations(); if ( isset( $args['theme_location'] ) && isset( $locations[ $args['theme_location'] ] ) ) { $args['menu'] = self::convert_nav_menu_id( $locations[ $args['theme_location'] ] ); } add_filter( 'theme_mod_nav_menu_locations', array( $this->nav_menu_actions, 'theme_mod_nav_menu_locations' ) ); } // $args[ "menu" ] can be an object consequently to widget's call if ( is_object( $args['menu'] ) && ( ! empty( $args['menu']->term_id ) ) ) { $args['menu'] = wp_get_nav_menu_object( self::convert_nav_menu_id( $args['menu']->term_id ) ); } if ( ( ! is_object( $args['menu'] ) ) && is_numeric( $args['menu'] ) ) { $args['menu'] = wp_get_nav_menu_object( self::convert_nav_menu_id( (int) $args['menu'] ) ); } if ( ( ! is_object( $args['menu'] ) ) && is_string( $args['menu'] ) ) { $term = get_term_by( 'slug', $args['menu'], 'nav_menu' ); if ( false === $term ) { $term = get_term_by( 'name', $args['menu'], 'nav_menu' ); } if ( false !== $term ) { $args['menu'] = wp_get_nav_menu_object( self::convert_nav_menu_id( $term->term_id ) ); } } if ( ! is_object( $args['menu'] ) ) { $args['menu'] = false; } return $args; } /** * It will fallback to the original if the translation * does not exist. This is required for nav menus in * a "widget" context. * * @param int $navMenuId * * @return int */ private static function convert_nav_menu_id( $navMenuId ) { return wpml_object_id_filter( $navMenuId, 'nav_menu', true ); } function wp_nav_menu_items_filter( $items ) { $items = preg_replace( '|<li id="([^"]+)" class="menu-item menu-item-type-taxonomy"><a href="([^"]+)">([^@]+) @([^<]+)</a>|', '<li id="$1" class="menu-item menu-item-type-taxonomy"><a href="$2">$3</a>', $items ); return $items; } function _set_custom_status_in_theme_location_switcher() { global $sitepress_settings, $sitepress, $wpdb; if ( ! $sitepress_settings ) { return; } $tl = (array) get_theme_mod( 'nav_menu_locations' ); $menus_not_translated = array(); foreach ( $tl as $k => $menu ) { $menu_tt_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id=%d AND taxonomy='nav_menu'", $menu ) ); $menu_trid = $sitepress->get_element_trid( $menu_tt_id, 'tax_nav_menu' ); $menu_translations = $sitepress->get_element_translations( $menu_trid, 'tax_nav_menu' ); if ( ! isset( $menu_translations[ $this->current_lang ] ) || ! $menu_translations[ $this->current_lang ] ) { $menus_not_translated[] = $k; } } if ( ! empty( $menus_not_translated ) ) { ?> <script type="text/javascript"> jQuery(document).ready(function () { addLoadEvent(function () { <?php foreach ( $menus_not_translated as $menu_id ) : ?> var menu_id = '<?php echo $menu_id; ?>'; var location_menu_id = jQuery('#locations-' + menu_id); if (location_menu_id.length > 0) { location_menu_id.find('option').first().html('<?php echo esc_js( __( 'not translated in current language', 'sitepress' ) ); ?>'); location_menu_id.css('font-style', 'italic'); location_menu_id.change(function () { if (jQuery(this).val() != 0) { jQuery(this).css('font-style', 'normal'); } else { jQuery(this).css('font-style', 'italic') } }); } <?php endforeach; ?> }); }); </script> <?php } } // on the nav menus when selecting pages using the pagination filter pages > 2 by language function _enable_sitepress_query_filters( $args ) { if ( isset( $args->_default_query ) ) { $args->_default_query['suppress_filters'] = false; } return $args; } function wp_get_nav_menus_filter( $menus ) { global $pagenow; if ( is_admin() && isset( $pagenow ) && $pagenow === 'customize.php' ) { $menus = $this->unfilter_non_default_language_menus( $menus ); } return $menus; } private function setup_menu_item() { add_action( 'wpml_admin_menu_configure', array( $this, 'admin_menu_setup' ) ); } private function setup_menu_synchronization() { global $icl_menus_sync, $wpml_post_translations, $wpml_term_translations; include_once WPML_PLUGIN_PATH . '/inc/wp-nav-menus/menus-sync.php'; $icl_menus_sync = new ICLMenusSync( $this->sitepress, $this->wpdb, $wpml_post_translations, $wpml_term_translations ); } private function unfilter_non_default_language_menus( $menus ) { global $sitepress, $wpml_term_translations; $default_language = $sitepress->get_default_language(); foreach ( $menus as $index => $menu ) { $menu_ttid = is_object( $menu ) ? $menu->term_taxonomy_id : $menu; /** @var WPML_Term_Translation $wpml_term_translations */ $menu_language = $wpml_term_translations->get_element_lang_code( $menu_ttid ); if ( $menu_language != $default_language && $menu_language != null ) { unset( $menus[ $index ] ); } } return $menus; } private function adjust_current_language_if_required() { global $pagenow; if ( $pagenow === 'nav-menus.php' && isset( $_GET['menu'] ) && $_GET['menu'] ) { $current_lang = $this->sitepress->get_current_language(); $menu_lang = $this->_get_menu_language( (int) $_GET['menu'] ); if ( $menu_lang && ( $current_lang !== $menu_lang ) ) { $this->sitepress->switch_lang( $menu_lang ); $_GET['lang'] = $menu_lang; } } } } wp-nav-menus/menus-sync.php 0000755 00000052241 14720425051 0011722 0 ustar 00 <?php class ICLMenusSync extends WPML_Menu_Sync_Functionality { public $menus; public $is_preview = false; public $sync_data = false; public $string_translation_links = array(); public $operations = array(); /** @var WPML_Menu_Item_Sync $menu_item_sync */ private $menu_item_sync; /** * @param SitePress $sitepress * @param wpdb $wpdb * @param WPML_Post_Translation $post_translations * @param WPML_Term_Translation $term_translations */ function __construct( &$sitepress, &$wpdb, &$post_translations, &$term_translations ) { parent::__construct( $sitepress, $wpdb, $post_translations, $term_translations ); $this->menu_item_sync = new WPML_Menu_Item_Sync( $this->sitepress, $this->wpdb, $this->post_translations, $this->term_translations ); $this->init_hooks(); } function init_hooks() { add_action( 'init', array( $this, 'init' ), 20 ); if ( isset( $_GET['updated'] ) ) { add_action( 'admin_notices', array( $this, 'admin_notices' ) ); } } function init( $previous_menu = false ) { $this->sitepress->switch_lang( $this->sitepress->get_default_language() ); $action = filter_input( INPUT_POST, 'action' ); $nonce = (string) filter_input( INPUT_POST, '_icl_nonce_menu_sync' ); if ( $action && ! wp_verify_nonce( $nonce, '_icl_nonce_menu_sync' ) ) { wp_send_json_error( 'Invalid nonce' ); } $this->menu_item_sync->cleanup_broken_page_items(); if ( ! session_id() ) { session_start(); } if ( $action === 'icl_msync_preview' ) { $this->is_preview = true; $this->sync_data = isset( $_POST['sync'] ) ? array_map( 'stripslashes_deep', $_POST['sync'] ) : false; $previous_menu = isset( $_SESSION['wpml_menu_sync_menu'] ) ? $_SESSION['wpml_menu_sync_menu'] : null; } if ( $previous_menu ) { $this->menus = $previous_menu; } else { $this->get_menus_tree(); $_SESSION['wpml_menu_sync_menu'] = $this->menus; } $this->sitepress->switch_lang(); } function get_menu_names() { $menu_names = array(); global $sitepress, $wpdb; $menus = $wpdb->get_results( $wpdb->prepare( " SELECT tm.term_id, tm.name FROM {$wpdb->terms} tm JOIN {$wpdb->term_taxonomy} tx ON tx.term_id = tm.term_id JOIN {$wpdb->prefix}icl_translations tr ON tr.element_id = tx.term_taxonomy_id AND tr.element_type='tax_nav_menu' WHERE tr.language_code=%s ", $sitepress->get_default_language() ) ); if ( $menus ) { foreach ( $menus as $menu ) { $menu_names[] = $menu->name; } } return $menu_names; } function get_menus_tree() { global $sitepress, $wpdb; $menus = $wpdb->get_results( $wpdb->prepare( " SELECT tm.term_id, tm.name FROM {$wpdb->terms} tm JOIN {$wpdb->term_taxonomy} tx ON tx.term_id = tm.term_id JOIN {$wpdb->prefix}icl_translations tr ON tr.element_id = tx.term_taxonomy_id AND tr.element_type='tax_nav_menu' WHERE tr.language_code=%s ", $sitepress->get_default_language() ) ); if ( $menus ) { foreach ( $menus as $menu ) { $this->menus[ $menu->term_id ] = array( 'name' => $menu->name, 'items' => $this->get_menu_items( $menu->term_id, true ), 'translations' => $this->get_menu_translations( $menu->term_id ), ); } $this->add_ghost_entries(); $this->set_new_menu_order(); } } private function get_menu_options( $menu_id ) { $menu_options = get_option( 'nav_menu_options' ); $options = array( 'auto_add' => isset( $menu_options['auto_add'] ) && in_array( $menu_id, $menu_options['auto_add'] ), ); return $options; } public function add_ghost_entries() { if ( is_array( $this->menus ) ) { foreach ( $this->menus as $menu_id => $menu ) { if ( ! is_array( $menu['translations'] ) ) { continue; } foreach ( $menu['translations'] as $language => $tmenu ) { if ( ! empty( $tmenu ) ) { $valid_items = array_filter( $this->menus[ $menu_id ]['items'], function ( $item ) use ( $language ) { return $item && isset( $item['translations'][ $language ]['ID'] ); } ); foreach ( $tmenu['items'] as $titem ) { // Has a place in the default menu? $exists = false; foreach ( $valid_items as $item ) { if ( (int) $item['translations'][ $language ]['ID'] === (int) $titem['ID'] ) { $exists = true; } } if ( ! $exists ) { $this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'][] = array( 'ID' => $titem['ID'], 'title' => $titem['title'], 'menu_order' => $titem['menu_order'], ); } } } } } } } public function set_new_menu_order() { if ( ! is_array( $this->menus ) ) { return; } foreach ( $this->menus as $menu_id => $menu ) { $menu_index_by_lang = array(); foreach ( $menu['items'] as $item_id => $item ) { $valid_translations = array_filter( $item['translations'], function ( $item ) { return $item && $item['ID']; } ); foreach ( $valid_translations as $language => $item_translation ) { $new_menu_order = empty( $menu_index_by_lang[ $language ] ) ? 1 : $menu_index_by_lang[ $language ] + 1; $menu_index_by_lang[ $language ] = $new_menu_order; $this->menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['menu_order_new'] = $new_menu_order; } } } } function do_sync( array $data ) { $this->menus = isset( $this->menus ) ? $this->menus : array(); $this->menus = empty( $data['menu_translation'] ) ? $this->menus : $this->menu_item_sync->sync_menu_translations( $data['menu_translation'], $this->menus ); if ( ! empty( $data['options_changed'] ) ) { $this->menu_item_sync->sync_menu_options( $data['options_changed'] ); } if ( ! empty( $data['del'] ) ) { $this->menu_item_sync->sync_deleted_menus( $data['del'] ); } $this->menus = empty( $data['mov'] ) ? $this->menus : $this->menu_item_sync->sync_moved_items( $data['mov'], $this->menus ); $this->menus = empty( $data['add'] ) ? $this->menus : $this->menu_item_sync->sync_added_items( $data['add'], $this->menus ); if ( ! empty( $data['label_changed'] ) ) { $this->menu_item_sync->sync_caption( $data['label_changed'] ); } if ( ! empty( $data['url_changed'] ) ) { $this->menu_item_sync->sync_urls( $data['url_changed'] ); } if ( ! empty( $data['label_missing'] ) ) { $this->menu_item_sync->sync_missing_captions( $data['label_missing'] ); } if ( ! empty( $data['url_missing'] ) ) { $this->menu_item_sync->sync_urls_to_add( $data['url_missing'] ); } $this->menu_item_sync->sync_custom_fields( $this->menus ); $this->menus = isset( $this->menus ) ? $this->menu_item_sync->sync_menu_order( $this->menus ) : $this->menus; $this->menu_item_sync->cleanup_broken_page_items(); return $this->menus; } function render_items_tree_default( $menu_id, $parent = 0, $depth = 0 ) { global $sitepress; $active_language_codes = array_keys( $sitepress->get_active_languages() ); $need_sync = 0; $default_language = $sitepress->get_default_language(); foreach ( $this->menus[ $menu_id ]['items'] as $item ) { // deleted items #2 (menu order beyond) static $d2_items = array(); $deleted_items = array(); if ( isset( $this->menus[ $menu_id ]['translation'] ) && is_array( $this->menus[ $menu_id ]['translation'] ) ) { foreach ( $this->menus[ $menu_id ]['translations'] as $language => $tmenu ) { if ( ! isset( $d2_items[ $language ] ) ) { $d2_items[ $language ] = array(); } if ( ! empty( $this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'] ) ) { foreach ( $this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'] as $deleted_item ) { if ( ! in_array( $deleted_item['ID'], $d2_items[ $language ] ) && $deleted_item['menu_order'] > count( $this->menus[ $menu_id ]['items'] ) ) { $deleted_items[ $language ][] = $deleted_item; $d2_items[ $language ][] = $deleted_item['ID']; } } } } } if ( $deleted_items ) { ?> <tr> <td> </td> <?php foreach ( $sitepress->get_active_languages() as $language ) : if ( $language['code'] == $default_language ) { continue; } ?> <td> <?php if ( isset( $deleted_items[ $language['code'] ] ) ) : ?> <?php $need_sync ++; ?> <?php foreach ( $deleted_items[ $language['code'] ] as $deleted_item ) : ?> <?php echo str_repeat( ' - ', $depth ); ?><span class="icl_msync_item icl_msync_del"><?php echo esc_html( $deleted_item['title'] ); ?></span> <input type="hidden" name="sync[del][<?php echo esc_attr( $menu_id ); ?>][<?php echo esc_attr( $language['code'] ); ?>][<?php echo esc_attr( $deleted_item['ID'] ); ?>]" value="<?php echo esc_attr( $deleted_item['title'] ); ?>"/> <?php $this->operations['del'] = empty( $this->operations['del'] ) ? 1 : $this->operations['del'] ++; ?> <br/> <?php endforeach; ?> <?php else : ?> <?php endif; ?> </td> <?php endforeach; ?> </tr> <?php } // show deleted item? static $mo_added = array(); $deleted_items = array(); if ( isset( $this->menus[ $menu_id ]['translation'] ) && is_array( $this->menus[ $menu_id ]['translation'] ) ) { foreach ( $this->menus[ $menu_id ]['translations'] as $language => $tmenu ) { if ( ! isset( $mo_added[ $language ] ) ) { $mo_added[ $language ] = array(); } if ( ! empty( $this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'] ) ) { foreach ( $this->menus[ $menu_id ]['translations'][ $language ]['deleted_items'] as $deleted_item ) { if ( ! in_array( $item['menu_order'], $mo_added[ $language ] ) && $deleted_item['menu_order'] == $item['menu_order'] ) { $deleted_items[ $language ] = $deleted_item; $mo_added[ $language ][] = $item['menu_order']; $need_sync ++; } } } } } $this->render_deleted_items( $deleted_items, $need_sync, $depth, $menu_id ); if ( $item['parent'] == $parent ) { ?> <tr> <td> <?php echo str_repeat( ' - ', $depth ) . $item['title']; ?> </td> <?php foreach ( $active_language_codes as $lang_code ) { if ( $lang_code === $default_language ) { continue; } ?> <td> <?php $item_translation = $item['translations'][ $lang_code ]; $item_id = $item['ID']; echo str_repeat( ' - ', $depth ); $need_sync ++; if ( ! empty( $item_translation['ID'] ) ) { // item translation exists $item_sync_needed = false; if ( $item_translation['menu_order'] != $item_translation['menu_order_new'] || $item_translation['depth'] != $item['depth'] ) { // MOVED echo '<span class="icl_msync_item icl_msync_mov">' . esc_html( $item_translation['title'] ) . '</span>'; echo '<input type="hidden" name="sync[mov][' . esc_attr( (string) $menu_id ) . '][' . esc_attr( (string) $item['ID'] ) . '][' . esc_attr( (string) $lang_code ) . '][' . esc_attr( (string) $item_translation['menu_order_new'] ) . ']" value="' . esc_attr( (string) $item_translation['title'] ) . '" />'; $this->operations['mov'] = empty( $this->operations['mov'] ) ? 1 : $this->operations['mov'] ++; $item_sync_needed = true; } if ( $item_translation['label_missing'] ) { $this->index_changed( 'label_missing', $item_id, $item_translation['title'], $menu_id, $lang_code ); $item_sync_needed = true; } if ( $item_translation['label_changed'] ) { $this->index_changed( 'label_changed', $item_id, $item_translation['title'], $menu_id, $lang_code ); $item_sync_needed = true; } if ( $item_translation['url_missing'] ) { $this->index_changed( 'url_missing', $item_id, $item_translation['url'], $menu_id, $lang_code ); $item_sync_needed = true; } if ( $item_translation['url_changed'] ) { $this->index_changed( 'url_changed', $item_id, $item_translation['url'], $menu_id, $lang_code ); $item_sync_needed = true; } if ( ! $item_sync_needed ) { // NO CHANGE $need_sync --; echo esc_html( $item_translation['title'] ); } } elseif ( $item_translation && 'custom' === $item_translation['object_type'] ) { // item translation does not exist but is a custom item that will be created echo '<span class="icl_msync_item icl_msync_add">' . esc_html( $item_translation['title'] ) . ' @' . esc_html( (string) $lang_code ) . '</span>'; echo '<input type="hidden" name="sync[add][' . esc_attr( $menu_id ) . '][' . esc_attr( $item['ID'] ) . '][' . esc_attr( (string) $lang_code ) . ']" value="' . esc_attr( $item_translation['title'] . ' @' . $lang_code ) . '" />'; $this->incOperation( 'add' ); } elseif ( ! empty( $item_translation['object_id'] ) ) { // item translation does not exist but translated object does if ( $item_translation['parent_not_translated'] ) { echo '<span class="icl_msync_item icl_msync_not">' . esc_html( $item_translation['title'] ) . '</span>'; $this->operations['not'] = empty( $this->operations['not'] ) ? 1 : $this->operations['not'] ++; } elseif ( ! icl_object_id( $item['ID'], 'nav_menu_item', false, (string) $lang_code ) ) { // item translation does not exist but translated object does echo '<span class="icl_msync_item icl_msync_add">' . esc_html( $item_translation['title'] ) . '</span>'; echo '<input type="hidden" name="sync[add][' . esc_attr( $menu_id ) . '][' . esc_attr( $item['ID'] ) . '][' . esc_attr( (string) $lang_code ) . ']" value="' . esc_attr( $item_translation['title'] ) . '" />'; $this->incOperation( 'add' ); } else { $need_sync --; } } elseif ( $item_translation && 'post_type_archive' === $item_translation['object_type'] ) { // item translation does not exist but is a post type archive item that will be created echo '<span class="icl_msync_item icl_msync_add">' . esc_html( $item_translation['title'] ) . ' @' . esc_html( (string) $lang_code ) . '</span>'; echo '<input type="hidden" name="sync[add][' . esc_attr( $menu_id ) . '][' . esc_attr( $item['ID'] ) . '][' . esc_attr( (string) $lang_code ) . ']" value="' . esc_attr( $item_translation['title'] . ' @' . $lang_code ) . '" />'; $this->incOperation( 'add' ); } else { // item translation and object translation do not exist echo '<i class="inactive">' . esc_html__( 'Not translated', 'sitepress' ) . '</i>'; $need_sync --; } ?> </td> <?php } ?> </tr> <?php if ( $this->_item_has_children( $menu_id, $item['ID'] ) ) { $need_sync += $this->render_items_tree_default( $menu_id, $item['ID'], $depth + 1 ); } } } if ( $depth == 0 ) { $this->render_option_update( $active_language_codes, $default_language, $menu_id, $need_sync ); } return $need_sync; } private function render_option_update( $active_language_codes, $default_language, $menu_id, &$need_sync ) { ?> <tr> <?php foreach ( $active_language_codes as $lang_code ) { ?> <td> <?php if ( $lang_code === $default_language ) { esc_html_e( 'Menu Option: auto_add', 'sitepress' ); continue; } $menu_options = $this->get_menu_options( $menu_id ); $translated_id = $this->get_translated_menu( $menu_id, $lang_code ); $change = false; if ( ! isset( $translated_id['id'] ) || $menu_options != $this->get_menu_options( $translated_id['id'] ) ) { $need_sync ++; $change = true; } if ( $change ) { $this->index_changed( 'options_changed', 'auto_add', $menu_options['auto_add'], $menu_id, $lang_code, $change ); } else { echo esc_html( $menu_options['auto_add'] ); } } ?> </td> <?php } private function render_deleted_items( $deleted_items, &$need_sync, $depth, $menu_id ) { global $sitepress; if ( $deleted_items ) { ?> <tr> <td> </td> <?php foreach ( $sitepress->get_active_languages() as $language ) : if ( $language['code'] === $sitepress->get_default_language() ) { continue; } ?> <td> <?php if ( isset( $deleted_items[ $language['code'] ] ) ) : ?> <?php $need_sync ++; ?> <?php echo str_repeat( ' - ', $depth ); ?><span class="icl_msync_item icl_msync_del"><?php echo esc_html( $deleted_items[ $language['code'] ]['title'] ); ?></span> <input type="hidden" name="sync[del][<?php echo esc_attr( $menu_id ); ?>][<?php echo esc_attr( $language['code'] ); ?>][<?php echo esc_attr( $deleted_items[ $language['code'] ]['ID'] ); ?>]" value="<?php echo esc_attr( $deleted_items[ $language['code'] ]['title'] ); ?>"/> <?php $this->operations['del'] = empty( $this->operations['del'] ) ? 1 : $this->operations['del'] ++; ?> <?php else : ?> <?php endif; ?> </td> <?php endforeach; ?> </tr> <?php } } private function index_changed( $index, $item_id, $item_translation, $menu_id, $lang_code, $change = true ) { $this->string_translation_links[ $this->menus[ $menu_id ]['name'] ] = 1; $additional_class = $change ? 'icl_msync_' . $index : ''; echo '<span class="icl_msync_item ' . esc_attr( $additional_class ) . '">' . ( ! $item_translation ? 0 : esc_html( $item_translation ) ) . '</span>' . '<input type="hidden" name="sync[' . esc_attr( $index ) . '][' . esc_attr( $menu_id ) . '][' . esc_attr( $item_id ) . '][' . esc_attr( $lang_code ) . ']" value="' . esc_attr( $item_translation ) . '" />'; if ( $change ) { $this->operations[ $index ] = empty( $this->operations[ $index ] ) ? 1 : $this->operations[ $index ] ++; } } function _item_has_children( $menu_id, $item_id ) { $has = false; foreach ( $this->menus[ $menu_id ]['items'] as $item ) { if ( $item['parent'] == $item_id ) { $has = true; } } return $has; } function get_item_depth( $menu_id, $item_id ) { $depth = 0; $parent = 0; do { foreach ( $this->menus[ $menu_id ]['items'] as $item ) { if ( $item['ID'] == $item_id ) { $parent = $item['parent']; if ( $parent > 0 ) { $depth++; $item_id = $parent; } else { break; } } } } while ( $parent > 0 ); return $depth; } function admin_notices() { echo '<div class="updated"><p>' . esc_html__( 'Menu(s) syncing complete.', 'sitepress' ) . '</p></div>'; } public function display_menu_links_to_string_translation() { $menu_links_data = $this->get_links_for_menu_strings_translation(); if ( count( $menu_links_data ) > 0 ) { echo '<p>'; esc_html_e( "Your menu includes custom items, which you need to translate using WPML's String Translation.", 'sitepress' ); echo '<br/>'; esc_html_e( '1. Translate these strings: ', 'sitepress' ); $i = 0; foreach ( $menu_links_data['items'] as $menu_name => $menu_url ) { if ( $i > 0 ) { echo ', '; } echo '<a href="' . esc_url( $menu_url ) . '">' . esc_html( $menu_name ) . '</a>' . PHP_EOL; $i ++; } echo '<br/>'; esc_html_e( "2. When you're done translating, return here and run the menu synchronization again. This will use the strings that you translated to update the menus.", 'sitepress' ); echo '</p>'; } } public function get_links_for_menu_strings_translation() { $menu_links = array(); $wpml_st_folder = $this->sitepress->get_wp_api()->constant( 'WPML_ST_FOLDER' ); if ( $wpml_st_folder ) { $wpml_st_contexts = icl_st_get_contexts( false ); $wpml_st_contexts = wp_list_pluck( $wpml_st_contexts, 'context' ); $menu_names = $this->get_menu_names(); foreach ( $menu_names as $k => $menu_name ) { if ( ! in_array( $menu_name . WPML_Menu_Sync_Functionality::STRING_CONTEXT_SUFFIX, $wpml_st_contexts, true ) ) { unset( $menu_names[ $k ] ); } } if ( ! empty( $menu_names ) ) { $menu_url_base = add_query_arg( 'page', urlencode( $wpml_st_folder . '/menu/string-translation.php' ), 'admin.php' ); foreach ( $menu_names as $menu_name ) { $menu_url = add_query_arg( 'context', urlencode( $menu_name . WPML_Menu_Sync_Functionality::STRING_CONTEXT_SUFFIX ), $menu_url_base ); $menu_links[ $menu_name ] = $menu_url; } } } $response = array(); if ( $menu_links ) { $response = array( 'label' => esc_html__( 'Translate menu strings and URLs for:', 'sitepress' ), 'items' => $menu_links, ); } return $response; } private function incOperation( $mode ) { $this->operations[ $mode ] = empty( $this->operations[ $mode ] ) ? 1 : $this->operations[ $mode ] ++; } } wp-nav-menus/TranslateMenu.php 0000755 00000005021 14720425051 0012375 0 ustar 00 <?php namespace WPML\Core\Menu; use WPML\LIB\WP\Hooks; use WPML\LIB\WP\Post; use function WPML\FP\spreadArgs; class Translate implements \IWPML_Frontend_Action { public function add_hooks() { Hooks::onFilter( 'wp_get_nav_menu_items', 10, 2 ) ->then( spreadArgs( [ self::class, 'translate' ] ) ); } /** * @param array $items An array of menu item post objects. * @param \WP_Term $menu The menu object. * * @return array */ public static function translate( $items, $menu ) { if ( self::doesNotHaveMenuInCurrentLanguage( $menu ) ) { $items = wpml_collect( $items ) ->filter( [ self::class, 'hasTranslation' ] ) ->map( [ self::class, 'translateItem' ] ) ->filter( [ self::class, 'canView' ] ) ->values() ->toArray(); } return $items; } /** * @param \WP_Post $item Menu item - post object. * * @return bool */ public static function hasTranslation( $item ) { global $sitepress; return 'post_type' !== $item->type || (bool) self::getTranslatedId( $item ) || $sitepress->is_display_as_translated_post_type( $item->object ); } /** * @param \WP_Post $item Menu item - post object. * * @return \WP_Post */ public static function translateItem( $item ) { if ( 'post_type' === $item->type ) { $translatedId = self::getTranslatedId( $item, true ); $post = Post::get( $translatedId ); if ( ! $post instanceof \WP_Post ) { return $item; } foreach ( get_object_vars( $post ) as $key => $value ) { // We won't send the translated ID, since it affects front-end styles negatively. if ( ! in_array( $key, [ 'menu_order', 'post_type', 'ID' ] ) ) { $item->$key = $value; } } $item->object_id = (string) $translatedId; $item->title = $item->post_title; } return $item; } /** * @param \WP_Post $item Menu item - post object. * * @return bool */ public static function canView( $item ) { return current_user_can( 'administrator' ) || 'post_type' !== $item->type || 'draft' !== $item->post_status; } /** * @param \WP_Term $menu The menu object. * * @return bool */ private static function doesNotHaveMenuInCurrentLanguage( $menu ) { return ! wpml_object_id_filter( $menu->term_id, 'nav_menu' ); } /** * @param \WP_Post $item Menu item - post object. * @param bool $return_original_if_missing * @return int|null */ private static function getTranslatedId( $item, $return_original_if_missing = false ) { return wpml_object_id_filter( $item->object_id, $item->object, $return_original_if_missing ); } } wp-nav-menus/menu-item-sync.class.php 0000755 00000035034 14720425051 0013600 0 ustar 00 <?php use WPML\FP\Fns; use WPML\FP\Lst; use WPML\FP\Maybe; use WPML\FP\Obj; use WPML\FP\Relation; use WPML\LIB\WP\Hooks; use function WPML\Container\make; use function WPML\FP\pipe; class WPML_Menu_Item_Sync extends WPML_Menu_Sync_Functionality { /** @var array $labels_to_add */ private $labels_to_add = array(); /** @var array $urls_to_add */ private $urls_to_add = array(); /** * @var string */ const MENU_ITEM_POST_TYPE = 'post_nav_menu_item'; /** * @return int the number of removed broken page items */ function cleanup_broken_page_items() { return $this->wpdb->query( " DELETE o FROM {$this->wpdb->term_relationships} o JOIN {$this->wpdb->postmeta} pm ON pm.post_id = o.object_id JOIN {$this->wpdb->posts} p ON p.ID = pm.post_id JOIN {$this->wpdb->postmeta} pm_type ON pm_type.post_id = pm.post_id WHERE p.post_type = 'nav_menu_item' AND pm.meta_key = '_menu_item_object_id' AND pm_type.meta_key = '_menu_item_type' AND pm_type.meta_value = 'post_type' AND pm.meta_value = 0" ); } function sync_deleted_menus( $deleted_data ) { foreach ( $deleted_data as $languages ) { foreach ( $languages as $items ) { foreach ( $items as $item_id => $name ) { wp_delete_post( $item_id, true ); $delete_trid = $this->post_translations->get_element_trid( $item_id ); if ( $delete_trid ) { $this->sitepress->delete_element_translation( $delete_trid, 'post_nav_menu_item' ); } } } } } function sync_menu_options( $options_data ) { foreach ( $options_data as $menu_id => $translations ) { foreach ( $translations as $language => $option ) { $translated_menu_id = $this->term_translations->term_id_in( $menu_id, $language ); if ( isset( $option['auto_add'] ) ) { $nav_menu_option = (array) get_option( 'nav_menu_options' ); $nav_menu_option['auto_add'] = isset( $nav_menu_option['auto_add'] ) ? $nav_menu_option['auto_add'] : array(); if ( $option['auto_add'] && ! in_array( $translated_menu_id, $nav_menu_option['auto_add'] ) ) { $nav_menu_option['auto_add'][] = $translated_menu_id; } elseif ( ! $option['auto_add'] && false !== ( $key = array_search( $translated_menu_id, $nav_menu_option['auto_add'] ) ) ) { unset( $nav_menu_option['auto_add'][ $key ] ); } /** * We need to disable Sitepress::get_term_adjust_id hook to avoid overriding menu_ids * present in $nav_menu_option['auto_add'] by their original menu_ids. */ $filterUnExistingMenuIds = function () use ( $nav_menu_option ) { return array_intersect( $nav_menu_option['auto_add'], wp_get_nav_menus( [ 'fields' => 'ids' ] ) ); }; $disableAdjustTermIds = Fns::always( true ); $nav_menu_option['auto_add'] = Hooks::callWithFilter( $filterUnExistingMenuIds, 'wpml_disable_term_adjust_id', $disableAdjustTermIds ); update_option( 'nav_menu_options', array_filter( $nav_menu_option ) ); wp_defer_term_counting( false ); do_action( 'wp_update_nav_menu', $translated_menu_id ); } } } } public function sync_menu_order( array $menus ) { global $wpdb; foreach ( $menus as $menu_id => $menu ) { $menu_index_by_lang = array(); foreach ( $menu['items'] as $item_id => $item ) { $valid_translations = array_filter( $item['translations'], function ( $item ) { return $item && $item['ID']; } ); foreach ( $valid_translations as $language => $item_translation ) { $new_menu_order = empty( $menu_index_by_lang[ $language ] ) ? 1 : $menu_index_by_lang[ $language ] + 1; $menu_index_by_lang[ $language ] = $new_menu_order; if ( $new_menu_order != $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['menu_order'] ) { $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['menu_order'] = $new_menu_order; $wpdb->update( $wpdb->posts, [ 'menu_order' => $new_menu_order ], [ 'ID' => $item_translation['ID'] ] ); } } } } return $menus; } function sync_added_items( array $added_data, array $menus ) { global $wpdb; foreach ( $added_data as $menu_id => $items ) { foreach ( $items as $language => $translations ) { foreach ( $translations as $item_id => $name ) { $trid = $this->get_or_set_trid( $item_id, $this->sitepress->get_default_language() ); $translated_object = $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]; $menu_name = $this->get_menu_name( $menu_id ); $object_type = $translated_object['object_type']; $object_title = $translated_object['title']; $object_url = $translated_object['url']; $icl_st_label_exists = false; $icl_st_url_exists = false; if ( $object_type === 'custom' && function_exists( 'icl_t' ) ) { $item = new stdClass(); $item->url = $object_url; $item->ID = $item_id; $item->post_title = $object_title; list( $object_title, $object_url ) = $this->icl_t_menu_item( $menu_name, $item, $language, $icl_st_label_exists, $icl_st_url_exists ); } $menu_data = array( 'menu-item-db-id' => 0, 'menu-item-object-id' => $translated_object['object_id'], 'menu-item-object' => $translated_object['object'], 'menu-item-parent-id' => 0, 'menu-item-position' => 0, 'menu-item-type' => $object_type, 'menu-item-title' => $object_title, 'menu-item-url' => $object_url, 'menu-item-description' => '', 'menu-item-attr-title' => $translated_object['attr-title'], 'menu-item-target' => $translated_object['target'], 'menu-item-classes' => ( $translated_object['classes'] ? implode( ' ', $translated_object['classes'] ) : '' ), 'menu-item-xfn' => $translated_object['xfn'], 'menu-item-status' => 'publish', ); $translated_menu_id = $menus[ $menu_id ]['translations'][ $language ]['id']; remove_filter( 'get_term', array( $this->sitepress, 'get_term_adjust_id' ), 1 ); $translated_item_id = wp_update_nav_menu_item( $translated_menu_id, 0, $menu_data ); // set language explicitly since the 'wp_update_nav_menu_item' is still TBD $this->sitepress->set_element_language_details( $translated_item_id, 'post_nav_menu_item', $trid, $language ); $menu_tax_id_prepared = $wpdb->prepare( "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id=%d AND taxonomy='nav_menu' LIMIT 1", $translated_menu_id ); $menu_tax_id = $wpdb->get_var( $menu_tax_id_prepared ); if ( $translated_item_id && $menu_tax_id ) { $rel_prepared = $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE object_id=%d AND term_taxonomy_id=%d LIMIT 1", $translated_item_id, $menu_tax_id ); $rel = $wpdb->get_var( $rel_prepared ); if ( ! $rel ) { $wpdb->insert( $wpdb->term_relationships, array( 'object_id' => $translated_item_id, 'term_taxonomy_id' => $menu_tax_id, ) ); } } $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['ID'] = $translated_item_id; } } } $this->fix_hierarchy_added_items( $added_data ); return $menus; } function sync_moved_items( array $moved_data, array $menus ) { global $wpdb; foreach ( $moved_data as $menu_id => $items ) { foreach ( $items as $language => $changes ) { foreach ( $changes as $item_id => $details ) { $trid = $this->get_or_set_trid( $item_id, $this->sitepress->get_default_language() ); $translated_item_id = $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['ID']; $new_menu_order = key( $details ); $menus[ $menu_id ]['items'][ $item_id ]['translations'][ $language ]['menu_order'] = $new_menu_order; $wpdb->update( $wpdb->posts, array( 'menu_order' => $new_menu_order ), array( 'ID' => $translated_item_id ) ); if ( $this->post_translations->get_element_trid( $translated_item_id ) != $trid ) { $this->sitepress->set_element_language_details( $translated_item_id, 'post_nav_menu_item', $trid, $language ); } $translated_menu_id = $menus[ $menu_id ]['translations'][ $language ]['id']; $this->assign_orphan_item_to_menu( $translated_item_id, $translated_menu_id, $language ); } } } $this->fix_hierarchy_moved_items( $moved_data ); return $menus; } /** * @param int $item_id * @param int $menu_id */ private function assign_orphan_item_to_menu( $item_id, $menu_id, $language ) { $this->sitepress->switch_lang( $language ); if ( ! wp_get_object_terms( $item_id, 'nav_menu' ) ) { wp_set_object_terms( $item_id, array( $menu_id ), 'nav_menu' ); } $this->sitepress->switch_lang(); } function sync_caption( $label_change_data ) { foreach ( $label_change_data as $languages ) { foreach ( $languages as $language => $items ) { foreach ( $items as $item_id => $name ) { $trid = $this->sitepress->get_element_trid( $item_id, 'post_nav_menu_item' ); if ( $trid ) { $item_translations = $this->sitepress->get_element_translations( $trid, 'post_nav_menu_item', true ); if ( isset( $item_translations[ $language ] ) ) { $translated_item = get_post( $item_translations[ $language ]->element_id ); if ( $translated_item && $translated_item->post_title != $name ) { $translated_item->post_title = $name; /** @phpstan-ignore-next-line WP doc issue. */ wp_update_post( $translated_item ); } } } } } } } function sync_urls( $url_change_data ) { foreach ( $url_change_data as $languages ) { foreach ( $languages as $language => $items ) { foreach ( $items as $item_id => $url ) { $trid = $this->sitepress->get_element_trid( $item_id, 'post_nav_menu_item' ); if ( $trid ) { $item_translations = $this->sitepress->get_element_translations( $trid, 'post_nav_menu_item', true ); if ( isset( $item_translations[ $language ] ) ) { $translated_item_id = $item_translations[ $language ]->element_id; if ( $url ) { update_post_meta( $translated_item_id, '_menu_item_url', $url ); } } } } } } } function sync_missing_captions( $label_missing ) { foreach ( $label_missing as $menu_id => $languages ) { foreach ( $languages as $items ) { foreach ( $items as $item_id => $name ) { if ( ! in_array( $menu_id . '-' . $item_id, $this->labels_to_add ) ) { $item = get_post( $item_id ); icl_register_string( $this->get_menu_name( $menu_id ) . WPML_Menu_Sync_Functionality::STRING_CONTEXT_SUFFIX, WPML_Menu_Sync_Functionality::STRING_NAME_LABEL_PREFIX . $item_id, $item->post_title ); $this->labels_to_add[] = $menu_id . '-' . $item_id; } } } } } function sync_urls_to_add( $url_missing_data ) { foreach ( $url_missing_data as $menu_id => $languages ) { foreach ( $languages as $items ) { foreach ( $items as $item_id => $url ) { if ( ! in_array( $menu_id . '-' . $item_id, $this->urls_to_add ) ) { icl_register_string( $this->get_menu_name( $menu_id ) . WPML_Menu_Sync_Functionality::STRING_CONTEXT_SUFFIX, WPML_Menu_Sync_Functionality::STRING_NAME_URL_PREFIX . $item_id, $url ); $this->urls_to_add[] = $menu_id . '-' . $item_id; } } } } } /** * @param array $menus Registered menus. */ public function sync_custom_fields( $menus ) { $syncMenuItem = function ( $menuItemId ) { $this->sync_custom_fields_set_to_copy( $menuItemId ); $this->sync_custom_fields_set_to_copy_once( $menuItemId ); }; $syncMenu = pipe( Obj::prop( 'items' ), Obj::keys(), Fns::each( $syncMenuItem ) ); Fns::each( $syncMenu, $menus ); } /** * @param int $menuItemId */ private function sync_custom_fields_set_to_copy( $menuItemId ) { $copy = new WPML_Sync_Custom_Fields( new WPML_Translation_Element_Factory( $this->sitepress ), $this->sitepress->get_custom_fields_translation_settings( WPML_COPY_CUSTOM_FIELD ) ); $copy->sync_all_custom_fields( $menuItemId ); } /** * @param int $menuItemId */ private function sync_custom_fields_set_to_copy_once( $menuItemId ) { $getItemTranslations = function( $menuItemId ) { return $this->sitepress->get_element_translations( $this->sitepress->get_element_trid( $menuItemId, self::MENU_ITEM_POST_TYPE ), self::MENU_ITEM_POST_TYPE ); }; Maybe::of( $menuItemId ) ->map( $getItemTranslations ) ->map( Lst::pluck( 'element_id' ) ) ->map( Fns::reject( Relation::equals( $menuItemId ) ) ) ->map( Fns::map( [ make( WPML_Copy_Once_Custom_Field::class ), 'copy' ] ) ); } private function fix_hierarchy_added_items( $added_data ) { foreach ( $added_data as $menu_id => $items ) { foreach ( $items as $language => $translations ) { foreach ( $translations as $item_id => $name ) { $this->fix_hierarchy_for_item( $item_id, $language ); } } } } private function fix_hierarchy_moved_items( $moved_data ) { foreach ( $moved_data as $menu_id => $items ) { foreach ( $items as $language => $changes ) { foreach ( $changes as $item_id => $details ) { $this->fix_hierarchy_for_item( $item_id, $language ); } } } } private function fix_hierarchy_for_item( $item_id, $language ) { $parent_item = get_post_meta( $item_id, '_menu_item_menu_item_parent', true ); $translated_item_id = $this->post_translations->element_id_in( $item_id, $language ); $translated_parent_menu_item_id = $this->post_translations->element_id_in( $parent_item, $language ); $translated_parent_menu_item_id = $translated_parent_menu_item_id == $translated_item_id ? false : $translated_parent_menu_item_id; update_post_meta( $translated_item_id, '_menu_item_menu_item_parent', $translated_parent_menu_item_id ); } private function get_or_set_trid( $item_id, $language_code ) { $trid = $this->post_translations->get_element_trid( $item_id ); if ( ! $trid ) { $this->sitepress->set_element_language_details( $item_id, 'post_nav_menu_item', false, $language_code ); $trid = $this->post_translations->get_element_trid( $item_id ); } return $trid; } } wp-nav-menus/wpml-menu-sync-functionality.class.php 0000755 00000046451 14720425051 0016514 0 ustar 00 <?php use WPML\FP\Lst; abstract class WPML_Menu_Sync_Functionality extends WPML_Full_Translation_API { const STRING_CONTEXT_SUFFIX = ' menu'; const STRING_NAME_LABEL_PREFIX = 'Menu Item Label '; const STRING_NAME_URL_PREFIX = 'Menu Item URL '; private $menu_items_cache; /** * @param SitePress $sitepress * @param wpdb $wpdb * @param WPML_Post_Translation $post_translations * @param WPML_Terms_Translations $term_translations */ function __construct( &$sitepress, &$wpdb, &$post_translations, &$term_translations ) { parent::__construct( $sitepress, $wpdb, $post_translations, $term_translations ); $this->menu_items_cache = array(); } function get_menu_items( $menu_id, $translations = true ) { $key = $menu_id . '-'; if ( $translations ) { $key .= 'trans'; } else { $key .= 'no-trans'; } if ( ! isset( $this->menu_items_cache[ $key ] ) ) { if ( ! isset( $this->menu_items_cache[ $menu_id ] ) ) { $this->menu_items_cache[ $menu_id ] = wp_get_nav_menu_items( (int) $menu_id ); } $items = $this->menu_items_cache[ $menu_id ]; $menu_items = array(); foreach ( $items as $item ) { $item->object_type = get_post_meta( $item->ID, '_menu_item_type', true ); $_item_add = array( 'ID' => $item->ID, 'menu_order' => $item->menu_order, 'parent' => $item->menu_item_parent, 'object' => $item->object, 'url' => $item->url, 'object_type' => $item->object_type, 'object_id' => empty( $item->object_id ) ? get_post_meta( $item->ID, '_menu_item_object_id', true ) : $item->object_id, 'title' => $item->title, 'depth' => $this->get_menu_item_depth( $item->ID ), ); if ( $translations ) { $_item_add['translations'] = $this->get_menu_item_translations( $item, $menu_id ); } $menu_items[ $item->ID ] = $_item_add; } $this->menu_items_cache[ $key ] = $menu_items; } return $this->menu_items_cache[ $key ]; } function sync_menu_translations( $menu_trans_data, $menus ) { global $wpdb; foreach ( $menu_trans_data as $menu_id => $translations ) { foreach ( $translations as $language => $name ) { $_POST['icl_translation_of'] = $wpdb->get_var( $wpdb->prepare( " SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id=%d AND taxonomy='nav_menu' LIMIT 1", $menu_id ) ); $_POST['icl_nav_menu_language'] = $language; $menu_indentation = ''; $menu_increment = 0; do { $new_menu_id = wp_update_nav_menu_object( 0, array( 'menu-name' => $name . $menu_indentation . ( $menu_increment ? $menu_increment : '' ), ) ); $menu_increment = $menu_increment != '' ? $menu_increment + 1 : 2; $menu_indentation = '-'; } while ( is_wp_error( $new_menu_id ) && $menu_increment < 10 ); $menus[ $menu_id ]['translations'][ $language ] = array( 'id' => $new_menu_id ); } } return $menus; } /** * @param \stdClass $item * @param int $menu_id * * @return array */ function get_menu_item_translations( $item, $menu_id ) { $languages = array_keys( $this->sitepress->get_active_languages() ); $item_translations = $this->post_translations->get_element_translations( $item->ID ); $languages = array_diff( $languages, array( $this->sitepress->get_default_language() ) ); $translations = array_fill_keys( $languages, false ); foreach ( $languages as $lang_code ) { $item->object_type = property_exists( $item, 'object_type' ) ? $item->object_type : $item->type; $translated_object_id = (int) icl_object_id( $item->object_type === 'post_type_archive' ? $item->ID : $item->object_id, Lst::includes( $item->object_type, [ 'custom', 'post_type_archive' ] ) ? 'nav_menu_item' : $item->object, false, $lang_code ); if ( ! $translated_object_id && $item->object_type !== 'custom' && $item->object_type !== 'post_type_archive' ) { continue; } $translated_object_title = ''; $translated_object_url = $item->url; $icl_st_label_exists = true; $icl_st_url_exists = true; $label_changed = false; $url_changed = false; if ( $item->object_type === 'post_type' ) { list( $translated_object_id, $item_translations ) = $this->maybe_reload_post_item( $translated_object_id, $item_translations, $item, $lang_code ); $translated_object = get_post( $translated_object_id ); if ( $translated_object->post_status === 'trash' ) { $translated_object_id = false; } else { $translated_object_title = $translated_object->post_title; } } elseif ( $item->object_type === 'taxonomy' ) { $translated_object = get_term( $translated_object_id, get_post_meta( $item->ID, '_menu_item_object', true ) ); $translated_object_title = $translated_object->name; } elseif ( $item->object_type === 'custom' ) { $translated_object_title = $item->post_title; if ( defined( 'WPML_ST_PATH' ) ) { list( $translated_object_url, $translated_object_title, $url_changed, $label_changed ) = $this->st_actions( $lang_code, $menu_id, $item, $translated_object_id, $translated_object_title, $translated_object_url, $icl_st_label_exists, $icl_st_url_exists ); } } elseif ( $item->object_type === 'post_type_archive' ) { if ( $translated_object_id ) { $translated_object = get_post( $translated_object_id ); $translated_object_title = $translated_object->post_title; } else { $translated_object_title = $item->post_title; } } $this->fix_assignment_to_menu( $item_translations, (int) $menu_id ); $this->fix_language_conflicts(); $translated_item_id = isset( $item_translations[ $lang_code ] ) ? (int) $item_translations[ $lang_code ] : false; $item_depth = $this->get_menu_item_depth( $translated_item_id ); if ( $translated_item_id ) { $translated_item = get_post( $translated_item_id ); $translated_object_title = ! empty( $translated_item->post_title ) && ! $icl_st_label_exists ? $translated_item->post_title : $translated_object_title; $translate_item_parent_item_id = (int) get_post_meta( $translated_item_id, '_menu_item_menu_item_parent', true ); if ( $item->menu_item_parent > 0 && $translate_item_parent_item_id != $this->post_translations->element_id_in( $item->menu_item_parent, $lang_code ) ) { $translate_item_parent_item_id = 0; $item_depth = 0; } $translation = array( 'menu_order' => $translated_item->menu_order, 'parent' => $translate_item_parent_item_id, ); } else { $translation = array( 'menu_order' => ( $item->object_type === 'custom' ? $item->menu_order : 0 ), 'parent' => 0, ); } $translation['ID'] = $translated_item_id; $translation['depth'] = $item_depth; $translation['parent_not_translated'] = $this->is_parent_not_translated( $item, $lang_code ); $translation['object'] = $item->object; $translation['object_type'] = $item->object_type; $translation['object_id'] = $translated_object_id; $translation['title'] = $translated_object_title; $translation['url'] = $translated_object_url; $translation['target'] = $item->target; $translation['classes'] = $item->classes; $translation['xfn'] = $item->xfn; $translation['attr-title'] = $item->attr_title; $translation['label_changed'] = $label_changed; $translation['url_changed'] = $url_changed; $translation['label_missing'] = ! $icl_st_label_exists; $translation['url_missing'] = ! $icl_st_url_exists; $translations[ $lang_code ] = $translation; } return $translations; } /** * Synchronises a page menu item's translations' trids according to the trids of the pages they link to. * * @param object $menu_item * * @return int number of affected menu item translations */ function sync_page_menu_item_trids( $menu_item ) { $changed = 0; if ( $menu_item->object_type === 'post_type' ) { $translations = $this->post_translations->get_element_translations( $menu_item->ID ); if ( (bool) $translations === true ) { get_post_meta( $menu_item->menu_item_parent, '_menu_item_object_id', true ); $orphans = $this->wpdb->get_results( $this->get_page_orphan_sql( array_keys( $translations ), $menu_item->ID ) ); if ( (bool) $orphans === true ) { $trid = $this->post_translations->get_element_trid( $menu_item->ID ); foreach ( $orphans as $orphan ) { $this->sitepress->set_element_language_details( $orphan->element_id, 'post_nav_menu_item', $trid, $orphan->language_code ); $changed ++; } } } } return $changed; } /** * @param int $menu_id * @param bool $include_original * * @return bool|array */ function get_menu_translations( $menu_id, $include_original = false ) { $languages = array_keys( $this->sitepress->get_active_languages() ); $translations = array(); foreach ( $languages as $lang_code ) { if ( $include_original || $lang_code !== $this->sitepress->get_default_language() ) { $menu_translated_id = $this->term_translations->term_id_in( $menu_id, $lang_code ); $menu_data = array(); if ( $menu_translated_id ) { /** @var \stdClass $menu_object */ $menu_object = $this->wpdb->get_row( $this->wpdb->prepare( " SELECT t.term_id, t.name FROM {$this->wpdb->terms} t JOIN {$this->wpdb->term_taxonomy} x ON t.term_id = t.term_id WHERE t.term_id = %d AND x.taxonomy='nav_menu' LIMIT 1", $menu_translated_id ) ); $current_lang = $this->sitepress->get_current_language(); $this->sitepress->switch_lang( $lang_code, false ); $menu_data = array( 'id' => $menu_object->term_id, 'name' => $menu_object->name, 'items' => $this->get_menu_items( $menu_translated_id, false ), ); $this->sitepress->switch_lang( $current_lang, false ); } $translations[ $lang_code ] = $menu_data; } } return $translations; } protected function get_menu_name( $menu_id ) { $menu = wp_get_nav_menu_object( $menu_id ); return $menu ? $menu->name : false; } /** * @param int $menu_id * @param string|false $language_code * * @return bool */ protected function get_translated_menu( $menu_id, $language_code = false ) { $language_code = $language_code ? $language_code : $this->sitepress->get_default_language(); $menus = $this->get_menu_translations( $menu_id, true ); return isset( $menus[ $language_code ] ) ? $menus[ $language_code ] : false; } /** * We need to register the string first in the default language * to avoid it being "auto-registered" in English * * @param string $menu_name * @param WP_Post|stdClass $item * @param string $lang * @param bool $has_label_translation * @param bool $has_url_translation * * @return array */ protected function icl_t_menu_item( $menu_name, $item, $lang, &$has_label_translation, &$has_url_translation ) { $default_lang = $this->sitepress->get_default_language(); $label = $item->post_title; $url = $item->url; if ( $lang !== $default_lang ) { icl_register_string( $menu_name . self::STRING_CONTEXT_SUFFIX, self::STRING_NAME_LABEL_PREFIX . $item->ID, $label, false, $default_lang ); $label = icl_t( $menu_name . self::STRING_CONTEXT_SUFFIX, self::STRING_NAME_LABEL_PREFIX . $item->ID, $label, $has_label_translation, true, $lang ); icl_register_string( $menu_name . self::STRING_CONTEXT_SUFFIX, self::STRING_NAME_URL_PREFIX . $item->ID, $url, false, $default_lang ); $url = icl_t( $menu_name . self::STRING_CONTEXT_SUFFIX, self::STRING_NAME_URL_PREFIX . $item->ID, $url, $has_url_translation, true, $lang ); } return array( $label, $url ); } /** * @param object $item * @param string $lang_code * * @return int */ private function is_parent_not_translated( $item, $lang_code ) { if ( $item->menu_item_parent > 0 ) { $item_parent_object_id = get_post_meta( $item->menu_item_parent, '_menu_item_object_id', true ); $item_parent_object = get_post_meta( $item->menu_item_parent, '_menu_item_object', true ); $parent_element_type = $item_parent_object === 'custom' ? 'nav_menu_item' : $item_parent_object; $parent_translated = icl_object_id( $item_parent_object_id, $parent_element_type, false, $lang_code ); } return isset( $parent_translated ) && ! $parent_translated ? 1 : 0; } private function get_page_orphan_sql( $existing_languages, $menu_item_id ) { $wpdb = &$this->wpdb; return $wpdb->prepare( "SELECT it.element_id, it.language_code FROM {$wpdb->prefix}icl_translations it JOIN {$wpdb->posts} pt ON pt.ID = it.element_id AND pt.post_type = 'nav_menu_item' AND it.element_type = 'post_nav_menu_item' AND it.language_code NOT IN (" . wpml_prepare_in( $existing_languages ) . ") JOIN {$wpdb->prefix}icl_translations io ON io.element_id = %d AND io.element_type = 'post_nav_menu_item' AND io.trid != it.trid JOIN {$wpdb->posts} po ON po.ID = io.element_id AND po.post_type = 'nav_menu_item' JOIN {$wpdb->postmeta} mo ON mo.post_id = po.ID AND mo.meta_key = '_menu_item_object_id' JOIN {$wpdb->postmeta} mt ON mt.post_id = pt.ID AND mt.meta_key = '_menu_item_object_id' JOIN {$wpdb->prefix}icl_translations page_t ON mt.meta_value = page_t.element_id AND page_t.element_type = 'post_page' JOIN {$wpdb->prefix}icl_translations page_o ON mo.meta_value = page_o.element_id AND page_o.trid = page_t.trid WHERE ( SELECT COUNT(count.element_id) FROM {$wpdb->prefix}icl_translations count WHERE count.trid = it.trid ) = 1", $menu_item_id ); } private function maybe_reload_post_item( $translated_object_id, $item_translations, $item, $lang_code ) { if ( $this->sync_page_menu_item_trids( $item ) > 0 ) { $item_translations = $this->post_translations->get_element_translations( $item->ID ); $translated_object_id = $this->post_translations->element_id_in( $item->object_id, $lang_code ); $translated_object_id = $translated_object_id === null ? false : $translated_object_id; } return array( $translated_object_id, $item_translations ); } private function get_menu_item_depth( $item_id ) { $depth = 0; do { $object_parent = get_post_meta( $item_id, '_menu_item_menu_item_parent', true ); if ( $object_parent == $item_id ) { $depth = 0; break; } elseif ( $object_parent ) { $item_id = $object_parent; $depth ++; } } while ( $object_parent > 0 ); return $depth; } private function st_actions( $lang_code, $menu_id, $item, $translated_object_id, $translated_object_title, $translated_object_url, &$icl_st_label_exists, &$icl_st_url_exists ) { if ( ! function_exists( 'icl_translate' ) ) { require WPML_ST_PATH . '/inc/functions.php'; } $this->sitepress->switch_lang( $lang_code ); $label_changed = false; $url_changed = false; $menu_name = $this->get_menu_name( $menu_id ); $translated_object_title_t = ''; $translated_object_url_t = ''; $translated_menu_id = $this->term_translations->term_id_in( $menu_id, $lang_code ); if ( function_exists( 'icl_t' ) ) { list( $translated_object_title_t, $translated_object_url_t ) = $this->icl_t_menu_item( $menu_name, $item, $lang_code, $icl_st_label_exists, $icl_st_url_exists ); } else { $translated_object_title_t = $item->post_title . ' @' . $lang_code; $translated_object_url_t = $item->url; } $this->sitepress->switch_lang(); if ( $translated_object_id ) { $translated_object = get_post( $translated_object_id ); $label_changed = $translated_object_title_t != $translated_object->post_title; $url_changed = $translated_object_url_t != get_post_meta( $translated_object_id, '_menu_item_url', true ); $translated_object_title = $icl_st_label_exists ? $translated_object_title_t : $translated_object_title; $translated_object_url = $icl_st_url_exists ? $translated_object_url_t : $translated_object_url; } return array( $translated_object_url, $translated_object_title, $url_changed, $label_changed, ); } /** * @param array<string,int> $item_translations * @param int $menu_id */ private function fix_assignment_to_menu( $item_translations, $menu_id ) { foreach ( $item_translations as $lang_code => $item_id ) { $correct_menu_id = $this->term_translations->term_id_in( $menu_id, $lang_code ); if ( $correct_menu_id ) { $ttid_trans = $this->wpdb->get_var( $this->wpdb->prepare( " SELECT tt.term_taxonomy_id FROM {$this->wpdb->term_taxonomy} tt LEFT JOIN {$this->wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tr.object_id = %d WHERE tt.taxonomy = 'nav_menu' AND tt.term_id = %d AND tr.term_taxonomy_id IS NULL LIMIT 1", $item_id, $correct_menu_id ) ); if ( $ttid_trans ) { $this->wpdb->insert( $this->wpdb->term_relationships, array( 'object_id' => $item_id, 'term_taxonomy_id' => $ttid_trans, ) ); } } } } /** * Removes potentially mis-assigned menu items from their menu, whose language differs from that of their * associated menu. */ private function fix_language_conflicts() { $wrong_items = $this->wpdb->get_results( " SELECT r.object_id, t.term_taxonomy_id FROM {$this->wpdb->term_relationships} r JOIN {$this->wpdb->prefix}icl_translations ip JOIN {$this->wpdb->posts} p ON ip.element_type = CONCAT('post_', p.post_type) AND ip.element_id = p.ID AND ip.element_id = r.object_id JOIN {$this->wpdb->prefix}icl_translations it JOIN {$this->wpdb->term_taxonomy} t ON it.element_type = CONCAT('tax_', t.taxonomy) AND it.element_id = t.term_taxonomy_id AND it.element_id = r.term_taxonomy_id WHERE p.post_type = 'nav_menu_item' AND t.taxonomy = 'nav_menu' AND ip.language_code != it.language_code" ); foreach ( $wrong_items as $item ) { $this->wpdb->delete( $this->wpdb->term_relationships, array( 'object_id' => $item->object_id, 'term_taxonomy_id' => $item->term_taxonomy_id, ) ); } } } functions-load.php 0000755 00000033244 14720425051 0010213 0 ustar 00 <?php /** * @global \WPML_Term_Translation $wpml_language_resolution * @global \WPML_Slug_Filter $wpml_slug_filter * @global \WPML_Term_Translation $wpml_term_translations */ use WPML\FP\Obj; use WPML\LIB\WP\WPDB as WpWPDB; use function WPML\Container\make; require_once __DIR__ . '/wpml_load_request_handler.php'; function wpml_is_setup_complete() { $settings = WPML\LIB\WP\Option::getOrAttemptRecovery( 'icl_sitepress_settings', [] ); return is_array( $settings ) && Obj::prop( 'setup_complete', $settings ); } /** * Loads global variables providing functionality that is used throughout the plugin. * * @param null|bool $is_admin If set to `null` it will read from `is_admin()` */ function load_essential_globals( $is_admin = null ) { global $wpml_language_resolution, $wpml_term_translations, $wpdb; $settings = get_option( 'icl_sitepress_settings' ); if ( (bool) $settings === false ) { icl_sitepress_activate(); } else { if ( wpml_is_setup_complete() ) { $active_plugins = get_option( 'active_plugins' ); $wpmu_sitewide_plugins = (array) maybe_unserialize( get_site_option( 'active_sitewide_plugins' ) ); if ( in_array( WPML_PLUGIN_BASENAME, $active_plugins, true ) === false && in_array( WPML_PLUGIN_BASENAME, array_keys( $wpmu_sitewide_plugins ), true ) === false ) { // The plugin has just be reactivated. // reset ajx_health_flag // set the just_reactivated flag so any posts created while // WPML was not activate will get the default language // https://onthegosystems.myjetbrains.com/youtrack/issue/wpmlcore-1924 $settings['ajx_health_checked'] = 0; $settings['just_reactivated'] = 1; update_option( 'icl_sitepress_settings', $settings ); } } } $active_language_codes = isset( $settings['active_languages'] ) ? $settings['active_languages'] : array(); $active_language_codes = (bool) $active_language_codes === true ? $active_language_codes : wpml_reload_active_languages_setting(); $default_lang_code = isset( $settings['default_language'] ) ? $settings['default_language'] : false; $wpml_language_resolution = new WPML_Language_Resolution( $active_language_codes, $default_lang_code ); $admin = $is_admin === null ? is_admin() : $is_admin; wpml_load_post_translation( $admin, $settings ); $wpml_term_translations = new WPML_Term_Translation( $wpdb ); $wpml_term_translations->add_hooks(); $domain_validation = filter_input( INPUT_GET, '____icl_validate_domain' ) ? 1 : false; $domain_validation = filter_input( INPUT_GET, '____icl_validate_directory' ) ? 2 : $domain_validation; $url_converter = load_wpml_url_converter( $settings, $domain_validation, $default_lang_code ); $directory = 2 === $domain_validation || ( is_multisite() && ! is_subdomain_install() ); if ( $domain_validation ) { echo wpml_validate_host( $_SERVER['REQUEST_URI'], $url_converter, $directory ); exit; } if ( $admin ) { wpml_load_admin_files(); } } function wpml_load_post_translation( $is_admin, $settings ) { global $wpml_post_translations, $wpdb; $http_referer = make( WPML_URL_HTTP_Referer::class ); if ( $is_admin === true || $http_referer->is_rest_request_called_from_post_edit_page() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $wpml_post_translations = new WPML_Admin_Post_Actions( $settings, $wpdb ); } else { $wpml_post_translations = new WPML_Frontend_Post_Actions( $settings, $wpdb ); wpml_load_frontend_tax_filters(); } $wpml_post_translations->init(); } function wpml_load_query_filter( $installed ) { global $wpml_query_filter, $sitepress, $wpdb, $wpml_post_translations, $wpml_term_translations; $wpml_query_filter = $wpml_query_filter ? $wpml_query_filter : new WPML_Query_Filter( $sitepress, $wpdb, $wpml_post_translations, $wpml_term_translations ); if ( $installed ) { if ( ! has_filter( 'posts_join', array( $wpml_query_filter, 'posts_join_filter' ) ) ) { add_filter( 'posts_join', array( $wpml_query_filter, 'posts_join_filter' ), 10, 2 ); add_filter( 'posts_where', array( $wpml_query_filter, 'posts_where_filter' ), 10, 2 ); } } } function load_wpml_url_converter( $settings, $domain_validation, $default_lang_code ) { /** * @var WPML_URL_Converter $wpml_url_converter * @var WPML_Language_Resolution $wpml_language_resolution */ global $wpml_url_converter, $wpml_language_resolution; $url_type = isset( $settings['language_negotiation_type'] ) ? $settings['language_negotiation_type'] : false; $url_type = $domain_validation ? $domain_validation : $url_type; $active_language_codes = $wpml_language_resolution->get_active_language_codes(); WPML_URL_Converter_Factory::remove_previous_hooks(); $factory = new WPML_URL_Converter_Factory( $settings, $default_lang_code, $active_language_codes ); $wpml_url_converter = $factory->create( (int) $url_type ); return $wpml_url_converter; } /** * @param string $req_uri * @param WPML_URL_Converter $wpml_url_converter * @param bool $directory * * @return string */ function wpml_validate_host( $req_uri, $wpml_url_converter, $directory = true ) { if ( $directory === true ) { $req_uri_parts = array_filter( explode( '/', $req_uri ) ); $lang_slug = array_pop( $req_uri_parts ); if ( strpos( $lang_slug, '?' ) === 0 ) { $lang_slug = array_pop( $req_uri_parts ); } elseif ( strpos( $lang_slug, '?' ) !== false ) { $parts = explode( '?', $lang_slug ); $lang_slug = array_shift( $parts ); } } else { $lang_slug = ''; } return '<!--' . esc_url( untrailingslashit( trailingslashit( $wpml_url_converter->get_abs_home() ) . $lang_slug ) ) . '-->'; } /** * Checks if a given taxonomy is currently translated * * @param string $taxonomy name/slug of a taxonomy * @return bool true if the taxonomy is currently set to being translatable in WPML */ function is_taxonomy_translated( $taxonomy ) { return in_array( $taxonomy, array( 'category', 'post_tag', 'nav_menu' ), true ) || in_array( $taxonomy, array_keys( array_filter( icl_get_setting( 'taxonomies_sync_option', array() ) ) ) ); } /** * Checks if a given post_type is currently translated * * @param string $post_type name/slug of a post_type * @return bool true if the post_type is currently set to being translatable in WPML */ function is_post_type_translated( $post_type ) { return 'nav_menu_item' === $post_type || in_array( $post_type, array_keys( array_filter( icl_get_setting( 'custom_posts_sync_option', array() ) ) ) ); } function setup_admin_menus() { global $pagenow, $sitepress; if ( $pagenow === 'edit-tags.php' || $pagenow === 'term.php' ) { maybe_load_translated_tax_screen(); $wpml_term_translation_help_notice = new WPML_Taxonomy_Translation_Help_Notice( wpml_get_admin_notices(), $sitepress ); $wpml_term_translation_help_notice->add_hooks(); } } function maybe_load_translated_tax_screen() { $taxonomy_get = (string) filter_input( INPUT_GET, 'taxonomy' ); $taxonomy_get = $taxonomy_get ? $taxonomy_get : 'post_tag'; if ( is_taxonomy_translated( $taxonomy_get ) ) { global $wpdb, $sitepress; require WPML_PLUGIN_PATH . '/menu/term-taxonomy-menus/wpml-tax-menu-loader.class.php'; new WPML_Tax_Menu_Loader( $wpdb, $sitepress, $taxonomy_get ); } } /** * @param bool $override * * @return array */ function wpml_reload_active_languages_setting( $override = false ) { global $wpdb, $sitepress_settings; if ( true === (bool) $sitepress_settings && ( $override || wpml_is_setup_complete() ) ) { // This won't output any MySQL error if `icl_languages` is missing on the // current site, and it will return an empty array in that case. $active_languages = WpWPDB::withoutError( function() use ( $wpdb ) { return $wpdb->get_col( " SELECT code FROM {$wpdb->prefix}icl_languages WHERE active = 1 " ); } ); $sitepress_settings['active_languages'] = $active_languages; icl_set_setting( 'active_languages', $active_languages, true ); } else { $active_languages = []; } return (array) $active_languages; } /** * Returns and if necessary instantiates an instance of the WPML_Installation Class * * @return \WPML_Installation */ function wpml_get_setup_instance() { global $wpml_installation, $wpdb, $sitepress; if ( ! isset( $wpml_installation ) ) { $wpml_installation = new WPML_Installation( $wpdb, $sitepress ); } return $wpml_installation; } function wpml_load_admin_files() { require_once WPML_PLUGIN_PATH . '/menu/wpml-troubleshooting-terms-menu.class.php'; require_once WPML_PLUGIN_PATH . '/inc/wpml-post-edit-ajax.class.php'; require_once WPML_PLUGIN_PATH . '/menu/wpml-post-status-display.class.php'; require_once WPML_PLUGIN_PATH . '/inc/utilities/wpml-color-picker.class.php'; } function wpml_get_post_status_helper() { global $wpml_post_status, $wpdb, $sitepress; if ( ! isset( $wpml_post_status ) ) { $wpml_post_status = new WPML_Post_Status( $wpdb, $sitepress->get_wp_api() ); } return $wpml_post_status; } function wpml_get_create_post_helper() { global $sitepress; return new WPML_Create_Post_Helper( $sitepress ); } /** * @return \TranslationManagement */ function wpml_load_core_tm() { global $iclTranslationManagement; if ( ! isset( $iclTranslationManagement ) ) { require_once WPML_PLUGIN_PATH . '/inc/translation-management/translation-management.class.php'; $iclTranslationManagement = new TranslationManagement(); } return $iclTranslationManagement; } function wpml_get_langs_in_dirs_val( $wpml_url_converter ) { global $sitepress; return new WPML_Lang_URL_Validator( $wpml_url_converter, $sitepress ); } function wpml_get_root_page_actions_obj() { global $wpml_root_page_actions, $sitepress_settings; if ( ! isset( $wpml_root_page_actions ) ) { require_once WPML_PLUGIN_PATH . '/inc/post-translation/wpml-root-page-actions.class.php'; $wpml_root_page_actions = new WPML_Root_Page_Actions( $sitepress_settings ); } return $wpml_root_page_actions; } function wpml_get_hierarchy_sync_helper( $type = 'post' ) { global $wpdb; if ( $type === 'post' ) { require_once WPML_PLUGIN_PATH . '/inc/post-translation/wpml-post-hierarchy-sync.class.php'; $hierarchy_helper = new WPML_Post_Hierarchy_Sync( $wpdb ); } elseif ( $type === 'term' ) { require_once WPML_PLUGIN_PATH . '/inc/taxonomy-term-translation/wpml-term-hierarchy-sync.class.php'; $hierarchy_helper = new WPML_Term_Hierarchy_Sync( $wpdb ); } else { $hierarchy_helper = false; } return $hierarchy_helper; } function wpml_maybe_setup_post_edit() { global $pagenow, $sitepress, $post_edit_screen; if ( in_array( $pagenow, [ 'post.php', 'post-new.php', 'edit.php' ], true ) || defined( 'DOING_AJAX' ) || apply_filters( 'wpml_enable_language_meta_box', false ) ) { $post_edit_screen = new WPML_Post_Edit_Screen( $sitepress ); add_action( 'admin_head', [ $sitepress, 'post_edit_language_options' ] ); } } /** * @return \WPML_Frontend_Tax_Filters */ function wpml_load_frontend_tax_filters() { global $wpml_term_filters; if ( ! isset( $wpml_term_filters ) ) { require WPML_PLUGIN_PATH . '/inc/taxonomy-term-translation/wpml-frontend-tax-filters.class.php'; $wpml_term_filters = new WPML_Frontend_Tax_Filters(); } return $wpml_term_filters; } /** * @return \WPML_Settings_Helper */ function wpml_load_settings_helper() { global $wpml_settings_helper, $sitepress, $wpml_post_translations; if ( ! isset( $wpml_settings_helper ) ) { $wpml_settings_helper = new WPML_Settings_Helper( $wpml_post_translations, $sitepress ); } return $wpml_settings_helper; } function wpml_get_term_translation_util() { global $sitepress; require_once WPML_PLUGIN_PATH . '/inc/taxonomy-term-translation/wpml-term-translation-utils.class.php'; return new WPML_Term_Translation_Utils( $sitepress ); } /** * @return \WPML_Term_Filters */ function wpml_load_term_filters() { global $wpml_term_filters_general, $sitepress, $wpdb; if ( ! isset( $wpml_term_filters_general ) ) { require WPML_PLUGIN_PATH . '/inc/taxonomy-term-translation/wpml-term-filters.class.php'; $wpml_term_filters_general = new WPML_Term_Filters( $wpdb, $sitepress ); $wpml_term_filters_general->init(); } return $wpml_term_filters_general; } function wpml_show_user_options() { global $sitepress, $current_user, $user_id, $pagenow; if ( ! isset( $user_id ) && 'profile.php' === $pagenow ) { $user_id = $current_user->ID; } $user = new WP_User( $user_id ); $user_options_menu = new WPML_User_Options_Menu( $sitepress, $user ); echo $user_options_menu->render(); } /** * @return \WPML_Upgrade_Command_Factory */ function wpml_get_upgrade_command_factory() { static $factory; if ( ! $factory ) { $factory = new WPML_Upgrade_Command_Factory(); } return $factory; } function wpml_get_upgrade_schema() { global $wpdb; static $instance; if ( ! $instance ) { $instance = new WPML_Upgrade_Schema( $wpdb ); } return $instance; } /** * @param string $class_name A class implementing \IWPML_Upgrade_Command. * @param array $dependencies An array of dependencies passed to the `$class_name`'s constructor. * @param array $scopes An array of scope values. Accepted values are: `\WPML_Upgrade::SCOPE_ADMIN`, `\WPML_Upgrade::SCOPE_AJAX`, and `\WPML_Upgrade::SCOPE_FRONT_END`. * @param string|null $method The method to call to run the upgrade (otherwise, it calls the "run" method), * * @return \WPML_Upgrade_Command_Definition */ function wpml_create_upgrade_command_definition( $class_name, array $dependencies, array $scopes, $method = null ) { return wpml_get_upgrade_command_factory()->create_command_definition( $class_name, $dependencies, $scopes, $method ); } if ( is_admin() ) { add_action( 'personal_options', 'wpml_show_user_options' ); } // TM require_once 'functions-load-tm.php'; cache.php 0000755 00000010554 14720425051 0006330 0 ustar 00 <?php if ( ! defined( 'ICL_DISABLE_CACHE' ) ) { define( 'ICL_DISABLE_CACHE', false ); } // phpcs:disable PEAR.NamingConventions.ValidClassName.Invalid // phpcs:disable PEAR.NamingConventions.ValidClassName.StartWithCapital /** * Class icl_cache */ class icl_cache { /** @var array */ protected $data; /** @var string */ protected $name; /** @var bool */ protected $cache_to_option; /** @var bool */ protected $cache_needs_saving; public function __construct( $name = '', $cache_to_option = false ) { $this->data = []; $this->name = $name; $this->cache_to_option = $cache_to_option; $this->cache_needs_saving = false; $this->init(); } public function init() { if ( $this->cache_to_option ) { $this->data = icl_cache_get( $this->name . '_cache_class' ); if ( false === $this->data ) { $this->data = []; } add_action( 'shutdown', [ $this, 'save_cache_if_required' ] ); } } public function save_cache_if_required() { if ( $this->cache_needs_saving ) { icl_cache_set( $this->name . '_cache_class', $this->data ); $this->cache_needs_saving = false; } } public function get( $key ) { if ( ICL_DISABLE_CACHE ) { return null; } return isset( $this->data[ $key ] ) ? $this->data[ $key ] : false; } public function has_key( $key ) { if ( ICL_DISABLE_CACHE ) { return false; } return array_key_exists( $key, (array) $this->data ); } public function set( $key, $value ) { if ( ICL_DISABLE_CACHE ) { return; } if ( $this->cache_to_option ) { $old_value = null; if ( isset( $this->data[ $key ] ) ) { $old_value = $this->data[ $key ]; } if ( $old_value !== $value ) { $this->data[ $key ] = $value; $this->cache_needs_saving = true; } } else { $this->data[ $key ] = $value; } } public function clear() { $this->data = array(); if ( $this->cache_to_option ) { icl_cache_clear( $this->name . '_cache_class' ); } } } if ( ! function_exists( 'icl_disable_cache' ) ) { function icl_disable_cache() { return defined( 'ICL_DISABLE_CACHE' ) && ICL_DISABLE_CACHE; } } if ( ! function_exists( 'icl_cache_get' ) ) { function icl_cache_get( $key ) { $result = false; if ( ! icl_disable_cache() ) { $icl_cache = get_option( '_icl_cache' ); $result = isset( $icl_cache[ $key ] ) ? $icl_cache[ $key ] : false; } return $result; } } if ( ! function_exists( 'icl_cache_set' ) ) { function icl_cache_set( $key, $value = null ) { global $switched; if ( empty( $switched ) && ! icl_disable_cache() ) { $icl_cache = get_option( '_icl_cache' ); if ( false === $icl_cache ) { $icl_cache = []; delete_option( '_icl_cache' ); } if ( ! isset( $icl_cache[ $key ] ) || $icl_cache[ $key ] !== $value ) { if ( ! is_null( $value ) ) { $icl_cache[ $key ] = $value; } elseif ( isset( $icl_cache[ $key ] ) ) { unset( $icl_cache[ $key ] ); } update_option( '_icl_cache', $icl_cache, 'no' ); } } } } if ( ! function_exists( 'icl_cache_clear' ) ) { function icl_cache_clear( $key = false, $key_as_prefix = false ) { if ( ! icl_disable_cache() ) { /** * @var WPML_Term_Translation $wpml_term_translations * @var WPML_Post_Translation $wpml_post_translations */ global $wpml_term_translations, $wpml_post_translations; $wpml_term_translations->reload(); $wpml_post_translations->reload(); if ( false === $key ) { delete_option( '_icl_cache' ); } else { /** @var array $icl_cache */ $icl_cache = get_option( '_icl_cache' ); if ( is_array( $icl_cache ) ) { if ( isset( $icl_cache[ $key ] ) ) { unset( $icl_cache[ $key ] ); } if ( $key_as_prefix ) { $cache_keys = array_keys( $icl_cache ); foreach ( $cache_keys as $cache_key ) { if ( strpos( $cache_key, $key ) === 0 ) { unset( $icl_cache[ $cache_key ] ); } } } // Special cache of 'per language' - clear different statuses. if ( false !== strpos( $key, '_per_language' ) ) { foreach ( $icl_cache as $k => $v ) { if ( false !== strpos( $k, $key . '#' ) ) { unset( $icl_cache[ $k ] ); } } } update_option( '_icl_cache', $icl_cache, 'no' ); } } } do_action( 'wpml_cache_clear' ); } } function w3tc_translate_cache_key_filter( $key ) { global $sitepress; return $sitepress->get_current_language() . $key; } query-filtering/wpml-slug-resolution.class.php 0000755 00000003216 14720425051 0015644 0 ustar 00 <?php /** * Class WPML_Slug_Resolution * * @package wpml-core * @subpackage post-translation */ abstract class WPML_Slug_Resolution extends WPML_WPDB_And_SP_User { const WPML_BACKUP_KEY = '_wpml_backup'; /** * Returns all active language codes ordered by the language order, but having the current language * at the beginning. * * @return string[] * * @uses \SitePress::get_setting to get the languages order from the sitepress settings */ protected function get_ordered_langs() { $lang_order = $this->sitepress->get_setting( 'languages_order' ); $lang_order = $lang_order ? $lang_order : array_keys( $this->sitepress->get_active_languages() ); $lang_order = is_array( $lang_order ) ? $lang_order : array(); array_unshift( $lang_order, $this->sitepress->get_current_language() ); return array_unique( $lang_order ); } /** * @param string $key * @param WP_Query $wp_query */ protected function set_query_var_to_restore( $key, WP_Query $wp_query ) { $wp_query->query_vars[ self::WPML_BACKUP_KEY ][ $key ] = $wp_query->query_vars[ $key ]; add_filter( 'the_posts', array( $this, 'restore_query_vars' ), 10, 2 ); } /** * @param WP_Post[] $posts * @param WP_Query $wp_query * * @return mixed */ public function restore_query_vars( $posts, $wp_query ) { if ( isset( $wp_query->query_vars[ self::WPML_BACKUP_KEY ] ) ) { foreach ( $wp_query->query_vars[ self::WPML_BACKUP_KEY ] as $key => $value ) { $wp_query->query_vars[ $key ] = $value; } unset( $wp_query->query_vars[ self::WPML_BACKUP_KEY ] ); remove_filter( 'the_posts', array( $this, 'restore_query_vars' ), 10 ); } return $posts; } } query-filtering/wpml-name-query-filter-translated.class.php 0000755 00000005436 14720425051 0020204 0 ustar 00 <?php /** * Class WPML_Name_Query_Filter_Translated * * @package wpml-core * @subpackage post-translation * * @since 3.2.3 */ class WPML_Name_Query_Filter_Translated extends WPML_Name_Query_Filter { private $pages_to_langs = array(); /** * @param array $pages_with_name * * @return int|null|string */ protected function select_best_match( $pages_with_name ) { if ( ! empty( $pages_with_name['matching_ids'] ) ) { $matching_page = $this->get_matching_page_in_requested_lang( $pages_with_name['matching_ids'] ); if ( $matching_page ) { return $matching_page; } $display_as_translated_page = $this->get_matching_page_displayed_as_translated(); if ( $display_as_translated_page ) { return $display_as_translated_page; } } if ( ! empty( $pages_with_name['related_ids'] ) ) { return $this->get_the_best_related_page_to_redirect( $pages_with_name['related_ids'] ); } return null; } /** * @param array $matching_ids * * @return int|null */ private function get_matching_page_in_requested_lang( array $matching_ids ) { foreach ( $matching_ids as $matching_id ) { $page_lang = $this->post_translation->get_element_lang_code( (int) $matching_id ); if ( $this->sitepress->get_current_language() === $page_lang ) { return (int) $matching_id; } $this->pages_to_langs[ $matching_id ] = $page_lang; } return null; } /** * @return int|null */ private function get_matching_page_displayed_as_translated() { foreach ( $this->pages_to_langs as $page_id => $lang ) { if ( $lang === $this->sitepress->get_default_language() && $this->sitepress->is_display_as_translated_post_type( get_post_type( $page_id ) ) ) { return $this->post_translation->element_id_in( $page_id, $this->sitepress->get_current_language(), true ); } } return null; } /** * Note: `$this->active_languages` is already ordered in `get_ordered_langs` * * @param array $related_page_ids * * @return int|null */ private function get_the_best_related_page_to_redirect( array $related_page_ids ) { foreach ( $this->active_languages as $lang_code ) { foreach ( $related_page_ids as $related_page_id ) { $page_lang = $this->post_translation->get_element_lang_code( (int) $related_page_id ); if ( $page_lang === $lang_code ) { return $related_page_id; } } } return null; } /** * Returns a SQL snippet for joining the posts table with icl translations filtered for the post_type * of this class. * * @return string */ protected function get_from_join_snippet() { return " FROM {$this->wpdb->posts} p JOIN {$this->wpdb->prefix}icl_translations wpml_translations ON p.ID = wpml_translations.element_id AND wpml_translations.element_type = CONCAT('post_', p.post_type ) "; } } query-filtering/wpml-pagename-query-filter.class.php 0000755 00000003247 14720425051 0016700 0 ustar 00 <?php /** * Class WPML_Page_Name_Query_Filter * * @package wpml-core * @subpackage post-translation * * @since 3.2 */ class WPML_Page_Name_Query_Filter extends WPML_Name_Query_Filter_Translated { protected $id_index = 'page_id'; /** * @param SitePress $sitepress * @param WPML_Post_Translation $post_translations * @param wpdb $wpdb */ public function __construct( &$sitepress, &$post_translations, &$wpdb ) { parent::__construct( 'page', $sitepress, $post_translations, $wpdb ); $this->indexes = array( 'name', 'pagename' ); } /** * @param WP_Query $page_query * @param int $pid * @param string $index * * @return WP_Query */ protected function maybe_adjust_query_by_pid( $page_query, $pid, $index ) { $page_query = parent::maybe_adjust_query_by_pid( $page_query, $pid, $index ); $is_page_for_posts = 'page' == get_option( 'show_on_front' ) && (int) $pid === (int) get_option( 'page_for_posts' ); if ( $is_page_for_posts ) { $page_query->query_vars['post_status'] = 'publish'; $page_query->query_vars['name'] = ''; $page_query->query_vars['page_id'] = 0; $page_query->post_status = 'publish'; $page_query->is_page = false; $page_query->is_singular = false; $page_query->is_home = true; $page_query->is_posts_page = true; } return $page_query; } /** * Called when the post id is being adjusted. Can be overridden. * * @param WP_Query $page_query * * @return WP_Query */ protected function adjusting_id( $page_query ) { $page_query->is_page = true; return $page_query; } } query-filtering/wpml-name-query-filter.class.php 0000755 00000021455 14720425051 0016044 0 ustar 00 <?php /** * Class WPML_Name_Query_Filter * * @package wpml-core * @subpackage post-translation * * @since 3.2.3 */ abstract class WPML_Name_Query_Filter extends WPML_Slug_Resolution { /** @var string $post_type */ protected $post_type; /** @var string[] $indexes */ protected $indexes = array( 'name' ); /** @var string $id_index */ protected $id_index = 'p'; /** @var string[] $active_languages */ protected $active_languages = array(); /** @var string $al_regexp */ protected $al_regexp; /** @var WPML_Post_Translation $post_translation */ protected $post_translation; protected $is_translated; /** * @param string $post_type * @param SitePress $sitepress * @param WPML_Post_Translation $post_translations * @param wpdb $wpdb */ public function __construct( $post_type, &$sitepress, &$post_translations, &$wpdb ) { parent::__construct( $wpdb, $sitepress ); $this->post_type = $post_type; $this->indexes[] = $post_type; $this->is_translated = $this->sitepress->is_translated_post_type( $post_type ); $this->post_translation = &$post_translations; } /** * Looks through the "name" and "pagename" query vars in a given query and identifies the correct page_id * corresponding to either of these two and then adjusts the query page_id to point at this correct page_id. * * @param WP_Query $page_query * * @return array * - WP_Query that uses the id index stored in \WPML_Name_Query_Filter::$id_index * instead of "name" or "pagename" in case a match was found, otherwise * returns the input query unaltered. * - int|false the page ID */ public function filter_page_name( WP_Query $page_query ) { $this->active_languages = $this->get_ordered_langs(); $this->al_regexp = $this->generate_al_regexp( $this->active_languages ); foreach ( $this->indexes as $index ) { list( $pages_with_name, $page_name_for_query ) = $this->query_needs_adjustment( $page_query, $index ); if ( ! empty( $pages_with_name['matching_ids'] ) || ! empty( $pages_with_name['related_ids'] ) ) { $pid = $this->select_best_match( $pages_with_name ); if ( isset( $pid ) ) { if ( ! isset( $page_query->queried_object_id ) || $pid != $page_query->queried_object_id ) { $page_query = $this->maybe_adjust_query_by_pid( $page_query, $pid, $index ); break; } else { unset( $pid ); } } elseif ( (bool) $page_name_for_query === true ) { $page_query->query_vars[ $index ] = $page_name_for_query; $page_query->query_vars['post_type'] = $this->post_type; } } } return array( $page_query, isset( $pid ) ? $pid : false ); } abstract protected function select_best_match( $pages_with_name ); /** * @param WP_Query $page_query * @param int $pid * @param string $index * * @return WP_Query */ protected function maybe_adjust_query_by_pid( $page_query, $pid, $index ) { if ( ! ( isset( $page_query->queried_object ) && isset( $page_query->queried_object->ID ) && (int) $page_query->queried_object->ID === (int) $pid ) ) { if ( isset( $page_query->query['page_id'] ) ) { $page_query->query['page_id'] = (int) $pid; } $page_query->query_vars['page_id'] = null; $page_query->query_vars[ $this->id_index ] = (int) $pid; if ( isset( $page_query->query[ $this->id_index ] ) ) { $page_query->query[ $this->id_index ] = (int) $pid; } $page_query->is_page = false; if ( isset( $page_query->query_vars[ $this->post_type ] ) && $this->post_type !== 'page' ) { $this->set_query_var_to_restore( $this->post_type, $page_query ); unset( $page_query->query_vars[ $this->post_type ] ); } unset( $page_query->query_vars[ $index ] ); if ( ! empty( $page_query->queried_object ) ) { $page_query->queried_object = get_post( $pid ); $page_query->queried_object_id = (int) $pid; } $page_query = $this->adjusting_id( $page_query ); } return $page_query; } /** * Called when the post id is being adjusted. Can be overridden. * * @param WP_Query $page_query * * @return WP_Query */ protected function adjusting_id( $page_query ) { $page_query->is_single = true; return $page_query; } /** * Returns a SQL snippet for joining the posts table with icl translations filtered for the post_type * of this class. * * @return string */ abstract protected function get_from_join_snippet(); /** * Generates a regular expression matcher for matching language slugs in a URI * * @param string[] $active_language_codes * * @return string */ private function generate_al_regexp( $active_language_codes ) { return '/^(' . implode( '|', $active_language_codes ) . ')\//'; } /** * @param WP_Query $page_query * @param string $index * * @return array */ private function query_needs_adjustment( WP_Query $page_query, $index ) { if ( empty( $page_query->query_vars[ $index ] ) ) { $pages_with_name = false; $page_name_for_query = false; } else { $page_name_for_query = preg_replace( $this->al_regexp, '', $page_query->query_vars[ $index ] ); if ( $this->page_name_has_parent( $page_name_for_query ) ) { $pages_with_name = $this->get_multiple_slug_adjusted_IDs( explode( '/', $page_name_for_query ) ); } else { $pages_with_name = $this->get_single_slug_adjusted_IDs( $page_name_for_query, $this->get_post_parent_query_var( $page_query ) ); } } return array( $pages_with_name, $page_name_for_query ); } /** * @param string $page_name_for_query * * @return bool */ private function page_name_has_parent( $page_name_for_query ) { return false !== strpos( $page_name_for_query, '/' ); } /** * @param WP_Query $page_query * * @return int|string */ private function get_post_parent_query_var( WP_Query $page_query ) { $post_parent = 0; if ( isset( $page_query->query_vars['post_parent'] ) ) { $post_parent = $page_query->query_vars['post_parent']; } return $post_parent; } /** * @param string $page_name_for_query * @param string|int $post_parent * * @return array */ private function get_single_slug_adjusted_IDs( $page_name_for_query, $post_parent ) { $cache = new WPML_WP_Cache( get_class( $this ) ); $cache_key = 'get_single_slug_adjusted_IDs' . $this->post_type . $page_name_for_query . $post_parent; $found = false; $pages_with_name = $cache->get( $cache_key, $found ); if ( ! $found ) { $pages_with_name = $this->get_single_slug_adjusted_IDs_from_DB( $page_name_for_query, $post_parent ); $cache->set( $cache_key, $pages_with_name ); } return array( 'matching_ids' => $pages_with_name ); } /** * @param string $page_name_for_query * @param string|int $post_parent * * @return array */ private function get_single_slug_adjusted_IDs_from_DB( $page_name_for_query, $post_parent ) { $pages_with_name = $this->wpdb->get_col( $this->wpdb->prepare( ' SELECT ID ' . $this->get_from_join_snippet() . $this->get_where_snippet() . ' p.post_name = %s ORDER BY p.post_parent = %d DESC ', $page_name_for_query, $post_parent ) ); return $pages_with_name; } /** * @param string[] $slugs slugs that were queried for * * @return int[] page_ids ordered by their likelihood of correctly matching the query target, * derived from checking all slugs against the sits pages slugs as well as their parent slugs. * Elements at the beginning of the array are more correct than later elements, but the results * are not yet filtered for the correct language. * * @used-by \WPML_Page_Name_Query_Filter::filter_page_name to find the correct page_id corresponding to a set of slugs, * by filtering the results of this function by language of the * returned page_ids. */ private function get_multiple_slug_adjusted_IDs( $slugs ) { $parent_slugs = array_slice( $slugs, 0, - 1 ); /** @var array<object>|null $pages_with_name */ $pages_with_name = $this->wpdb->get_results( ' SELECT p.ID, p.post_name, p.post_parent, par.post_name as parent_name ' . $this->get_from_join_snippet() . " LEFT JOIN {$this->wpdb->posts} par ON par.ID = p.post_parent " . $this->get_where_snippet() . ' p.post_name IN (' . wpml_prepare_in( $slugs ) . ') ORDER BY par.post_name IN (' . wpml_prepare_in( $parent_slugs ) . ') DESC' ); if ( ! $pages_with_name ) { return []; } $query_scorer = new WPML_Score_Hierarchy( $pages_with_name, $slugs ); return $query_scorer->get_possible_ids_ordered(); } private function get_where_snippet() { return $this->wpdb->prepare( ' WHERE p.post_type = %s AND ', $this->post_type ); } } query-filtering/wpml-name-query-filter-untranslated.class.php 0000755 00000001405 14720425051 0020537 0 ustar 00 <?php /** * Class WPML_Name_Query_Filter_Untranslated * * @package wpml-core * @subpackage post-translation * * @since 3.2.3 */ class WPML_Name_Query_Filter_Untranslated extends WPML_Name_Query_Filter { protected function select_best_match( $pages_with_name ) { if ( ! empty( $pages_with_name['matching_ids'] ) ) { return reset( $pages_with_name['matching_ids'] ); } elseif ( ! empty( $pages_with_name['related_ids'] ) ) { return reset( $pages_with_name['related_ids'] ); } return null; } /** * Returns a SQL snippet for joining the posts table with icl translations filtered for the post_type * of this class. * * @return string */ protected function get_from_join_snippet() { return " FROM {$this->wpdb->posts} p "; } } post-translation/wpml-post-translation.class.php 0000755 00000033443 14720425051 0016212 0 ustar 00 <?php /** * Class WPML_Post_Translation * * @package wpml-core * @subpackage post-translation */ abstract class WPML_Post_Translation extends WPML_Element_Translation { protected $settings; protected $post_translation_sync; public static $defer_term_counting = false; /** * @var WPML_Debug_BackTrace */ private $debug_backtrace; /** * @param array $settings * @param wpdb $wpdb */ public function __construct( &$settings, &$wpdb ) { parent::__construct( $wpdb ); $this->settings = $settings; } protected function is_setup_complete( ) { return isset( $this->settings[ 'setup_complete' ]) && $this->settings[ 'setup_complete' ]; } public function init() { if ( $this->is_setup_complete() ) { add_action( 'save_post', [ $this, 'save_post_actions' ], 100, 2 ); add_action( 'shutdown', [ $this, 'shutdown_action' ], PHP_INT_MAX ); add_action( 'edit_attachment', [ $this, 'attachment_actions' ], 100 ); add_action( 'add_attachment', [ $this, 'attachment_actions' ], 100 ); } } public function get_original_post_status( $trid, $source_lang_code = null ) { return $this->get_original_post_attr ( $trid, 'post_status', $source_lang_code ); } public function get_original_post_ID( $trid, $source_lang_code = null ) { return $this->get_original_post_attr ( $trid, 'ID', $source_lang_code ); } public function get_original_menu_order ( $trid, $source_lang_code = null ) { return $this->get_original_post_attr ( $trid, 'menu_order', $source_lang_code ); } public function get_original_comment_status( $trid, $source_lang_code = null ) { return $this->get_original_post_attr ( $trid, 'comment_status', $source_lang_code ); } public function get_original_ping_status( $trid, $source_lang_code = null ) { return $this->get_original_post_attr ( $trid, 'ping_status', $source_lang_code ); } public function get_original_post_format( $trid, $source_lang_code = null ) { return get_post_format ( $this->get_original_post_ID ( $trid, $source_lang_code ) ); } /** * @param int $pidd * @param WP_Post $post * * @return void */ public abstract function save_post_actions( $pidd, $post ); /** @param int $post_id */ public function attachment_actions( $post_id ) { /** * This filter hooks determines whether we should apply the * "save_post" actions on an attachment. * * @since 4.4.0 * * @param bool $apply_save_post_actions True if we should apply save post actions on the attachment, false otherwise (default false). * @param int $post_id The attachment post ID. */ if ( apply_filters( 'wpml_apply_save_attachment_actions', false, $post_id ) ) { $post = get_post( $post_id ); if ( $post ) { $this->save_post_actions( $post_id, $post ); } } } public function shutdown_action() { if ( self::$defer_term_counting ) { self::$defer_term_counting = false; wp_defer_term_counting( false ); } } public function trash_translation ( $trans_id ) { if ( !WPML_WordPress_Actions::is_bulk_trash( $trans_id ) ) { wp_trash_post( $trans_id ); } } public function untrash_translation( $trans_id ) { if ( ! WPML_WordPress_Actions::is_bulk_untrash( $trans_id ) ) { wp_untrash_post( $trans_id ); } } function untrashed_post_actions( $post_id ) { $translation_sync = $this->get_sync_helper (); $translation_sync->untrashed_post_actions ( $post_id ); } public function delete_post_translation_entry( $post_id ) { $update_args = array( 'context' => 'post', 'element_id' => $post_id ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'before_delete' ) ) ); $sql = $this->wpdb->prepare( "DELETE FROM {$this->wpdb->prefix}icl_translations WHERE element_id = %d AND element_type LIKE 'post%%' LIMIT 1", $post_id ); $res = $this->wpdb->query( $sql ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'after_delete' ) ) ); return $res; } public function trashed_post_actions( $post_id ) { $this->delete_post_actions( $post_id, true ); } /** * This function holds all actions to be run after deleting a post. * 1. Delete the posts entry in icl_translations. * 2. Set one of the posts translations or delete all translations of the post, depending on sitepress settings. * * @param Integer $post_id * @param bool $keep_db_entries Sets whether icl_translations entries are to be deleted or kept, when hooking this to * post trashing we want them to be kept. */ public function delete_post_actions( $post_id, $keep_db_entries = false ) { $translation_sync = $this->get_sync_helper (); $translation_sync->delete_post_actions ( $post_id, $keep_db_entries ); } /** * @param int $post_id * @param string $post_status * * @return null|int */ abstract function get_save_post_trid( $post_id, $post_status ); /** * @param integer $post_id * @param SitePress $sitepress * @return bool|mixed|null|string|void */ public function get_save_post_lang( $post_id, $sitepress ) { $language_code = $this->get_element_lang_code ( $post_id ); $language_code = $language_code ? $language_code : $sitepress->get_current_language (); $language_code = $sitepress->is_active_language ( $language_code ) ? $language_code : $sitepress->get_default_language (); return apply_filters ( 'wpml_save_post_lang', $language_code ); } /** * @param int $trid * @param string $language_code * @param string $default_language * * @return string|null */ protected abstract function get_save_post_source_lang( $trid, $language_code, $default_language ); /** * Sets a posts language details, invalidates caches relating to the post and triggers * synchronisation actions across translations of the just saved post. * * @param int $trid * @param array $post_vars * @param string $language_code * @param string $source_language * * @used-by \WPML_Post_Translation::save_post_actions as final step of the WPML Core save_post actions */ protected function after_save_post( $trid, $post_vars, $language_code, $source_language ) { $this->maybe_set_elid( $trid, $post_vars['post_type'], $language_code, $post_vars['ID'], $source_language ); $translation_sync = $this->get_sync_helper(); $original_id = $this->get_original_element( $post_vars['ID'] ); $translation_sync->sync_with_translations( $original_id ? $original_id : $post_vars['ID'], $post_vars ); $translation_sync->sync_with_duplicates( $post_vars['ID'] ); if ( ! function_exists( 'icl_cache_clear' ) ) { require_once WPML_PLUGIN_PATH . '/inc/cache.php'; } icl_cache_clear( $post_vars['post_type'] . 's_per_language', true ); if ( ! in_array( $post_vars['post_type'], array( 'nav_menu_item', 'attachment' ), true ) ) { do_action( 'wpml_tm_save_post', $post_vars['ID'], get_post( $post_vars['ID'] ), false ); } // Flush object cache. $this->flush_object_cache_for_groups( array( 'ls_languages', WPML_ELEMENT_TRANSLATIONS_CACHE_GROUP ) ); do_action( 'wpml_after_save_post', $post_vars['ID'], $trid, $language_code, $source_language ); } /** * Create new instance of WPML_WP_Cache for each group and flush cache for group. * @param array $groups */ private function flush_object_cache_for_groups( $groups = array() ) { if ( ! empty( $groups ) ) { foreach ( $groups as $group ) { $cache = new WPML_WP_Cache( $group ); $cache->flush_group_cache(); } } } private function get_original_post_attr( $trid, $attribute, $source_lang_code ) { $legal_attributes = array( 'post_status', 'post_date', 'menu_order', 'comment_status', 'ping_status', 'ID' ); $res = false; if ( in_array ( $attribute, $legal_attributes, true ) ) { $attribute = 'p.' . $attribute; $source_snippet = $source_lang_code === null ? " AND wpml_translations.source_language_code IS NULL " : $this->wpdb->prepare ( " AND wpml_translations.language_code = %s ", $source_lang_code ); $res = $this->wpdb->get_var ( $this->wpdb->prepare ( "SELECT {$attribute} FROM {$this->wpdb->prefix}icl_translations wpml_translations " . $this->get_element_join() . " WHERE wpml_translations.trid=%d {$source_snippet} LIMIT 1", $trid ) ); } return $res; } public function has_save_post_action( $post ) { if ( ! $post ) { return false; } $is_auto_draft = isset( $post->post_status ) && $post->post_status === 'auto-draft'; $is_editing_different_post = $this->is_editing_different_post( $post->ID ); $is_saving_a_revision = array_key_exists( 'post_type', $_POST ) && 'revision' === $_POST['post_type']; $is_untrashing = array_key_exists( 'action', $_GET ) && 'untrash' === $_GET['action']; $is_auto_save = array_key_exists( 'autosave', $_POST ); $skip_sitepress_actions = array_key_exists( 'skip_sitepress_actions', $_POST ); $is_post_a_revision = 'revision' === $post->post_type; $is_scheduled_to_be_trashed = get_post_meta( $post->ID, '_wp_trash_meta_status', true ); $is_add_meta_action = isset( $_POST['action'] ) && 'add-meta' === $_POST['action']; $is_inner_post_insertion = $this->is_inner_post_insertion(); return $this->is_translated_type( $post->post_type ) && ! ( $is_auto_draft || $is_auto_save || $skip_sitepress_actions || ( $is_editing_different_post && ! $is_inner_post_insertion ) || $is_saving_a_revision || $is_post_a_revision || $is_scheduled_to_be_trashed || $is_add_meta_action || $is_untrashing ); } /** * @param int $post_id * * @return bool */ protected function is_editing_different_post( $post_id ) { return array_key_exists( 'post_ID', $_POST ) && (int) $_POST['post_ID'] && $post_id != $_POST['post_ID']; } protected function get_element_join() { return " JOIN {$this->wpdb->posts} p ON wpml_translations.element_id = p.ID AND wpml_translations.element_type = CONCAT('post_', p.post_type) "; } protected function get_type_prefix() { return 'post_'; } public function is_translated_type( $post_type ) { global $sitepress; return $sitepress->is_translated_post_type ( $post_type ); } /** * @param WP_Post $post * * @return string[] all language codes the post can be translated into */ public function get_allowed_target_langs( $post ) { global $sitepress; $active_languages = $sitepress->get_active_languages (); $can_translate = array_keys ( $active_languages ); $can_translate = array_diff ( $can_translate, array( $this->get_element_lang_code ( $post->ID ) ) ); return apply_filters ( 'wpml_allowed_target_langs', $can_translate, $post->ID, 'post' ); } /** * Before setting the language of the post to be saved, check if a translation in this language already exists * This check is necessary, so that synchronization actions like thrashing or un-trashing of posts, do not lead to * database corruption, due to erroneously changing a posts language into a state, * where it collides with an existing translation. While the UI prevents this sort of action for the most part, * this is not necessarily the case for other plugins like TM. * The logic here first of all checks if an existing translation id is present in the desired language_code. * If so but this translation is actually not the currently to be saved post, * then this post will be saved to its current language. If the translation already exists, * the existing translation id will be used. In all other cases a new entry in icl_translations will be created. * * @param Integer $trid * @param String $post_type * @param String $language_code * @param Integer $post_id * @param String $source_language */ private function maybe_set_elid( $trid, $post_type, $language_code, $post_id, $source_language ) { global $sitepress; $element_type = 'post_' . $post_type; $sitepress->set_element_language_details ( $post_id, $element_type, $trid, $language_code, $source_language ); } /** * @return WPML_Post_Synchronization */ private function get_sync_helper() { global $sitepress; $this->post_translation_sync = $this->post_translation_sync ? $this->post_translation_sync : new WPML_Post_Synchronization( $this->settings, $this, $sitepress ); return $this->post_translation_sync; } /** * @return WPML_Debug_BackTrace */ private function get_debug_backtrace() { if ( ! $this->debug_backtrace ) { $this->debug_backtrace = new WPML\Utils\DebugBackTrace( 20 ); } return $this->debug_backtrace; } public function set_debug_backtrace( WPML_Debug_BackTrace $debug_backtrace ) { $this->debug_backtrace = $debug_backtrace; } /** * @return bool */ protected function is_inner_post_insertion() { $debug_backtrace = $this->get_debug_backtrace(); return 1 < $debug_backtrace->count_function_in_call_stack( 'wp_insert_post' ); } /** * @param WP_Post $post * * @return array */ protected function get_post_vars( $post ) { $post_vars = array(); if ( ! $this->is_inner_post_insertion() ) { $post_vars = (array) $_POST; } foreach ( (array) $post as $k => $v ) { $post_vars[ $k ] = $v; } $post_vars['post_type'] = isset( $post_vars['post_type'] ) ? $post_vars['post_type'] : $post->post_type; return $post_vars; } protected function defer_term_counting() { if ( ! self::$defer_term_counting ) { self::$defer_term_counting = true; wp_defer_term_counting( true ); } } /** * @return self|WPML_Frontend_Post_Actions|WPML_Admin_Post_Actions */ public static function getGlobalInstance() { global $wpml_post_translations, $sitepress; if ( ! isset( $wpml_post_translations ) ) { wpml_load_post_translation( is_admin(), $sitepress->get_settings() ); } return $wpml_post_translations; } } post-translation/wpml-comment-duplication.class.php 0000755 00000005762 14720425051 0016647 0 ustar 00 <?php class WPML_Comment_Duplication{ public function move_to_original($duplicate_of, $post_duplicates, $comment){ global $wpml_post_translations, $wpdb; $_orig_lang = $wpml_post_translations->get_element_lang_code ( $duplicate_of ); $post_duplicates[ $_orig_lang ] = $duplicate_of; $original_parent = get_comment_meta ( $comment[ 'comment_parent' ], '_icl_duplicate_of', true ); $wpdb->update ( $wpdb->comments, array( 'comment_post_ID' => $duplicate_of, 'comment_parent' => $original_parent ), array( 'comment_ID' => $comment['comment_ID'] ), array( '%d', '%d' ), array( '%d' ) ); wp_update_comment_count_now($duplicate_of); } public function get_correct_parent($comment, $dup_id){ global $wpdb; $translated_parent = $wpdb->get_var ( $wpdb->prepare ( " SELECT cmb.comment_id FROM {$wpdb->commentmeta} cm JOIN {$wpdb->commentmeta} cmb ON ( cmb.meta_value = cm.meta_value AND cmb.meta_key = cm.meta_key) OR cm.comment_id = cmb.meta_value JOIN {$wpdb->comments} c ON c.comment_ID = cmb.comment_id WHERE cm.meta_key = '_icl_duplicate_of' AND ( cm.comment_id = %d OR cm.meta_value = %d ) AND c.comment_post_ID = %d", $comment[ 'comment_parent' ], $comment[ 'comment_parent' ], $dup_id ) ); return $translated_parent; } public function insert_duplicated_comment( $comment, $dup_id, $original_cid ) { global $wpdb, $iclTranslationManagement; $dup_comment_id = $this->duplicate_exists ( $dup_id, $original_cid ); remove_action ( 'wp_insert_comment', array( $iclTranslationManagement, 'duplication_insert_comment' ), 100 ); if ( $dup_comment_id ) { $comment[ 'comment_ID' ] = $dup_comment_id; wp_update_comment ( $comment ); } else { $wpdb->insert ( $wpdb->comments, $comment ); $dup_comment_id = $wpdb->insert_id; add_action ( 'wp_insert_comment', array( $iclTranslationManagement, 'duplication_insert_comment' ), 100 ); update_comment_meta ( $dup_comment_id, '_icl_duplicate_of', $original_cid ); // comment meta $meta = $wpdb->get_results ( $wpdb->prepare ( "SELECT meta_key, meta_value FROM {$wpdb->commentmeta} WHERE comment_id=%d", $original_cid ) ); foreach ( $meta as $meta_row ) { $wpdb->insert ( $wpdb->commentmeta, array( 'comment_id' => $dup_comment_id, 'meta_key' => $meta_row->meta_key, 'meta_value' => $meta_row->meta_value ), array( '%d', '%s', '%s' ) ); } } wp_update_comment_count_now ( $dup_id ); } private function duplicate_exists( $dup_id, $original_cid ) { global $wpdb; $duplicate = $wpdb->get_var ( $wpdb->prepare ( " SELECT comm.comment_ID FROM {$wpdb->comments} comm JOIN {$wpdb->commentmeta} cm ON comm.comment_ID = cm.comment_id WHERE comm.comment_post_ID = %d AND cm.meta_key = '_icl_duplicate_of' AND cm.meta_value = %d LIMIT 1", $dup_id, $original_cid ) ); return $duplicate; } } post-translation/wpml-post-synchronization.class.php 0000755 00000034062 14720425051 0017113 0 ustar 00 <?php use WPML\FP\Lst; use WPML\FP\Maybe; use WPML\FP\Obj; use function WPML\FP\partial; /** * Class WPML_Post_Synchronization * * @package wpml-core * @subpackage post-translation */ class WPML_Post_Synchronization extends WPML_SP_And_PT_User { /** @var bool[] */ private $sync_parent_cpt = array(); /** @var bool $sync_parent */ private $sync_parent; /** @var bool $sync_delete */ private $sync_delete; /** @var bool $sync_ping_status */ private $sync_ping_status; /** @var bool $sync_post_date */ private $sync_post_date; /** @var bool $sync_post_format */ private $sync_post_format; /** @var bool $sync_comment_status */ private $sync_comment_status; /** @var bool $sync_page_template */ private $sync_page_template; /** @var bool $sync_menu_order */ private $sync_menu_order; /** @var bool $sync_password */ private $sync_password; /** @var bool $sync_private_flag */ private $sync_private_flag; /** @var bool $is_deleting_all_translations */ private $is_deleting_all_translations = false; /** @var array $deleted_post_types */ private $deleted_post_types = array(); /** * @var int */ private $sync_document_status; /** * @param array $settings * @param WPML_Post_Translation $post_translations * @param SitePress $sitepress */ public function __construct( &$settings, &$post_translations, &$sitepress ) { parent::__construct( $post_translations, $sitepress ); $this->sync_delete = isset( $settings[ 'sync_delete' ] ) ? $settings[ 'sync_delete' ] : false; $this->sync_parent = isset( $settings[ 'sync_page_parent' ] ) ? $settings[ 'sync_page_parent' ] : false; $this->sync_ping_status = isset( $settings[ 'sync_ping_status' ] ) ? $settings[ 'sync_ping_status' ] : false; $this->sync_post_date = isset( $settings[ 'sync_post_date' ] ) ? $settings[ 'sync_post_date' ] : false; $this->sync_post_format = isset( $settings[ 'sync_post_format' ] ) ? $settings[ 'sync_post_format' ] : false; $this->sync_comment_status = isset( $settings[ 'sync_comment_status' ] ) ? $settings[ 'sync_comment_status' ] : false; $this->sync_page_template = isset( $settings[ 'sync_page_template' ] ) ? $settings[ 'sync_page_template' ] : false; $this->sync_password = isset( $settings[ 'sync_password' ] ) ? $settings[ 'sync_password' ] : false; $this->sync_private_flag = isset( $settings[ 'sync_private_flag' ] ) ? $settings[ 'sync_private_flag' ] : false; $this->sync_document_status = isset( $settings[ 'translated_document_status' ] ) ? $settings[ 'translated_document_status' ] : 1; $this->sync_menu_order = isset( $settings[ 'sync_page_ordering' ] ) ? $settings[ 'sync_page_ordering' ] : array(); } private function must_sync_parents( $post_type ) { if ( ! array_key_exists( $post_type, $this->sync_parent_cpt ) ) { $this->sync_parent_cpt[ $post_type ] = apply_filters( 'wpml_sync_parent_for_post_type', $this->sync_parent, $post_type ); } return $this->sync_parent_cpt[ $post_type ]; } /** * Fixes parents of translations for hierarchical post types * * User changed parent for a post in $post_type and we are setting proper parent for $translation_id in * $language_code_translated language * * @param string $post_type - post_type that should have the translated parents fixed */ private function maybe_fix_translated_parent( $post_type ) { if ( $this->must_sync_parents( $post_type ) ) { $sync_helper = wpml_get_hierarchy_sync_helper(); $sync_helper->sync_element_hierarchy( $post_type ); } } public function sync_with_duplicates( $post_id ) { $duplicates = $this->sitepress->get_duplicates( $post_id ); foreach ( array_keys( $duplicates ) as $lang_code ) { $this->sitepress->make_duplicate( $post_id, $lang_code ); } } /** * @param int $post_id * @param bool $keep_db_entries */ public function delete_post_actions( $post_id, $keep_db_entries = false ) { $post_type = get_post_type( $post_id ); $post_type_exceptions = array( 'nav_menu_item' ); if ( ! $post_type || in_array( $post_type, $post_type_exceptions ) ) { return; } $trid = null; if ( ! $this->is_deleting_all_translations ) { $this->is_deleting_all_translations = ! $this->post_translation->get_original_element( $post_id, true ); $trid = $this->post_translation->get_element_trid( $post_id ); $translated_ids = $this->get_translations_without_source( $post_id, $trid ); if ( $this->sync_delete || Lst::includes( $post_type, [ 'wp_template', 'wp_template_part' ] ) ) { $this->delete_translations( $translated_ids, $keep_db_entries ); } $this->is_deleting_all_translations = false; } if ( ! $keep_db_entries ) { $this->post_translation->delete_post_translation_entry( $post_id ); if ( $trid && ! $this->is_deleting_all_translations ) { $lang_code = $this->post_translation->get_element_lang_code( $post_id ); $this->set_new_original( $trid, $lang_code ); } } if ( ! $this->is_deleting_all_translations ) { $this->run_final_actions_for_delete_post( $post_type ); } } /** * @param int $post_id * @param int $trid * * @return array */ private function get_translations_without_source( $post_id, $trid ) { $actual_translations_only = ! $this->is_deleting_all_translations; $translated_ids = $this->post_translation->get_element_translations( $post_id, $trid, $actual_translations_only ); unset( $translated_ids[ array_search( $post_id, $translated_ids ) ] ); return $translated_ids; } private function is_bulk_delete() { return ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] || isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) && ( isset( $_REQUEST['post'] ) && is_array( $_REQUEST['post'] ) || isset( $_REQUEST['media'] ) && is_array( $_REQUEST['media'] ) ); } /** @param string $post_type */ private function reset_cache( $post_type ) { require_once WPML_PLUGIN_PATH . '/inc/cache.php'; icl_cache_clear( $post_type . 's_per_language', true ); } /** @param string $post_type */ private function defer_delete_actions( $post_type ) { if ( ! in_array( $post_type, $this->deleted_post_types, true ) ) { $this->deleted_post_types[] = $post_type; if ( ! has_action( 'shutdown', array( $this, 'shutdown_action' ) ) ) { add_action( 'shutdown', array( $this, 'shutdown_action' ) ); } } } public function shutdown_action() { $this->post_translation->reload(); foreach ( $this->deleted_post_types as $post_type ) { $this->reset_cache( $post_type ); $this->maybe_fix_translated_parent( $post_type ); } } /** * @param array $translated_ids * @param bool $keep_db_entries */ private function delete_translations( array $translated_ids, $keep_db_entries ) { if ( ! empty( $translated_ids ) ) { foreach ( $translated_ids as $trans_id ) { if ( ! $this->is_bulk_prevented( $trans_id ) ) { if ( $keep_db_entries ) { $this->post_translation->trash_translation( $trans_id ); } else { wp_delete_post( $trans_id, true ); } } } } } /** @param string $post_type */ private function run_final_actions_for_delete_post( $post_type ) { if ( $this->is_bulk_delete() ) { $this->defer_delete_actions( $post_type ); } else { $this->post_translation->reload(); $this->reset_cache( $post_type ); $this->maybe_fix_translated_parent( $post_type ); } } private function is_bulk_prevented( $post_id ) { return ( isset( $_GET[ 'delete_all' ] ) && $_GET[ 'delete_all' ] === 'Empty Trash' ) || in_array( $post_id, ( isset( $_GET[ 'ids' ] ) ? $_GET[ 'ids' ] : array() ) ); } function untrashed_post_actions( $post_id ) { if ( $this->sync_delete ) { $translations = $this->post_translation->get_element_translations( $post_id, false, true ); foreach ( $translations as $t_id ) { $this->post_translation->untrash_translation( $t_id ); } } $post_type = get_post_type( $post_id ); require_once WPML_PLUGIN_PATH . '/inc/cache.php'; icl_cache_clear( $post_type . 's_per_language', true ); } public function sync_with_translations( $post_id, $post_vars = false ) { global $wpdb; $wp_api = $this->sitepress->get_wp_api(); $term_count_update = new WPML_Update_Term_Count( $wp_api ); $post = get_post ( $post_id ); $source_post_status = $this->get_post_status( $post_id ); $translated_ids = $this->post_translation->get_element_translations( $post_id, false, true ); $post_format = $this->sync_post_format ? get_post_format( $post_id ) : null; $ping_status = $this->sync_ping_status ? ( pings_open( $post_id ) ? 'open' : 'closed' ) : null; $comment_status = $this->sync_comment_status ? ( comments_open( $post_id ) ? 'open' : 'closed' ) : null; $post_password = $this->sync_password ? $post->post_password : null; $menu_order = $this->sync_menu_order && ! empty( $post->menu_order ) ? $post->menu_order : null; $page_template = $this->sync_page_template && get_post_type( $post_id ) === 'page' ? get_post_meta( $post_id, '_wp_page_template', true ) : null; $post_date = $this->sync_post_date ? $wpdb->get_var( $wpdb->prepare( "SELECT post_date FROM {$wpdb->posts} WHERE ID=%d LIMIT 1", $post_id ) ) : null; foreach ( $translated_ids as $lang_code => $translated_pid ) { $post_status = $this->get_post_status( $translated_pid ); $post_status_differs = ( 'private' === $source_post_status && 'publish' === $post_status ) || ( 'publish' === $source_post_status && 'private' === $post_status ); if ( $this->sync_private_flag && $post_status_differs ) { $post_status = $source_post_status; } $this->sync_custom_fields ( $post_id, $translated_pid ); if ( $post_format ) { set_post_format ( $translated_pid, $post_format ); } if ( $post_date !== null ) { $post_date_gmt = get_gmt_from_date ( $post_date ); $data = array( 'post_date' => $post_date, 'post_date_gmt' => $post_date_gmt ); $now = gmdate('Y-m-d H:i:59'); $allow_post_statuses = array( 'private', 'pending', 'draft' ); if ( mysql2date('U', $post_date_gmt, false) > mysql2date('U', $now, false) ) { if ( ! in_array( $post_status, $allow_post_statuses, true ) ) { $post_status = 'future'; } } $data[ 'post_status' ] = $post_status; $wpdb->update ( $wpdb->posts, $data, array( 'ID' => $translated_pid ) ); $time = strtotime( $post_date_gmt . '+1 second' ); $time && wp_schedule_single_event( $time, 'publish_future_post', array( $translated_pid ) ); } if ( $post_password !== null ) { $wpdb->update ( $wpdb->posts, array( 'post_password' => $post_password ), array( 'ID' => $translated_pid ) ); } if ( $post_status !== null && ! in_array( $this->get_post_status( $translated_pid ), array( 'auto-draft', 'draft', 'inherit', 'trash' ) ) ) { $wpdb->update ( $wpdb->posts, array( 'post_status' => $post_status ), array( 'ID' => $translated_pid ) ); $term_count_update->update_for_post( $translated_pid ); } elseif ( $post_status == null && $this->sync_private_flag && $this->get_post_status( $translated_pid ) === 'private' ) { $wpdb->update ( $wpdb->posts, array( 'post_status' => $this->get_post_status( $post_id ) ), array( 'ID' => $translated_pid ) ); $term_count_update->update_for_post( $translated_pid ); } if ( $ping_status !== null ) { $wpdb->update ( $wpdb->posts, array( 'ping_status' => $ping_status ), array( 'ID' => $translated_pid ) ); } if ( $comment_status !== null ) { $wpdb->update ( $wpdb->posts, array( 'comment_status' => $comment_status ), array( 'ID' => $translated_pid ) ); } if ( $page_template !== null ) { update_post_meta ( $translated_pid, '_wp_page_template', $page_template ); } $this->sync_with_translations ( $translated_pid ); } $post_type = get_post_type( $post_id ); $post_type && $this->maybe_fix_translated_parent( $post_type ); if ( $menu_order !== null && (bool) $translated_ids !== false ) { $query = $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order=%s WHERE ID IN (" . wpml_prepare_in( $translated_ids, '%d' ) . ')', $menu_order ); $wpdb->query( $query ); } } /** * The function `get_post_status` does not return the raw status for attachments. * As we are running direct DB updates here, we need the actual DB value. * * @param int $post_id * * @return string|false */ private function get_post_status( $post_id ) { $isAttachment = function( $post_id ) { return 'attachment' === get_post_type( $post_id ); }; return Maybe::of( $post_id ) ->filter( $isAttachment ) ->map( 'get_post' ) ->map( Obj::prop( 'post_status' ) ) ->getOrElse( partial( 'get_post_status', $post_id ) ); } private function sync_custom_fields( $original_id, $post_id ) { if ( $original_id && $original_id != $post_id ) { $this->sitepress->copy_custom_fields ( $original_id, $post_id ); } else { $translations = $this->post_translation->get_element_translations ( $post_id, false, true ); foreach ( $translations as $t_id ) { $this->sitepress->copy_custom_fields ( $post_id, $t_id ); } } } private function set_new_original( $trid, $removed_lang_code ) { if ( $trid && $removed_lang_code ) { $priorities = $this->sitepress->get_setting( 'languages_order' ); $this->post_translation->reload(); $translations = $this->post_translation->get_element_translations( false, $trid ); $new_source_lang_code = false; foreach ( $priorities as $lang_code ) { if ( isset( $translations[ $lang_code ] ) ) { $new_source_lang_code = $lang_code; break; } } if ( $new_source_lang_code ) { global $wpdb; $rows_updated = $wpdb->update( $wpdb->prefix . 'icl_translations', array( 'source_language_code' => $new_source_lang_code ), array( 'trid' => $trid, 'source_language_code' => $removed_lang_code ) ); if( 0 < $rows_updated ) { do_action( 'wpml_translation_update', array( 'trid' => $trid ) ); } $wpdb->query( " UPDATE {$wpdb->prefix}icl_translations SET source_language_code = NULL WHERE language_code = source_language_code" ); } } } } post-translation/wpml-wordpress-actions.class.php 0000755 00000004137 14720425051 0016355 0 ustar 00 <?php /** * Class WPML_WordPress_Actions * @package wpml-core * @subpackage post-translation */ class WPML_WordPress_Actions { /** * @param int $post_id * * @return bool */ public static function is_bulk_trash( $post_id ) { if ( self::is_trash_action() && self::post_id_in_bulk( $post_id ) ) { return true; } else { return false; } } /** * @param int $post_id * * @return bool */ public static function is_bulk_untrash( $post_id ) { if ( self::is_untrash_action() && self::post_id_in_bulk( $post_id, true ) ) { return true; } else { return false; } } public static function is_heartbeat( ) { return self::is_action( 'heartbeat', 'post' ); } protected static function is_trash_action() { return self::is_action( 'trash' ); } protected static function is_untrash_action() { return self::is_action( 'untrash' ); } /** * @param string $action * * @return bool */ protected static function is_action( $action, $type = 'get' ) { if ( $type == 'get' ) { return ( isset( $_GET[ 'action' ] ) && $_GET[ 'action' ] == $action ) || ( isset( $_GET[ 'action2' ] ) && $_GET[ 'action2' ] == $action ); } elseif ( $type == 'post' ) { return ( isset( $_POST[ 'action' ] ) && $_POST[ 'action' ] == $action ) || ( isset( $_POST[ 'action2' ] ) && $_POST[ 'action2' ] == $action ); } else { return false; } } /** * @param int $post_id * @param bool $check_ids * * @return bool */ protected static function post_id_in_bulk( $post_id, $check_ids = false ) { if ( isset( $_GET[ 'post' ] ) && is_array( $_GET[ 'post' ] ) && in_array( $post_id, $_GET[ 'post' ] ) ) { return true; } elseif ( $check_ids ) { // We need to check the ids parameter when user clicks on 'undo' after trashing. return isset( $_GET[ 'ids' ] ) && is_string( $_GET[ 'ids' ] ) && in_array( $post_id, explode( ',', $_GET[ 'ids' ] ) ); } else { return false; } } } post-translation/wpml-frontend-post-actions.class.php 0000755 00000004173 14720425051 0017127 0 ustar 00 <?php use WPML\LIB\WP\Hooks; class WPML_Frontend_Post_Actions extends WPML_Post_Translation { public function init() { parent::init (); if ( $this->is_setup_complete() ) { /** * In theory, we could add the 'delete_post` action for all frontend requests * We'll limit it to REST to avoid any unexpected problems */ Hooks::onAction( 'rest_api_init' ) ->then( function () { add_action( 'delete_post', [ $this, 'delete_post_actions' ] ); } ); } } /** * @param int $post_id * @param string $post_status * * @return null|int */ function get_save_post_trid( $post_id, $post_status ) { return $this->get_element_trid( $post_id ); } /** * @param int $pidd * @param WP_Post $post * * @return void */ public function save_post_actions( $pidd, $post ) { global $sitepress; $this->defer_term_counting(); if ( ! $post ) { $post = get_post( $pidd ); } $http_referer = new WPML_URL_HTTP_Referer( new WPML_Rest( new WP_Http() ) ); // exceptions if ( ! $this->has_save_post_action( $post ) || $http_referer->is_rest_request_called_from_post_edit_page() ) { return; } $default_language = $sitepress->get_default_language(); $post_vars = $this->get_post_vars( $post ); $post_id = isset( $post_vars['post_ID'] ) ? $post_vars['post_ID'] : $pidd; //latter case for XML-RPC publishing $language_code = $this->get_save_post_lang( $post_id, $sitepress ); $trid = $this->get_save_post_trid( $post_id, $post->post_status ); // after getting the right trid set the source language from it by referring to the root translation // of this trid, in case no proper source language has been set yet $source_language = $this->get_save_post_source_lang( $trid, $language_code, $default_language ); $this->after_save_post( $trid, $post_vars, $language_code, $source_language ); } protected function get_save_post_source_lang( $trid, $language_code, $default_language ) { $post_id = $this->get_element_id ( $trid, $language_code ); return $post_id ? $this->get_source_lang_code ( $post_id ) : null; } } post-translation/wpml-post-duplication.class.php 0000755 00000035661 14720425051 0016173 0 ustar 00 <?php require_once dirname( __FILE__ ) . '/wpml-wordpress-actions.class.php'; /** * Class WPML_Post_Duplication * * @package wpml-core * @subpackage post-translation */ class WPML_Post_Duplication extends WPML_WPDB_And_SP_User { function get_duplicates( $master_post_id ) { global $wpml_post_translations; $duplicates = array(); $post_ids_query = " SELECT post_id FROM {$this->wpdb->postmeta} WHERE meta_key='_icl_lang_duplicate_of' AND meta_value = %d AND post_id <> %d"; $post_ids_prepare = $this->wpdb->prepare( $post_ids_query, array( $master_post_id, $master_post_id ) ); $post_ids = $this->wpdb->get_col( $post_ids_prepare ); foreach ( $post_ids as $post_id ) { $language_code = $wpml_post_translations->get_element_lang_code( $post_id ); if ( ! $language_code ) { continue; } $duplicates[ $language_code ] = $post_id; } return $duplicates; } function make_duplicate( $master_post_id, $lang ) { global $wpml_post_translations; /** * @deprecated Use 'wpml_before_make_duplicate' instead * @since 3.4 */ do_action( 'icl_before_make_duplicate', $master_post_id, $lang ); do_action( 'wpml_before_make_duplicate', $master_post_id, $lang ); $master_post = get_post( $master_post_id ); if ( ! $master_post || $this->is_external( $master_post_id ) ) { return true; } $is_duplicated = false; $translations = $wpml_post_translations->get_element_translations( $master_post_id, false, false ); if ( isset( $translations[ $lang ] ) ) { $post_array[ 'ID' ] = $translations[ $lang ]; if ( WPML_WordPress_Actions::is_bulk_trash( $post_array[ 'ID' ] ) || WPML_WordPress_Actions::is_bulk_untrash( $post_array[ 'ID' ] ) ) { return true; } $is_duplicated = get_post_meta( $translations[ $lang ], '_icl_lang_duplicate_of', true ); } $post_array['post_author'] = $master_post->post_author; $post_array['post_date'] = $master_post->post_date; $post_array['post_date_gmt'] = $master_post->post_date_gmt; $duplicated_post_content = $this->duplicate_post_content( $lang, $master_post ); $post_array['post_content'] = $duplicated_post_content; $duplicated_post_title = $this->duplicate_post_title( $lang, $master_post ); $post_array['post_title'] = $duplicated_post_title; $duplicated_post_excerpt = $this->duplicate_post_excerpt( $lang, $master_post ); $post_array['post_excerpt'] = $duplicated_post_excerpt; if ( $this->sitepress->get_setting('sync_post_status' ) ) { $sync_post_status = true; } else { $sync_post_status = ( ! isset( $post_array[ 'ID' ] ) || ( $this->sitepress->get_setting( 'sync_delete' ) && $master_post->post_status === 'trash' ) || $is_duplicated ); } if ( $sync_post_status || ( isset( $post_array[ 'ID' ] ) && get_post_status( $post_array[ 'ID' ] ) === 'auto-draft' ) ) { $post_array[ 'post_status' ] = $master_post->post_status; } $post_array[ 'comment_status' ] = $master_post->comment_status; $post_array[ 'ping_status' ] = $master_post->ping_status; $post_array[ 'post_name' ] = $master_post->post_name; if ( $master_post->post_parent ) { $parent = $this->sitepress->get_object_id( $master_post->post_parent, $master_post->post_type, false, $lang ); $post_array[ 'post_parent' ] = $parent; } $post_array['menu_order'] = $master_post->menu_order; $post_array['post_type'] = $master_post->post_type; $post_array['post_mime_type'] = $master_post->post_mime_type; $trid = $this->sitepress->get_element_trid( $master_post->ID, 'post_' . $master_post->post_type ); $id = $this->save_duplicate( $post_array, $lang ); require_once WPML_PLUGIN_PATH . '/inc/cache.php'; icl_cache_clear(); global $ICL_Pro_Translation; /** @var WPML_Pro_Translation $ICL_Pro_Translation */ if ( $ICL_Pro_Translation ) { $ICL_Pro_Translation->fix_links_to_translated_content( $id, $lang ); } if ( ! is_wp_error( $id ) ) { $ret = $this->run_wpml_actions( $master_post, $trid, $lang, $id, $post_array ); } else { throw new Exception( $id->get_error_message() ); } return $ret; } /** * @param int $element_id * * @return null|string */ private function is_external( $element_id ) { $query = "SELECT element_type FROM {$this->wpdb->prefix}icl_translations WHERE element_id=%d AND element_type LIKE %s LIMIT 1"; return ! $this->wpdb->get_var( $this->wpdb->prepare( $query, $element_id, 'post_%' ) ); } private function run_wpml_actions( $master_post, $trid, $lang, $id, $post_array ) { $master_post_id = $master_post->ID; $this->sitepress->set_element_language_details( $id, 'post_' . $master_post->post_type, $trid, $lang ); $this->sync_duplicate_password( $master_post_id, $id ); $this->sync_page_template( $master_post_id, $id ); $this->duplicate_fix_children( $master_post_id, $lang ); // make sure post name is copied $this->wpdb->update( $this->wpdb->posts, array( 'post_name' => $master_post->post_name ), array( 'ID' => $id ) ); if ( $this->sitepress->get_setting( 'sync_post_taxonomies', false ) ) { $this->duplicate_taxonomies( $master_post_id, $lang ); } $this->duplicate_custom_fields( $master_post_id, $lang ); update_post_meta( $id, '_icl_lang_duplicate_of', $master_post->ID ); // Duplicate post format after the taxonomies because post format is stored // as a taxonomy by WP. if ( $this->sitepress->get_setting( 'sync_post_format' ) ) { $_wp_post_format = get_post_format( $master_post_id ); $_wp_post_format && set_post_format( $id, $_wp_post_format ); } if ( $this->sitepress->get_setting( 'sync_comments_on_duplicates' ) ) { $this->duplicate_comments( $master_post_id, $id ); } $status_helper = wpml_get_post_status_helper(); $status_helper->set_status( $id, ICL_TM_DUPLICATE ); $status_helper->set_update_status( $id, false ); do_action( 'icl_make_duplicate', $master_post_id, $lang, $post_array, $id ); clean_post_cache( $id ); return $id; } private function sync_page_template( $master_post_id, $duplicate_post_id ) { $_wp_page_template = get_post_meta( $master_post_id, '_wp_page_template', true ); if ( ! empty( $_wp_page_template ) ) { update_post_meta( $duplicate_post_id, '_wp_page_template', $_wp_page_template ); } } private function duplicate_comments( $master_post_id, $translated_id ) { global $sitepress; remove_filter( 'comments_clauses', array( $sitepress, 'comments_clauses' ), 10 ); $comments_on_master = get_comments( array( 'post_id' => $master_post_id ) ); $comments_on_translation = get_comments( array( 'post_id' => $translated_id, 'status' => 'any' ) ); add_filter( 'comments_clauses', array( $sitepress, 'comments_clauses' ), 10, 2 ); foreach ( $comments_on_translation as $comment ) { wp_delete_comment( $comment->comment_ID, true ); clean_comment_cache( $comment->comment_ID ); } $iclTranslationManagement = wpml_load_core_tm(); foreach ( $comments_on_master as $comment ) { $iclTranslationManagement->duplication_insert_comment( $comment->comment_ID ); clean_comment_cache( $comment->comment_ID ); } wp_update_comment_count_now( $master_post_id ); wp_update_comment_count_now( $translated_id ); } /** * @param array $post_array * @param string $lang * * @return int|WP_Error */ private function save_duplicate( array $post_array, $lang ) { return wpml_get_create_post_helper()->insert_post( $post_array, $lang, true ); } private function duplicate_fix_children( $master_post_id, $lang ) { $post_type = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT post_type FROM {$this->wpdb->posts} WHERE ID=%d", $master_post_id ) ); $master_children = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT ID FROM {$this->wpdb->posts} WHERE post_parent=%d AND post_type != 'revision'", $master_post_id ) ); $dup_parent = icl_object_id( $master_post_id, $post_type, false, $lang ); if ( $master_children ) { foreach ( $master_children as $master_child ) { $dup_child = icl_object_id( $master_child, $post_type, false, $lang ); if ( $dup_child ) { $this->wpdb->update( $this->wpdb->posts, array( 'post_parent' => $dup_parent ), array( 'ID' => $dup_child ) ); } $this->duplicate_fix_children( $master_child, $lang ); } } } private function duplicate_taxonomies( $master_post_id, $lang ) { $post_type = get_post_field( 'post_type', $master_post_id ); $taxonomies = get_object_taxonomies( $post_type ); $trid = $this->sitepress->get_element_trid( $master_post_id, 'post_' . $post_type ); if ( $trid ) { $translations = $this->sitepress->get_element_translations( $trid, 'post_' . $post_type, false, false, true ); if ( isset( $translations[ $lang ] ) ) { $duplicate_post_id = $translations[ $lang ]->element_id; /* If we have an existing post, we first of all remove all terms currently attached to it. * The main reason behind is the removal of the potentially present default category on the post. */ wp_delete_object_term_relationships( $duplicate_post_id, $taxonomies ); } else { return false; // translation not found! } } $term_helper = wpml_get_term_translation_util(); $term_helper->duplicate_terms( $master_post_id, $lang ); return true; } private function sync_duplicate_password( $master_post_id, $duplicate_post_id ) { if ( post_password_required( $master_post_id ) ) { $sql = $this->wpdb->prepare( "UPDATE {$this->wpdb->posts} AS dupl, (SELECT org.post_password FROM {$this->wpdb->posts} AS org WHERE ID = %d ) AS pwd SET dupl.post_password = pwd.post_password WHERE dupl.ID = %d", array( $master_post_id, $duplicate_post_id ) ); $this->wpdb->query( $sql ); } } private function duplicate_custom_fields( $master_post_id, $lang ) { $duplicate_post_id = false; $post_type = get_post_field( 'post_type', $master_post_id ); $trid = $this->sitepress->get_element_trid( $master_post_id, 'post_' . $post_type ); if ( $trid ) { $translations = $this->sitepress->get_element_translations( $trid, 'post_' . $post_type ); if ( isset( $translations[ $lang ] ) ) { $duplicate_post_id = $translations[ $lang ]->element_id; } else { return false; // translation not found! } } $default_exceptions = WPML_Config::get_custom_fields_translation_settings(); $exceptions = apply_filters( 'wpml_duplicate_custom_fields_exceptions', array() ); $exceptions = array_merge( $exceptions, $default_exceptions ); $exceptions = array_unique( $exceptions ); $exceptions_in = ! empty( $exceptions ) ? 'AND meta_key NOT IN ( ' . wpml_prepare_in( $exceptions ) . ') ' : ''; $from_where_string = "FROM {$this->wpdb->postmeta} WHERE post_id = %d " . $exceptions_in; $post_meta_master = $this->wpdb->get_results( "SELECT meta_key, meta_value " . $this->wpdb->prepare( $from_where_string, $master_post_id ) ); $this->wpdb->query( "DELETE " . $this->wpdb->prepare( $from_where_string, $duplicate_post_id ) ); $values = []; foreach ( $post_meta_master as $post_meta ) { $is_serialized = is_serialized( $post_meta->meta_value ); $meta_data = array( 'context' => 'custom_field', 'attribute' => 'value', 'key' => $post_meta->meta_key, 'is_serialized' => $is_serialized, 'post_id' => $duplicate_post_id, 'master_post_id' => $master_post_id, ); /** * @deprecated use 'wpml_duplicate_generic_string' instead, with the same arguments */ $icl_duplicate_generic_string = apply_filters( 'icl_duplicate_generic_string', $post_meta->meta_value, $lang, $meta_data ); $post_meta->meta_value = $icl_duplicate_generic_string; $wpml_duplicate_generic_string = apply_filters( 'wpml_duplicate_generic_string', $post_meta->meta_value, $lang, $meta_data ); $post_meta->meta_value = $wpml_duplicate_generic_string; if ( ! is_serialized( $post_meta->meta_value ) ) { $post_meta->meta_value = maybe_serialize( $post_meta->meta_value ); } $values[] = $this->wpdb->prepare( '( %d, %s, %s )', $duplicate_post_id, $post_meta->meta_key, $post_meta->meta_value ); } if ( ! empty( $values ) ) { $values = implode( ', ', $values ); $this->wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "INSERT INTO `{$this->wpdb->postmeta}` (`post_id`, `meta_key`, `meta_value`) VALUES {$values}" ); } return true; } /** * @param string $lang * @param \WP_Post $master_post * * @return array<string,mixed> */ private function duplicate_post_content( $lang, $master_post ) { $duplicated_post_content_meta = array( 'context' => 'post', 'attribute' => 'content', 'key' => $master_post->ID ); $duplicated_post_content = $master_post->post_content; $duplicated_post_content = apply_filters( 'icl_duplicate_generic_string', $duplicated_post_content, $lang, $duplicated_post_content_meta ); $duplicated_post_content = apply_filters( 'wpml_duplicate_generic_string', $duplicated_post_content, $lang, $duplicated_post_content_meta ); return $duplicated_post_content; } /** * @param string $lang * @param \WP_Post $master_post * * @return mixed */ private function duplicate_post_title( $lang, $master_post ) { $duplicated_post_title_meta = array( 'context' => 'post', 'attribute' => 'title', 'key' => $master_post->ID ); $duplicated_post_title = $master_post->post_title; $duplicated_post_title = apply_filters( 'icl_duplicate_generic_string', $duplicated_post_title, $lang, $duplicated_post_title_meta ); $duplicated_post_title = apply_filters( 'wpml_duplicate_generic_string', $duplicated_post_title, $lang, $duplicated_post_title_meta ); return $duplicated_post_title; } /** * @param string $lang * @param WP_Post $master_post * * @return mixed */ private function duplicate_post_excerpt( $lang, $master_post ) { $duplicated_post_excerpt_meta = array( 'context' => 'post', 'attribute' => 'excerpt', 'key' => $master_post->ID ); $duplicated_post_excerpt = $master_post->post_excerpt; $duplicated_post_excerpt = apply_filters( 'icl_duplicate_generic_string', $duplicated_post_excerpt, $lang, $duplicated_post_excerpt_meta ); $duplicated_post_excerpt = apply_filters( 'wpml_duplicate_generic_string', $duplicated_post_excerpt, $lang, $duplicated_post_excerpt_meta ); return $duplicated_post_excerpt; } } post-translation/wpml-admin-post-actions.class.php 0000755 00000020103 14720425051 0016367 0 ustar 00 <?php use WPML\API\Sanitize; /** * Class WPML_Admin_Post_Actions * * @package wpml-core * @subpackage post-translation */ class WPML_Admin_Post_Actions extends WPML_Post_Translation { /** * @depecated since 4.6.5 You should use constants from WPML\Media\Option */ const DUPLICATE_MEDIA_META_KEY = \WPML\Media\Option::DUPLICATE_MEDIA_KEY; const DUPLICATE_FEATURED_META_KEY = \WPML\Media\Option::DUPLICATE_FEATURED_KEY; const DUPLICATE_MEDIA_GLOBAL_KEY = 'duplicate_media'; const DUPLICATE_FEATURED_GLOBAL_KEY = 'duplicate_media'; private $http_referer; public function init() { parent::init (); if ( $this->is_setup_complete() ) { add_action ( 'delete_post', array( $this, 'delete_post_actions' ) ); add_action ( 'wp_trash_post', array( $this, 'trashed_post_actions' ) ); add_action ( 'untrashed_post', array( $this, 'untrashed_post_actions' ) ); } } /** * @param int $post_id * @param string $post_status * * @return null|int */ function get_save_post_trid( $post_id, $post_status ) { $trid = $this->get_element_trid( $post_id ); if ( ! ( $this->is_inner_post_insertion() && $this->is_editing_different_post( $post_id ) ) ) { $trid = $trid ? $trid : filter_var( isset( $_POST['icl_trid'] ) ? $_POST['icl_trid'] : '', FILTER_SANITIZE_NUMBER_INT ); $trid = $trid ? $trid : filter_var( isset( $_GET['trid'] ) ? $_GET['trid'] : '', FILTER_SANITIZE_NUMBER_INT ); $trid = $trid ? $trid : $this->get_trid_from_referer(); } $trid = apply_filters( 'wpml_save_post_trid_value', $trid, $post_status ); return $trid; } /** * @param int $post_id * @param WP_Post $post */ public function save_post_actions( $post_id, $post ) { global $sitepress; $this->defer_term_counting(); if ( ! $post ) { $post = get_post( $post_id ); } // exceptions $http_referer = $this->get_http_referer(); if ( ! $this->has_save_post_action( $post ) && ! $http_referer->is_rest_request_called_from_post_edit_page() ) { return; } if ( WPML_WordPress_Actions::is_bulk_trash( $post_id ) || WPML_WordPress_Actions::is_bulk_untrash( $post_id ) || $this->has_invalid_language_details_on_heartbeat() ) { return; } $default_language = $sitepress->get_default_language(); $post_vars = $this->get_post_vars( $post ); if ( isset( $post_vars['action'] ) && $post_vars['action'] === 'post-quickpress-publish' ) { $language_code = $default_language; } else { if( isset( $post_vars['post_ID'] ) ){ $post_id = $post_vars['post_ID']; } $language_code = $this->get_save_post_lang( $post_id, $sitepress ); } if ( $this->is_inline_action( $post_vars ) && ! ( $language_code = $this->get_element_lang_code( $post_id ) ) ) { return; } if ( isset( $post_vars['icl_translation_of'] ) && is_numeric( $post_vars['icl_translation_of'] ) ) { $translation_of_data_prepared = $this->wpdb->prepare( "SELECT trid, language_code FROM {$this->wpdb->prefix}icl_translations WHERE element_id=%d AND element_type=%s LIMIT 1", $post_vars['icl_translation_of'], 'post_' . $post->post_type ); list( $trid, $source_language ) = $this->wpdb->get_row( $translation_of_data_prepared, 'ARRAY_N' ); } if ( isset( $post_vars['icl_translation_of'] ) && $post_vars['icl_translation_of'] == 'none' ) { $trid = null; $source_language = $language_code; } else { $trid = isset( $trid ) && $trid ? $trid : $this->get_save_post_trid( $post_id, $post->post_status ); // after getting the right trid set the source language from it by referring to the root translation // of this trid, in case no proper source language has been set yet $source_language = isset( $source_language ) ? $source_language : $this->get_save_post_source_lang( $trid, $language_code, $default_language ); } if ( isset( $post_vars['icl_tn_note'] ) ) { update_post_meta( $post_id, '_icl_translator_note', $post_vars['icl_tn_note'] ); } $this->after_save_post( $trid, $post_vars, $language_code, $source_language ); if ( 'attachment' !== $post->post_type ) { $this->save_media_options( $post_id, $source_language ); } } /** * @param int $post_id * @param string|null $source_language */ private function save_media_options( $post_id, $source_language ) { if ( $this->has_post_media_options_metabox() ) { $original_post_id = isset( $_POST['icl_translation_of'] ) ? filter_var( $_POST['icl_translation_of'], FILTER_SANITIZE_NUMBER_INT ) : $post_id; $duplicate_media = isset( $_POST['wpml_duplicate_media'] ) ? filter_var( $_POST['wpml_duplicate_media'], FILTER_SANITIZE_NUMBER_INT ) : false; $duplicate_featured = isset( $_POST['wpml_duplicate_featured'] ) ? filter_var( $_POST['wpml_duplicate_featured'], FILTER_SANITIZE_NUMBER_INT ) : false; \WPML\Media\Option::setDuplicateMediaForIndividualPost( $post_id, (bool) $duplicate_media ); \WPML\Media\Option::setDuplicateFeaturedForIndividualPost( $post_id, (bool) $duplicate_featured ); } } private function has_post_media_options_metabox() { return array_key_exists( WPML_Meta_Boxes_Post_Edit_HTML::FLAG_HAS_MEDIA_OPTIONS, $_POST ); } private function has_invalid_language_details_on_heartbeat() { if ( ! WPML_WordPress_Actions::is_heartbeat() ) { return false; } if ( isset( $_POST['data']['icl_post_language'], $_POST['data']['icl_trid'] ) ) { $_POST['icl_post_language'] = Sanitize::string( $_POST['data']['icl_post_language'] ); $_POST['icl_trid'] = filter_var( $_POST['data']['icl_trid'], FILTER_SANITIZE_NUMBER_INT ); return false; } return true; } /** * @param integer $post_id * @param SitePress $sitepress * * @return null|string */ public function get_save_post_lang( $post_id, $sitepress ) { $language_code = null; if ( isset( $_POST['post_ID'] ) && (int) $_POST['post_ID'] === (int) $post_id ) { $language_code = filter_var( ( isset( $_POST['icl_post_language'] ) ? $_POST['icl_post_language'] : '' ), FILTER_SANITIZE_FULL_SPECIAL_CHARS ); } $language_code = $language_code ? $language_code : filter_input( INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); return $language_code ? $language_code : parent::get_save_post_lang( $post_id, $sitepress ); } /** * @param array $post_vars * @return bool */ private function is_inline_action( $post_vars ) { return isset( $post_vars[ 'action' ] ) && $post_vars[ 'action' ] == 'inline-save' || isset( $_GET[ 'bulk_edit' ] ) || isset( $_GET[ 'doing_wp_cron' ] ) || ( isset( $_GET[ 'action' ] ) && $_GET[ 'action' ] == 'untrash' ); } /** * @param int $trid * @param string $language_code * @param string $default_language * * @return null|string */ protected function get_save_post_source_lang( $trid, $language_code, $default_language ) { $source_language = filter_input ( INPUT_GET, 'source_lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $source_language = $source_language ? $source_language : $this->get_source_language_from_referer(); $source_language = $source_language ? $source_language : SitePress::get_source_language_by_trid ( $trid ); $source_language = $source_language === 'all' ? $default_language : $source_language; $source_language = $source_language !== $language_code ? $source_language : null; return $source_language; } /** * Gets the source_language $_GET parameter from the HTTP_REFERER * * @return string|bool */ private function get_source_language_from_referer() { if ( ! isset( $_SERVER['HTTP_REFERER'] ) ) { return false; } $referer = $_SERVER['HTTP_REFERER']; $query = (string) wpml_parse_url( $referer, PHP_URL_QUERY ); parse_str( $query, $query_parts ); return isset( $query_parts['source_lang'] ) ? $query_parts['source_lang'] : false; } public function get_trid_from_referer() { $http_referer = $this->get_http_referer(); return $http_referer->get_trid(); } protected function get_http_referer() { if ( ! $this->http_referer ) { $factory = new WPML_URL_HTTP_Referer_Factory(); $this->http_referer = $factory->create(); } return $this->http_referer; } } post-translation/wpml-create-post-helper.class.php 0000755 00000005507 14720425051 0016374 0 ustar 00 <?php /** * Class WPML_Create_Post_Helper * * @since 3.2 */ class WPML_Create_Post_Helper { /** @var SitePress $sitepress */ private $sitepress; public function __construct( SitePress $sitepress ) { $this->sitepress = $sitepress; } /** * @param array $postarr will be escaped inside the method * @param string|null $lang * @param bool $wp_error * * @return int|WP_Error */ public function insert_post( array $postarr, $lang = null, $wp_error = false ) { $current_language = null; $postarr = $this->slash_and_preserve_tag_ids( $postarr ); if ( $lang ) { $current_language = $this->sitepress->get_current_language(); $this->sitepress->switch_lang( $lang, false ); } if ( isset( $postarr['ID'] ) ) { /** * Prevents from bug on multiple update post calls during one request. * During first update post call wrong terms in the original language were put to the cache in the process of post revision save. * * @see https://onthegosystems.myjetbrains.com/youtrack/issue/wpmldev-672 */ $returnTrue = \WPML\FP\Fns::always( true ); add_filter( 'wpml_disable_term_adjust_id', $returnTrue ); $new_post_id = wp_update_post( $postarr, $wp_error ); remove_filter( 'wpml_disable_term_adjust_id', $returnTrue ); } else { add_filter( 'wp_insert_post_empty_content', array( $this, 'allow_empty_post' ), 10, 0 ); $new_post_id = wp_insert_post( $postarr, $wp_error ); remove_filter( 'wp_insert_post_empty_content', array( $this, 'allow_empty_post' ) ); } if ( $current_language ) { $this->sitepress->switch_lang( $current_language, false ); } return $new_post_id; } public function allow_empty_post() { return false; // We need to return false to indicate that the post is not empty } /** * We need to make sure that tag IDs are not casted into strings. * This is a side effect of https://core.trac.wordpress.org/ticket/45121 * (wp_update_post() can modify post tag) for which we have * a temporary fix in `\WPML_Page_Builders_Media_Shortcodes_Update::translate`. * * @param array $postarr * * @return array */ private function slash_and_preserve_tag_ids( array $postarr ) { if ( array_key_exists( 'tags_input', $postarr ) ) { $tagIds = array_filter( $postarr['tags_input'], 'is_int' ); $postarr = wp_slash( $postarr ); $postarr['tags_input'] = array_merge( $tagIds, $this->parse_tag( $postarr['tags_input'] ) ); } else { $postarr = wp_slash( $postarr ); } return $postarr; } private function parse_tag( $tags ) { if ( empty( $tags ) ) { $tags = array(); } if ( ! is_array( $tags ) ) { $comma = _x( ',', 'tag delimiter' ); if ( ',' !== $comma ) { $tags = str_replace( $comma, ',', $tags ); } $tags = explode( ',', trim( $tags, " \n\t\r\0\x0B," ) ); } return $tags; } } post-translation/wpml-root-page-actions.class.php 0000755 00000022213 14720425051 0016215 0 ustar 00 <?php class WPML_Root_Page_Actions { /** @var array $sp_settings */ private $sp_settings; public function __construct( &$sitepress_settings ) { $this->sp_settings = &$sitepress_settings; } public function delete_root_page_lang() { global $wpdb; $root_id = $this->get_root_page_id (); if ( $root_id ) { $update_args = array( 'element_id' => $root_id, 'element_type' => 'post_page', 'context' => 'post' ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'before_delete' ) ) ); $wpdb->delete ( $wpdb->prefix . 'icl_translations', array( 'element_id' => $root_id, 'element_type' => 'post_page' ), array( '%d', '%s' ) ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'after_delete' ) ) ); } } /** * Checks if a given $url points at the root page * * @param string $url * * @return bool * * @uses \WPML_Root_Page::is_root_page */ public function is_url_root_page( $url ) { $ret = false; if ( $this->get_root_page_id() ) { $ret = WPML_Root_Page::is_root_page( $url ); } return $ret; } /** * If a page is used as the root page, returns the id of that page, otherwise false. * * @return int|false */ public function get_root_page_id() { $urls_in_dirs = isset($this->sp_settings['language_negotiation_type']) && (int)$this->sp_settings['language_negotiation_type'] === 1; $urls = isset( $this->sp_settings['urls'] ) ? $this->sp_settings['urls'] : array(); return $urls_in_dirs && isset( $urls['root_page'] ) && ! empty( $urls['directory_for_default_language'] ) && isset( $urls['show_on_root'] ) && $urls['show_on_root'] === 'page' && $urls['root_page'] ? $urls['root_page'] : false; } function wpml_home_url_init() { global $pagenow, $sitepress; if ( $pagenow == 'post.php' || $pagenow == 'post-new.php' ) { $root_id = $this->get_root_page_id(); if ( ! empty( $_GET['wpml_root_page'] ) && ! empty( $root_id ) ) { $rp = get_post( $root_id ); if ( $rp && $rp->post_status != 'trash' ) { wp_redirect( get_edit_post_link( $root_id, 'no-display' ) ); exit; } } if ( isset( $_GET['wpml_root_page'] ) && $_GET['wpml_root_page'] || ( isset( $_GET['post'] ) && $_GET['post'] == $root_id ) ) { remove_action( 'admin_head', array( $sitepress, 'post_edit_language_options' ) ); add_action( 'admin_head', array( $this, 'wpml_home_url_language_box_setup' ) ); remove_action( 'page_link', array( $sitepress, 'permalink_filter' ), 1 ); } } } function wpml_home_url_exclude_root_page_from_menus( $args ) { if ( !empty( $args[ 'exclude' ] ) ) { $args[ 'exclude' ] .= ','; } else { $args[ 'exclude' ] = ''; } $args[ 'exclude' ] .= $this->get_root_page_id(); return $args; } /** * Filters out all page menu items that point to the root page. * * @param object[] $items * * @return array * * @hook wp_get_nav_menu_items */ function exclude_root_page_menu_item( $items ) { $root_id = $this->get_root_page_id(); foreach ( $items as $key => $item ) { if ( isset( $item->object_id ) && isset( $item->type ) && $item->object_id == $root_id && $item->type === 'post_type' ) { unset( $items[ $key ] ); } } return $items; } function wpml_home_url_exclude_root_page( $excludes ) { $excludes[ ] = $this->get_root_page_id(); return $excludes; } function wpml_home_url_exclude_root_page2( $args ) { $args[ 'exclude' ][ ] = $this->get_root_page_id(); return $args; } function wpml_home_url_get_pages( $pages ) { $root_id = $this->get_root_page_id(); foreach ( $pages as $k => $page ) { if ( $page->ID == $root_id ) { unset( $pages[ $k ] ); $pages = array_values ( $pages ); break; } } return $pages; } function wpml_home_url_language_box_setup() { add_meta_box( WPML_Meta_Boxes_Post_Edit_HTML::WRAPPER_ID, __( 'Language', 'sitepress' ), array( $this, 'wpml_home_url_language_box' ), 'page', /** * Filter meta box position. * * The context within the screen where the boxes should display. Available contexts vary from screen to screen. * Post edit screen contexts include 'normal', 'side', and 'advanced'. * * @param 'advanced'|'normal'|'side' $position * @param string $meta_box_id Meta box ID. * * @since 4.2.8 * */ apply_filters( 'wpml_post_edit_meta_box_context', 'side', WPML_Meta_Boxes_Post_Edit_HTML::WRAPPER_ID ), apply_filters( 'wpml_post_edit_meta_box_priority', 'high' ) ); } function wpml_home_url_language_box( $post ) { $root_id = $this->get_root_page_id(); if ( isset( $_GET[ 'wpml_root_page' ] ) || ( !empty( $root_id ) && $post->ID == $root_id ) ) { _e ( "This page does not have a language since it's the site's root page." ); echo '<input type="hidden" name="_wpml_root_page" value="1" />'; } } function wpml_home_url_save_post_actions( $pidd, $post ) { global $sitepress, $wpdb, $iclTranslationManagement; if ( (bool) filter_input ( INPUT_POST, '_wpml_root_page' ) === true ) { if ( isset( $_POST[ 'autosave' ] ) || ( isset( $post->post_type ) && $post->post_type == 'revision' ) ) { return; } $iclsettings[ 'urls' ][ 'root_page' ] = $post->ID; $sitepress->save_settings ( $iclsettings ); remove_action( 'save_post', array( $sitepress, 'save_post_actions' ), 10 ); if ( !is_null ( $iclTranslationManagement ) ) { remove_action( 'save_post', array( $iclTranslationManagement, 'save_post_actions' ), 11 ); } $update_args = array( 'element_id' => $post->ID, 'element_type' => 'post_page', 'context' => 'post' ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'before_delete' ) ) ); $wpdb->query ( $wpdb->prepare ( "DELETE FROM {$wpdb->prefix}icl_translations WHERE element_type='post_page' AND element_id=%d", $post->ID ) ); do_action( 'wpml_translation_update', array_merge( $update_args, array( 'type' => 'after_delete' ) ) ); } } function wpml_home_url_setup_root_page() { global $sitepress, $wpml_query_filter; remove_action( 'template_redirect', 'redirect_canonical' ); add_action( 'parse_query', array( $this, 'action_wpml_home_url_parse_query' ) ); remove_filter( 'posts_join', array( $wpml_query_filter, 'posts_join_filter' ), 10 ); remove_filter( 'posts_where', array( $wpml_query_filter, 'posts_where_filter' ), 10 ); $root_id = $this->get_root_page_id(); $rp = $root_id ? get_post( $root_id ) : false; if ( $rp && $rp->post_status != 'trash' ) { $sitepress->ROOT_URL_PAGE_ID = $root_id; } } /** * @param WP_Query $q * * @return mixed */ function wpml_home_url_parse_query( $q, $remove_filter = 'wpml_home_url_parse_query' ) { if ( ! $q->is_main_query() ) { return $q; } if ( ! WPML_Root_Page::is_current_request_root() ) { return $q; } else { remove_action( 'parse_query', array( $this, $remove_filter ) ); $uri_path = trim( wpml_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' ); $uri_parts = explode( '/', $uri_path ); $potential_pagination_parameter = array_pop( $uri_parts ); $query_args = array(); wp_parse_str( wpml_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ), $query_args ); foreach ( $query_args as $key => $value ) { $query_args[ $key ] = \WPML\API\Sanitize::string( $value); } if ( is_numeric( $potential_pagination_parameter ) ) { $query_args['page'] = $potential_pagination_parameter; } $q->parse_query( $query_args ); $root_id = $this->get_root_page_id(); add_action( 'parse_query', array( $this, $remove_filter ) ); if ( false !== $root_id ) { $q = $this->set_page_query_parameters( $q, $root_id ); } else { $front_page = get_option( 'page_on_front' ); if ( $front_page ) { $q = $this->set_page_query_parameters( $q, $front_page ); } } } return $q; } function action_wpml_home_url_parse_query( $q ) { $this->wpml_home_url_parse_query( $q, 'action_wpml_home_url_parse_query' ); } private function set_page_query_parameters( $q, $page_id ) { $q->query_vars['page_id'] = $page_id; $q->query['page_id'] = $page_id; $q->is_page = 1; $q->queried_object = new WP_Post( get_post( $page_id ) ); $q->queried_object_id = $page_id; $q->query_vars['error'] = ''; $q->is_404 = false; $q->query['error'] = null; $q->is_home = false; $q->is_singular = true; return $q; } } /** * Checks if the language switcher is to be displayed. * Used to check if the displayed page is a root page and the switcher is to be hidden because of it. * * @return bool true if the switcher is to be hidden */ function wpml_home_url_ls_hide_check() { global $sitepress; return $sitepress->get_setting( 'language_negotiation_type' ) == 1 && (bool) ( $urls = $sitepress->get_setting( 'urls' ) ) === true && ! empty( $urls['directory_for_default_language'] ) && isset( $urls['show_on_root'] ) && $urls['show_on_root'] === 'page' && ! empty( $urls['hide_language_switchers'] ) && WPML_Root_Page::is_current_request_root(); } post-translation/wpml-post-hierarchy-sync.class.php 0000755 00000001174 14720425051 0016600 0 ustar 00 <?php class WPML_Post_Hierarchy_Sync extends WPML_Hierarchy_Sync { protected $element_id_column = 'ID'; protected $parent_element_id_column = 'ID'; protected $parent_id_column = 'post_parent'; protected $element_type_column = 'post_type'; protected $element_type_prefix = 'post_'; /** * @param wpdb $wpdb */ public function __construct( &$wpdb ) { parent::__construct( $wpdb ); $this->elements_table = $wpdb->posts; } /** * @param string $element_type * * @return bool */ public function is_hierarchical( $element_type ) { return is_post_type_hierarchical( $element_type ); } } post-translation/SyncTranslationDocumentStatus.php 0000755 00000002473 14720425051 0016645 0 ustar 00 <?php namespace WPML\Core\PostTranslation; use WPML\FP\Fns; use WPML\FP\Obj; use WPML\FP\Lst; class SyncTranslationDocumentStatus implements \IWPML_Action, \IWPML_DIC_Action, \IWPML_Backend_Action, \IWPML_REST_Action { private $sitepress; public function __construct( \SitePress $sitepress ) { $this->sitepress = $sitepress; } public function add_hooks() { add_action( 'transition_post_status', [$this, 'onPostStatusChange'], 10, 3 ); } /** * @param string $newStatus * @param string $oldStatus * @param \WP_Post $post */ public function onPostStatusChange( $newStatus, $oldStatus, $post ) { if ( $newStatus === $oldStatus || 'publish' !== $newStatus ) { return; } if ( 'draft' !== $oldStatus ) { return; } $settings = $this->sitepress->get_settings(); if ( ! $settings['translated_document_status_sync'] ) { return; } $allPosts = \WPML\Element\API\PostTranslations::getIfOriginal( $post->ID ); $originalPost = Lst::nth( 0, Fns::filter( Obj::prop( 'original' ), $allPosts ) ); $postTranslations = Fns::reject( Obj::prop( 'original' ), $allPosts ); if ( ! $originalPost || empty( $postTranslations ) ) { return; } foreach ( $postTranslations as $sourceLangCode => $data ) { wp_update_post( ['ID' => $data->element_id, 'post_status' => $newStatus] ); } } } functions-sanitation.php 0000755 00000003642 14720425051 0011444 0 ustar 00 <?php /** * @param string $input * @param string $default_if_invalid * * @return string */ function wpml_sanitize_hex_color( $input, $default_if_invalid = '' ) { $input = sanitize_text_field( $input ); $result = $input; if ( ! is_string( $input ) || ! wpml_is_valid_hex_color( $input ) ) { $result = $default_if_invalid; } return $result; } function wpml_sanitize_hex_color_array( $input, $default_if_invalid = '', $bypass_non_strings = true, $recursive = false ) { $result = $input; if ( is_array( $input ) ) { $result = array(); foreach ( $input as $key => $value ) { if ( is_array( $value ) && $recursive ) { $result[ $key ] = wpml_sanitize_hex_color_array( $value, $default_if_invalid, $recursive ); } elseif ( is_string( $value ) ) { $result[ $key ] = wpml_sanitize_hex_color( $value, $default_if_invalid ); } elseif ( $bypass_non_strings ) { $result[ $key ] = $value; } } } return $result; } /** * @param string|array $input * * @return bool */ function wpml_is_valid_hex_color( $input ) { if ( 'transparent' === $input || ( is_string( $input ) && preg_match( '/' . wpml_get_valid_hex_color_pattern() . '/i', $input ) ) ) { $is_valid = true; } else { $try_rgb2hex = is_array( $input ) ? wpml_rgb_to_hex( $input ) : false; $is_valid = $try_rgb2hex ? preg_match( '/' . wpml_get_valid_hex_color_pattern() . '/i', $try_rgb2hex ) : false; } return $is_valid; } function wpml_get_valid_hex_color_pattern() { return '(^#[a-fA-F0-9]{6}$)|(^#[a-fA-F0-9]{3}$)'; } /** * Convert RGB color code to HEX code. * * @param array $rgb * * @return string|false */ function wpml_rgb_to_hex( $rgb ) { if ( ! is_array( $rgb ) || count( $rgb ) < 3 ) { return false; } $hex = '#'; $hex .= str_pad( dechex( $rgb[0] ), 2, '0', STR_PAD_LEFT ); $hex .= str_pad( dechex( $rgb[1] ), 2, '0', STR_PAD_LEFT ); $hex .= str_pad( dechex( $rgb[2] ), 2, '0', STR_PAD_LEFT ); return $hex; } hacks.php 0000755 00000001726 14720425051 0006357 0 ustar 00 <?php // using this file to handle particular situations that would involve more ellaborate solutions add_action( 'init', 'icl_load_hacks' ); function icl_dev_mode_warning() { ?> <div class="error message"> <p>This is a development version of WPML, provided for evaluation purposes only. The code you are using did not go through any testing or QA. Do not use it in production sites.</strong></p> <p>To obtain production versions of WPML, visit: <a href="https://wpml.org">wpml.org</a>.</p> </div> <?php } function icl_load_hacks() { if ( file_exists( WPML_PLUGIN_PATH . '/inc/hacks/misc-constants.php' ) ) { include WPML_PLUGIN_PATH . '/inc/hacks/misc-constants.php'; } include WPML_PLUGIN_PATH . '/inc/hacks/language-canonical-redirects.php'; if ( is_admin() && ! defined( 'ICL_PRODUCTION_MODE' ) ) { add_action( 'admin_notices', 'icl_dev_mode_warning' ); icl_dev_mode_warning(); } } require WPML_PLUGIN_PATH . '/inc/hacks/missing-php-functions.php'; functions.php 0000755 00000064356 14720425051 0007306 0 ustar 00 <?php /** * SitePress Template functions * * @package wpml-core */ use function WPML\Container\make; /** * Returns true if the site uses ICanLocalize. * * @return bool */ function wpml_site_uses_icl() { global $wpdb; $setting = 'site_does_not_use_icl'; if ( icl_get_setting( $setting, false ) ) { return false; } $cache = new WPML_WP_Cache( 'wpml-checks' ); $found = false; $site_uses_icl = $cache->get( 'site_uses_icl', $found ); if ( ! $found ) { $site_uses_icl = false; $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}icl_translation_status'" ); if ( $table_exists ) { $icl_job_count_query = $wpdb->prepare( "SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_service = %s LIMIT 1;", 'icanlocalize' ); $site_uses_icl = (bool) $wpdb->get_var( $icl_job_count_query ); } $cache->set( 'site_uses_icl', $site_uses_icl ); } if ( icl_get_setting( 'setup_complete', false ) && ! $site_uses_icl ) { icl_set_setting( $setting, true, true ); } return $site_uses_icl; } /** * Returns the value of a given key setting. * * @param string $key The setting's key. * @param mixed|false $default The value to use if the setting does not exist. * * @return bool|mixed * @since 3.1 * @deprecated 3.2 use `\wpml_setting` or 'wpml_get_setting_filter' filter instead */ function icl_get_setting( $key, $default = false ) { return wpml_get_setting( $key, $default ); } /** * Get a WPML setting value. * If the Main SitePress Class cannot be accessed by the function it will read the setting from the database. * It will return `$default` if the requested key is not set. * * @param string $key The setting's key. * @param mixed|null $default Required. The value to return if the settings key does not exist * (typically it's false, but you may want to use something else). * * @return mixed The value of the requested setting, or `$default` * @since 4.1 */ function wpml_get_setting( $key, $default = null ) { global $sitepress_settings; $sitepress_settings = isset( $sitepress_settings ) ? $sitepress_settings : get_option( 'icl_sitepress_settings' ); return isset( $sitepress_settings[ $key ] ) ? $sitepress_settings[ $key ] : $default; } /** * Get a WPML setting value. * If the Main SitePress Class cannot be access to the function will read the setting from the database. * Will return false if the requested key is not set or. * the default value passed in the function's second parameter. * * @param mixed|false $default Required. The value to return if the settings key does not exist * (typically it's false, but you may want to use something else). * @param string $key The setting's key. * * @return mixed The value of the requested setting, or $default * @since 3.2 * @uses \SitePress::api_hooks */ function wpml_get_setting_filter( $default, $key ) { $args = func_get_args(); if ( count( $args ) > 2 && $args[2] !== null ) { $default = $args[2]; } return wpml_get_setting( $key, $default ); } /** * Returns the value of a given key sub-setting. * * @param string $key The setting's key. * @param string $sub_key The settings name key to return the value of. * @param mixed|false $default Required. The value to return if the settings key does not exist * (typically it's false, but you may want to use something else). * * @return bool|mixed * @since 3.1 * @deprecated 3.2 use 'wpml_sub_setting' filter instead */ function icl_get_sub_setting( $key, $sub_key, $default = false ) { $parent = icl_get_setting( $key, array() ); return isset( $parent[ $sub_key ] ) ? $parent[ $sub_key ] : $default; } /** * Gets a WPML sub setting value. * * @param mixed|false $default Required. The value to return if the settings key does not exist * (typically it's false, but you may want to use something else). * @param string $key The settings name key the sub key belongs to. * @param string $sub_key The sub key to return the value of. * @param mixed $deprecated Deprecated param. * * @return mixed The value of the requested setting, or $default * @todo [WPML 3.3] Remove deprecated argument * * @uses \wpml_get_setting_filter * * @since 3.2 * @uses \SitePress::api_hooks */ function wpml_get_sub_setting_filter( $default, $key, $sub_key, $deprecated = null ) { $default = null !== $deprecated && ! $default ? $deprecated : $default; $parent = wpml_get_setting_filter( array(), $key ); return isset( $parent[ $sub_key ] ) ? $parent[ $sub_key ] : $default; } /** * Saves the value of a given key. * * @param string $key The settings name key the sub key belongs to. * @param mixed $value The value to assign to the given key. * @param bool $save_now Must call icl_save_settings() to permanently store the value. * * @return bool Always True. If `$save_now === true`, it returns the result of `update_option` */ function icl_set_setting( $key, $value, $save_now = false ) { global $sitepress_settings; $result = true; $sitepress_settings[ $key ] = $value; if ( true === $save_now ) { /* We need to save settings anyway, in this case. */ $result = update_option( 'icl_sitepress_settings', $sitepress_settings ); do_action( 'icl_save_settings', $sitepress_settings ); } return $result; } /** * Save the settings in the db. */ function icl_save_settings() { global $sitepress; $sitepress->save_settings(); } /** * Gets all the settings. * * @return array|false */ function icl_get_settings() { global $sitepress; return isset( $sitepress ) ? $sitepress->get_settings() : false; } /** * Add settings link to plugin page. * * @param SitePress $sitepress * @param array<string> $links * @param string $file * * @return array */ function icl_plugin_action_links( SitePress $sitepress, $links, $file ) { if ( $file == WPML_PLUGIN_BASENAME ) { $endpoint = $sitepress->is_setup_complete() ? 'languages.php' : 'setup.php'; $links[] = '<a href="admin.php?page=' . WPML_PLUGIN_FOLDER . '/menu/' . $endpoint . '">' . __( 'Configure', 'sitepress' ) . '</a>'; } return $links; } if ( defined( 'ICL_DEBUG_MODE' ) && ICL_DEBUG_MODE ) { add_action( 'admin_notices', '_icl_deprecated_icl_debug_mode' ); } function _icl_deprecated_icl_debug_mode() { echo '<div class="updated"><p><strong>ICL_DEBUG_MODE</strong> no longer supported. Please use <strong>WP_DEBUG</strong> instead.</p></div>'; } if ( ! function_exists( 'icl_js_escape' ) ) { function icl_js_escape( $str ) { $str = esc_js( $str ); $str = htmlspecialchars_decode( $str ); return $str; } } /** * Read and, if needed, generate the site ID based on the scope. * * @param string $scope Defaults to "global". * Use a different value when the ID is used for specific scopes. * * @param bool $create_new Forces the creation of a new ID. * * @return string|null The generated/stored ID or null if it wasn't possible to generate/store the value. */ function wpml_get_site_id( $scope = WPML_Site_ID::SITE_SCOPES_GLOBAL, $create_new = false ) { static $site_id; if ( ! $site_id ) { $site_id = new WPML_Site_ID(); } return $site_id->get_site_id( $scope, $create_new ); } function _icl_tax_has_objects_recursive( $id, $term_id = -1, $rec = 0 ) { // based on the case where two categories were one the parent of another // eliminating the chance of infinite loops by letting this function calling itself too many times // 100 is the default limit in most of teh php configuration // // this limit this function to work only with categories nested up to 60 levels // should enough for most cases if ( $rec > 60 ) { return false; } global $wpdb; if ( $term_id === -1 ) { $term_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id=%d", $id ) ); } $children = $wpdb->get_results( $wpdb->prepare( " SELECT term_taxonomy_id, term_id, count FROM {$wpdb->term_taxonomy} WHERE parent = %d ", $term_id ) ); $count = 0; foreach ( $children as $ch ) { $count += $ch->count; } if ( $count ) { return true; } else { foreach ( $children as $ch ) { if ( _icl_tax_has_objects_recursive( $ch->term_taxonomy_id, $ch->term_id, $rec + 1 ) ) { return true; } } } return false; } function _icl_trash_restore_prompt() { if ( isset( $_GET['lang'] ) ) { $post = get_post( intval( $_GET['post'] ) ); if ( isset( $post->post_status ) && $post->post_status == 'trash' ) { $post_type_object = get_post_type_object( $post->post_type ); $delete_post_url = get_delete_post_link( $post->ID, '', true ); if ( is_string( $delete_post_url ) ) { $delete_post_link = '<a href="' . esc_url( $delete_post_url ) . '">' . esc_html__( 'delete it permanently', 'sitepress' ) . '</a>'; $restore_post_link = '<a href="' . wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&action=untrash', $post->ID ) ), 'untrash-post_' . $post->ID ) . '">' . esc_html__( 'restore', 'sitepress' ) . '</a>'; $ret = '<p>' . sprintf( esc_html__( 'This translation is currently in the trash. You need to either %1$s or %2$s it in order to continue.' ), $delete_post_link, $restore_post_link ); wp_die( $ret ); } } } } /** * Build or update duplicated posts from a master post. * * @param string $master_post_id The ID of the post to duplicate from. Master post doesn't need to be in the default language. * * @uses SitePress * @uses TranslationManagement * @since unknown * @deprecated 3.2 use 'wpml_admin_make_duplicates' action instead */ function icl_makes_duplicates( $master_post_id ) { wpml_admin_make_post_duplicates_action( (int) $master_post_id ); } /** * Build or update duplicated posts from a master post. * To be used only for admin backend actions * * @param int $master_post_id The ID of the post to duplicate from. * The ID can be that of a post, page or custom post * Master post doesn't need to be in the default language. * * @see $iclTranslationManagement in \SitePress:: __construct * * @uses SitePress * @uses TranslationManagement * @since 3.2 * @uses \SitePress::api_hooks */ function wpml_admin_make_post_duplicates_action( $master_post_id ) { $post = get_post( $master_post_id ); $post_type = $post->post_type; if ( $post->post_status == 'auto-draft' || $post->post_type == 'revision' ) { return; } global $sitepress; $iclTranslationManagement = wpml_load_core_tm(); if ( $sitepress->is_translated_post_type( $post_type ) ) { $iclTranslationManagement->make_duplicates_all( $master_post_id ); } } /** * Build duplicated posts from a master post only in case of the duplicate not being present at the time. * * @param string $master_post_id The ID of the post to duplicate from. Master post doesn't need to be in the default language. * * @uses SitePress * @since unknown * @deprecated 3.2 use 'wpml_make_post_duplicates' action instead */ function icl_makes_duplicates_public( $master_post_id ) { wpml_make_post_duplicates_action( (int) $master_post_id ); } /** * Build duplicated posts from a master post only in case of the duplicate not being present at the time. * * @param int $master_post_id The ID of the post to duplicate from. * Master post doesn't need to be in the default language. * * @uses SitePress * @since 3.2 * @uses \SitePress::api_hooks */ function wpml_make_post_duplicates_action( $master_post_id ) { global $sitepress; $master_post = get_post( $master_post_id ); if ( 'auto-draft' === $master_post->post_status || 'revision' === $master_post->post_type ) { return; } $active_langs = $sitepress->get_active_languages(); foreach ( $active_langs as $lang_to => $one ) { $trid = $sitepress->get_element_trid( $master_post->ID, 'post_' . $master_post->post_type ); $lang_from = $sitepress->get_source_language_by_trid( $trid ); if ( $lang_from == $lang_to ) { continue; } $sitepress->make_duplicate( $master_post_id, $lang_to ); } } /** * Wrapper function for deprecated like_escape() and recommended wpdb::esc_like() * * @global wpdb $wpdb * * @param string $text * * @return string */ function wpml_like_escape( $text ) { global $wpdb; if ( method_exists( $wpdb, 'esc_like' ) ) { return $wpdb->esc_like( $text ); } /** @noinspection PhpDeprecationInspection */ return like_escape( $text ); } function icl_do_not_promote() { return defined( 'ICL_DONT_PROMOTE' ) && ICL_DONT_PROMOTE; } /** * @param string $time * * @return string */ function icl_convert_to_user_time( $time ) { // offset between server time and user time in seconds $time_offset = get_option( 'gmt_offset' ) * 3600; $local_time = __( 'Last Update Time could not be determined', 'sitepress' ); try { // unix time stamp in server time $creation_time = strtotime( $time ); // creating dates before 2014 are impossible if ( $creation_time !== false ) { $local_time = date( 'Y-m-d H:i:s', $creation_time + $time_offset ); } } catch ( Exception $e ) { // Ignoring the exception, as we already set the default value in $local_time } return $local_time; } /** * Check if given language is activated * * @global sitepress $sitepress * * @param string $language 2 letters language code * * @return boolean * @since unknown * @deprecated 3.2 use 'wpml_language_is_active' filter instead */ function icl_is_language_active( $language ) { global $sitepress; $active_languages = $sitepress->get_active_languages(); return isset( $active_languages[ $language ] ); } /** * Checks if given language is enabled * * @param mixed $empty_value This is normally the value the filter will be modifying. * We are not filtering anything here therefore the NULL value * This for the filter function to actually receive the full argument list: * apply_filters('wpml_language_is_active', '', $language_code); * @param string $language_code The language code to check Accepts a 2-letter language code * * @return boolean * @global sitepress $sitepress * * @since 3.2 * @uses \SitePress::api_hooks */ function wpml_language_is_active_filter( $empty_value, $language_code ) { global $sitepress; return $sitepress->is_active_language( $language_code ); } /** * @param string $url url either with or without schema * Removes the subdirectory in which WordPress is installed from a url. * If WordPress is not installed in a subdirectory, then the input is returned unaltered. * * @return string the url input without the blog's subdirectory. Potentially existing schemata on the input are kept intact. */ function wpml_strip_subdir_from_url( $url ) { /** @var WPML_URL_Converter $wpml_url_converter */ global $wpml_url_converter; $subdir = wpml_parse_url( $wpml_url_converter->get_abs_home(), PHP_URL_PATH ); $subdir_slugs = ! empty( $subdir ) ? array_values( array_filter( explode( '/', $subdir ) ) ) : ['']; $url_path_expl = explode( '/', preg_replace( '#^(http|https)://#', '', $url ) ); array_shift( $url_path_expl ); $url_slugs = array_values( array_filter( $url_path_expl ) ); $url_slugs_before = $url_slugs; $url_slugs = array_diff_assoc( $url_slugs, $subdir_slugs ); $url = str_replace( '/' . join( '/', $url_slugs_before ), '/' . join( '/', $url_slugs ), $url ); return untrailingslashit( $url ); } /** * Changes array of items into string of items, separated by comma and sql-escaped * * @see https://coderwall.com/p/zepnaw * @global wpdb $wpdb * * @param mixed|array $items item(s) to be joined into string * @param string $format %s or %d * * @return string Items separated by comma and sql-escaped */ function wpml_prepare_in( $items, $format = '%s' ) { global $wpdb; $items = (array) $items; $how_many = count( $items ); if ( $how_many > 0 ) { $placeholders = array_fill( 0, $how_many, $format ); $prepared_format = implode( ',', $placeholders ); $prepared_in = $wpdb->prepare( $prepared_format, $items ); } else { $prepared_in = ''; } return $prepared_in; } function is_not_installing_plugins() { global $sitepress; $checked = isset( $_REQUEST['checked'] ) ? (array) $_REQUEST['checked'] : array(); if ( ! isset( $_REQUEST['action'] ) ) { return true; } elseif ( $_REQUEST['action'] != 'activate' && $_REQUEST['action'] != 'activate-selected' ) { return true; } elseif ( ( ! isset( $_REQUEST['plugin'] ) || $_REQUEST['plugin'] != WPML_PLUGIN_FOLDER . '/' . basename( __FILE__ ) ) && ! in_array( WPML_PLUGIN_FOLDER . '/' . basename( __FILE__ ), $checked ) ) { return true; } elseif ( in_array( WPML_PLUGIN_FOLDER . '/' . basename( __FILE__ ), $checked ) && ! isset( $sitepress ) ) { return true; } return false; } function wpml_mb_strtolower( $string ) { if ( function_exists( 'mb_strtolower' ) ) { return mb_strtolower( $string ); } return strtolower( $string ); } function wpml_mb_strpos( $haystack, $needle, $offset = 0 ) { if ( function_exists( 'mb_strpos' ) ) { return mb_strpos( $haystack, $needle, $offset ); } return strpos( $haystack, $needle, $offset ); } function wpml_set_plugin_as_inactive() { global $icl_plugin_inactive; if ( ! defined( 'ICL_PLUGIN_INACTIVE' ) ) { define( 'ICL_PLUGIN_INACTIVE', true ); } $icl_plugin_inactive = true; } function wpml_version_is( $version_to_check, $comparison = '==' ) { return version_compare( ICL_SITEPRESS_VERSION, $version_to_check, $comparison ) && function_exists( 'wpml_site_uses_icl' ); } /** * Interrupts the plugin activation process if the WPML Core Plugin could not be activated */ function icl_suppress_activation() { $active_plugins = get_option( 'active_plugins' ); $icl_sitepress_idx = array_search( WPML_PLUGIN_BASENAME, $active_plugins ); if ( false !== $icl_sitepress_idx ) { unset( $active_plugins[ $icl_sitepress_idx ] ); update_option( 'active_plugins', $active_plugins ); unset( $_GET['activate'] ); $recently_activated = get_option( 'recently_activated' ); if ( ! isset( $recently_activated[ WPML_PLUGIN_BASENAME ] ) ) { $recently_activated[ WPML_PLUGIN_BASENAME ] = time(); update_option( 'recently_activated', $recently_activated ); } } } /** * @param SitePress $sitepress */ function activate_installer( $sitepress = null ) { // installer hook - start include_once WPML_PLUGIN_PATH . '/vendor/otgs/installer/loader.php'; // produces global variable $wp_installer_instance $args = array( 'plugins_install_tab' => 1, ); if ( $sitepress ) { $args['site_key_nags'] = array( array( 'repository_id' => 'wpml', 'product_name' => 'WPML', 'condition_cb' => array( $sitepress, 'setup' ), ), ); } /** * @var WP_Installer $wp_installer_instance */ /** @phpstan-ignore-next-line */ WP_Installer_Setup( $wp_installer_instance, $args ); // installer hook - end } function wpml_missing_filter_input_notice() { ?> <div class="message error"> <h3><?php esc_html_e( "WPML can't be functional because it requires a disabled PHP extension!", 'sitepress' ); ?></h3> <p><?php esc_html_e( 'To ensure and improve the security of your website, WPML makes use of the ', 'sitepress' ); ?><a href="http://php.net/manual/en/book.filter.php">PHP Data Filtering</a> extension.<br><br> <?php esc_html_e( 'The filter extension is enabled by default as of PHP 5.2.0. Before this time an experimental PECL extension was used, however, the PECL version is no longer recommended to be used or updated. (source: ', 'sitepress' ); ?> <a href="http://php.net/manual/en/filter.installation.php">PHP Manual Function Reference Variable and Type Related Extensions Filter Installing/Configuring</a>)<br> <br> <?php esc_html_e( 'The filter extension is enabled by default as of PHP 5.2, therefore it must have been disabled by either you or your host.', 'sitepress' ); ?> <br><?php esc_html_e( "To enable it, either you or your host will need to open your website's php.ini file and either:", 'sitepress' ); ?><br> <ol> <li><?php esc_html_e( "Remove the 'filter_var' string from the 'disable_functions' directive or...", 'sitepress' ); ?> </li> <li><?php esc_html_e( 'Add the following line:', 'sitepress' ); ?> <code class="inline-code">extension=filter.so</code></li> </ol> <?php $ini_location = php_ini_loaded_file(); if ( $ini_location !== false ) { ?> <strong><?php esc_html_e( 'Your php.ini file is located at', 'sitepress' ) . ' ' . esc_html( $ini_location ); ?>.</strong> <?php } ?> </div> <?php } function repair_el_type_collate() { global $wpdb; $correct_collate = $wpdb->get_var( $wpdb->prepare( "SELECT collation_name FROM information_schema.COLUMNS WHERE TABLE_NAME = '%s' AND COLUMN_NAME = 'post_type' AND table_schema = (SELECT DATABASE()) LIMIT 1", $wpdb->posts ) ); // translations $table_name = $wpdb->prefix . 'icl_translations'; $sql = $wpdb->prepare( "ALTER TABLE `$table_name` CHANGE `element_type` `element_type` VARCHAR( 36 ) NOT NULL DEFAULT 'post_post' COLLATE %s", $correct_collate ); if ( $wpdb->query( $sql ) === false ) { throw new Exception( $wpdb->last_error ); } } /** * Wrapper for `parse_url` using `wp_parse_url` * * @param string $url * @param int $component * * @return array|string|int|null * * @phpstan-return ($component is -1 * ? array|null * : ($component is PHP_URL_PORT ? int|null : string|null) * ) */ function wpml_parse_url( $url, $component = -1 ) { $ret = null; $component_map = array( PHP_URL_SCHEME => 'scheme', PHP_URL_HOST => 'host', PHP_URL_PORT => 'port', PHP_URL_USER => 'user', PHP_URL_PASS => 'pass', PHP_URL_PATH => 'path', PHP_URL_QUERY => 'query', PHP_URL_FRAGMENT => 'fragment', ); if ( $component === -1 ) { $ret = wp_parse_url( $url ); } elseif ( isset( $component_map[ $component ] ) ) { $key = $component_map[ $component ]; $parsed = wp_parse_url( $url ); $ret = isset( $parsed[ $key ] ) ? $parsed[ $key ] : null; } return $ret; } /** * Wrapper function to prevent ampersand to be encoded (depending on some PHP versions) * * @link http://php.net/manual/en/function.http-build-query.php#102324 * * @param array|object $query_data * * @return string */ function wpml_http_build_query( $query_data ) { return http_build_query( $query_data, '', '&' ); } /** * @param array $array * @param int $sort_flags * * @uses \wpml_array_unique_fallback * * @return array */ function wpml_array_unique( $array, $sort_flags = SORT_REGULAR ) { if ( version_compare( phpversion(), '5.2.9', '>=' ) ) { // phpcs:disable PHPCompatibility.FunctionUse.NewFunctionParameters.array_unique_sort_flagsFound -- This statement is preceded by a version check return array_unique( $array, $sort_flags ); // phpcs:enable PHPCompatibility.FunctionUse.NewFunctionParameters.array_unique_sort_flagsFound } return wpml_array_unique_fallback( $array, true ); } /** * @param array<mixed> $array * @param bool $keep_key_assoc * * @return array * @see \wpml_array_unique */ function wpml_array_unique_fallback( $array, $keep_key_assoc ) { $duplicate_keys = array(); $tmp = array(); foreach ( $array as $key => $val ) { // convert objects to arrays, in_array() does not support objects if ( is_object( $val ) ) { $val = (array) $val; } if ( ! in_array( $val, $tmp ) ) { $tmp[] = $val; } else { $duplicate_keys[] = $key; } } foreach ( $duplicate_keys as $key ) { unset( $array[ $key ] ); } return $keep_key_assoc ? $array : array_values( $array ); } /** * @return bool */ function wpml_is_rest_request() { return make( WPML_REST_Request_Analyze::class )->is_rest_request(); } /** * @return bool */ function wpml_is_rest_enabled() { return make( \WPML\Core\REST\Status::class )->isEnabled(); } function wpml_is_cli() { return defined( 'WP_CLI' ) && WP_CLI; } function wpml_sticky_post_sync( SitePress $sitepress = null ) { static $instance; if ( ! $instance ) { global $wpml_post_translations; if ( ! $sitepress ) { global $sitepress; } $instance = new WPML_Sticky_Posts_Sync( $sitepress, $wpml_post_translations, new WPML_Sticky_Posts_Lang_Filter( $sitepress, $wpml_post_translations ) ); } return $instance; } /** * @return WP_Filesystem_Direct */ function wpml_get_filesystem_direct() { static $instance; if ( ! $instance ) { $wp_api = new WPML_WP_API(); $instance = $wp_api->get_wp_filesystem_direct(); } return $instance; } /** * @param array $postarray It will be escaped inside the function * @param string|null $lang * @param bool $wp_error * * @return int|\WP_Error */ function wpml_update_escaped_post( array $postarray, $lang = null, $wp_error = false ) { return wpml_get_create_post_helper()->insert_post( $postarray, $lang, $wp_error ); } /** * @param string $group * * @return WPML_WP_Cache */ function wpml_get_cache( $group = '' ) { return new WPML_WP_Cache( $group ); } if ( ! function_exists( 'wpml_is_ajax' ) ) { /** * wpml_is_ajax - Returns true when the page is loaded via ajax. * * @since 3.1.5 * * @return bool */ function wpml_is_ajax() { if ( defined( 'DOING_AJAX' ) ) { return true; } return ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && wpml_mb_strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) ? true : false; } } if ( ! function_exists( 'wpml_get_flag_file_name' ) ) { /** * wpml_get_flag_file_name - Returns the SVG or PNG flag file name based on language code if it exists in '/res/flags/' dir. * * @param $lang_code * * @return string * @since 4.6.0 * */ function wpml_get_flag_file_name( $lang_code ) { if ( file_exists( WPML_PLUGIN_PATH . '/res/flags/' . $lang_code . '.svg' ) ) { $file = $lang_code . '.svg'; } elseif ( file_exists( WPML_PLUGIN_PATH . '/res/flags/' . $lang_code . '.png' ) ) { $file = $lang_code . '.png'; } elseif ( file_exists( WPML_PLUGIN_PATH . '/res/flags/nil.svg' ) ) { $file = 'nil.svg'; } else { $file = 'nil.png'; } return $file; } } hacks/supress-warnings-for-xmlrpc.php 0000755 00000000553 14720425051 0013775 0 ustar 00 <?php /* $_pingback_url_parts = parse_url(get_bloginfo('pingback_url')); if($_SERVER['REQUEST_URI'] == $_pingback_url_parts['path']){ function __icl_void_error_handler($errno, $errstr, $errfile, $errline){ throw new Exception ($errstr . ' [' . $errno . '] in '. $errfile . ':' . $errline); } set_error_handler('__icl_void_error_handler',E_ALL); } */ hacks/misc-constants.php 0000755 00000000060 14720425051 0011312 0 ustar 00 <?php define( 'ICL_PRODUCTION_MODE', '2.1.1' ); hacks/missing-php-functions.php 0000755 00000011566 14720425051 0012626 0 ustar 00 <?php if ( ! function_exists( '_cleanup_header_comment' ) ) { function _cleanup_header_comment( $str ) { return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) ); } } if ( ! defined( 'E_DEPRECATED' ) ) { define( 'E_DEPRECATED', 8192 ); } if ( ! function_exists( 'esc_textarea' ) ) : function esc_textarea( $text ) { $safe_text = esc_html( $text ); return apply_filters( 'esc_textarea', $safe_text, $text ); } endif; /** * This file is part of the array_column library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) 2013 Ben Ramsey <http://benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ if ( ! function_exists( 'array_column' ) ) { /** * Returns the values from a single column of the input array, identified by * the $columnKey. * * Optionally, you may provide an $indexKey to index the values in the returned * array by the values from the $indexKey column in the input array. * * @param array $input A multi-dimensional array (record set) from which to pull * a column of values. * @param mixed $columnKey The column of values to return. This value may be the * integer key of the column you wish to retrieve, or it * may be the string key name for an associative array. * @param mixed $indexKey (Optional.) The column to use as the index/keys for * the returned array. This value may be the integer key * of the column, or it may be the string key name. * @return array|false */ function array_column( $input = null, $columnKey = null, $indexKey = null ) { // Using func_get_args() in order to check for proper number of // parameters and trigger errors exactly as the built-in array_column() // does in PHP 5.5. $argc = func_num_args(); $params = func_get_args(); if ( $argc < 2 ) { trigger_error( "array_column() expects at least 2 parameters, {$argc} given", E_USER_WARNING ); return null; } if ( ! is_array( $params[0] ) ) { trigger_error( 'array_column() expects parameter 1 to be array, ' . gettype( $params[0] ) . ' given', E_USER_WARNING ); return null; } if ( ! is_int( $params[1] ) && ! is_float( $params[1] ) && ! is_string( $params[1] ) && $params[1] !== null && ! ( is_object( $params[1] ) && method_exists( $params[1], '__toString' ) ) ) { trigger_error( 'array_column(): The column key should be either a string or an integer', E_USER_WARNING ); return false; } if ( isset( $params[2] ) && ! is_int( $params[2] ) && ! is_float( $params[2] ) && ! is_string( $params[2] ) && ! ( is_object( $params[2] ) && method_exists( $params[2], '__toString' ) ) ) { trigger_error( 'array_column(): The index key should be either a string or an integer', E_USER_WARNING ); return false; } $paramsInput = $params[0]; $paramsColumnKey = ( $params[1] !== null ) ? (string) $params[1] : null; $paramsIndexKey = null; if ( isset( $params[2] ) ) { if ( is_float( $params[2] ) || is_int( $params[2] ) ) { $paramsIndexKey = (int) $params[2]; } else { $paramsIndexKey = (string) $params[2]; } } $resultArray = array(); foreach ( $paramsInput as $row ) { $key = $value = null; $keySet = $valueSet = false; if ( $paramsIndexKey !== null && array_key_exists( $paramsIndexKey, $row ) ) { $keySet = true; $key = (string) $row[ $paramsIndexKey ]; } if ( $paramsColumnKey === null ) { $valueSet = true; $value = $row; } elseif ( is_array( $row ) && array_key_exists( $paramsColumnKey, $row ) ) { $valueSet = true; $value = $row[ $paramsColumnKey ]; } if ( $valueSet ) { if ( $keySet ) { $resultArray[ $key ] = $value; } else { $resultArray[] = $value; } } } return $resultArray; } } if ( ! function_exists( 'array_replace_recursive' ) ) { function array_replace_recursive( $array, $array1 ) { // handle the arguments, merge one by one $args = func_get_args(); $array = $args[0]; if ( ! is_array( $array ) ) { return $array; } $args_count = count( $args ); for ( $i = 1; $i < $args_count; $i ++ ) { if ( is_array( $args[ $i ] ) ) { $array = array_replace_recursive_recurse( $array, $args[ $i ] ); } } return $array; } function array_replace_recursive_recurse( $array, $array1 ) { foreach ( $array1 as $key => $value ) { // create new key in $array, if it is empty or not an array if ( ! isset( $array[ $key ] ) || ( isset( $array[ $key ] ) && ! is_array( $array[ $key ] ) ) ) { $array[ $key ] = array(); } // overwrite the value in the base array if ( is_array( $value ) ) { $value = array_replace_recursive_recurse( $array[ $key ], $value ); } $array[ $key ] = $value; } return $array; } } hacks/language-canonical-redirects.php 0000755 00000001053 14720425051 0014042 0 ustar 00 <?php if ( defined( 'WP_ADMIN' ) ) { return; } add_action( 'template_redirect', 'icl_language_canonical_redirects', 1 ); function icl_language_canonical_redirects() { global $wp_query, $sitepress_settings; if ( 3 == $sitepress_settings['language_negotiation_type'] && is_singular() && empty( $wp_query->posts ) ) { $pid = get_query_var( 'p' ); $permalink = $pid ? get_permalink( $pid ) : false; $permalink = $permalink ? html_entity_decode( $permalink ) : false; if ( $permalink ) { wp_redirect( $permalink, 301 ); exit; } } } hacks/vars-php-multisite.php 0000755 00000002060 14720425051 0012124 0 ustar 00 <?php global $PHP_SELF; if ( is_admin() ) { // wp-admin pages are checked more carefully if ( is_network_admin() ) { preg_match( '#/wp-admin/network/?(.*?)$#i', $PHP_SELF, $self_matches ); } elseif ( is_user_admin() ) { preg_match( '#/wp-admin/user/?(.*?)$#i', $PHP_SELF, $self_matches ); } else { preg_match( '#/wp-admin/?(.*?)$#i', $PHP_SELF, $self_matches ); } $pagenow = $self_matches[1]; $pagenow = trim( $pagenow, '/' ); $pagenow = preg_replace( '#\?.*?$#', '', $pagenow ); if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) { $pagenow = 'index.php'; } else { preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches ); $pagenow = strtolower( $self_matches[1] ); if ( '.php' !== substr( $pagenow, -4, 4 ) ) { $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried) } } } else { if ( preg_match( '#([^/]+\.php)([?/].*?)?$#i', $PHP_SELF, $self_matches ) ) { $pagenow = strtolower( $self_matches[1] ); } else { $pagenow = 'index.php'; } } unset( $self_matches ); tools/sunrise.php 0000755 00000011003 14720425051 0010103 0 ustar 00 <?php /** * WPML Sunrise Script - START * * @author OnTheGoSystems * @version 3.7.0 * * Place this script in the wp-content folder and add "define('SUNRISE', 'on');" in wp-config.php * in order to enable using different domains for different languages in multisite mode * * Experimental feature */ /** * Class WPML_Sunrise_Lang_In_Domains * * @author OnTheGoSystems */ class WPML_Sunrise_Lang_In_Domains { /** @var wpdb $wpdb */ private $wpdb; /** @var string $table_prefix */ private $table_prefix; /** @var string $current_blog */ private $current_blog; /** @var bool $no_recursion */ private $no_recursion; /** * Method init */ public function init() { if ( ! defined( 'WPML_SUNRISE_MULTISITE_DOMAINS' ) ) { define( 'WPML_SUNRISE_MULTISITE_DOMAINS', true ); } add_filter( 'query', array( $this, 'query_filter' ) ); } /** * @param string $q * * @return string */ public function query_filter( $q ) { $this->set_private_properties(); if ( ! $this->current_blog && ! $this->no_recursion ) { $this->no_recursion = true; $domains = $this->extract_variables_from_query( $q, 'domain' ); if ( $domains && $this->query_has_no_result( $q ) ) { $q = $this->transpose_query_if_one_domain_is_matching( $q, $domains ); } $this->no_recursion = false; } return $q; } /** * method set_private_properties */ private function set_private_properties() { global $wpdb, $table_prefix, $current_blog; $this->wpdb = $wpdb; $this->table_prefix = $table_prefix; $this->current_blog = $current_blog; } /** * @param string $query * * @return array */ private function extract_variables_from_query( $query, $field ) { $variables = array(); $patterns = array( '#WHERE\s+' . $field . '\s+IN\s*\(([^\)]+)\)#', '#WHERE\s+' . $field . '\s*=\s*([^\s]+)#', '#AND\s+' . $field . '\s+IN\s*\(([^\)]+)\)#', '#AND\s+' . $field . '\s*=\s*([^\s]+)#', ); foreach ( $patterns as $pattern ) { $found = preg_match( $pattern, $query, $matches ); if ( $found && array_key_exists( 1, $matches ) ) { $variables = $matches[1]; $variables = preg_replace( '/\s+/', '', $variables ); $variables = preg_replace( '/[\'"]/', '', $variables ); $variables = explode( ',', $variables ); break; } } return $variables; } /** * @param string $q * * @return bool */ private function query_has_no_result( $q ) { return ! (bool) $this->wpdb->get_row( $q ); } /** * @param string $q * @param array $domains * * @return string */ private function transpose_query_if_one_domain_is_matching( $q, $domains ) { $paths = $this->extract_variables_from_query( $q, 'path' ); // Create as many placeholders as $paths we have. $placeholders = implode( ',', array_fill( 0, sizeof( $paths ), '%s' ) ); // Array with all the parameters for preparing the SQL. $parameters = $paths; $parameters[] = BLOG_ID_CURRENT_SITE; // The ORDER is there to get the default site at the end of the results. $blogs = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT blog_id FROM {$this->wpdb->blogs} WHERE path IN ($placeholders) ORDER BY blog_id = %d", $parameters ) ); $found_blog_id = null; foreach ( (array) $blogs as $blog_id ) { $prefix = $this->table_prefix; if ( $blog_id > 1 ) { $prefix .= $blog_id . '_'; } $icl_settings = $this->wpdb->get_var( "SELECT option_value FROM {$prefix}options WHERE option_name = 'icl_sitepress_settings'" ); if ( $icl_settings ) { $icl_settings = unserialize( $icl_settings ); if ( $icl_settings && 2 === (int) $icl_settings['language_negotiation_type'] ) { $found_blog_id = $this->get_blog_id_from_domain( $domains, $icl_settings, $blog_id ); if ( $found_blog_id ) { $q = $this->wpdb->prepare( "SELECT blog_id FROM {$this->wpdb->blogs} WHERE blog_id = %d", $found_blog_id ); break; } } } } return $q; } /** * @param array $domains * @param array $wpml_settings * @param int $blog_id * * @return mixed */ private function get_blog_id_from_domain( array $domains, array $wpml_settings, $blog_id ) { foreach ( $domains as $domain ) { if ( in_array( 'http://' . $domain, $wpml_settings['language_domains'], true ) ) { return $blog_id; } elseif ( in_array( $domain, $wpml_settings['language_domains'], true ) ) { return $blog_id; } } return null; } } $wpml_sunrise_lang_in_domains = new WPML_Sunrise_Lang_In_Domains(); $wpml_sunrise_lang_in_domains->init(); /** * WPML Sunrise Script - END */ ajax.php 0000755 00000000351 14720425051 0006202 0 ustar 00 <?php global $wpdb; $basket_ajax = new WPML_Basket_Tab_Ajax( TranslationProxy::get_current_project(), wpml_tm_load_basket_networking(), new WPML_Translation_Basket( $wpdb ) ); add_action( 'init', array( $basket_ajax, 'init' ) ); wpml-api.php 0000755 00000046444 14720425051 0007022 0 ustar 00 <?php /* @todo: [WPML 3.3] check if needed in 3.3 */ /* This file includes a set of functions that can be used by WP plugins developers to make their plugins interact with WPML */ /* constants */ define( 'WPML_API_SUCCESS', 0 ); define( 'WPML_API_ERROR', 99 ); define( 'WPML_API_INVALID_LANGUAGE_CODE', 1 ); define( 'WPML_API_INVALID_TRID', 2 ); define( 'WPML_API_LANGUAGE_CODE_EXISTS', 3 ); define( 'WPML_API_CONTENT_NOT_FOUND', 4 ); define( 'WPML_API_TRANSLATION_NOT_FOUND', 5 ); define( 'WPML_API_INVALID_CONTENT_TYPE', 6 ); define( 'WPML_API_CONTENT_EXISTS', 7 ); define( 'WPML_API_FUNCTION_ALREADY_DECLARED', 8 ); define( 'WPML_API_CONTENT_TRANSLATION_DISABLED', 9 ); define( 'WPML_API_GET_CONTENT_ERROR', 0 ); define( 'WPML_API_MAGIC_NUMBER', 6 ); define( 'WPML_API_ASIAN_LANGUAGES', 'zh-hans|zh-hant|ja|ko' ); define( 'WPML_API_COST_PER_WORD', 0.09 ); function _wpml_api_allowed_content_type( $content_type ) { $reserved_types = array( 'post' => 1, 'page' => 1, 'tax_post_tag' => 1, 'tax_category' => 1, 'comment' => 1, ); return ! isset( $reserved_types[ $content_type ] ) && preg_match( '#([a-z0-9_\-])#i', $content_type ); } /** * Add translatable content to the WPML translations table * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param bool|string $language_code Content language code. (defaults to current language) * @param bool|int $trid Content trid - if a translation in a different language already exists. * * @return int error code */ function wpml_add_translatable_content( $content_type, $content_id, $language_code = false, $trid = false ) { global $sitepress, $wpdb; if ( ! _wpml_api_allowed_content_type( $content_type ) ) { return WPML_API_INVALID_CONTENT_TYPE; } if ( $language_code && ! $sitepress->get_language_details( $language_code ) ) { return WPML_API_INVALID_LANGUAGE_CODE; } if ( $trid ) { $trid_type = $wpdb->get_var( $wpdb->prepare( " SELECT element_type FROM {$wpdb->prefix}icl_translations WHERE trid = %d ", $trid ) ); if ( ! $trid_type || $trid_type != $content_type ) { return WPML_API_INVALID_TRID; } } if ( $wpdb->get_var( $wpdb->prepare( " SELECT translation_id FROM {$wpdb->prefix}icl_translations WHERE element_type=%s AND element_id=%d", $content_type, $content_id ) ) ) { return WPML_API_CONTENT_EXISTS; } $t = $sitepress->set_element_language_details( $content_id, $content_type, $trid, $language_code ); if ( ! $t ) { return WPML_API_ERROR; } else { return WPML_API_SUCCESS; } } /** * Update translatable content in the WPML translations table * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param string $language_code Content language code. * * @return int error code */ function wpml_update_translatable_content( $content_type, $content_id, $language_code ) { global $sitepress; if ( ! _wpml_api_allowed_content_type( $content_type ) ) { return WPML_API_INVALID_CONTENT_TYPE; } if ( ! $sitepress->get_language_details( $language_code ) ) { return WPML_API_INVALID_LANGUAGE_CODE; } $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_CONTENT_NOT_FOUND; } $translations = $sitepress->get_element_translations( $trid ); if ( isset( $translations[ $language_code ] ) && ! $translations[ $language_code ]->element_id != $content_id ) { return WPML_API_LANGUAGE_CODE_EXISTS; } $t = $sitepress->set_element_language_details( $content_id, $content_type, $trid, $language_code ); if ( ! $t ) { return WPML_API_ERROR; } else { return WPML_API_SUCCESS; } } /** * Update translatable content in the WPML translations table * * @since 1.3 * @deprecated deprecated since 3.2 * * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param bool|string $language_code Content language code. (when ommitted - delete all translations associated with the respective content) * * @return int error code */ function wpml_delete_translatable_content( $content_type, $content_id, $language_code = false ) { global $sitepress; if ( ! _wpml_api_allowed_content_type( $content_type ) ) { return WPML_API_INVALID_CONTENT_TYPE; } if ( $language_code && ! $sitepress->get_language_details( $language_code ) ) { return WPML_API_INVALID_LANGUAGE_CODE; } $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_CONTENT_NOT_FOUND; } if ( $language_code ) { $translations = $sitepress->get_element_translations( $trid ); if ( ! isset( $translations[ $language_code ] ) ) { return WPML_API_TRANSLATION_NOT_FOUND; } } $sitepress->delete_element_translation( $trid, $content_type, $language_code ); return WPML_API_SUCCESS; } /** * Get trid value for a specific piece of content * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * * @return int trid or 0 for error * */ function wpml_get_content_trid( $content_type, $content_id ) { global $sitepress; if ( ! _wpml_api_allowed_content_type( $content_type ) ) { return WPML_API_GET_CONTENT_ERROR; // WPML_API_INVALID_CONTENT_TYPE; } $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_GET_CONTENT_ERROR; } else { return $trid; } } /** * Detects the current language and returns the language relevant content id. optionally it can return the original id if a translation is not found * See also wpml_object_id_filter() in \template-functions.php * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param bool $return_original return the original id when translation not found. * * @return int trid or 0 for error */ function wpml_get_content( $content_type, $content_id, $return_original = true ) { global $sitepress, $wpdb; $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_GET_CONTENT_ERROR; } else { if ( $content_id <= 0 ) { return $content_id; } if ( $content_type == 'category' || $content_type == 'post_tag' || $content_type == 'tag' ) { $content_id = $wpdb->get_var( $wpdb->prepare( " SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s", $content_id, $content_type ) ); } if ( $content_type == 'post_tag' ) { $icl_element_type = 'tax_post_tag'; } elseif ( $content_type == 'category' ) { $icl_element_type = 'tax_category'; } elseif ( $content_type == 'page' ) { $icl_element_type = 'post'; } else { $icl_element_type = $content_type; } $trid = $sitepress->get_element_trid( $content_id, $icl_element_type ); $translations = $sitepress->get_element_translations( $trid, $icl_element_type ); if ( isset( $translations[ ICL_LANGUAGE_CODE ]->element_id ) ) { $ret_element_id = $translations[ ICL_LANGUAGE_CODE ]->element_id; if ( $content_type == 'category' || $content_type == 'post_tag' ) { $ret_element_id = $wpdb->get_var( $wpdb->prepare( " SELECT t.term_id FROM {$wpdb->term_taxonomy} tx JOIN {$wpdb->terms} t ON t.term_id = tx.term_id WHERE tx.term_taxonomy_id = %d AND tx.taxonomy=%s", $ret_element_id, $content_type ) ); } } else { $ret_element_id = $return_original ? $content_id : null; } return $ret_element_id; } } /** * Get translations for a certain piece of content * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param bool $skip_missing * * @internal param bool $return_original return the original id when translation not found. * * @return array|int translations or error code */ function wpml_get_content_translations( $content_type, $content_id, $skip_missing = true ) { global $sitepress; $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_TRANSLATION_NOT_FOUND; } $translations = $sitepress->get_element_translations( $trid, $content_type, $skip_missing ); $tr = array(); foreach ( $translations as $k => $v ) { $tr[ $k ] = $v->element_id; } return $tr; } /** * Returns a certain translation for a piece of content * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * @param int $content_id Content ID. * @param bool $language_code * * @return int|array error code or array('lang'=>element_id) */ function wpml_get_content_translation( $content_type, $content_id, $language_code ) { global $sitepress; $trid = $sitepress->get_element_trid( $content_id, $content_type ); if ( ! $trid ) { return WPML_API_CONTENT_NOT_FOUND; } $translations = $sitepress->get_element_translations( $trid, $content_type, true ); if ( ! isset( $translations[ $language_code ] ) ) { return WPML_API_TRANSLATION_NOT_FOUND; } else { return array( $language_code => $translations[ $language_code ]->element_id ); } } /** * Returns the list of active languages * See also wpml_get_active_languages_filter() in \template-functions.php * * @since 1.3 * @package WPML * @subpackage WPML API * * @return array * */ function wpml_get_active_languages() { global $sitepress; $langs = $sitepress->get_active_languages(); return $langs; } /** * Get contents of a specific type * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $content_type Content type. * * @param bool $language_code * * @return int or array */ function wpml_get_contents( $content_type, $language_code = false ) { global $sitepress, $wpdb; if ( $language_code && ! $sitepress->get_language_details( $language_code ) ) { return WPML_API_INVALID_LANGUAGE_CODE; } if ( ! $language_code ) { $language_code = $sitepress->get_current_language(); } $contents = $wpdb->get_col( $wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE element_type = %s AND language_code = %s", $content_type, $language_code ) ); return $contents; } /** * Returns the number of the words that will be sent to translation and a cost estimate * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $string * @param bool|string $language - should be specified when the language is one of zh-hans|zh-hant|ja|ko * * @return array (count, cost) */ function wpml_get_word_count( $string, $language = false ) { $asian_languages = explode( '|', WPML_API_ASIAN_LANGUAGES ); $count = 0; if ( $language && in_array( $language, $asian_languages ) ) { $count = ceil( strlen( $string ) / WPML_API_MAGIC_NUMBER ); } elseif ( is_string( $string ) ) { $words = preg_split( '/[\s\/]+/', $string, 0, PREG_SPLIT_NO_EMPTY ); $count = is_array( $words ) ? count( $words ) : 0; } $cost = $count * WPML_API_COST_PER_WORD; $ret = array( 'count' => $count, 'cost' => $cost, ); return $ret; } /** * Check user is translator * * @since 1.3 * @package WPML * @subpackage WPML API * * @param string $from_language Language to translate from * @param string $to_language Language to translate into * * @return bool (true if translator) */ function wpml_check_user_is_translator( $from_language, $to_language ) { global $wpdb; $is_translator = false; if ( $current_user_id = get_current_user_id() ) { $translation_languages = $wpdb->get_row( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = %s", $current_user_id, $wpdb->prefix . 'language_pairs' ) ); if ( $translation_languages ) { foreach ( unserialize( $translation_languages->meta_value ) as $key => $language ) { if ( $key == $from_language ) { $is_translator = array_key_exists( $to_language, $language ); } } } } return $is_translator; } /** * Check user is translator * * @param int $post_id Post ID * @param int $cred_form_id * @param bool|string $current_language (optional) current language * * @return bool (true if translator) * @package WPML * @subpackage WPML API * * @internal param int $form_id Form ID * @since 1.3 */ function wpml_generate_controls( $post_id, $cred_form_id, $current_language = false ) { global $sitepress,$sitepress_settings; if ( ! $current_language ) { $current_language = $sitepress->get_default_language(); } if ( $current_language != $sitepress->get_language_for_element( $post_id, 'post_' . get_post_type( $post_id ) ) ) { $current_language = $sitepress->get_language_for_element( $post_id, 'post_' . get_post_type( $post_id ) ); } $controls = array(); $trid = $sitepress->get_element_trid( $post_id, 'post_' . get_post_type( $post_id ) ); $translations = $sitepress->get_element_translations( $trid, 'post_' . get_post_type( $post_id ) ); foreach ( $sitepress->get_active_languages() as $active_language ) { if ( $current_language == $active_language['code'] || ! wpml_check_user_is_translator( $current_language, $active_language['code'] ) ) { continue; } if ( array_key_exists( $active_language['code'], $translations ) ) { // edit translation $controls[ $active_language['code'] ]['action'] = 'edit'; $post_url = get_permalink( $translations[ $active_language['code'] ]->element_id ); if ( $post_url && ( false === strpos( $post_url, '?' ) || ( false === strpos( $post_url, '?' ) && $sitepress_settings['language_negotiation_type'] != '3' ) ) ) { $controls[ $active_language['code'] ]['url'] = $post_url . '?action=edit_translation&cred-edit-form=' . $cred_form_id; // CRED edit form ID } else { $controls[ $active_language['code'] ]['url'] = $post_url . '&action=edit_translation&cred-edit-form=' . $cred_form_id; // CRED edit form ID } } else { // add translation $controls[ $active_language['code'] ]['action'] = 'create'; $post_url = get_permalink( $post_id ); if ( $post_url && ( false === strpos( $post_url, '?' ) || ( false === strpos( $post_url, '?' ) && $sitepress_settings['language_negotiation_type'] != '3' ) ) ) { $controls[ $active_language['code'] ]['url'] = get_permalink( $post_id ) . '?action=create_translation&trid=' . $trid . '&to_lang=' . $active_language['code'] . '&source_lang=' . $current_language . '&cred-edit-form=' . $cred_form_id; // CRED new form ID } else { $controls[ $active_language['code'] ]['url'] = get_permalink( $post_id ) . '&action=create_translation&trid=' . $trid . '&to_lang=' . $active_language['code'] . '&source_lang=' . $current_language . '&cred-edit-form=' . $cred_form_id; // CRED new form ID } } $controls[ $active_language['code'] ]['language'] = $sitepress->get_display_language_name( $active_language['code'], $current_language ); $controls[ $active_language['code'] ]['flag'] = $sitepress->get_flag_url( $active_language['code'] ); } return $controls; } /** * Get original content * * @since 1.3 * @package WPML * @subpackage WPML API * * @param int $post_id Post ID * @param string $field Post field * * @param string|false $field_name * * @return string or array */ function wpml_get_original_content( $post_id, $field, $field_name = false ) { $post = get_post( $post_id ); switch ( $field ) { case 'title': return $post->post_title; break; case 'content': return $post->post_content; break; case 'excerpt': return $post->post_excerpt; break; case 'categories': $terms = get_the_terms( $post->ID, 'category' ); $taxs = array(); if ( $terms ) { foreach ( $terms as $term ) { $taxs[ $term->term_taxonomy_id ] = $term->name; } } return $taxs; break; case 'tags': $terms = get_the_terms( $post->ID, 'post_tag' ); $taxs = array(); if ( $terms ) { foreach ( $terms as $term ) { $taxs[ $term->term_taxonomy_id ] = $term->name; } } return $taxs; break; case 'taxonomies': return $field_name ? wpml_get_synchronizing_taxonomies( $post_id, $field_name ) : []; case 'custom_fields': return $field_name ? wpml_get_synchronizing_fields( $post_id, $field_name ) : []; default: break; } return WPML_API_ERROR; } /** * Get synchronizing taxonomies * * @since 1.3 * @package WPML * @subpackage WPML API * * @param int $post_id Post ID * * @param string $tax_name * * @return array */ function wpml_get_synchronizing_taxonomies( $post_id, $tax_name ) { global $wpdb, $sitepress_settings; $taxs = array(); // get custom taxonomies if ( ! empty( $post_id ) ) { $taxonomies = $wpdb->get_col( $wpdb->prepare( " SELECT DISTINCT tx.taxonomy FROM {$wpdb->term_taxonomy} tx JOIN {$wpdb->term_relationships} tr ON tx.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = %d ", $post_id ) ); sort( $taxonomies, SORT_STRING ); foreach ( $taxonomies as $t ) { if ( $tax_name == $t && @intval( $sitepress_settings['taxonomies_sync_option'][ $t ] ) == 1 ) { foreach ( wp_get_object_terms( $post_id, $t ) as $trm ) { $taxs[ $t ][ $trm->term_taxonomy_id ] = $trm->name; } } } } return $taxs; } /** * Get synchronizing fields * * @since 1.3 * @package WPML * @subpackage WPML API * * @param int $post_id Post ID * @param string $field_name Field name * @return array */ function wpml_get_synchronizing_fields( $post_id, $field_name ) { global $sitepress_settings; $custom_fields_values = array(); if ( ! empty( $post_id ) ) { if ( is_array( $sitepress_settings['translation-management']['custom_fields_translation'] ) ) { foreach ( $sitepress_settings['translation-management']['custom_fields_translation'] as $cf => $op ) { if ( $cf == $field_name && ( $op == '2' || $op == '1' ) ) { $values = get_post_meta( $post_id, $cf, false ); if ( ! empty( $values ) ) { foreach ( $values as $key => $value ) { $custom_fields_values[ $key ] = $value; } } } } } } return $custom_fields_values; } wpml_zip.php 0000755 00000056056 14720425051 0007135 0 ustar 00 <?php /** * Class to create and manage a Zip file. * * Initially inspired by CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html) * and * http://www.pkware.com/documents/casestudies/APPNOTE.TXT Zip file specification. * * License: GNU LGPL 2.1. * * @author A. Grandt <php@grandt.com> * @copyright 2009-2014 A. Grandt * @license GNU LGPL 2.1 * @link http://www.phpclasses.org/package/6110 * @link https://github.com/Grandt/PHPZip * @version 1.62 */ class wpml_zip { const VERSION = 1.62; const ZIP_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04"; // Local file header signature const ZIP_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02"; // Central file header signature const ZIP_END_OF_CENTRAL_DIRECTORY = "\x50\x4b\x05\x06\x00\x00\x00\x00"; // end of Central directory record const EXT_FILE_ATTR_DIR = 010173200020; // Permission 755 drwxr-xr-x = (((S_IFDIR | 0755) << 16) | S_DOS_D); const EXT_FILE_ATTR_FILE = 020151000040; // Permission 644 -rw-r--r-- = (((S_IFREG | 0644) << 16) | S_DOS_A); const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version // UID 1000, GID 0 const EXTRA_FIELD_NEW_UNIX_GUID = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; // Unix file types const S_IFIFO = 0010000; // named pipe (fifo) const S_IFCHR = 0020000; // character special const S_IFDIR = 0040000; // directory const S_IFBLK = 0060000; // block special const S_IFREG = 0100000; // regular const S_IFLNK = 0120000; // symbolic link const S_IFSOCK = 0140000; // socket // setuid/setgid/sticky bits, the same as for chmod: const S_ISUID = 0004000; // set user id on execution const S_ISGID = 0002000; // set group id on execution const S_ISTXT = 0001000; // sticky bit // And of course, the other 12 bits are for the permissions, the same as for chmod: // When addding these up, you can also just write the permissions as a simgle octal number // ie. 0755. The leading 0 specifies octal notation. const S_IRWXU = 0000700; // RWX mask for owner const S_IRUSR = 0000400; // R for owner const S_IWUSR = 0000200; // W for owner const S_IXUSR = 0000100; // X for owner const S_IRWXG = 0000070; // RWX mask for group const S_IRGRP = 0000040; // R for group const S_IWGRP = 0000020; // W for group const S_IXGRP = 0000010; // X for group const S_IRWXO = 0000007; // RWX mask for other const S_IROTH = 0000004; // R for other const S_IWOTH = 0000002; // W for other const S_IXOTH = 0000001; // X for other const S_ISVTX = 0001000; // save swapped text even after use // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags. // DOS file type flags, we really only use the S_DOS_D flag. const S_DOS_A = 0000040; // DOS flag for Archive const S_DOS_D = 0000020; // DOS flag for Directory const S_DOS_V = 0000010; // DOS flag for Volume const S_DOS_S = 0000004; // DOS flag for System const S_DOS_H = 0000002; // DOS flag for Hidden const S_DOS_R = 0000001; // DOS flag for Read Only private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB) private $zipData = null; private $zipFile = null; private $zipComment = null; private $cdRec = array(); // central directory private $offset = 0; private $isFinalized = false; private $addExtraField = true; private $streamChunkSize = 65536; private $streamFilePath = null; private $streamTimestamp = null; private $streamFileComment = null; private $streamFile = null; private $streamData = null; private $streamFileLength = 0; private $streamExtFileAttr = null; /** * A custom temporary folder, or a callable that returns a custom temporary file. * * @var string|callable */ public static $temp = null; /** * Constructor. * * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE */ function __construct( $useZipFile = false ) { if ( $useZipFile ) { $this->zipFile = tmpfile(); } else { $this->zipData = ''; } } function __destruct() { if ( is_resource( $this->zipFile ) ) { fclose( $this->zipFile ); } $this->zipData = null; } /** * Set Zip archive comment. * * @param string $newComment New comment. NULL to clear. * @return bool $success */ public function setComment( $newComment = null ) { if ( $this->isFinalized ) { return false; } $this->zipComment = $newComment; return true; } /** * Set zip file to write zip data to. * This will cause all present and future data written to this class to be written to this file. * This can be used at any time, even after the Zip Archive have been finalized. Any previous file will be closed. * Warning: If the given file already exists, it will be overwritten. * * @param string $fileName * @return bool $success */ public function setZipFile( $fileName ) { if ( is_file( $fileName ) ) { unlink( $fileName ); } $fd = fopen( $fileName, 'x+b' ); if ( ! $fd ) { return false; } if ( is_resource( $this->zipFile ) ) { rewind( $this->zipFile ); while ( ! feof( $this->zipFile ) ) { fwrite( $fd, (string) fread( $this->zipFile, $this->streamChunkSize ) ); } fclose( $this->zipFile ); } else { fwrite( $fd, $this->zipData ); $this->zipData = null; } $this->zipFile = $fd; return true; } /** * Add an empty directory entry to the zip archive. * Basically this is only used if an empty directory is added. * * @param string $directoryPath Directory Path and name to be added to the archive. * @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addDirectory( $directoryPath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_DIR ) { if ( $this->isFinalized ) { return false; } $directoryPath = str_replace( '\\', '/', $directoryPath ); $directoryPath = rtrim( $directoryPath, '/' ); if ( strlen( $directoryPath ) > 0 ) { $this->buildZipEntry( $directoryPath . '/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr ); return true; } return false; } /** * Add a file to the archive at the specified location and file name. * * @param string $data File data. * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @param bool $compress (Optional) Compress file, if set to FALSE the file will only be stored. Default TRUE. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addFile( $data, $filePath, $timestamp = 0, $fileComment = null, $compress = true, $extFileAttr = self::EXT_FILE_ATTR_FILE ) { if ( $this->isFinalized ) { return false; } if ( is_resource( $data ) && get_resource_type( $data ) == 'stream' ) { $this->addLargeFile( $data, $filePath, $timestamp, $fileComment, $extFileAttr ); return false; } $gzData = ''; $gzType = "\x08\x00"; // Compression type 8 = deflate $gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression. $dataLength = strlen( $data ); $fileCRC32 = pack( 'V', crc32( $data ) ); if ( $compress ) { $gzTmp = gzcompress( $data ); if ( ! $gzTmp ) { return false; } $gzTmp = substr( $gzTmp, 0, strlen( $gzTmp ) - 4 ); if ( ! $gzTmp ) { return false; } $gzData = substr( $gzTmp, 2 ); // gzcompress adds a 2 byte header and 4 byte CRC we can't use. // The 2 byte header does contain useful data, though in this case the 2 parameters we'd be interrested in will always be 8 for compression type, and 2 for General purpose flag. $gzLength = strlen( $gzData ); } else { $gzLength = $dataLength; } if ( $gzLength >= $dataLength ) { $gzLength = $dataLength; $gzData = $data; $gzType = "\x00\x00"; // Compression type 0 = stored $gpFlags = "\x00\x00"; // Compression type 0 = stored } if ( ! is_resource( $this->zipFile ) && ( $this->offset + $gzLength ) > $this->zipMemoryThreshold ) { $this->zipflush(); } $this->buildZipEntry( $filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr ); $this->zipwrite( $gzData ); return true; } /** * Add a file to the archive at the specified location and file name. * * @param string $dataFile File name/path. * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addLargeFile( $dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE ) { if ( $this->isFinalized ) { return false; } if ( is_string( $dataFile ) && is_file( $dataFile ) ) { $this->processFile( $dataFile, $filePath, $timestamp, $fileComment, $extFileAttr ); } elseif ( is_resource( $dataFile ) && get_resource_type( $dataFile ) == 'stream' ) { $fh = $dataFile; $this->openStream( $filePath, $timestamp, $fileComment, $extFileAttr ); while ( ! feof( $fh ) ) { $data = fread( $fh, $this->streamChunkSize ); if ( $data ) { $this->addStreamData( $data ); } } $this->closeStream(); } return true; } /** * Create a stream to be used for large entries. * * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @throws Exception Throws an exception in case of errors * @return bool $success */ public function openStream( $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE ) { if ( ! function_exists( 'sys_get_temp_dir' ) ) { throw new Exception( 'Zip ' . self::VERSION . ' requires PHP version 5.2.1 or above if large files are used.' ); } if ( $this->isFinalized ) { return false; } $this->zipflush(); if ( strlen( $this->streamFilePath ) > 0 ) { $this->closeStream(); } $this->streamFile = self::getTemporaryFile(); $this->streamData = fopen( $this->streamFile, 'wb' ); $this->streamFilePath = $filePath; $this->streamTimestamp = $timestamp; $this->streamFileComment = $fileComment; $this->streamFileLength = 0; $this->streamExtFileAttr = $extFileAttr; return true; } /** * Add data to the open stream. * * @param string $data * @throws Exception Throws an exception in case of errors * @return mixed length in bytes added or FALSE if the archive is finalized or there are no open stream. */ public function addStreamData( $data ) { if ( $this->isFinalized || strlen( $this->streamFilePath ) == 0 ) { return false; } $length = fwrite( $this->streamData, $data, strlen( $data ) ); if ( $length != strlen( $data ) ) { throw new Exception( 'File IO: Error writing; Length mismatch: Expected ' . strlen( $data ) . ' bytes, wrote ' . ( $length === false ? 'NONE!' : $length ) ); } $this->streamFileLength += $length; return $length; } /** * Close the current stream. * * @return bool $success */ public function closeStream() { if ( $this->isFinalized || strlen( $this->streamFilePath ) == 0 ) { return false; } fflush( $this->streamData ); fclose( $this->streamData ); $this->processFile( $this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment, $this->streamExtFileAttr ); $this->streamData = null; $this->streamFilePath = null; $this->streamTimestamp = null; $this->streamFileComment = null; $this->streamFileLength = 0; $this->streamExtFileAttr = null; // Windows is a little slow at times, so a millisecond later, we can unlink this. unlink( $this->streamFile ); $this->streamFile = null; return true; } private function processFile( $dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE ) { if ( $this->isFinalized ) { return false; } $tempzip = self::getTemporaryFile(); $zip = new ZipArchive(); if ( $zip->open( $tempzip ) === true ) { $zip->addFile( $dataFile, 'file' ); $zip->close(); } $file_handle = fopen( $tempzip, 'rb' ); if ( ! $file_handle ) { return false; } $stats = fstat( $file_handle ); $eof = $stats['size'] - 72; fseek( $file_handle, 6 ); $gpFlags = fread( $file_handle, 2 ); $gzType = fread( $file_handle, 2 ); fread( $file_handle, 4 ); $fileCRC32 = fread( $file_handle, 4 ); $v = unpack( 'Vval', (string) fread( $file_handle, 4 ) ); $gzLength = $v['val']; $v = unpack( 'Vval', (string) fread( $file_handle, 4 ) ); $dataLength = $v['val']; $this->buildZipEntry( $filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr ); fseek( $file_handle, 34 ); $pos = 34; while ( ! feof( $file_handle ) && $pos < $eof ) { $datalen = $this->streamChunkSize; if ( $pos + $this->streamChunkSize > $eof ) { $datalen = $eof - $pos; } $data = fread( $file_handle, $datalen ); $pos += $datalen; $this->zipwrite( $data ); } fclose( $file_handle ); unlink( $tempzip ); } /** * Close the archive. * A closed archive can no longer have new files added to it. * * @return bool $success */ public function finalize() { if ( ! $this->isFinalized ) { if ( isset( $this->streamFilePath ) && strlen( $this->streamFilePath ) > 0 ) { $this->closeStream(); } $cd = implode( '', $this->cdRec ); $cdRecSize = pack( 'v', sizeof( $this->cdRec ) ); $cdRec = $cd . self::ZIP_END_OF_CENTRAL_DIRECTORY . $cdRecSize . $cdRecSize . pack( 'VV', strlen( $cd ), $this->offset ); if ( ! empty( $this->zipComment ) ) { $cdRec .= pack( 'v', strlen( $this->zipComment ) ) . $this->zipComment; } else { $cdRec .= "\x00\x00"; } $this->zipwrite( $cdRec ); $this->isFinalized = true; $this->cdRec = null; return true; } return false; } /** * Get the zip file contents * If the zip haven't been finalized yet, this will cause it to become finalized * * @return string data */ public function getZipData() { if ( ! $this->isFinalized ) { $this->finalize(); } if ( ! is_resource( $this->zipFile ) ) { return $this->zipData; } else { rewind( $this->zipFile ); $filestat = fstat( $this->zipFile ); return fread( $this->zipFile, $filestat['size'] > 0 ? $filestat['size'] : 0 ); } } /** * Send the archive as a zip download * * @param String $fileName The name of the Zip archive, in ISO-8859-1 (or ASCII) encoding, ie. "archive.zip". Optional, defaults to NULL, which means that no ISO-8859-1 encoded file name will be specified. * @param String $contentType Content mime type. Optional, defaults to "application/zip". * @param String $utf8FileName The name of the Zip archive, in UTF-8 encoding. Optional, defaults to NULL, which means that no UTF-8 encoded file name will be specified. * @param bool $inline Use Content-Disposition with "inline" instead of "attached". Optional, defaults to FALSE. * @throws Exception Throws an exception in case of errors * @return bool Always returns true (for backward compatibility). */ function sendZip( $fileName = null, $contentType = 'application/zip', $utf8FileName = null, $inline = false ) { if ( ! $this->isFinalized ) { $this->finalize(); } $headerFile = null; $headerLine = null; if ( headers_sent( $headerFile, $headerLine ) ) { throw new Exception( "Unable to send file '$fileName'. Headers have already been sent from '$headerFile' in line $headerLine" ); } if ( ob_get_contents() !== false && strlen( ob_get_contents() ) ) { throw new Exception( "Unable to send file '$fileName'. Output buffer contains the following text (typically warnings or errors):\n" . ob_get_contents() ); } if ( @ini_get( 'zlib.output_compression' ) ) { @ini_set( 'zlib.output_compression', 'Off' ); } header( 'Pragma: public' ); header( 'Last-Modified: ' . @gmdate( 'D, d M Y H:i:s T' ) ); header( 'Expires: 0' ); header( 'Accept-Ranges: bytes' ); header( 'Connection: close' ); header( 'Content-Type: ' . $contentType ); $cd = 'Content-Disposition: '; if ( $inline ) { $cd .= 'inline'; } else { $cd .= 'attached'; } if ( $fileName ) { $cd .= '; filename="' . $fileName . '"'; } if ( $utf8FileName ) { $cd .= "; filename*=UTF-8''" . rawurlencode( $utf8FileName ); } header( $cd ); header( 'Content-Length: ' . $this->getArchiveSize() ); if ( ! is_resource( $this->zipFile ) ) { echo $this->zipData; } else { rewind( $this->zipFile ); while ( ! feof( $this->zipFile ) ) { echo fread( $this->zipFile, $this->streamChunkSize ); } } return true; } /** * Return the current size of the archive * * @return $size Size of the archive */ public function getArchiveSize() { if ( ! is_resource( $this->zipFile ) ) { return strlen( $this->zipData ); } $filestat = fstat( $this->zipFile ); return $filestat['size']; } /** * Calculate the 2 byte dostime used in the zip entries. * * @param int $timestamp * @return 2-byte encoded DOS Date */ private function getDosTime( $timestamp = 0 ) { $timestamp = (int) $timestamp; $oldTZ = @date_default_timezone_get(); date_default_timezone_set( 'UTC' ); $date = ( $timestamp == 0 ? getdate() : getdate( $timestamp ) ); date_default_timezone_set( $oldTZ ); if ( $date['year'] >= 1980 ) { return pack( 'V', ( ( $date['mday'] + ( $date['mon'] << 5 ) + ( ( $date['year'] - 1980 ) << 9 ) ) << 16 ) | ( ( $date['seconds'] >> 1 ) + ( $date['minutes'] << 5 ) + ( $date['hours'] << 11 ) ) ); } return "\x00\x00\x00\x00"; } /** * Build the Zip file structures * * @param string $filePath * @param string $fileComment * @param string|false $gpFlags * @param string|false $gzType * @param int $timestamp * @param string|false $fileCRC32 * @param int $gzLength * @param int $dataLength * @param int $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories. */ private function buildZipEntry( $filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr ) { $filePath = str_replace( '\\', '/', $filePath ); $fileCommentLength = ( empty( $fileComment ) ? 0 : strlen( $fileComment ) ); $timestamp = (int) $timestamp; $timestamp = ( $timestamp == 0 ? time() : $timestamp ); $dosTime = $this->getDosTime( $timestamp ); $tsPack = pack( 'V', $timestamp ); if ( $gpFlags && strlen( $gpFlags ) !== 2 ) { $gpFlags = "\x00\x00"; } $isFileUTF8 = mb_check_encoding( $filePath, 'UTF-8' ) && ! mb_check_encoding( $filePath, 'ASCII' ); $isCommentUTF8 = ! empty( $fileComment ) && mb_check_encoding( $fileComment, 'UTF-8' ) && ! mb_check_encoding( $fileComment, 'ASCII' ); $localExtraField = ''; $centralExtraField = ''; if ( $this->addExtraField ) { $localExtraField .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . self::EXTRA_FIELD_NEW_UNIX_GUID; $centralExtraField .= "\x55\x54\x05\x00\x03" . $tsPack . self::EXTRA_FIELD_NEW_UNIX_GUID; } if ( $isFileUTF8 || $isCommentUTF8 ) { $flag = 0; $gpFlagsV = unpack( 'vflags', (string) $gpFlags ); if ( isset( $gpFlagsV['flags'] ) ) { $flag = $gpFlagsV['flags']; } $gpFlags = pack( 'v', $flag | ( 1 << 11 ) ); if ( $isFileUTF8 ) { $utfPathExtraField = "\x75\x70" . pack( 'v', ( 5 + strlen( $filePath ) ) ) . "\x01" . pack( 'V', crc32( $filePath ) ) . $filePath; $localExtraField .= $utfPathExtraField; $centralExtraField .= $utfPathExtraField; } if ( $isCommentUTF8 ) { $centralExtraField .= "\x75\x63" // utf8 encoded file comment extra field . pack( 'v', ( 5 + strlen( $fileComment ) ) ) . "\x01" . pack( 'V', crc32( $fileComment ) ) . $fileComment; } } $header = $gpFlags . $gzType . $dosTime . $fileCRC32 . pack( 'VVv', $gzLength, $dataLength, strlen( $filePath ) ); // File name length $zipEntry = self::ZIP_LOCAL_FILE_HEADER . self::ATTR_VERSION_TO_EXTRACT . $header . pack( 'v', strlen( $localExtraField ) ) // Extra field length . $filePath // FileName . $localExtraField; // Extra fields $this->zipwrite( $zipEntry ); $cdEntry = self::ZIP_CENTRAL_FILE_HEADER . self::ATTR_MADE_BY_VERSION . ( $dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT ) . $header . pack( 'v', strlen( $centralExtraField ) ) // Extra field length . pack( 'v', $fileCommentLength ) // File comment length . "\x00\x00" // Disk number start . "\x00\x00" // internal file attributes . pack( 'V', $extFileAttr ) // External file attributes . pack( 'V', $this->offset ) // Relative offset of local header . $filePath // FileName . $centralExtraField; // Extra fields if ( ! empty( $fileComment ) ) { $cdEntry .= $fileComment; // Comment } $this->cdRec[] = $cdEntry; $this->offset += strlen( $zipEntry ) + $gzLength; } private function zipwrite( $data ) { if ( ! is_resource( $this->zipFile ) ) { $this->zipData .= $data; } else { fwrite( $this->zipFile, $data ); fflush( $this->zipFile ); } } private function zipflush() { if ( ! is_resource( $this->zipFile ) ) { $this->zipFile = tmpfile(); $this->zipFile && fwrite( $this->zipFile, $this->zipData ); $this->zipData = null; } } /** * Returns the path to a temporary file. * * @return string */ private static function getTemporaryFile() { if ( is_callable( self::$temp ) ) { $temporaryFile = @call_user_func( self::$temp ); if ( is_string( $temporaryFile ) && strlen( $temporaryFile ) && is_writable( $temporaryFile ) ) { return $temporaryFile; } } $temporaryDirectory = ( is_string( self::$temp ) && strlen( self::$temp ) ) ? self::$temp : sys_get_temp_dir(); return tempnam( $temporaryDirectory, 'Zip' ); } } translation-management/wpml-translator.class.php 0000755 00000001307 14720425051 0016203 0 ustar 00 <?php class WPML_Translator { /** @var int */ public $ID; /** @var string */ public $display_name; /** @var string */ public $user_login; /** * Array where key represents a source language code and values are codes of target languages. * * @var array<string, string[]> */ public $language_pairs; /** * @param string $property * * @return int */ public function __get( $property ) { if ( $property == 'translator_id' ) { return $this->ID; } return null; } /** * @param string $property * @param int $value * * @return null */ public function __set( $property, $value ) { if ( $property == 'translator_id' ) { $this->ID = $value; } return null; } } translation-management/translation-management.class.php 0000755 00000242144 14720425051 0017513 0 ustar 00 <?php /** * @package wpml-core */ use WPML\Auryn\InjectionException; use WPML\FP\Fns; use WPML\FP\Logic; use WPML\FP\Maybe; use WPML\FP\Obj; use WPML\FP\Lst; use WPML\FP\Relation; use WPML\LIB\WP\User; use WPML\TM\API\Batch; use WPML\TM\API\Jobs; use WPML\TM\Jobs\Dispatch\BatchBuilder; use WPML\TM\Jobs\Dispatch\Posts; use WPML\TM\Jobs\Dispatch\Packages; use WPML\TM\Jobs\Dispatch\Messages; use WPML\TM\API\Basket; use WPML\UIPage; use WPML\TM\TranslationDashboard\SentContentMessages; use WPML\TM\TranslationDashboard\FiltersStorage; use WPML\TM\TranslationDashboard\EncodedFieldsValidation\Validator; use function WPML\Container\make; use function WPML\FP\invoke; use function WPML\FP\partialRight; use function WPML\FP\pipe; /** * Class TranslationManagement * * Use `wpml_load_core_tm` to get an instance * * @package wpml-core */ class TranslationManagement { const INIT_PRIORITY = 1500; const DUPLICATE_ELEMENT_ACTION = 2; const TRANSLATE_ELEMENT_ACTION = 1; /** * @var WPML_Translator */ private $selected_translator; /** * @var WPML_Translator */ private $current_translator; private $messages = array(); public $settings; public $admin_texts_to_translate = array(); private $comment_duplicator; /** @var WPML_Custom_Field_Setting_Factory $settings_factory */ private $settings_factory; /** @var WPML_Cache_Factory */ private $cache_factory; /** * Keep list of message ID suffixes. * * @access private */ private $message_ids = array( 'add_translator', 'edit_translator', 'remove_translator', 'save_notification_settings', 'cancel_jobs' ); /** * @var \WPML_Translation_Management_Filters_And_Actions */ private $filters_and_actions; /** * @var \WPML_Cookie */ private $wpml_cookie; /** * @var array */ private static $send_jobs_added_for_types = []; function __construct( WPML_Cookie $wpml_cookie = null ) { global $sitepress, $wpml_cache_factory; $this->selected_translator = new WPML_Translator(); $this->selected_translator->ID = 0; $this->current_translator = new WPML_Translator(); $this->current_translator->ID = 0; $this->cache_factory = $wpml_cache_factory; add_action( 'init', array( $this, 'init' ), self::INIT_PRIORITY ); add_action( 'wp_loaded', array( $this, 'wp_loaded' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 10, 0 ); add_action( 'delete_post', array( $this, 'delete_post_actions' ), 1, 1 ); // Calling *before* the SitePress actions. add_action( 'icl_ajx_custom_call', array( $this, 'ajax_calls' ), 10, 2 ); add_action( 'wpml_tm_basket_add_message', array( $this, 'add_basket_message' ), 10, 3 ); // 1. on Translation Management dashboard and jobs tabs // 2. on Translation Management dashboard tab (called without sm parameter as default page) // 3. Translations queue if ( ( isset( $_GET['sm'] ) && ( $_GET['sm'] == 'dashboard' || $_GET['sm'] == 'jobs' ) ) || ( isset( $_GET['page'] ) && preg_match( '@/menu/main\.php$@', $_GET['page'] ) && ! isset( $_GET['sm'] ) ) || ( isset( $_GET['page'] ) && preg_match( '@/menu/translations-queue\.php$@', $_GET['page'] ) ) ) { @session_start(); } add_filter( 'icl_additional_translators', array( $this, 'icl_additional_translators' ), 99, 3 ); add_action( 'display_basket_notification', array( $this, 'display_basket_notification' ), 10, 1 ); Fns::each( function( $type ) { if ( ! in_array( $type, self::$send_jobs_added_for_types, true ) ) { add_action( "wpml_tm_send_{$type}_jobs", [ $this, 'action_send_jobs' ], 10, 3 ); self::$send_jobs_added_for_types[] = $type; } }, [ 'post', 'package', 'st-batch' ] ); $this->init_comments_synchronization(); add_action( 'wpml_loaded', array( $this, 'wpml_loaded_action' ) ); /** * @api * @uses \TranslationManagement::get_translation_job_id */ add_filter( 'wpml_translation_job_id', array( $this, 'get_translation_job_id_filter' ), 10, 2 ); $this->filters_and_actions = new WPML_Translation_Management_Filters_And_Actions( $this, $sitepress ); $this->wpml_cookie = $wpml_cookie ?: new WPML_Cookie(); } public function wpml_loaded_action() { $this->load_settings_if_required(); if ( is_admin() ) { add_action( 'wpml_config', array( $this, 'wpml_config_action' ), 10, 1 ); } } public function load_settings_if_required() { if ( ! $this->settings ) { $this->settings = apply_filters( 'wpml_setting', null, 'translation-management' ); } } /** * @param array $args { * * @type string $section * @type string $key * @type mixed $value (when used as translation action: 0: do not translate, 1: copy, 2: translate) * @type bool $read_only Options. Default to true. * } */ public function wpml_config_action( $args ) { if ( current_user_can( 'manage_options' ) ) { $this->update_section_translation_setting( $args ); } } /** * @return WPML_Custom_Field_Setting_Factory */ public function settings_factory() { $this->settings_factory = $this->settings_factory ? $this->settings_factory : new WPML_Custom_Field_Setting_Factory( $this ); return $this->settings_factory; } /** * @param WP_User $current_user * @param WPML_Translator $current_translator * * @return WPML_Translator */ private function init_translator_language_pairs( WP_User $current_user, WPML_Translator $current_translator ) { global $wpdb; $current_translator_language_pairs = get_user_meta( $current_user->ID, $wpdb->prefix . 'language_pairs', true ); $current_translator->language_pairs = $this->sanitize_language_pairs( $current_translator_language_pairs ); if ( ! count( $current_translator->language_pairs ) ) { $current_translator->language_pairs = array(); } return $current_translator; } /** * @param string $code * * @return bool */ private function is_valid_language_code_format( $code ) { return $code && is_string( $code ) && strlen( $code ) >= 2; } /** * @param array $language_pairs * * @return array */ private function sanitize_language_pairs( $language_pairs ) { if ( ! $language_pairs || ! is_array( $language_pairs ) ) { $language_pairs = array(); } else { $language_codes_from = array_keys( $language_pairs ); foreach ( $language_codes_from as $code_from ) { $language_codes_to = array_keys( $language_pairs[ $code_from ] ); foreach ( $language_codes_to as $code_to ) { if ( ! $this->is_valid_language_code_format( (string) $code_to ) ) { unset( $language_pairs[ $code_from ][ $code_to ] ); } } if ( ! $this->is_valid_language_code_format( $code_from ) || ! count( $language_pairs[ $code_from ] ) ) { unset( $language_pairs[ $code_from ] ); } } } return $language_pairs; } /** * @param array $args @see \TranslationManagement::wpml_config_action */ private function update_section_translation_setting( $args ) { $section = $args['section']; $key = $args['key']; $value = $args['value']; $read_only = isset( $args['read_only'] ) ? $args['read_only'] : true; $section = preg_replace( '/-/', '_', $section ); $config_section = $this->get_translation_setting_name( $section ); $custom_config_readonly_section = $this->get_custom_readonly_translation_setting_name( $section ); if ( isset( $this->settings[ $config_section ] ) ) { $this->settings[ $config_section ][ esc_sql( $key ) ] = esc_sql( $value ); if ( ! isset( $this->settings[ $custom_config_readonly_section ] ) ) { $this->settings[ $custom_config_readonly_section ] = array(); } if ( $read_only === true && ! in_array( $key, $this->settings[ $custom_config_readonly_section ] ) ) { $this->settings[ $custom_config_readonly_section ][] = esc_sql( $key ); } $this->save_settings(); } } public function init() { $this->init_comments_synchronization(); $this->init_default_settings(); WPML_Config::load_config(); if ( is_admin() ) { if ( $GLOBALS['pagenow'] === 'edit.php' ) { // use standard WP admin notices add_action( 'admin_notices', array( $this, 'show_messages' ) ); } else { // use custom WP admin notices add_action( 'icl_tm_messages', array( $this, 'show_messages' ) ); } // Add duplicate identifier actions. $this->wpml_add_duplicate_check_actions(); // default settings if ( empty( $this->settings['doc_translation_method'] ) || ! defined( 'WPML_TM_VERSION' ) ) { $this->settings['doc_translation_method'] = ICL_TM_TMETHOD_MANUAL; } } } public function get_settings() { $this->load_settings_if_required(); return $this->settings; } public function wpml_add_duplicate_check_actions() { global $pagenow; if ( 'post.php' === $pagenow || ( isset( $_POST['action'] ) && 'check_duplicate' === $_POST['action'] && DOING_AJAX ) ) { return new WPML_Translate_Independently(); } } public function wp_loaded() { if ( isset( $_POST['icl_tm_action'] ) ) { $this->process_request( $_POST ); } elseif ( isset( $_GET['icl_tm_action'] ) ) { $this->process_request( $_GET ); } } public function admin_enqueue_scripts() { if ( ! defined( 'WPML_TM_FOLDER' ) ) { return; } if ( UIPage::isTMBasket( $_GET ) ) { wp_register_style( 'translation-basket', WPML_TM_URL . '/res/css/translation-basket.css', array(), ICL_SITEPRESS_VERSION ); wp_enqueue_style( 'translation-basket' ); } elseif ( UIPage::isTMTranslators( $_GET ) ) { wp_register_style( 'translation-translators', WPML_TM_URL . '/res/css/translation-translators.css', array( 'otgs-icons' ), ICL_SITEPRESS_VERSION ); wp_enqueue_style( 'translation-translators' ); } elseif ( UIPage::isSettings( $_GET ) ) { wp_register_style( 'sitepress-translation-options', ICL_PLUGIN_URL . '/res/css/translation-options.css', array(), ICL_SITEPRESS_VERSION ); wp_enqueue_style( 'sitepress-translation-options' ); } elseif ( UIPage::isTMDashboard( $_GET ) ) { wp_register_style( 'translation-dashboard', WPML_TM_URL . '/res/css/translation-dashboard.css', array(), ICL_SITEPRESS_VERSION ); wp_enqueue_style( 'translation-dashboard' ); wp_register_style( 'translation-translators', WPML_TM_URL . '/res/css/translation-translators.css', array( 'otgs-icons' ), ICL_SITEPRESS_VERSION ); wp_enqueue_style( 'translation-translators' ); } } public static function get_batch_name( $batch_id ) { $batch_data = self::get_batch_data( $batch_id ); if ( ! $batch_data || ! isset( $batch_data->batch_name ) ) { $batch_name = __( 'No Batch', 'sitepress' ); } else { $batch_name = $batch_data->batch_name; } return $batch_name; } public static function get_batch_url( $batch_id ) { $batch_data = self::get_batch_data( $batch_id ); $batch_url = ''; if ( $batch_data && isset( $batch_data->tp_id ) && $batch_data->tp_id != 0 ) { $batch_url = OTG_TRANSLATION_PROXY_URL . "/projects/{$batch_data->tp_id}/external"; } return $batch_url; } public static function get_batch_last_update( $batch_id ) { $batch_data = self::get_batch_data( $batch_id ); return $batch_data ? $batch_data->last_update : false; } public static function get_batch_tp_id( $batch_id ) { $batch_data = self::get_batch_data( $batch_id ); return $batch_data ? $batch_data->tp_id : false; } public static function get_batch_data( $batch_id ) { $cache_key = $batch_id; $cache_group = 'get_batch_data'; $cache_found = false; $batch_data = wp_cache_get( $cache_key, $cache_group, false, $cache_found ); if ( $cache_found ) { return $batch_data; } global $wpdb; $batch_data_sql = "SELECT * FROM {$wpdb->prefix}icl_translation_batches WHERE id=%d"; $batch_data_prepared = $wpdb->prepare( $batch_data_sql, array( $batch_id ) ); $batch_data = $wpdb->get_row( $batch_data_prepared ); wp_cache_set( $cache_key, $batch_data, $cache_group ); return $batch_data; } function save_settings() { global $sitepress; $icl_settings['translation-management'] = $this->settings; $cpt_sync_option = $sitepress->get_setting( 'custom_posts_sync_option', array() ); $cpt_sync_option = (bool) $cpt_sync_option === false ? $sitepress->get_setting( 'custom-types_sync_option', array() ) : $cpt_sync_option; $cpt_unlock_options = $sitepress->get_setting( 'custom_posts_unlocked_option', array() ); if ( ! isset( $icl_settings['custom_posts_sync_option'] ) ) { $icl_settings['custom_posts_sync_option'] = array(); } foreach ( $cpt_sync_option as $k => $v ) { $icl_settings['custom_posts_sync_option'][ $k ] = $v; } $icl_settings['translation-management']['custom-types_readonly_config'] = isset( $icl_settings['translation-management']['custom-types_readonly_config'] ) ? $icl_settings['translation-management']['custom-types_readonly_config'] : array(); foreach ( $icl_settings['translation-management']['custom-types_readonly_config'] as $k => $v ) { if ( ! $this->is_unlocked_type( $k, $cpt_unlock_options ) ) { $icl_settings['custom_posts_sync_option'][ $k ] = $v; } } $sitepress->set_setting( 'translation-management', $icl_settings['translation-management'], true ); $sitepress->set_setting( 'custom_posts_sync_option', $icl_settings['custom_posts_sync_option'], true ); $this->settings = $sitepress->get_setting( 'translation-management' ); } /** * @return string[] */ public function initial_custom_field_translate_states() { global $wpdb; $this->initial_term_custom_field_translate_states(); return $this->initial_translation_states( $wpdb->postmeta ); } /** * @return string[] */ public function initial_term_custom_field_translate_states() { global $wpdb; return ! empty( $wpdb->termmeta ) ? $this->initial_translation_states( $wpdb->termmeta ) : array(); } function process_request( $data ) { $nonce = isset( $data['nonce'] ) ? sanitize_text_field( $data['nonce'] ) : ''; $action = isset( $data['icl_tm_action'] ) ? sanitize_text_field( $data['icl_tm_action'] ) : ''; $data = stripslashes_deep( $data ); switch ( $action ) { case 'edit': $this->selected_translator->ID = intval( $data['user_id'] ); break; case 'dashboard_filter': if ( wp_verify_nonce( $nonce, 'dashboard_filter' ) ) { $cookie_data = filter_var( http_build_query( $data['filter'] ), FILTER_SANITIZE_URL ); $cookie_data && $this->set_cookie( 'wp-translation_dashboard_filter', $cookie_data, time() + HOUR_IN_SECONDS ); wp_safe_redirect( 'admin.php?page=' . WPML_TM_FOLDER . '/menu/main.php&sm=dashboard', 302, 'WPML' ); } break; case 'reset_dashboard_filters': if ( wp_verify_nonce( $nonce, 'reset_dashboard_filters' ) ) { unset( $_COOKIE['wp-translation_dashboard_filter'] ); $this->set_cookie( 'wp-translation_dashboard_filter', '', time() - HOUR_IN_SECONDS ); } break; case 'sort': if ( wp_verify_nonce( $nonce, 'sort' ) ) { $cookie_data = $this->get_cookie( 'wp-translation_dashboard_filter' ); if ( isset( $data['sort_by'] ) ) { $cookie_data['sort_by'] = $data['sort_by']; } if ( isset( $data['sort_order'] ) ) { $cookie_data['sort_order'] = $data['sort_order']; } $cookie_data = filter_var( http_build_query( $cookie_data ), FILTER_SANITIZE_URL ); $cookie_data && $this->set_cookie( 'wp-translation_dashboard_filter', $cookie_data, time() + HOUR_IN_SECONDS ); wp_safe_redirect( 'admin.php?page=' . WPML_TM_FOLDER . '/menu/main.php&sm=dashboard', 302, 'WPML' ); } break; case 'add_jobs': if ( isset( $data['iclnonce'] ) && wp_verify_nonce( $data['iclnonce'], 'pro-translation-icl' ) && Obj::prop( 'tr_action', $data ) ) { /** @var SentContentMessages $sentContentMessages */ $sentContentMessages = make( SentContentMessages::class ); $translateAutomatically = Relation::propEq( 'wpml-how-to-translate', 'automatic', $data ); $isDuplicatingAnyLanguage = Lst::includes( '2', $data['tr_action'] ); $isTranslatingAnyLanguage = Lst::includes( '1', $data['tr_action'] ); $hasAnyPostChecked = false; $hasAnyPackageChecked = false; if ( $isTranslatingAnyLanguage ) { /** @var Validator $validator */ $validator = make( Validator::class ); $data = $validator->validateTMDashboardInput( $data ); $hasAnyChecked = pipe( Fns::filter( Obj::prop( 'checked' ) ), Logic::isEmpty(), Logic::not() ); $hasAnyPostChecked = $hasAnyChecked( Obj::propOr( [], 'post', $data ) ); $hasAnyPackageChecked = $hasAnyChecked( Obj::propOr( [], 'package', $data ) ); // After the validation is performed, we need to check if there are still any checked items. $isTranslatingAnyLanguage = $hasAnyPostChecked || $hasAnyPackageChecked; } if ( $isTranslatingAnyLanguage ) { if ( ! Basket::shouldUse( FiltersStorage::getFromLanguage() ) || $translateAutomatically ) { if ( $translateAutomatically ) { $isDuplicatingAnyLanguage ? $sentContentMessages->duplicateAndAutomatic() : $sentContentMessages->automatic(); } else { $isDuplicatingAnyLanguage ? $sentContentMessages->duplicateAndMyself() : $sentContentMessages->myself(); } $sendPosts = partialRight( Batch::class . '::sendPosts', Jobs::SENT_VIA_DASHBOARD ); if ( $hasAnyPostChecked ) { Posts::dispatch( $sendPosts, new Messages(), BatchBuilder::buildPostsBatch(), $data ); } if ( $hasAnyPackageChecked ) { Packages::dispatch( $sendPosts, new Messages(), BatchBuilder::buildPostsBatch(), $data ); } } else { $isDuplicatingAnyLanguage ? $sentContentMessages->duplicateAndBasket() : $sentContentMessages->basket(); TranslationProxy_Basket::add_posts_to_basket( $data ); do_action( 'wpml_tm_add_to_basket', $data ); } } elseif ( $isDuplicatingAnyLanguage ) { $sentContentMessages->duplicate(); } } break; case 'ujobs_filter': $cookie_data = filter_var( http_build_query( $data['filter'] ), FILTER_SANITIZE_URL ); $_COOKIE['wp-translation_ujobs_filter'] = $cookie_data; $cookie_data && $this->set_cookie( 'wp-translation_ujobs_filter', $cookie_data, time() + HOUR_IN_SECONDS ); wp_safe_redirect( 'admin.php?page=' . WPML_TM_FOLDER . '/menu/translations-queue.php', 302, 'WPML' ); break; case 'save_translation': if ( wp_verify_nonce( $nonce, 'save_translation' ) ) { if ( ! empty( $data['resign'] ) ) { $this->resign_translator( $data['job_id'] ); if ( wp_safe_redirect( admin_url( 'admin.php?page=' . WPML_TM_FOLDER . '/menu/translations-queue.php&resigned=' . $data['job_id'] ), 302, 'WPML' ) ) { exit; } } else { do_action( 'wpml_save_translation_data', $data ); } } break; case 'save_notification_settings': $this->icl_tm_save_notification_settings( $data ); break; case 'cancel_jobs': $this->icl_tm_cancel_jobs( $data ); break; } } /** * @param string $name * @param string $value * @param int $expiration */ private function set_cookie( $name, $value, $expiration ) { $this->wpml_cookie->set_cookie( $name, $value, $expiration, COOKIEPATH, COOKIE_DOMAIN ); } /** * @param string $name * * @return array */ private function get_cookie( $name ) { $result = []; $cookie = new WPML_Cookie(); $value = $cookie->get_cookie( $name ); parse_str( $value, $result ); return $result; } function ajax_calls( $call, $data ) { global $wpdb, $sitepress; switch ( $call ) { case 'assign_translator': $translator_data = TranslationProxy_Service::get_translator_data_from_wpml( $data['translator_id'] ); $service_id = $translator_data['translation_service']; $translator_id = $translator_data['translator_id']; $assign_translation_job = $this->assign_translation_job( $data['job_id'], $translator_id, $service_id, $data['job_type'] ); if ( $assign_translation_job ) { $translator_edit_link = ''; if ( $translator_id ) { if ( $service_id == TranslationProxy::get_current_service_id() ) { $job = $this->get_translation_job( $data['job_id'] ); /** @var WPML_Pro_Translation $ICL_Pro_Translation */ global $ICL_Pro_Translation; $ICL_Pro_Translation->send_post( $job->original_doc_id, array( $job->language_code ), $translator_id, $data['job_id'] ); $project = TranslationProxy::get_current_project(); $translator_edit_link = TranslationProxy_Popup::get_link( $project->translator_contact_iframe_url( $translator_id ), array( 'title' => __( 'Contact the translator', 'sitepress' ), 'unload_cb' => 'icl_thickbox_refresh', ) ) . esc_html( (string) TranslationProxy_Translator::get_translator_name( $translator_id ) ) . "</a> ($project->service->name)"; } else { $translator_edit_link = '<a href="' . self::get_translator_edit_url( $data['translator_id'] ) . '">' . esc_html( $wpdb->get_var( $wpdb->prepare( "SELECT display_name FROM {$wpdb->users} WHERE ID=%d", $data['translator_id'] ) ) ) . '</a>'; } } echo wp_json_encode( array( 'error' => 0, 'message' => $translator_edit_link, 'status' => self::status2text( ICL_TM_WAITING_FOR_TRANSLATOR ), 'service' => $service_id, ) ); } else { echo wp_json_encode( array( 'error' => 1 ) ); } break; case 'icl_cf_translation': case 'icl_tcf_translation': foreach ( array( 'cf' => $call === 'icl_tcf_translation' ? WPML_TERM_META_SETTING_INDEX_PLURAL : WPML_POST_META_SETTING_INDEX_PLURAL, 'cf_unlocked' => $call === 'icl_tcf_translation' ? WPML_TERM_META_UNLOCKED_SETTING_INDEX : WPML_POST_META_UNLOCKED_SETTING_INDEX, ) as $field => $setting ) { if ( ! empty( $data[ $field ] ) ) { $cft = array(); foreach ( $data[ $field ] as $k => $v ) { $cft[ base64_decode( $k ) ] = $v; } if ( ! empty( $cft ) ) { if ( ! isset( $this->settings[ $setting ] ) ) { $this->settings[ $setting ] = array(); } $this->settings[ $setting ] = array_merge( $this->settings[ $setting ], $cft ); $this->save_settings(); /** * Fires after update of custom fields synchronisation preferences in WPML > Settings */ do_action( 'wpml_custom_fields_sync_option_updated', $cft ); } } } echo '1|'; break; case 'icl_doc_translation_method': if ( Obj::prop( 't_method', $data ) ) { $this->settings['doc_translation_method'] = Obj::prop( 't_method', $data ); $sitepress->set_setting( 'doc_translation_method', $this->settings['doc_translation_method'] ); } if ( isset( $data['tm_block_retranslating_terms'] ) ) { $sitepress->set_setting( 'tm_block_retranslating_terms', $data['tm_block_retranslating_terms'] ); } else { $sitepress->set_setting( 'tm_block_retranslating_terms', '' ); } if ( isset( $data['translation_memory'] ) ) { $sitepress->set_setting( 'translation_memory', $data['translation_memory'] ); } $this->save_settings(); echo '1|'; break; case 'reset_duplication': $this->reset_duplicate_flag( $_POST['post_id'] ); break; case 'set_duplication': $new_id = $this->set_duplicate( $_POST['wpml_original_post_id'], $_POST['post_lang'] ); wp_send_json_success( array( 'id' => $new_id ) ); break; } } /** * @param string $element_type_full * * @return mixed */ public function get_element_prefix( $element_type_full ) { $element_type_parts = explode( '_', $element_type_full ); $element_type = $element_type_parts[0]; return $element_type; } /** * @param int $job_id * * @return mixed */ public function get_element_type_prefix_from_job_id( $job_id ) { $job = $this->get_translation_job( $job_id ); if ( isset( $job->element_type_prefix ) ) { return $job->element_type_prefix; } return $job ? $this->get_element_type_prefix_from_job( $job ) : false; } /** * @param \stdClass $job * * @return mixed */ public function get_element_type_prefix_from_job( $job ) { if ( is_object( $job ) ) { $element_type = $this->get_element_type( $job->trid ); $element_type_prefix = $this->get_element_prefix( $element_type ); } else { $element_type_prefix = false; } return $element_type_prefix; } /** * Display admin notices. */ public function show_messages() { foreach ( $this->message_ids as $message_suffix ) { $message_id = 'icl_tm_message_' . $message_suffix; $message = ICL_AdminNotifier::get_message( $message_id ); if ( isset( $message['type'], $message['text'] ) ) { echo '<div class="' . esc_attr( $message['type'] ) . ' below-h2"><p>' . esc_html( $message['text'] ) . '</p></div>'; ICL_AdminNotifier::remove_message( $message_id ); } } } /* TRANSLATORS */ /** * @deprecated use `WPML_TM_Blog_Translators::get_blog_translators` instead * * @return bool */ public function has_translators() { if ( function_exists( 'wpml_tm_load_blog_translators' ) ) { return wpml_tm_load_blog_translators()->has_translators(); } return false; } /** * @deprecated use `WPML_TM_Blog_Translators::get_blog_translators` instead * * @param array $args * * @return array */ public static function get_blog_translators( $args = array() ) { $translators = array(); if ( function_exists( 'wpml_tm_load_blog_translators' ) ) { $translators = wpml_tm_load_blog_translators()->get_blog_translators( $args ); } return $translators; } /** * @return WPML_Translator */ function get_selected_translator() { global $wpdb; if ( $this->selected_translator && $this->selected_translator->ID ) { $user = new WP_User( $this->selected_translator->ID ); $this->selected_translator->display_name = $user->data->display_name; $this->selected_translator->user_login = $user->data->user_login; $this->selected_translator->language_pairs = get_user_meta( $this->selected_translator->ID, $wpdb->prefix . 'language_pairs', true ); } else { $this->selected_translator->ID = 0; } return $this->selected_translator; } /** * @return WPML_Translator */ function get_current_translator() { $current_translator = $this->current_translator; $current_translator_is_set = $current_translator && $current_translator->ID > 0 && $current_translator->language_pairs; if ( ! $current_translator_is_set ) { $this->init_current_translator(); } return $this->current_translator; } public static function get_translator_edit_url( $translator_id ) { $url = ''; if ( ! empty( $translator_id ) ) { $url = 'admin.php?page=' . WPML_TM_FOLDER . '/menu/main.php&sm=translators&icl_tm_action=edit&user_id=' . $translator_id; } return $url; } /* HOOKS */ function make_duplicates( $data ) { foreach ( $data['iclpost'] as $master_post_id ) { foreach ( $data['duplicate_to'] as $lang => $one ) { $this->make_duplicate( $master_post_id, $lang ); } } } function make_duplicate( $master_post_id, $lang ) { global $sitepress; return $sitepress->make_duplicate( $master_post_id, $lang ); } function make_duplicates_all( $master_post_id ) { global $sitepress; $master_post = get_post( $master_post_id ); if ( $master_post->post_status == 'auto-draft' || $master_post->post_type == 'revision' ) { return; } $language_details_original = $sitepress->get_element_language_details( $master_post_id, 'post_' . $master_post->post_type ); if ( ! $language_details_original ) { return; } $data['iclpost'] = array( $master_post_id ); foreach ( $sitepress->get_active_languages() as $lang => $details ) { if ( $lang != $language_details_original->language_code ) { $data['duplicate_to'][ $lang ] = 1; } } $this->make_duplicates( $data ); } function reset_duplicate_flag( $post_id ) { global $sitepress; $post = get_post( $post_id ); $trid = $sitepress->get_element_trid( $post_id, 'post_' . $post->post_type ); $translations = $sitepress->get_element_translations( $trid, 'post_' . $post->post_type ); foreach ( $translations as $tr ) { if ( $tr->element_id == $post_id ) { $this->update_translation_status( array( 'translation_id' => $tr->translation_id, 'status' => ICL_TM_COMPLETE, ) ); } } delete_post_meta( $post_id, '_icl_lang_duplicate_of' ); } function set_duplicate( $master_post_id, $post_lang ) { $new_id = 0; if ( $master_post_id && $post_lang ) { $new_id = $this->make_duplicate( $master_post_id, $post_lang ); } return $new_id; } function duplication_delete_comment( $comment_id ) { global $wpdb; $original_comment = (bool) get_comment_meta( $comment_id, '_icl_duplicate_of', true ) === false; if ( $original_comment ) { $duplicates = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key='_icl_duplicate_of' AND meta_value=%d", $comment_id ) ); foreach ( $duplicates as $dup ) { wp_delete_comment( $dup, true ); } } } function duplication_edit_comment( $comment_id ) { global $wpdb; $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID=%d", $comment_id ), ARRAY_A ); unset( $comment['comment_ID'], $comment['comment_post_ID'] ); $comment_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->commentmeta} WHERE comment_id=%d AND meta_key <> '_icl_duplicate_of'", $comment_id ) ); $original_comment = get_comment_meta( $comment_id, '_icl_duplicate_of', true ); if ( $original_comment ) { $duplicates = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key='_icl_duplicate_of' AND meta_value=%d", $original_comment ) ); $duplicates = array( $original_comment ) + array_diff( $duplicates, array( $comment_id ) ); } else { $duplicates = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key='_icl_duplicate_of' AND meta_value=%d", $comment_id ) ); } if ( ! empty( $duplicates ) ) { foreach ( $duplicates as $dup ) { $wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $dup ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id=%d AND meta_key <> '_icl_duplicate_of'", $dup ) ); if ( $comment_meta ) { foreach ( $comment_meta as $key => $value ) { wp_cache_delete( $dup, 'comment_meta' ); update_comment_meta( $dup, $value->meta_key, $value->meta_value ); } } } } } function duplication_status_comment( $comment_id, $comment_status ) { global $wpdb; static $_avoid_8_loop; if ( isset( $_avoid_8_loop ) ) { return; } $_avoid_8_loop = true; $original_comment = get_comment_meta( $comment_id, '_icl_duplicate_of', true ); if ( $original_comment ) { $duplicates = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key='_icl_duplicate_of' AND meta_value=%d", $original_comment ) ); $duplicates = array( $original_comment ) + array_diff( $duplicates, array( $comment_id ) ); } else { $duplicates = $wpdb->get_col( $wpdb->prepare( "SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key='_icl_duplicate_of' AND meta_value=%d", $comment_id ) ); } if ( ! empty( $duplicates ) ) { foreach ( $duplicates as $duplicate ) { wp_set_comment_status( $duplicate, $comment_status ); } } unset( $_avoid_8_loop ); } function duplication_insert_comment( $comment_id ) { global $wpdb, $sitepress; $duplicator = $this->get_comment_duplicator(); $comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->comments} WHERE comment_ID=%d", $comment_id ), ARRAY_A ); // loop duplicate posts, add new comment $post_id = $comment['comment_post_ID']; // if this is a duplicate post $duplicate_of = get_post_meta( $post_id, '_icl_lang_duplicate_of', true ); if ( $duplicate_of ) { $post_duplicates = $sitepress->get_duplicates( $duplicate_of ); $duplicator->move_to_original( $duplicate_of, $post_duplicates, $comment ); $this->duplication_insert_comment( $comment_id ); return; } else { $post_duplicates = $sitepress->get_duplicates( $post_id ); } unset( $comment['comment_ID'], $comment['comment_post_ID'] ); foreach ( $post_duplicates as $lang => $dup_id ) { $comment['comment_post_ID'] = $dup_id; if ( $comment['comment_parent'] ) { $translated_parent = $duplicator->get_correct_parent( $comment, $dup_id ); if ( ! $translated_parent ) { $this->duplication_insert_comment( $comment['comment_parent'] ); $translated_parent = $duplicator->get_correct_parent( $comment, $dup_id ); } $comment['comment_parent'] = $translated_parent; } $duplicator->insert_duplicated_comment( $comment, $dup_id, $comment_id ); } } private function get_comment_duplicator() { if ( ! $this->comment_duplicator ) { $this->comment_duplicator = new WPML_Comment_Duplication(); } return $this->comment_duplicator; } /** * @param int $post_id Post ID. */ public function delete_post_actions( $post_id ) { global $wpdb; $post_type = $wpdb->get_var( $wpdb->prepare( "SELECT post_type FROM {$wpdb->posts} WHERE ID=%d", $post_id ) ); if ( ! empty( $post_type ) ) { $trid_subquery = $wpdb->prepare( "SELECT trid FROM {$wpdb->prefix}icl_translations WHERE element_id=%d AND element_type=%s AND source_language_code IS NULL", $post_id, 'post_' . $post_type ); $translation_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "SELECT translation_id FROM {$wpdb->prefix}icl_translations WHERE trid = (" . $trid_subquery . ')' ); if ( $translation_ids ) { $rids = $wpdb->get_col( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id IN (" . wpml_prepare_in( $translation_ids, '%d' ) . ')' ); $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "DELETE FROM {$wpdb->prefix}icl_translation_status WHERE translation_id IN (" . wpml_prepare_in( $translation_ids, '%d' ) . ')' ); if ( $rids ) { $job_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid IN (" . wpml_prepare_in( $rids, '%d' ) . ')' ); $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "DELETE FROM {$wpdb->prefix}icl_translate_job WHERE rid IN (" . wpml_prepare_in( $rids, '%d' ) . ')' ); if ( $job_ids ) { $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared "DELETE FROM {$wpdb->prefix}icl_translate WHERE job_id IN (" . wpml_prepare_in( $job_ids, '%d' ) . ')' ); } } } } } /* TRANSLATIONS */ /** * calculate post md5 * * @param object|int $post * * @return string */ function post_md5( $post ) { return apply_filters( 'wpml_tm_element_md5', $post ); } function get_element_translation( $element_id, $language, $element_type = 'post_post' ) { global $wpdb, $sitepress; $trid = $sitepress->get_element_trid( $element_id, $element_type ); $translation = array(); if ( $trid ) { $translation = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}icl_translations tr JOIN {$wpdb->prefix}icl_translation_status ts ON tr.translation_id = ts.translation_id WHERE tr.trid=%d AND tr.language_code= %s ", $trid, $language ) ); } return $translation; } function get_element_translations( $element_id, $element_type = 'post_post', $service = false ) { global $wpdb, $sitepress; $trid = $sitepress->get_element_trid( $element_id, $element_type ); $translations = array(); if ( $trid ) { $service = $service ? $wpdb->prepare( ' AND translation_service = %s ', $service ) : ''; $translations = $wpdb->get_results( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}icl_translations tr JOIN {$wpdb->prefix}icl_translation_status ts ON tr.translation_id = ts.translation_id WHERE tr.trid=%d {$service} ", $trid ) ); foreach ( $translations as $k => $v ) { $translations[ $v->language_code ] = $v; unset( $translations[ $k ] ); } } return $translations; } /** * returns icon class according to status code * * @param int $status * @param int $needs_update * @param bool $needs_review * * @return string */ public function status2icon_class( $status, $needs_update = 0, $needs_review = false ) { if ( $needs_update ) { $icon_class = 'otgs-ico-needs-update'; } elseif($needs_review) { $icon_class = 'otgs-ico-needs-review'; } else { switch ( $status ) { case ICL_TM_NOT_TRANSLATED: $icon_class = 'otgs-ico-not-translated'; break; case ICL_TM_WAITING_FOR_TRANSLATOR: $icon_class = 'otgs-ico-waiting'; break; case ICL_TM_IN_PROGRESS: case ICL_TM_TRANSLATION_READY_TO_DOWNLOAD: case ICL_TM_ATE_NEEDS_RETRY: $icon_class = 'otgs-ico-in-progress'; break; case ICL_TM_IN_BASKET: $icon_class = 'otgs-ico-basket'; break; case ICL_TM_NEEDS_UPDATE: $icon_class = 'otgs-ico-needs-update'; break; case ICL_TM_DUPLICATE: $icon_class = 'otgs-ico-duplicate'; break; case ICL_TM_COMPLETE: $icon_class = 'otgs-ico-translated'; break; default: $icon_class = 'otgs-ico-not-translated'; } } return $icon_class; } public static function status2text( $status ) { switch ( $status ) { case ICL_TM_NOT_TRANSLATED: $text = __( 'Not translated', 'sitepress' ); break; case ICL_TM_WAITING_FOR_TRANSLATOR: $text = __( 'Waiting for translator', 'sitepress' ); break; case ICL_TM_IN_PROGRESS: $text = __( 'In progress', 'sitepress' ); break; case ICL_TM_NEEDS_UPDATE: $text = __( 'Needs update', 'sitepress' ); break; case ICL_TM_DUPLICATE: $text = __( 'Duplicate', 'sitepress' ); break; case ICL_TM_COMPLETE: $text = __( 'Complete', 'sitepress' ); break; case ICL_TM_TRANSLATION_READY_TO_DOWNLOAD: $text = __( 'Translation ready to download', 'sitepress' ); break; case ICL_TM_ATE_NEEDS_RETRY: $text = __( 'In progress - needs retry', 'sitepress' ); break; default: $text = ''; } return $text; } public function decode_field_data( $data, $format ) { if ( $format == 'base64' ) { $data = base64_decode( $data ); } elseif ( $format == 'csv_base64' ) { $exp = explode( ',', $data ); foreach ( $exp as $k => $e ) { $exp[ $k ] = base64_decode( trim( $e, '"' ) ); } $data = $exp; } return $data; } /** * create translation package * * @param object|int $post * * @return array|false */ function create_translation_package( $post ) { $wpmlElementTranslationPackage = make( WPML_Element_Translation_Package::class ); return $wpmlElementTranslationPackage->create_translation_package( $post, true ) ?: false; } function messages_by_type( $type ) { $messages = $this->messages; $result = []; foreach ( $messages as $message ) { if ( $type === false || ( ! empty( $message['type'] ) && $message['type'] == $type ) ) { $result[] = $message; } } return $result ?: false; } public function add_basket_message( $type, $message, $id = null ) { $message = array( 'type' => $type, 'text' => $message, ); if ( $id ) { $message['id'] = $id; } $this->add_message( $message ); } function add_message( $message ) { $this->messages[] = $message; $this->messages = array_unique( $this->messages, SORT_REGULAR ); } /** * add/update icl_translation_status record * * @param array $data * @param int $rid * * @return array */ function update_translation_status( $data, $rid = null ) { global $wpdb; if ( ! isset( $data['translation_id'] ) ) { return array( false, false ); } if ( ! $rid ) { $rid = $this->get_rid_from_translation_id( $data['translation_id'] ); } $update = (bool) $rid; if ( true === $update ) { $data_where = array( 'rid' => $rid ); $wpdb->update( $wpdb->prefix . 'icl_translation_status', $data, $data_where ); } else { $wpdb->insert( $wpdb->prefix . 'icl_translation_status', $data ); $rid = $wpdb->insert_id; } $data['rid'] = $rid; do_action( 'wpml_updated_translation_status', $data ); return array( $rid, $update ); } /** * @param int $translation_id * * @return int */ private function get_rid_from_translation_id( $translation_id ) { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id = %d", $translation_id ) ); } /* TRANSLATION JOBS */ /** * @param \WPML_TM_Translation_Batch $batch * @param string $type * @param int|null $sendFrom * * @return void */ public function action_send_jobs( \WPML_TM_Translation_Batch $batch, $type = 'post', $sendFrom = null ) { $this->send_jobs( $batch, $type, $sendFrom ); } /** * @param \WPML_TM_Translation_Batch $batch * @param string $type * @param int|null $sendFrom * * @return array */ function send_jobs( \WPML_TM_Translation_Batch $batch, $type = 'post', $sendFrom = null ) { global $sitepress; $job_ids = array(); $added_jobs = array(); $batch_id = TranslationProxy_Batch::update_translation_batch( $batch->get_basket_name() ); /** * Allows to filter the translation batch * * @since 4.3.0 * * @param \WPML_TM_Translation_Batch $batch */ $batch = apply_filters( 'wpml_send_jobs_batch', $batch ); foreach ( $batch->get_elements_by_type( $type ) as $element ) { $post = $this->get_post( $element->get_element_id(), $type ); if ( ! $post ) { continue; } if ( $post instanceof \WP_Post ) { /** * Registers strings coming from page builder shortcodes * * @param \WP_Post $post * * @since 4.3.16 */ do_action( 'wpml_pb_register_all_strings_for_translation', $post ); } $element_type = $type . '_' . $post->post_type; $post_trid = $sitepress->get_element_trid( $element->get_element_id(), $element_type ); $post_translations = $sitepress->get_element_translations( $post_trid, $element_type ); $md5 = $this->post_md5( $post ); $translation_package = $this->create_translation_package( $post ); foreach ( $element->get_target_langs() as $lang => $action ) { if ( $action == self::DUPLICATE_ELEMENT_ACTION ) { // don't send documents that are in progress $current_translation_status = $this->get_element_translation( $element->get_element_id(), $lang, $element_type ); if ( $current_translation_status && $current_translation_status->status == ICL_TM_IN_PROGRESS ) { continue; } $job_ids[] = $this->make_duplicate( $element->get_element_id(), $lang ); } elseif ( $action == self::TRANSLATE_ELEMENT_ACTION ) { // INSERT DATA TO icl_translations if ( empty( $post_translations[ $lang ] ) ) { $translation_id = $sitepress->set_element_language_details( null, $element_type, $post_trid, $lang, $element->get_source_lang() ); } else { $translation_id = $post_translations[ $lang ]->translation_id; $sitepress->set_element_language_details( $post_translations[ $lang ]->element_id, $element_type, $post_trid, $lang, $element->get_source_lang() ); } $current_translation_status = $this->get_element_translation( $element->get_element_id(), $lang, $element_type ); if ( $current_translation_status ) { if ( $current_translation_status->status == ICL_TM_IN_PROGRESS ) { $this->cancel_previous_job_if_in_progress( $translation_id ); } else { $this->cancel_previous_job_if_still_waiting( $translation_id, $current_translation_status->status ); } } $_status = ICL_TM_WAITING_FOR_TRANSLATOR; $translator = $batch->get_translator( $lang ); $translation_data = TranslationProxy_Service::get_translator_data_from_wpml( $translator ); $translator_id = $sendFrom === Jobs::SENT_AUTOMATICALLY ? 0 : $translation_data['translator_id']; $translation_service = $translation_data['translation_service']; /** * Filter translation package before creating the translation job. * * @param array|false $translation_package * @param \WP_Post $post * @param string $targetLang * * @since 4.5.12 */ $translation_package = apply_filters( 'wpml_translation_package_by_language', $translation_package, $post, $lang ); // add translation_status record $data = array( 'translation_id' => $translation_id, 'status' => $_status, 'translator_id' => $translator_id, 'needs_update' => 0, 'md5' => $md5, 'translation_service' => $translation_service, 'translation_package' => serialize( $translation_package ), 'batch_id' => $batch_id, 'uuid' => $this->get_uuid( $current_translation_status, $post ), 'ts_status' => null, 'timestamp' => date( 'Y-m-d H:i:s', time() ), ); $backup_translation_status = $this->get_translation_status_data( $translation_id ); $prevstate = $this->get_translation_prev_state( $backup_translation_status ); if ( $prevstate ) { $data['_prevstate'] = serialize( $prevstate ); } $rid = isset( $backup_translation_status['rid'] ) ? $backup_translation_status['rid'] : null; list( $rid ) = $this->update_translation_status( $data, $rid ); if ( $translation_package ) { $job_id = wpml_tm_add_translation_job( $rid, $translator_id, $translation_package, $batch->get_batch_options() ); wpml_tm_load_job_factory()->update_job_data( $job_id, array( 'editor' => WPML_TM_Editors::NONE ) ); $job_ids[] = $job_id; if ( $translation_service !== 'local' ) { /** @global WPML_Pro_Translation $ICL_Pro_Translation */ global $ICL_Pro_Translation; $tp_job_id = $ICL_Pro_Translation->send_post( $post, array( $lang ), $translator_id, $job_id ); if ( ! $tp_job_id ) { $this->revert_job_when_tp_job_could_not_be_created( $job_ids, $rid, $data['translation_id'], $backup_translation_status ); } // save associated TP JOB ID $this->update_translation_status( array( 'translation_id' => $translation_id, 'tp_id' => $tp_job_id, ), $rid ); } $added_jobs[ $translation_service ][] = $job_id; } } /** * @param WPML_TM_Translation_Batch_Element $element * @param mixed $post * @since 4.4.0 */ do_action( 'wpml_tm_added_translation_element', $element, $post ); } } do_action( 'wpml_added_translation_jobs', $added_jobs, $sendFrom, $batch ); icl_cache_clear(); do_action( 'wpml_tm_empty_mail_queue' ); return $job_ids; } private function revert_job_when_tp_job_could_not_be_created( $job_ids, $rid, $translator_id, $backup_translation_status ) { /** @global WPML_Pro_Translation $ICL_Pro_Translation */ global $wpdb, $ICL_Pro_Translation; $job_id = array_pop( $job_ids ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translate_job WHERE job_id=%d", $job_id ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE rid=%d ORDER BY job_id DESC LIMIT 1", $rid ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translate WHERE job_id=%d", $job_id ) ); if ( $backup_translation_status ) { $wpdb->update( "{$wpdb->prefix}icl_translation_status", $backup_translation_status, [ 'translation_id' => $translator_id ] ); } else { $wpdb->delete( "{$wpdb->prefix}icl_translation_status", [ 'translation_id' => $translator_id ] ); } foreach ( $ICL_Pro_Translation->errors as $error ) { if ( $error instanceof Exception ) { /** @var Exception $error */ $message = [ 'type' => 'error', 'text' => $error->getMessage(), ]; $this->add_message( $message ); } } } /** * @param stdClass|null $current_translation_status * @param WP_Post|WPML_Package $post * * @return string */ private function get_uuid( $current_translation_status, $post ) { if ( ! empty( $current_translation_status->uuid ) ) { return $current_translation_status->uuid; } else { return wpml_uuid( $post->ID, $post->post_type ); } } private function get_translation_status_data( $translation_id ) { global $wpdb; $data = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}icl_translation_status WHERE translation_id = %d", $translation_id ), ARRAY_A ); return isset( $data[0] ) ? $data[0] : array(); } /** * @param string $translation_id * @param string $status */ private function cancel_previous_job_if_still_waiting( $translation_id, $status ) { if ( ICL_TM_WAITING_FOR_TRANSLATOR === (int) $status ) { $this->cancel_translation_request( $translation_id, false ); } } private function cancel_previous_job_if_in_progress( $translation_id ) { global $wpdb; $sql = " SELECT j.job_id FROM {$wpdb->prefix}icl_translate_job j INNER JOIN {$wpdb->prefix}icl_translation_status ts ON ts.rid = j.rid WHERE ts.translation_id = %d AND ts.status = %s ORDER BY job_id DESC "; $job_id = (int) $wpdb->get_var( $wpdb->prepare( $sql, $translation_id, ICL_TM_IN_PROGRESS ) ); if ( ! $job_id ) { return; } $wpdb->update( $wpdb->prefix . 'icl_translate_job', array( 'translated' => 1 ), array( 'job_id' => $job_id ) ); } function get_translation_jobs( $args = array() ) { return apply_filters( 'wpml_translation_jobs', array(), $args ); } /** * Adds a translation job record in icl_translate_job * * @depreacted 4.6.7 Use "wpml_tm_add_translation_job" function instead of this one. * @param mixed $rid * @param mixed $translator_id * @param array<string,string|array<string,string>> $translation_package * @param array $batch_options * * @return bool|int false on failure, job_id on success */ function add_translation_job( $rid, $translator_id, $translation_package, $batch_options = array() ) { return wpml_tm_add_translation_job( $rid, $translator_id, $translation_package, $batch_options ); } /** * Clean orphan jobs in posts * * @param array $posts */ function cleanup_translation_jobs_cart_posts( $posts ) { if ( empty( $posts ) ) { return; } foreach ( $posts as $post_id => $post_data ) { if ( ! get_post( $post_id ) ) { TranslationProxy_Basket::delete_item_from_basket( $post_id ); } } } /** * Incorporates posts in cart data with post title, post date, post notes, * post type, post status * * @param array $posts * * @return boolean | array */ function get_translation_jobs_basket_posts( $posts ) { if ( empty( $posts ) ) { return false; } $this->cleanup_translation_jobs_cart_posts( $posts ); global $sitepress; $posts_ids = array_keys( $posts ); $args = array( 'posts_per_page' => -1, 'include' => $posts_ids, 'post_type' => get_post_types(), 'post_status' => get_post_stati(), // All post statuses ); /** @phpstan-ignore-next-line WP doc issue. */ $new_posts = get_posts( $args ); $final_posts = array(); foreach ( $new_posts as $post_data ) { // set post_id $final_posts[ $post_data->ID ] = []; // set post_title $final_posts[ $post_data->ID ]['post_title'] = $post_data->post_title; // set post_date $final_posts[ $post_data->ID ]['post_date'] = $post_data->post_date; // set post_notes $final_posts[ $post_data->ID ]['post_notes'] = get_post_meta( $post_data->ID, '_icl_translator_note', true ); // set post_type $final_posts[ $post_data->ID ]['post_type'] = $post_data->post_type; // set post_status $final_posts[ $post_data->ID ]['post_status'] = $post_data->post_status; // set from_lang $final_posts[ $post_data->ID ]['from_lang'] = $posts[ $post_data->ID ]['from_lang']; $final_posts[ $post_data->ID ]['from_lang_string'] = ucfirst( $sitepress->get_display_language_name( $posts[ $post_data->ID ]['from_lang'], $sitepress->get_admin_language() ) ); // set to_langs $final_posts[ $post_data->ID ]['to_langs'] = $posts[ $post_data->ID ]['to_langs']; // set comma separated to_langs -> to_langs_string $language_names = array(); foreach ( $final_posts[ $post_data->ID ]['to_langs'] as $language_code => $value ) { $language_names[] = ucfirst( $sitepress->get_display_language_name( $language_code, $sitepress->get_admin_language() ) ); } $final_posts[ $post_data->ID ]['to_langs_string'] = implode( ', ', $language_names ); $final_posts[ $post_data->ID ]['auto_added'] = isset( $posts[ $post_data->ID ]['auto_added'] ) && $posts[ $post_data->ID ]['auto_added']; } return $final_posts; } /** * Incorporates strings in cart data * * @param array $strings * @param bool|string $source_language * * @return boolean | array */ function get_translation_jobs_basket_strings( $strings, $source_language = false ) { $final_strings = array(); if ( class_exists( 'WPML_String_Translation' ) ) { global $sitepress; $source_language = $source_language ? $source_language : TranslationProxy_Basket::get_source_language(); foreach ( $strings as $string_id => $data ) { if ( $source_language ) { // set post_id $final_strings[ $string_id ] = []; // set post_title $final_strings[ $string_id ]['post_title'] = icl_get_string_by_id( $string_id ) ?: ''; // set post_type $final_strings[ $string_id ]['post_type'] = 'string'; // set from_lang $final_strings[ $string_id ]['from_lang'] = $source_language; $final_strings[ $string_id ]['from_lang_string'] = ucfirst( $sitepress->get_display_language_name( $source_language, $sitepress->get_admin_language() ) ); // set to_langs $final_strings[ $string_id ]['to_langs'] = $data['to_langs']; // set comma separated to_langs -> to_langs_string // set comma separated to_langs -> to_langs_string $language_names = array(); foreach ( $final_strings[ $string_id ]['to_langs'] as $language_code => $value ) { $language_names[] = ucfirst( $sitepress->get_display_language_name( $language_code, $sitepress->get_admin_language() ) ); } $final_strings[ $string_id ]['to_langs_string'] = implode( ', ', $language_names ); } } } return $final_strings; } function get_translation_job( $job_id, $include_non_translatable_elements = false, $auto_assign = false, $revisions = 0 ) { return apply_filters( 'wpml_get_translation_job', $job_id, $include_non_translatable_elements, $revisions ); } function get_translation_job_id_filter( $empty, $args ) { $trid = $args['trid']; $language_code = $args['language_code']; return $this->get_translation_job_id( $trid, $language_code ); } /** * @param int $trid * * @return array */ private function get_translation_job_info( $trid ) { global $wpdb; // Cache key must be integer or non-empty string, WP_Object_Cache::get will crash with empty $trid. if( ! $trid ) { return []; } $found = false; $cache = $this->cache_factory->get( 'TranslationManagement::get_translation_job_id' ); $job_info = $cache->get( $trid, $found ); if ( ! $found ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT tj.job_id, tj.editor, t.language_code FROM {$wpdb->prefix}icl_translate_job tj JOIN {$wpdb->prefix}icl_translation_status ts ON tj.rid = ts.rid JOIN {$wpdb->prefix}icl_translations t ON ts.translation_id = t.translation_id WHERE t.trid = %d ORDER BY tj.job_id DESC", $trid ) ); $job_info = array(); foreach ( $results as $result ) { if ( ! isset( $job_info[ $result->language_code ] ) ) { $job_info[ $result->language_code ] = [ 'job_id' => $result->job_id, 'editor' => $result->editor, ]; } } $cache->set( $trid, $job_info ); } return $job_info; } /** * @param int $trid * @param string $language_code * * @return int|null */ public function get_translation_job_id( $trid, $language_code ) { $job_info = $this->get_translation_job_info( $trid ); return isset( $job_info[ $language_code ] ) ? $job_info[ $language_code ]['job_id'] : null; } /** * @param int $trid * @param string $language_code * * @return string|null */ public function get_translation_job_editor( $trid, $language_code ) { $job_info = $this->get_translation_job_info( $trid ); return isset( $job_info[ $language_code ] ) ? $job_info[ $language_code ]['editor'] : null; } function save_translation( $data ) { do_action( 'wpml_save_translation_data', $data ); } /** * Saves the contents a job's post to the job itself * * @param int $job_id * * @hook wpml_save_job_fields_from_post * @deprecated since WPML 3.2.3 use the action hook wpml_save_job_fields_from_post */ function save_job_fields_from_post( $job_id ) { do_action( 'wpml_save_job_fields_from_post', $job_id ); } function mark_job_done( $job_id ) { global $wpdb; $wpdb->update( $wpdb->prefix . 'icl_translate_job', array( 'translated' => 1 ), array( 'job_id' => $job_id ) ); $wpdb->update( $wpdb->prefix . 'icl_translate', array( 'field_finished' => 1 ), array( 'job_id' => $job_id ) ); do_action( 'wpml_tm_empty_mail_queue' ); } function resign_translator( $job_id, $skip_notification = false ) { global $wpdb; list( $translator_id, $rid ) = $wpdb->get_row( $wpdb->prepare( "SELECT translator_id, rid FROM {$wpdb->prefix}icl_translate_job WHERE job_id=%d", $job_id ), ARRAY_N ); if ( ! $skip_notification && ! empty( $translator_id ) && $this->settings['notification']['resigned'] != ICL_TM_NOTIFICATION_NONE && $job_id ) { do_action( 'wpml_tm_resign_job_notification', $translator_id, $job_id ); } $wpdb->update( $wpdb->prefix . 'icl_translate_job', array( 'translator_id' => 0 ), array( 'job_id' => $job_id ) ); $wpdb->update( $wpdb->prefix . 'icl_translation_status', array( 'translator_id' => 0, 'status' => ICL_TM_WAITING_FOR_TRANSLATOR, ), array( 'rid' => $rid ) ); } /** * Resign the given translator from all unfinished translation jobs. * * @param WP_User $translator */ public function resign_translator_from_unfinished_jobs( WP_User $translator ) { global $wpdb; $unfinished_job_ids = $wpdb->get_col( $wpdb->prepare( "SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE translator_id = %d AND translated = 0", $translator->ID ) ); $remove_job_without_notification = partialRight( [ $this, 'resign_translator' ], true ); // Very unexpected to have a long list here, so it's fine deleting jobs one by one // instead of introducing new logic for resign. array_map( $remove_job_without_notification, $unfinished_job_ids ); } function remove_translation_job( $job_id, $new_translation_status = ICL_TM_WAITING_FOR_TRANSLATOR, $new_translator_id = 0 ) { global $wpdb; $error = false; list( $prev_translator_id, $rid ) = $wpdb->get_row( $wpdb->prepare( "SELECT translator_id, rid FROM {$wpdb->prefix}icl_translate_job WHERE job_id=%d", $job_id ), ARRAY_N ); $wpdb->update( $wpdb->prefix . 'icl_translate_job', array( 'translator_id' => $new_translator_id ), array( 'job_id' => $job_id ) ); $wpdb->update( $wpdb->prefix . 'icl_translate', array( 'field_data_translated' => '', 'field_finished' => 0, ), array( 'job_id' => $job_id ) ); if ( $rid ) { $data = array( 'status' => $new_translation_status, 'translator_id' => $new_translator_id, ); $data_where = array( 'rid' => $rid ); $wpdb->update( $wpdb->prefix . 'icl_translation_status', $data, $data_where ); if ( $this->settings['notification']['resigned'] == ICL_TM_NOTIFICATION_IMMEDIATELY && ! empty( $prev_translator_id ) ) { do_action( 'wpml_tm_remove_job_notification', $prev_translator_id, $job_id ); } } else { $error = sprintf( __( 'Translation entry not found for: %d', 'sitepress' ), $job_id ); } return $error; } // $translation_id - int or array function cancel_translation_request( $translation_id, $remove_translation_record = true ) { global $wpdb, $WPML_String_Translation; if ( is_array( $translation_id ) ) { foreach ( $translation_id as $id ) { $this->cancel_translation_request( $id ); } } else { if ( $WPML_String_Translation && wpml_mb_strpos( $translation_id, 'string|' ) === 0 ) { // string translations get handled in wpml-string-translation // first remove the "string|" prefix $id = substr( $translation_id, 7 ); // then send it to the respective function in wpml-string-translation $WPML_String_Translation->cancel_local_translation( $id ); return; } list( $rid, $translator_id ) = $wpdb->get_row( $wpdb->prepare( "SELECT rid, translator_id FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d AND ( status = %d OR status = %d )", $translation_id, ICL_TM_WAITING_FOR_TRANSLATOR, ICL_TM_IN_PROGRESS ), ARRAY_N ); if ( ! $rid ) { return; } $job_id = $wpdb->get_var( $wpdb->prepare( "SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d AND revision IS NULL ", $rid ) ); if ( isset( $this->settings['notification']['resigned'] ) && $this->settings['notification']['resigned'] == ICL_TM_NOTIFICATION_IMMEDIATELY && ! empty( $translator_id ) ) { do_action( 'wpml_tm_remove_job_notification', $translator_id, $job_id ); } $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translate_job WHERE job_id=%d", $job_id ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translate WHERE job_id=%d", $job_id ) ); $max_job_id = \WPML\TM\API\Job\Map::fromRid( $rid ); if ( $max_job_id ) { $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id=%d", $max_job_id ) ); $previous_state = $wpdb->get_var( $wpdb->prepare( "SELECT _prevstate FROM {$wpdb->prefix}icl_translation_status WHERE translation_id = %d", $translation_id ) ); if ( ! empty( $previous_state ) ) { $previous_state = unserialize( $previous_state ); $arr_data = array( 'status' => $previous_state['status'], 'translator_id' => $previous_state['translator_id'], 'needs_update' => $previous_state['needs_update'], 'md5' => $previous_state['md5'], 'translation_service' => $previous_state['translation_service'], 'translation_package' => $previous_state['translation_package'], 'timestamp' => $previous_state['timestamp'], 'links_fixed' => $previous_state['links_fixed'], ); $data_where = array( 'translation_id' => $translation_id ); $wpdb->update( $wpdb->prefix . 'icl_translation_status', $arr_data, $data_where ); } } else { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d", $translation_id ) ); } // delete record from icl_translations if element_id is null if ( $remove_translation_record ) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}icl_translations WHERE translation_id=%d AND element_id IS NULL", $translation_id ) ); } icl_cache_clear(); } } function render_option_writes( $name, $value, $key = '' ) { if ( ! defined( 'WPML_ST_FOLDER' ) ) { return; } // Cache the previous option, when called recursively static $option = false; if ( ! $key ) { $option = maybe_unserialize( get_option( $name ) ); if ( is_object( $option ) ) { $option = (array) $option; } } $admin_option_names = get_option( '_icl_admin_option_names' ); // determine theme/plugin name (string context) $es_context = ''; $context = ''; $slug = ''; foreach ( $admin_option_names as $context => $element ) { $found = false; foreach ( (array) $element as $slug => $options ) { $found = false; foreach ( (array) $options as $option_key => $option_value ) { $found = false; $es_context = ''; if ( $option_key == $name ) { if ( is_scalar( $option_value ) ) { $es_context = 'admin_texts_' . $context . '_' . $slug; $found = true; } elseif ( is_array( $option_value ) && is_array( $value ) && ( $option_value == $value ) ) { $es_context = 'admin_texts_' . $context . '_' . $slug; $found = true; } } if ( $found ) { break; } } if ( $found ) { break; } } if ( $found ) { break; } } echo '<ul class="icl_tm_admin_options">'; echo '<li>'; $context_html = ''; if ( ! $key ) { $context_html = '[' . esc_html( $context ) . ': ' . esc_html( $slug ) . '] '; } if ( is_scalar( $value ) ) { preg_match_all( '#\[([^\]]+)\]#', $key, $matches ); if ( count( $matches[1] ) > 1 ) { $o_value = $option; for ( $i = 1; $i < count( $matches[1] ); $i++ ) { $o_value = $o_value[ $matches[1][ $i ] ]; } $o_value = $o_value[ $name ]; $edit_link = ''; } else { if ( is_scalar( $option ) ) { $o_value = $option; } elseif ( isset( $option[ $name ] ) ) { $o_value = $option[ $name ]; } else { $o_value = ''; } if ( ! $key ) { if ( icl_st_is_registered_string( $es_context, $name ) ) { $edit_link = '[<a href="' . admin_url( 'admin.php?page=' . WPML_ST_FOLDER . '/menu/string-translation.php&context=' . esc_html( $es_context ) ) . '">' . esc_html__( 'translate', 'sitepress' ) . '</a>]'; } else { $edit_link = '<div class="updated below-h2">' . esc_html__( 'string not registered', 'sitepress' ) . '</div>'; } } else { $edit_link = ''; } } if ( false !== strpos( $name, '*' ) ) { $o_value = '<span style="color:#bbb">{{ ' . esc_html__( 'Multiple options', 'sitepress' ) . ' }}</span>'; } else { $o_value = esc_html( $o_value ); if ( strlen( $o_value ) > 200 ) { $o_value = substr( $o_value, 0, 200 ) . ' ...'; } } echo $context_html . esc_html( $name ) . ': <i>' . $o_value . '</i> ' . $edit_link; } else { $edit_link = '[<a href="' . admin_url( 'admin.php?page=' . WPML_ST_FOLDER . '/menu/string-translation.php&context=' . esc_html( $es_context ) ) . '">' . esc_html__( 'translate', 'sitepress' ) . '</a>]'; echo '<strong>' . $context_html . $name . '</strong> ' . $edit_link; if ( ! icl_st_is_registered_string( $es_context, $name ) ) { $notice = '<div class="updated below-h2">' . esc_html__( 'some strings might be not registered', 'sitepress' ) . '</div>'; echo $notice; } foreach ( (array) $value as $o_key => $o_value ) { $this->render_option_writes( $o_key, $o_value, $o_key . '[' . $name . ']' ); } // Reset cached data $option = false; } echo '</li>'; echo '</ul>'; } /** * @param array $info * * @deprecated @since 3.2 Use TranslationProxy::get_current_service_info instead * @return array */ public static function current_service_info( $info = array() ) { return TranslationProxy::get_current_service_info( $info ); } // set slug according to user preference static function set_page_url( $post_id ) { global $wpdb; if ( wpml_get_setting_filter( false, 'translated_document_page_url' ) === 'copy-encoded' ) { $post = $wpdb->get_row( $wpdb->prepare( "SELECT post_type FROM {$wpdb->posts} WHERE ID=%d", $post_id ) ); $translation_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}icl_translations WHERE element_id=%d AND element_type=%s", $post_id, 'post_' . $post->post_type ) ); $encode_url = $wpdb->get_var( $wpdb->prepare( "SELECT encode_url FROM {$wpdb->prefix}icl_languages WHERE code=%s", $translation_row->language_code ) ); if ( $encode_url ) { $trid = $translation_row->trid; $original_post_id = $wpdb->get_var( $wpdb->prepare( "SELECT element_id FROM {$wpdb->prefix}icl_translations WHERE trid=%d AND source_language_code IS NULL", $trid ) ); $post_name_original = $wpdb->get_var( $wpdb->prepare( "SELECT post_name FROM {$wpdb->posts} WHERE ID = %d", $original_post_id ) ); $post_name_to_be = $post_name_original; $incr = 1; do { $taken = $wpdb->get_var( $wpdb->prepare( " SELECT ID FROM {$wpdb->posts} p JOIN {$wpdb->prefix}icl_translations t ON p.ID = t.element_id WHERE ID <> %d AND t.element_type = %s AND t.language_code = %s AND p.post_name = %s ", $post_id, 'post_' . $post->post_type, $translation_row->language_code, $post_name_to_be ) ); if ( $taken ) { $incr ++; $post_name_to_be = $post_name_original . '-' . $incr; } else { $taken = false; } } while ( $taken == true ); $post_to_update = new WPML_WP_Post( $wpdb, $post_id ); $post_to_update->update( array( 'post_name' => $post_name_to_be ), true ); } } } /** * @param array<string,mixed> $postarr * @param string $lang * * @return int|WP_Error * @deprecated since 4.2.8 Use directly `wpml_get_create_post_helper()` instead. * */ public function icl_insert_post( $postarr, $lang ) { $create_post_helper = wpml_get_create_post_helper(); return $create_post_helper->insert_post( $postarr, $lang ); } /** * Add missing language to posts * * @param array $post_types */ private function add_missing_language_to_posts( $post_types ) { global $wpdb; // This will be improved when it will be possible to pass an array to the IN clause $posts_prepared = "SELECT ID, post_type, post_status FROM {$wpdb->posts} WHERE post_type IN ('" . implode( "', '", esc_sql( $post_types ) ) . "')"; $posts = $wpdb->get_results( $posts_prepared ); if ( $posts ) { foreach ( $posts as $post ) { $this->add_missing_language_to_post( $post ); } } } /** * Add missing language to a given post * * @param WP_Post $post */ private function add_missing_language_to_post( $post ) { global $sitepress, $wpdb; $query_prepared = $wpdb->prepare( "SELECT translation_id, language_code FROM {$wpdb->prefix}icl_translations WHERE element_type=%s AND element_id=%d", array( 'post_' . $post->post_type, $post->ID ) ); $query_results = $wpdb->get_row( $query_prepared ); // if translation exists if ( ! is_null( $query_results ) ) { $translation_id = $query_results->translation_id; $language_code = $query_results->language_code; } else { $translation_id = null; $language_code = null; } $urls = $sitepress->get_setting( 'urls' ); $is_root_page = $urls && isset( $urls['root_page'] ) && $urls['root_page'] == $post->ID; $default_language = $sitepress->get_default_language(); if ( ! $translation_id && ! $is_root_page && ! in_array( $post->post_status, array( 'auto-draft' ) ) ) { $sitepress->set_element_language_details( $post->ID, 'post_' . $post->post_type, null, $default_language, null, true, true ); } elseif ( $translation_id && $is_root_page ) { $trid = $sitepress->get_element_trid( $post->ID, 'post_' . $post->post_type ); if ( $trid ) { $sitepress->delete_element_translation( $trid, 'post_' . $post->post_type ); } } elseif ( $translation_id && ! $language_code && $default_language ) { $where = array( 'translation_id' => $translation_id ); $data = array( 'language_code' => $default_language ); $wpdb->update( $wpdb->prefix . 'icl_translations', $data, $where ); do_action( 'wpml_translation_update', array( 'type' => 'update', 'element_id' => $post->ID, 'element_type' => 'post_' . $post->post_type, 'translation_id' => $translation_id, 'context' => 'post', ) ); } } /** * Add missing language to taxonomies * * @param array $post_types */ private function add_missing_language_to_taxonomies( $post_types ) { global $sitepress, $wpdb; $taxonomy_types = array(); foreach ( $post_types as $post_type ) { $taxonomy_types = array_merge( $sitepress->get_translatable_taxonomies( true, $post_type ), $taxonomy_types ); } $taxonomy_types = array_unique( $taxonomy_types ); $taxonomies = $wpdb->get_results( "SELECT taxonomy, term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy IN (" . wpml_prepare_in( $taxonomy_types ) . ')' ); if ( $taxonomies ) { foreach ( $taxonomies as $taxonomy ) { $this->add_missing_language_to_taxonomy( $taxonomy ); } } } /** * Add missing language to a given taxonomy * * @param OBJECT $taxonomy */ private function add_missing_language_to_taxonomy( $taxonomy ) { global $sitepress, $wpdb; $tid_prepared = $wpdb->prepare( "SELECT translation_id FROM {$wpdb->prefix}icl_translations WHERE element_type=%s AND element_id=%d", 'tax_' . $taxonomy->taxonomy, $taxonomy->term_taxonomy_id ); $tid = $wpdb->get_var( $tid_prepared ); if ( ! $tid ) { $sitepress->set_element_language_details( $taxonomy->term_taxonomy_id, 'tax_' . $taxonomy->taxonomy, null, $sitepress->get_default_language(), null, true, true ); } } /** * Add missing language information to entities that don't have this * information configured. */ public function add_missing_language_information() { global $sitepress; $translatable_documents = array_keys( $sitepress->get_translatable_documents( false ) ); if ( $translatable_documents ) { $this->add_missing_language_to_posts( $translatable_documents ); $this->add_missing_language_to_taxonomies( $translatable_documents ); } } public static function include_underscore_templates( $name ) { $dir_str = WPML_TM_PATH . '/res/js/' . $name . '/templates/'; $dir = opendir( $dir_str ); if ( ! $dir ) { return; } while ( ( $currentFile = readdir( $dir ) ) !== false ) { if ( $currentFile == '.' || $currentFile == '..' || $currentFile[0] == '.' ) { continue; } /** @noinspection PhpIncludeInspection */ include $dir_str . $currentFile; } closedir( $dir ); } public static function get_job_status_string( $status_id, $needs_update = false ) { $job_status_text = self::status2text( $status_id ); if ( $needs_update ) { $job_status_text .= __( ' - (needs update)', 'sitepress' ); } return $job_status_text; } function display_basket_notification( $position ) { if ( class_exists( 'ICL_AdminNotifier' ) && class_exists( 'TranslationProxy_Basket' ) ) { $positions = TranslationProxy_Basket::get_basket_notification_positions(); if ( isset( $positions[ $position ] ) ) { ICL_AdminNotifier::display_messages( 'translation-basket-notification' ); } } } public function get_element_type( $trid ) { global $wpdb; $element_type_query = "SELECT element_type FROM {$wpdb->prefix}icl_translations WHERE trid=%d LIMIT 0,1"; $element_type_prepare = $wpdb->prepare( $element_type_query, $trid ); return $wpdb->get_var( $element_type_prepare ); } /** * @param string $type * * @return bool */ public function is_external_type( $type ) { return apply_filters( 'wpml_is_external', false, $type ); } /** * @param int $post_id * @param string $element_type_prefix * * @return mixed|null|void|WP_Post */ public function get_post( $post_id, $element_type_prefix ) { $item = null; if ( $this->is_external_type( $element_type_prefix ) ) { $item = apply_filters( 'wpml_get_translatable_item', null, $post_id, $element_type_prefix ); } if ( ! $item ) { $item = get_post( $post_id ); } return $item; } private function init_comments_synchronization() { if ( wpml_get_setting_filter( null, 'sync_comments_on_duplicates' ) ) { add_action( 'delete_comment', array( $this, 'duplication_delete_comment' ) ); add_action( 'edit_comment', array( $this, 'duplication_edit_comment' ) ); add_action( 'wp_set_comment_status', array( $this, 'duplication_status_comment' ), 10, 2 ); add_action( 'wp_insert_comment', array( $this, 'duplication_insert_comment' ), 100 ); } } private function init_default_settings() { if ( ! isset( $this->settings[ $this->get_translation_setting_name( 'custom-fields' ) ] ) ) { $this->settings[ $this->get_translation_setting_name( 'custom-fields' ) ] = array(); } if ( ! isset( $this->settings[ $this->get_readonly_translation_setting_name( 'custom-fields' ) ] ) ) { $this->settings[ $this->get_readonly_translation_setting_name( 'custom-fields' ) ] = array(); } if ( ! isset( $this->settings[ $this->get_custom_translation_setting_name( 'custom-fields' ) ] ) ) { $this->settings[ $this->get_custom_translation_setting_name( 'custom-fields' ) ] = array(); } if ( ! isset( $this->settings[ $this->get_custom_readonly_translation_setting_name( 'custom-fields' ) ] ) ) { $this->settings[ $this->get_custom_readonly_translation_setting_name( 'custom-fields' ) ] = array(); } if ( ! isset( $this->settings['doc_translation_method'] ) ) { $this->settings['doc_translation_method'] = ICL_TM_TMETHOD_MANUAL; } } public function init_current_translator() { if ( did_action( 'init' ) ) { global $current_user; $current_translator = null; $user = false; if ( isset( $current_user->ID ) ) { $user = new WP_User( $current_user->ID ); } if ( $user && isset( $user->data ) && $user->data ) { $current_translator = new WPML_Translator(); $current_translator->ID = $current_user->ID; $current_translator->user_login = isset( $user->data->user_login ) ? $user->data->user_login : false; $current_translator->display_name = isset( $user->data->display_name ) ? $user->data->display_name : $current_translator->user_login; $current_translator = $this->init_translator_language_pairs( $current_user, $current_translator ); } $this->current_translator = $current_translator; } } public function get_translation_setting_name( $section ) { return $this->get_sanitized_translation_setting_section( $section ) . '_translation'; } public function get_custom_translation_setting_name( $section ) { return $this->get_translation_setting_name( $section ) . '_custom'; } public function get_custom_readonly_translation_setting_name( $section ) { return $this->get_custom_translation_setting_name( $section ) . '_readonly'; } public function get_readonly_translation_setting_name( $section ) { return $this->get_sanitized_translation_setting_section( $section ) . '_readonly_config'; } private function get_sanitized_translation_setting_section( $section ) { $section = preg_replace( '/-/', '_', $section ); return $section; } private function assign_translation_job( $job_id, $translator_id, $service = 'local', $type = 'post' ) { do_action( 'wpml_tm_assign_translation_job', $job_id, $translator_id, $service, $type ); return true; } /** * @param string $table * * @return string[] */ private function initial_translation_states( $table ) { global $wpdb; $custom_keys = $wpdb->get_col( "SELECT DISTINCT meta_key FROM {$table}" ); return $custom_keys; } /** * Save notification settings. * * @param array $data Request data */ public function icl_tm_save_notification_settings( $data ) { if ( wp_verify_nonce( $data['save_notification_settings_nonce'], 'save_notification_settings_nonce' ) ) { foreach ( array( 'new-job', 'include_xliff', 'resigned', 'completed', 'completed_frequency', 'overdue', 'overdue_offset', ) as $setting ) { if ( ! Obj::hasPath( [ 'notification', $setting ], $data ) ) { $data['notification'][ $setting ] = ICL_TM_NOTIFICATION_NONE; } } $this->settings['notification'] = $data['notification']; $this->save_settings(); $message = array( 'id' => 'icl_tm_message_save_notification_settings', 'type' => 'updated', 'text' => __( 'Preferences saved.', 'sitepress' ), ); ICL_AdminNotifier::add_message( $message ); do_action( 'wpml_tm_notification_settings_saved', $this->settings['notification'] ); } } /** * Cancel translation jobs. * * @param array $data Request data */ public function icl_tm_cancel_jobs( $data ) { $message = array( 'id' => 'icl_tm_message_cancel_jobs', 'type' => 'updated', ); if ( isset( $data['icl_translation_id'] ) ) { $this->cancel_translation_request( $data['icl_translation_id'] ); $message['text'] = __( 'Translation requests cancelled.', 'sitepress' ); } else { $message['text'] = __( 'No Translation requests selected.', 'sitepress' ); } ICL_AdminNotifier::add_message( $message ); } /** @return int */ public function get_init_priority() { return self::INIT_PRIORITY; } /** * @param array $translation_status_data * * @return mixed */ private function get_translation_prev_state( array $translation_status_data ) { $prevstate = array(); if ( ! empty( $translation_status_data ) ) { $keys = array( 'status', 'translator_id', 'needs_update', 'md5', 'translation_service', 'translation_package', 'timestamp', 'links_fixed', ); $prevstate = array_intersect_key( $translation_status_data, array_flip( $keys ) ); } return $prevstate; } private function is_unlocked_type( $type, $unlocked_options ) { return isset( $unlocked_options[ $type ] ) && $unlocked_options[ $type ]; } } translation-proxy/wpml-translationproxy-basket-networking.class.php 0000755 00000007563 14720425051 0022165 0 ustar 00 <?php /** * Class WPML_Translation_Proxy_Basket_Networking */ class WPML_Translation_Proxy_Basket_Networking { /** @var WPML_Translation_Basket $basket */ private $basket; /** @var TranslationManagement $tm_instance */ private $tm_instance; /** * @param WPML_Translation_Basket $basket * @param TranslationManagement $tm_instance */ function __construct( $basket, &$tm_instance ) { $this->basket = $basket; $this->tm_instance = $tm_instance; } /** * @param WPML_TM_Translation_Batch $batch * * @uses \WPML_Translation_Basket::get_basket Gets the array representation of the translation basket * @uses \WPML_Translation_Proxy_Basket_Networking::generate_batch generates the batch in case no chunk was given for the commit from the basket * @uses \WPML_Translation_Proxy_Basket_Networking::get_batch_name * @uses \WPML_Translation_Proxy_Basket_Networking::send_all_jobs * * @return array */ function commit_basket_chunk( WPML_TM_Translation_Batch $batch ) { $result = $this->send_all_jobs( $batch ); $error_messages = $this->tm_instance->messages_by_type( 'error' ); if ( ( $has_error = (bool) $error_messages ) === true ) { \WPML\TM\API\Batch::rollback( $batch->get_basket_name() ); $result = [ 'message' => '', 'additional_messages' => $error_messages, ]; } return array( $has_error, $result, $error_messages ); } /** * Checks if an array of translators has any remote translators in it. * * @param array $translators * * @return bool */ function contains_remote_translators( array $translators ) { return count( array_filter( $translators, 'is_numeric' ) ) < count( $translators ); } /** * Sends all jobs from basket in batch mode to translation proxy * * @param WPML_TM_Translation_Batch $batch * @param array $translators * @param array $batch_options * * @return bool false in case of errors (read from TranslationManagement::get_messages('error') to get errors details) */ private function send_all_jobs( WPML_TM_Translation_Batch $batch ) { $this->basket->set_options( $batch->get_batch_options() ); $this->basket->set_name( $batch->get_basket_name() ); $this->basket->set_remote_target_languages( $batch->get_remote_target_languages() ); $basket_items_types = $this->basket->get_item_types(); foreach ( $basket_items_types as $item_type_name => $item_type ) { do_action( 'wpml_tm_send_' . $item_type_name . '_jobs', $batch, $item_type_name, \WPML\TM\API\Jobs::SENT_VIA_BASKET ); } // check if there were no errors return ! $this->tm_instance->messages_by_type( 'error' ); } /** * Generates the batch array for posts in the basket. * * @param array $basket * * @return array */ private function generate_batch( array $basket ) { $batch = array(); $posts = isset( $basket['post'] ) ? $basket['post'] : array(); foreach ( $posts as $post_id => $post ) { $batch[] = array( 'type' => 'post', 'post_id' => $post_id, ); } return $batch; } /** * Returns the name of the batch that contains the given post_id. * * @param int $post_id * * @return null|string */ private function get_batch_name( $post_id ) { global $wpdb; $name = $wpdb->get_var( $wpdb->prepare( " SELECT b.batch_name FROM {$wpdb->prefix}icl_translation_batches b JOIN {$wpdb->prefix}icl_translation_status s ON s.batch_id = b.id JOIN {$wpdb->prefix}icl_translations t ON t.translation_id = s.translation_id JOIN {$wpdb->prefix}icl_translations o ON o.trid = t.trid AND o.language_code = t.source_language_code JOIN {$wpdb->posts} p ON o.element_id = p.ID AND o.element_type = CONCAT('post_', p.post_type) WHERE o.element_id = %d ORDER BY b.id LIMIT 1", $post_id ) ); $this->basket->set_name( $name ); return $name; } } translation-proxy/wpml-pro-translation.class.php 0000755 00000046323 14720425051 0016222 0 ustar 00 <?php /** * @package wpml-core * @package wpml-core-pro-translation */ use WPML\FP\Fns; use WPML\FP\Lst; use WPML\FP\Str; use function WPML\Container\make; use function WPML\FP\pipe; /** * Class WPML_Pro_Translation */ class WPML_Pro_Translation extends WPML_TM_Job_Factory_User { public $errors = array(); /** @var TranslationManagement $tmg */ private $tmg; /** @var WPML_TM_CMS_ID $cms_id_helper */ private $cms_id_helper; /** @var WPML_TM_Xliff_Reader_Factory $xliff_reader_factory */ private $xliff_reader_factory; private $sitepress; private $update_pm; /** * WPML_Pro_Translation constructor. * * @param WPML_Translation_Job_Factory $job_factory */ function __construct( &$job_factory ) { parent::__construct( $job_factory ); global $iclTranslationManagement, $wpdb, $sitepress, $wpml_post_translations, $wpml_term_translations; $this->tmg =& $iclTranslationManagement; $this->xliff_reader_factory = new WPML_TM_Xliff_Reader_Factory( $this->job_factory ); $wpml_tm_records = new WPML_TM_Records( $wpdb, $wpml_post_translations, $wpml_term_translations ); $this->cms_id_helper = new WPML_TM_CMS_ID( $wpml_tm_records, $job_factory ); $this->sitepress = $sitepress; add_filter( 'xmlrpc_methods', array( $this, 'custom_xmlrpc_methods' ) ); add_action( 'post_submitbox_start', array( $this, 'post_submitbox_start', ) ); add_action( 'icl_ajx_custom_call', array( $this, 'ajax_calls', ), 10, 2 ); add_action( 'wpml_minor_edit_for_gutenberg', array( $this, 'gutenberg_minor_edit' ), 10, 0 ); $this->update_pm = new WPML_Update_PickUp_Method( $this->sitepress ); } /** * @return WPML_TM_CMS_ID */ public function &get_cms_id_helper() { return $this->cms_id_helper; } /** * @param string $call * @param array $data */ function ajax_calls( $call, $data ) { switch ( $call ) { case 'set_pickup_mode': $response = $this->update_pm->update_pickup_method( $data, $this->get_current_project() ); if ( 'no-ts' === $response ) { wp_send_json_error( array( 'message' => __( 'Please activate translation service first.', 'wpml-translation-management' ) ) ); } if ( 'cant-update' === $response ) { wp_send_json_error( array( 'message' => __( 'Could not update the translation pickup mode.', 'wpml-translation-management' ) ) ); } wp_send_json_success( array( 'message' => __( 'Ok', 'wpml-translation-management' ) ) ); break; } } public function get_current_project() { return TranslationProxy::get_current_project(); } /** * @param WP_Post|WPML_Package $post * @param array $target_languages * @param int $translator_id * @param int $job_id * * @return bool|int */ function send_post( $post, $target_languages, $translator_id, $job_id ) { /** @var TranslationManagement $iclTranslationManagement */ global $sitepress, $iclTranslationManagement; $this->maybe_init_translation_management( $iclTranslationManagement ); if ( is_numeric( $post ) ) { $post = get_post( $post ); } if ( ! $post ) { return false; } $post_id = $post->ID; $post_type = $post->post_type; $element_type_prefix = $iclTranslationManagement->get_element_type_prefix_from_job_id( $job_id ); $element_type = $element_type_prefix . '_' . $post_type; $note = WPML_TM_Translator_Note::get( $post_id ); if ( ! $note ) { $note = null; } $err = false; $tp_job_id = false; $source_language = $sitepress->get_language_for_element( $post_id, $element_type ); $target_language = is_array( $target_languages ) ? end( $target_languages ) : $target_languages; if ( empty( $target_language ) || $target_language === $source_language ) { return false; } $translation = $this->tmg->get_element_translation( $post_id, $target_language, $element_type ); if ( ! $translation ) { // translated the first time $err = true; } if ( ! $err && ( $translation->needs_update || $translation->status == ICL_TM_NOT_TRANSLATED || $translation->status == ICL_TM_WAITING_FOR_TRANSLATOR ) ) { $project = TranslationProxy::get_current_project(); if ( $iclTranslationManagement->is_external_type( $element_type_prefix ) ) { $job_object = new WPML_External_Translation_Job( $job_id ); } else { $job_object = new WPML_Post_Translation_Job( $job_id ); $job_object->load_terms_from_post_into_job(); } list( $err, $project, $tp_job_id ) = $job_object->send_to_tp( $project, $translator_id, $this->cms_id_helper, $this->tmg, $note ); if ( $err ) { $this->enqueue_project_errors( $project ); } } return $err ? false : $tp_job_id; // last $ret } function server_languages_map( $language_name, $server2plugin = false ) { if ( is_array( $language_name ) ) { return array_map( array( $this, 'server_languages_map' ), $language_name ); } $map = array( 'Norwegian Bokmål' => 'Norwegian', 'Portuguese, Brazil' => 'Portuguese', 'Portuguese, Portugal' => 'Portugal Portuguese', ); $map = $server2plugin ? array_flip( $map ) : $map; return isset( $map[ $language_name ] ) ? $map[ $language_name ] : $language_name; } /** * @param $methods * * @return array */ public function custom_xmlrpc_methods( $methods ) { $icl_methods['translationproxy.test_xmlrpc'] = '__return_true'; $icl_methods['translationproxy.updated_job_status'] = array( $this, 'xmlrpc_updated_job_status', ); $methods = array_merge( $methods, $icl_methods ); if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST && preg_match( '#<methodName>([^<]+)</methodName>#i', $this->sitepress->get_wp_api()->get_raw_post_data(), $matches ) ) { $method = $matches[1]; if ( array_key_exists( $method, $icl_methods ) ) { set_error_handler( array( $this, 'translation_error_handler' ), E_ERROR | E_USER_ERROR ); } } return $methods; } /** * @param array $args * * @return int|IXR_Error */ public function xmlrpc_updated_job_status( $args ) { global $wpdb; $tp_id = isset( $args[0] ) ? $args[0] : 0; $cms_id = isset( $args[1] ) ? $args[1] : 0; $status = isset( $args[2] ) ? $args[2] : ''; $signature = isset( $args[3] ) ? $args[3] : ''; if ( ! $this->authenticate_request( $tp_id, $cms_id, $status, $signature ) ) { return new IXR_Error( 401, 'Wrong signature' ); } try { /** @var WPML_TM_Jobs_Repository $jobs_repository */ $jobs_repository = wpml_tm_get_jobs_repository(); $job_match = $jobs_repository->get( new WPML_TM_Jobs_Search_Params( array( 'scope' => 'remote', 'tp_id' => $tp_id, ) ) ); if ( $job_match ) { $jobs_array = $job_match->toArray(); $job = $jobs_array[0]; $job->set_status( WPML_TP_Job_States::map_tp_state_to_local( $status ) ); $tp_sync_updated_job = new WPML_TP_Sync_Update_Job( $wpdb, $this->sitepress ); $job_updated = $tp_sync_updated_job->update_state( $job ); if ( $job_updated && WPML_TP_Job_States::CANCELLED !== $status ) { $apply_tp_translation = new WPML_TP_Apply_Single_Job( wpml_tm_get_tp_translations_repository(), new WPML_TP_Apply_Translation_Strategies( $wpdb ) ); $apply_tp_translation->apply( $job ); } return 1; } } catch ( Exception $e ) { return new IXR_Error( $e->getCode(), $e->getMessage() ); } return 0; } /** * @return bool */ private function authenticate_request( $tp_id, $cms_id, $status, $signature ) { $project = TranslationProxy::get_current_project(); return sha1( $project->id . $project->access_key . $tp_id . $cms_id . $status ) === $signature; } /** * @return WPML_WP_API */ function get_wpml_wp_api() { return $this->sitepress->get_wp_api(); } /** * * Cancel translation for given cms_id * * @param $rid * @param $cms_id * * @return bool */ function cancel_translation( $rid, $cms_id ) { /** * @var WPML_String_Translation|null $WPML_String_Translation * @var TranslationManagement $iclTranslationManagement */ global $WPML_String_Translation, $iclTranslationManagement; $res = false; if ( empty( $cms_id ) ) { // it's a string if ( $WPML_String_Translation ) { $res = $WPML_String_Translation->cancel_remote_translation( $rid ); } } else { $translation_id = $this->cms_id_helper->get_translation_id( $cms_id ); if ( $translation_id ) { $iclTranslationManagement->cancel_translation_request( $translation_id ); $res = true; } } return $res; } /** * * Downloads translation from TP and updates its document * * @param $translation_proxy_job_id * @param $cms_id * * @return bool|string */ function download_and_process_translation( $translation_proxy_job_id, $cms_id ) { global $wpdb; if ( empty( $cms_id ) ) { // it's a string // TODO: [WPML 3.3] this should be handled as any other element type in 3.3 $target = $wpdb->get_var( $wpdb->prepare( "SELECT target FROM {$wpdb->prefix}icl_core_status WHERE rid=%d", $translation_proxy_job_id ) ); return $this->process_translated_string( $translation_proxy_job_id, $target ); } else { $translation_id = $this->cms_id_helper->get_translation_id( $cms_id, TranslationProxy::get_current_service() ); return ! empty( $translation_id ) && $this->add_translated_document( $translation_id, $translation_proxy_job_id ); } } /** * @param int $translation_id * @param int $translation_proxy_job_id * * @return bool */ function add_translated_document( $translation_id, $translation_proxy_job_id ) { global $wpdb, $sitepress; $project = TranslationProxy::get_current_project(); $translation_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}icl_translations WHERE translation_id=%d", $translation_id ) ); $translation = $project->fetch_translation( $translation_proxy_job_id ); if ( ! $translation ) { $this->errors = array_merge( $this->errors, $project->errors ); } else { $translation = apply_filters( 'icl_data_from_pro_translation', $translation ); } $ret = true; if ( ! empty( $translation ) && strpos( $translation, 'xliff' ) !== false ) { try { /** @var $job_xliff_translation WP_Error|array */ $job_xliff_translation = $this->xliff_reader_factory ->general_xliff_import()->import( $translation, $translation_id ); if ( is_wp_error( $job_xliff_translation ) ) { $this->add_error( $job_xliff_translation->get_error_message() ); return false; } kses_remove_filters(); wpml_tm_save_data( $job_xliff_translation ); kses_init(); $translations = $sitepress->get_element_translations( $translation_info->trid, $translation_info->element_type, false, true, true ); if ( isset( $translations[ $translation_info->language_code ] ) ) { $translation = $translations[ $translation_info->language_code ]; if ( isset( $translation->element_id ) && $translation->element_id ) { $translation_post_type_prepared = $wpdb->prepare( "SELECT post_type FROM $wpdb->posts WHERE ID=%d", array( $translation->element_id ) ); $translation_post_type = $wpdb->get_var( $translation_post_type_prepared ); } else { $translation_post_type = implode( '_', array_slice( explode( '_', $translation_info->element_type ), 1 ) ); } if ( $translation_post_type == 'page' ) { $url = get_option( 'home' ) . '?page_id=' . $translation->element_id; } else { $url = get_option( 'home' ) . '?p=' . $translation->element_id; } $project->update_job( $translation_proxy_job_id, $url ); } else { $project->update_job( $translation_proxy_job_id ); } } catch ( Exception $e ) { $ret = false; } } return $ret; } private static function content_get_link_paths( $body ) { $regexp_links = array( "/<a[^>]*href\s*=\s*([\"\']??)([^\"^>]+)[\"\']??([^>]*)>/i", ); $links = array(); foreach ( $regexp_links as $regexp ) { if ( preg_match_all( $regexp, is_null( $body ) ? '' : $body, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $links[] = $match; } } } return $links; } public function fix_links_to_translated_content( $element_id, $target_lang_code, $element_type = 'post', $preLoaded = [] ) { global $wpdb, $sitepress; $sitepress->switch_lang( $target_lang_code ); $wpml_element_type = $element_type; $body = ''; $string_type = null; if ( strpos( $element_type, 'post' ) === 0 ) { $post_prepared = $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID=%d", array( $element_id ) ); $post = $wpdb->get_row( $post_prepared ); $body = $post->post_content; $wpml_element_type = 'post_' . $post->post_type; } elseif ( $element_type == 'string' ) { if ( is_array( $preLoaded ) && array_key_exists( 'value', $preLoaded ) && array_key_exists( 'string_id', $preLoaded ) ) { $body = $preLoaded['value']; $original_string_id = $preLoaded['string_id']; } else { $data = $wpdb->get_row( $wpdb->prepare( "SELECT string_id, value FROM {$wpdb->prefix}icl_string_translations WHERE id=%d", array( $element_id ) ) ); $body = $data->value; $original_string_id = $data->string_id; } $string_type = $wpdb->get_var( $wpdb->prepare( "SELECT type FROM {$wpdb->prefix}icl_strings WHERE id=%d", $original_string_id ) ); if ( 'LINK' === $string_type ) { $body = '<a href="' . $body . '">removeit</a>'; } } $translate_link_targets = make( 'WPML_Translate_Link_Targets' ); $absolute_links = make( 'AbsoluteLinks' ); $getTranslatedLink = function ( $link ) use ( $translate_link_targets, $absolute_links, $element_type, $target_lang_code ) { if ( $absolute_links->is_home( $link[2] ) ) { $translatedLink = $absolute_links->convert_url( $link[2], $target_lang_code ); $translatedLink = Str::replace( $link[2], $translatedLink, $link[0] ); } else { add_filter( 'wpml_force_translated_permalink', '__return_true' ); $translatedLink = $translate_link_targets->convert_text( $link[0] ); remove_filter( 'wpml_force_translated_permalink', '__return_true' ); if ( self::should_links_be_converted_back_to_sticky( $element_type ) ) { $translatedLink = $absolute_links->convert_text( $translatedLink ); } } return $translatedLink !== $link[0] ? [ 'from' => $link[0], 'to' => $translatedLink, ] : null; }; $getTranslatedLinks = pipe( Fns::map( $getTranslatedLink ), Fns::filter( Fns::identity() ) ); $links = self::content_get_link_paths( $body ); $translatedLinks = $getTranslatedLinks( $links ); $replaceLink = function ( $body, $link ) { return str_replace( $link['from'], $link['to'], $body ); }; $new_body = Fns::reduce( $replaceLink, $body, $translatedLinks ); if ( $new_body != $body ) { if ( strpos( $element_type, 'post' ) === 0 ) { $wpdb->update( $wpdb->posts, array( 'post_content' => $new_body ), array( 'ID' => $element_id ) ); } elseif ( $element_type == 'string' ) { if ( 'LINK' === $string_type ) { $new_body = str_replace( array( '<a href="', '">removeit</a>' ), array( '', '' ), $new_body ); $wpdb->update( $wpdb->prefix . 'icl_string_translations', array( 'value' => $new_body, 'status' => ICL_TM_COMPLETE, ), array( 'id' => $element_id ) ); do_action( 'icl_st_add_string_translation', $element_id ); } else { $wpdb->update( $wpdb->prefix . 'icl_string_translations', array( 'value' => $new_body ), array( 'id' => $element_id ) ); } } } $links_fixed_status_factory = new WPML_Links_Fixed_Status_Factory( $wpdb, new WPML_WP_API() ); $links_fixed_status = $links_fixed_status_factory->create( $element_id, $wpml_element_type ); $links_fixed_status->set( Lst::length( $links ) === Lst::length( $translatedLinks ) ); $sitepress->switch_lang(); return sizeof( $translatedLinks ); } function translation_error_handler( $error_number, $error_string, $error_file, $error_line ) { switch ( $error_number ) { case E_ERROR: case E_USER_ERROR: throw new Exception( $error_string . ' [code:e' . $error_number . '] in ' . $error_file . ':' . $error_line ); case E_WARNING: case E_USER_WARNING: return true; default: return true; } } private static function should_links_be_converted_back_to_sticky( $element_type ) { return 'string' !== $element_type && ! empty( $GLOBALS['WPML_Sticky_Links'] ); } function post_submitbox_start() { $show_box_style = $this->get_show_minor_edit_style(); if ( false !== $show_box_style ) { ?> <p id="icl_minor_change_box" style="float:left;padding:0;margin:3px;<?php echo $show_box_style; ?>"> <label><input type="checkbox" name="icl_minor_edit" value="1" style="min-width:15px;"/> <?php esc_html_e( 'Minor edit - don\'t update translation', 'wpml-translation-management' ); ?> </label> <br clear="all"/> </p> <?php } } public function gutenberg_minor_edit() { $show_box_style = $this->get_show_minor_edit_style(); if ( false !== $show_box_style ) { ?> <div id="icl_minor_change_box" style="<?php echo $show_box_style; ?>" class="icl_box_paragraph"> <p> <strong><?php esc_html_e( 'Minor edit', 'wpml-translation-management' ); ?></strong> </p> <label><input type="checkbox" name="icl_minor_edit" value="1" style="min-width:15px;"/> <?php esc_html_e( "Don't update translation", 'wpml-translation-management' ); ?> </label> </div> <?php } } private function get_show_minor_edit_style() { global $post, $iclTranslationManagement; if ( empty( $post ) || ! $post->ID ) { return false; } $translations = $iclTranslationManagement->get_element_translations( $post->ID, 'post_' . $post->post_type ); $show_box_style = 'display:none'; foreach ( $translations as $t ) { if ( $t->element_id == $post->ID ) { return false; } if ( $t->status == ICL_TM_COMPLETE && ! $t->needs_update ) { $show_box_style = ''; break; } } return $show_box_style; } private function process_translated_string( $translation_proxy_job_id, $language ) { $project = TranslationProxy::get_current_project(); $translation = $project->fetch_translation( $translation_proxy_job_id ); $translation = apply_filters( 'icl_data_from_pro_translation', $translation ); $ret = false; $translation = $this->xliff_reader_factory->string_xliff_reader()->get_data( $translation ); if ( $translation ) { $ret = icl_translation_add_string_translation( $translation_proxy_job_id, $translation, $language ); if ( $ret ) { $project->update_job( $translation_proxy_job_id ); } } return $ret; } private function add_error( $project_error ) { $this->errors[] = $project_error; } /** * @param $project TranslationProxy_Project */ function enqueue_project_errors( $project ) { if ( isset( $project ) && isset( $project->errors ) && $project->errors ) { foreach ( $project->errors as $project_error ) { $this->add_error( $project_error ); } } } /** * @param TranslationManagement $iclTranslationManagement */ private function maybe_init_translation_management( $iclTranslationManagement ) { if ( empty( $this->tmg->settings ) ) { $iclTranslationManagement->init(); } } } translation-proxy/wpml-translation-basket.class.php 0000755 00000017502 14720425051 0016670 0 ustar 00 <?php use WPML\LIB\WP\Cache; use \WPML\Collect\Support\Traits\Macroable; use WPML\FP\Logic; use WPML\FP\Str; use function \WPML\FP\System\sanitizeString; use function \WPML\FP\pipe; use function \WPML\FP\curryN; /** * @method static int get_batch_id_from_name( string $basket_name ) */ class WPML_Translation_Basket { use Macroable; /** @var wpdb $wpdb */ private $wpdb; public function __construct( \wpdb $wpdb ) { $this->wpdb = $wpdb; } /** * Returns an array representation of the current translation basket * * @param bool|false $force if true reloads the baskets contents from the database * * @return array */ function get_basket( $force = false ) { $basket = TranslationProxy_Basket::get_basket( $force ); $basket = $basket ? $basket : array(); return $basket; } /** * @return bool|TranslationProxy_Project */ public function get_project() { return TranslationProxy::get_current_project(); } function get_item_types() { return TranslationProxy_Basket::get_basket_items_types(); } /** * Returns a batch instance by basket- or batch-name * * @param string $basket_name * * @return WPML_Translation_Batch */ function get_basket_batch( $basket_name ) { return new WPML_Translation_Batch( $this->wpdb, self::get_batch_id_from_name( $basket_name ) ); } /** * Sets the remote target languages before committing the basket to a translation service. * * @param array $remote_languages */ function set_remote_target_languages( $remote_languages ) { TranslationProxy_Basket::set_remote_target_languages( $remote_languages ); } /** * Removes all items from the current translation basket. */ function delete_all_items() { TranslationProxy_Basket::delete_all_items_from_basket(); } /** * Returns the name of the current translation basket. * * @return bool|string */ function get_name() { return TranslationProxy_Basket::get_basket_name(); } function set_name( $basket_name ) { TranslationProxy_Basket::set_basket_name( $basket_name ); } function set_options( array $batch_options ) { TranslationProxy_Basket::set_options( $batch_options ); } /** @return array */ function get_options() { return TranslationProxy_Basket::get_options(); } /** * @param string $basket_name * @param int $basket_name_max_length * * @return array */ function check_basket_name( $basket_name, $basket_name_max_length ) { $result = array( 'modified' => false, 'valid' => true, 'message' => '', 'new_value' => '', ); $old_value = $basket_name; $basket_name = strip_tags( $basket_name ); if ( mb_strlen( $basket_name ) > $basket_name_max_length ) { $result['valid'] = true; $result['message'] = sprintf( __( 'The length of the batch name exceeds the maximum length of %s', 'wpml-translation-management' ), $basket_name_max_length ); $result['new_value'] = $this->get_unique_basket_name( $basket_name, $basket_name_max_length ); } elseif ( self::get_batch_id_from_name( $basket_name ) ) { $result['valid'] = true; $result['new_value'] = $this->get_unique_basket_name( $basket_name, $basket_name_max_length ); $result['message'] = __( 'This batch name already exists and was modified to ensure unique naming', 'wpml-translation-management' ); } elseif ( count( $basket_name_array = explode( '|', $basket_name ) ) === 1 ) { $result['valid'] = true; $result['new_value'] = $this->get_unique_basket_name( $basket_name, $basket_name_max_length ); $result['message'] = __( 'The batch name was appended with the source language of its elements.', 'wpml-translation-management' ); } $result['modified'] = $result['new_value'] !== '' && $result['new_value'] !== $old_value; $result['message'] = $result['modified'] || ! $result['valid'] ? $result['message'] : ''; return $result; } /** * Returns a unique name derived from an input name for a Translation Proxy Basket * * @param string $name * @param bool|int $max_length * * @return bool|string */ function get_unique_basket_name( $name, $max_length ) { $basket_name_array = explode( '|', $name ); $name = count( $basket_name_array ) === 1 || ( ! is_numeric( $basket_name_array[ count( $basket_name_array ) - 1 ] ) && $basket_name_array[ count( $basket_name_array ) - 1 ] !== $this->get_source_language() ) || ( is_numeric( $basket_name_array[ count( $basket_name_array ) - 1 ] ) && $basket_name_array[ count( $basket_name_array ) - 2 ] !== $this->get_source_language() ) ? $name . '|' . $this->get_source_language() : $name; $name = mb_strlen( $name ) > $max_length ? $this->sanitize_basket_name( $name, $max_length ) : $name; if ( self::get_batch_id_from_name( $name ) ) { $suffix = 2; $name = $this->sanitize_basket_name( $name, $max_length - mb_strlen( (string) $suffix ) - 1 ); while ( self::get_batch_id_from_name( $name . '|' . $suffix ) ) { $suffix ++; $name = $this->sanitize_basket_name( $name, $max_length - mb_strlen( (string) $suffix ) - 1 ); } $name .= '|' . $suffix; } return $name; } /** * @return string */ public function get_source_language() { return TranslationProxy_Basket::get_source_language(); } /** * @param int $package_id */ public function remove_package( $package_id ) { TranslationProxy_Basket::delete_item_from_basket( $package_id, 'package' ); } /** * @param int $id * @param string $kind */ public function remove_item( $id, $kind ) { TranslationProxy_Basket::delete_item_from_basket( $id, $kind ); } /** * Merge the basket portion with the saved basket * * @param array $basket_portion */ public function update_basket( $basket_portion = array() ) { TranslationProxy_Basket::update_basket( $basket_portion ); } private function sanitize_basket_name( $basket_name, $max_length ) { // input basket name is separated by pipes so we explode it $to_trim = mb_strlen( $basket_name ) - $max_length; if ( $to_trim <= 0 ) { return $basket_name; } $basket_name_array = explode( '|', $basket_name ); $wpml_flag = count( $basket_name_array ) < 3; if ( $wpml_flag === false && count( $basket_name_array ) < 2 ) { return mb_substr( $basket_name, $max_length - 1 ); } // first we trim the middle part holding the "WPML" if ( $wpml_flag ) { list( $basket_name_array, $to_trim ) = $this->shorten_basket_name( $basket_name_array, 1, $to_trim ); } // then trim the site name first, if that's not enough move the array index and also trim the language for ( $i = 0; $i <= 1; $i ++ ) { if ( $to_trim > 0 ) { list( $basket_name_array, $to_trim ) = $this->shorten_basket_name( $basket_name_array, 0, $to_trim ); $basket_name_array = array_filter( $basket_name_array ); } else { break; } } $basket_name_array = array_filter( $basket_name_array ); return implode( '|', $basket_name_array ); } private function shorten_basket_name( $name_array, $index, $to_trim ) { if ( mb_strlen( $name_array [ $index ] ) > $to_trim ) { $name_array[ $index ] = mb_substr( $name_array[ $index ], 0, mb_strlen( $name_array [ $index ] ) - $to_trim - 1 ); $name_array = array_filter( $name_array ); $to_trim = 0; } else { $to_trim = $to_trim - mb_strlen( $name_array [ $index ] ) - 1; // subtract one here since we lose a downstroke unset( $name_array [ $index ] ); } return array( $name_array, $to_trim ); } } /** * Returns the batch id for a given basket or batch name * * @param string $basket_name * * @return int|bool */ WPML_Translation_Basket::macro( 'get_batch_id_from_name', curryN( 1, Cache::memorizeWithCheck( 'get_batch_id_from_name', Logic::isNotEmpty(), 0, pipe( Str::replace( '"', '\\"' ), sanitizeString(), TranslationProxy_Batch::getBatchId() ) ) ) ); translation-proxy/translationproxy-translator.class.php 0000755 00000020021 14720425051 0017723 0 ustar 00 <?php class TranslationProxy_Translator { /** * Get information about translators from current project. Works only for ICL as a Translation Service * * @param bool $force * * @return array|bool */ public static function get_icl_translator_status( $force = false ) { /** @var SitePress $sitepress */ /** @var WPML_Pro_Translation $ICL_Pro_Translation */ global $sitepress, $ICL_Pro_Translation; if ( ! $ICL_Pro_Translation ) { $job_factory = wpml_tm_load_job_factory(); $ICL_Pro_Translation = new WPML_Pro_Translation( $job_factory ); } if ( ! TranslationProxy::translator_selection_available() ) { return array(); } $project = TranslationProxy::get_current_project(); if ( ! $project ) { return array(); } $cache_key = md5( serialize( $project ) ); $cache_group = 'get_icl_translator_status'; $found = false; $result = wp_cache_get( $cache_key, $cache_group, false, $found ); if ( $found ) { return $result; } $translator_status = array(); $translation_service = TranslationProxy::get_current_service(); if ( ! $translation_service instanceof TranslationProxy_Service ) { return []; } $website_details = self::get_website_details( new TranslationProxy_Project( $translation_service, 'xmlrpc', TranslationProxy::get_tp_client() ), $force ); if ( false === (bool) $website_details ) { return array(); } $language_pairs = array(); if ( isset( $website_details['translation_languages']['translation_language'] ) ) { $translation_languages = $website_details['translation_languages']['translation_language']; if ( ! isset( $translation_languages[0] ) ) { $buf = $translation_languages; $translation_languages = array( 0 => $buf ); } foreach ( $translation_languages as $lang ) { $translators = $_tr = array(); $max_rate = false; if ( isset( $lang['translators'], $lang['translators']['translator'] ) && ! empty( $lang['translators'] ) ) { if ( ! isset( $lang['translators']['translator'][0] ) ) { $_tr[0] = $lang['translators']['translator']; } else { $_tr = $lang['translators']['translator']; } foreach ( $_tr as $t ) { if ( false === $max_rate || $t['attr']['amount'] > $max_rate ) { $max_rate = $t['attr']['amount']; } $translators[] = array( 'id' => $t['attr']['id'], 'nickname' => $t['attr']['nickname'], 'contract_id' => $t['attr']['contract_id'], ); } } $language_pairs[] = array( 'from' => $sitepress->get_language_code( $ICL_Pro_Translation->server_languages_map( $lang['attr']['from_language_name'], true ) ), 'to' => $sitepress->get_language_code( $ICL_Pro_Translation->server_languages_map( $lang['attr']['to_language_name'], true ) ), 'have_translators' => $lang['attr']['have_translators'], 'available_translators' => $lang['attr']['available_translators'], 'applications' => $lang['attr']['applications'], 'contract_id' => $lang['attr']['contract_id'], 'id' => $lang['attr']['id'], 'translators' => $translators, 'max_rate' => $max_rate, ); } } $translator_status['icl_lang_status'] = $language_pairs; $translator_status['icl_support_ticket_id'] = null; wp_cache_set( $cache_key, $translator_status, $cache_group ); return $translator_status; } private static function get_popup_link( $matches ) { global $sitepress; return TranslationProxy_Popup::get_link( $matches[2] ); } /** * * Get information about language pairs (including translators). Works only for ICL as a Translation Service * * @return array */ public static function get_language_pairs() { global $sitepress; $icl_lang_status = $sitepress->get_setting( 'icl_lang_status', array() ); if ( ! empty( $icl_lang_status ) ) { $missing_translators = false; foreach ( $icl_lang_status as $lang ) { if ( empty( $lang['translators'] ) ) { $missing_translators = true; break; } } if ( ! $missing_translators ) { $icl_lang_sub_status = $icl_lang_status; } } if ( ! isset( $icl_lang_sub_status ) ) { $translator_status = self::get_icl_translator_status(); $icl_lang_sub_status = isset( $translator_status['icl_lang_status'] ) ? $translator_status['icl_lang_status'] : array(); } foreach ( $icl_lang_sub_status as $key => $status ) { if ( ! isset( $status['from'] ) ) { unset( $icl_lang_sub_status[ $key ] ); } } array_filter( $icl_lang_sub_status ); return $icl_lang_sub_status; } /** * Sends request to ICL to get website details (including language pairs) * * @param TranslationProxy_Project $project * @param bool $force * * @return array */ private static function get_website_details( $project, $force = false ) { require_once ICL_PLUGIN_PATH . '/inc/utilities/xml2array.php'; require_once ICL_PLUGIN_PATH . '/lib/icl_api.php'; $site_id = $project->ts_id; $access_key = $project->ts_access_key; $default = array(); if ( ! $site_id ) { return $default; } $icl_query = new ICanLocalizeQuery( $site_id, $access_key ); return $icl_query->get_website_details( $force ); } /** * @param $translator_id * * @return string|false */ public static function get_translator_name( $translator_id ) { if ( TranslationProxy::translator_selection_available() ) { $lang_status = self::get_language_pairs(); if ( $lang_status ) { foreach ( $lang_status as $lp ) { $lp_trans = ! empty( $lp['translators'] ) ? $lp['translators'] : array(); foreach ( $lp_trans as $tr ) { $translators[ $tr['id'] ] = $tr['nickname']; } } } } return isset( $translators[ $translator_id ] ) ? $translators[ $translator_id ] : false; } /** * Synchronizes language pairs with ICL * * @global object $sitepress * * @param $project * @param $language_pairs */ public static function update_language_pairs( $project, $language_pairs ) { /** @var WPML_Pro_Translation $ICL_Pro_Translation */ global $sitepress, $ICL_Pro_Translation; $params = array( 'site_id' => $project->ts_id, 'accesskey' => $project->ts_access_key, 'create_account' => 0, ); $lang_server = array(); foreach ( $sitepress->get_active_languages() as $lang ) { $lang_server[ $lang['code'] ] = $ICL_Pro_Translation->server_languages_map( $lang['english_name'] ); } // update account - add language pair $incr = 0; foreach ( $language_pairs as $k => $v ) { if ( ! array_key_exists( $k, $lang_server ) ) { unset( $language_pairs[ $k ] ); continue; } foreach ( $v as $k2 => $v2 ) { if ( ! array_key_exists( $k2, $lang_server ) ) { unset( $language_pairs[ $k ][ $k2 ] ); if ( (bool) $language_pairs[ $k ] === false ) { unset( $language_pairs[ $k ] ); } continue; } $incr ++; $params[ 'from_language' . $incr ] = $lang_server[ $k ]; $params[ 'to_language' . $incr ] = $lang_server[ $k2 ]; } } require_once ICL_PLUGIN_PATH . '/inc/utilities/xml2array.php'; require_once ICL_PLUGIN_PATH . '/lib/icl_api.php'; $icl_query = new ICanLocalizeQuery(); $icl_query->updateAccount( $params ); } public static function flush_website_details_cache() { delete_transient( WEBSITE_DETAILS_TRANSIENT_KEY ); } public static function flush_website_details_cache_action() { $nonce = array_key_exists( 'nonce', $_POST ) ? $_POST['nonce'] : null; $action = array_key_exists( 'action', $_POST ) ? $_POST['action'] : null; $nonce_is_valid = wp_verify_nonce( $nonce, $action ); if ( $nonce_is_valid ) { self::flush_website_details_cache(); $query_args = array( 'page' => urlencode( WPML_TM_FOLDER . '/menu/main.php' ), 'sm' => urlencode( 'translators' ), ); $link_url = add_query_arg( $query_args, get_admin_url( null, 'admin.php' ) ); wp_send_json_success( array( 'redirectTo' => $link_url ) ); } else { wp_send_json_error( 'Nonce is not valid.' ); } } } translation-proxy/translationproxy-api.class.php 0000755 00000007326 14720425051 0016320 0 ustar 00 <?php /** * @package wpml-core * @subpackage wpml-core */ if( class_exists( 'TranslationProxy_Api' ) ) { // Workaround for UnitTests. return; } class TranslationProxy_Api { const API_VERSION = 1.1; public static function proxy_request( $path, $params = array(), $method = 'GET', $multi_part = false, $has_return_value = true ) { return wpml_tm_load_tp_networking()->send_request( OTG_TRANSLATION_PROXY_URL . $path, $params, $method, $has_return_value ); } public static function proxy_download( $path, $params ) { return wpml_tm_load_tp_networking()->send_request( OTG_TRANSLATION_PROXY_URL . $path, $params, 'GET', true, false ); } public static function service_request( $url, $params = array(), $method = 'GET', $has_return_value = true, $json_response = false, $has_api_response = false ) { return wpml_tm_load_tp_networking()->send_request( $url, $params, $method, $has_return_value, $json_response, $has_api_response ); } public static function add_parameters_to_url( $url, $params ) { if ( preg_match_all( '/\{.+?\}/', $url, $symbs ) ) { foreach ( $symbs[0] as $symb ) { $without_braces = preg_replace( '/\{|\}/', '', $symb ); if ( preg_match_all( '/\w+/', $without_braces, $indexes ) ) { foreach ( $indexes[0] as $index ) { if ( isset( $params[ $index ] ) ) { $value = $params[ $index ]; $url = preg_replace( preg_quote( "/$symb/" ), $value, $url ); } } } } } return $url; } } if ( ! function_exists( 'gzdecode' ) ) { /** * Inflates a string enriched with gzip headers. Counterpart to gzencode(). * Extracted from upgradephp * http://include-once.org/p/upgradephp/ * * officially available by default in php @since 5.4. */ function gzdecode( $gzdata, $maxlen = null ) { // -- decode header $len = strlen( $gzdata ); if ( $len < 20 ) { return; } $head = substr( $gzdata, 0, 10 ); $head = unpack( 'n1id/C1cm/C1flg/V1mtime/C1xfl/C1os', $head ); if( ! $head ) { return; } list( $ID, $CM, $FLG, $MTIME, $XFL, $OS ) = array_values( $head ); $FTEXT = 1 << 0; $FHCRC = 1 << 1; $FEXTRA = 1 << 2; $FNAME = 1 << 3; $FCOMMENT = 1 << 4; $head = unpack( 'V1crc/V1isize', substr( $gzdata, $len - 8, 8 ) ); if ( ! $head ) { return; } list( $CRC32, $ISIZE ) = array_values( $head ); // -- check gzip stream identifier if ( $ID != 0x1f8b ) { trigger_error( 'gzdecode: not in gzip format', E_USER_WARNING ); return; } // -- check for deflate algorithm if ( $CM != 8 ) { trigger_error( 'gzdecode: cannot decode anything but deflated streams', E_USER_WARNING ); return; } // -- start of data, skip bonus fields $s = 10; if ( $FLG & $FEXTRA ) { $s += $XFL; } if ( $FLG & $FNAME ) { $s = strpos( $gzdata, "\000", $s ) + 1; } if ( $FLG & $FCOMMENT ) { $s = strpos( $gzdata, "\000", $s ) + 1; } if ( $FLG & $FHCRC ) { $s += 2; // cannot check } // -- get data, uncompress $gzdata = substr( $gzdata, $s, $len - $s ); if ( $maxlen ) { $gzdata = gzinflate( $gzdata, $maxlen ); return ( $gzdata ); // no checks(?!) } else { $gzdata = gzinflate( $gzdata ); } // -- check+fin $chk = crc32( (string) $gzdata ); if ( $CRC32 != $chk ) { trigger_error( "gzdecode: checksum failed (real$chk != comp$CRC32)", E_USER_WARNING ); } elseif ( $ISIZE != strlen( (string) $gzdata ) ) { trigger_error( 'gzdecode: stream size mismatch', E_USER_WARNING ); } else { return ( $gzdata ); } } } translation-proxy/functions.php 0000755 00000001575 14720425051 0013015 0 ustar 00 <?php function translation_service_details( $service, $show_project = false ) { $service_details = ''; if ( defined( 'OTG_SANDBOX_DEBUG' ) && OTG_SANDBOX_DEBUG ) { $service_details .= '<h3>Service details:</h3>' . PHP_EOL; $service_details .= '<pre>' . PHP_EOL; $service_details .= print_r( $service, true ); $service_details .= '</pre>' . PHP_EOL; if ( $show_project ) { $project = TranslationProxy::get_current_project(); echo '<pre>$project' . PHP_EOL; echo print_r( $project, true ); echo '</pre>'; } } return $service_details; } if ( ! function_exists( 'object_to_array' ) ) { function object_to_array( $obj ) { if ( is_object( $obj ) ) { $obj = (array) $obj; } if ( is_array( $obj ) ) { $new = array(); foreach ( $obj as $key => $val ) { $new[ $key ] = object_to_array( $val ); } } else { $new = $obj; } return $new; } } translation-proxy/translationproxy.class.php 0000755 00000046617 14720425051 0015557 0 ustar 00 <?php /** * @package wpml-core * @subpackage wpml-core */ use WPML\TM\TranslationProxy\Services\AuthorizationFactory; use WPML\FP\Obj; require_once WPML_TM_PATH . '/inc/translation-proxy/functions.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-basket.class.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-api.class.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-project.class.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-service.class.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-popup.class.php'; require_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-translator.class.php'; define( 'CUSTOM_TEXT_MAX_LENGTH', 1000 ); class TranslationProxy { private static $tp_client; /** * @param bool $reload * * @return WPML_TP_Service[] */ public static function services( $reload = true ) { return self::get_tp_client()->services()->get_all( $reload ); } public static function get_tp_default_suid() { if ( defined( 'WPML_TP_DEFAULT_SUID' ) ) { return WPML_TP_DEFAULT_SUID; } return self::get_preferred_translation_service() ?: false; } /** * @param string $suid * @return 'wpml_list'|'config'|'account' */ public static function get_service_linked_by_suid( $suid ) { if ( defined( 'WPML_TP_DEFAULT_SUID' ) && WPML_TP_DEFAULT_SUID === $suid ) { return 'config'; } if ( self::get_preferred_translation_service() === $suid ) { return 'account'; } return 'wpml_list'; } public static function has_preferred_translation_service() { return self::get_tp_default_suid() !== false; } public static function clear_preferred_translation_service() { WP_Installer_API::set_preferred_ts( 'clear' ); } /** * @param int $service_id * * @return stdClass */ public static function get_service( $service_id ) { // @todo: implement usage of WPML_TP_Service for the active service return (object) (array) self::get_tp_client()->services()->get_service( $service_id, true ); } /** * @param int $service_id * * @return TranslationProxy_Service|WP_Error */ public static function select_service( $service_id, $credentials = null ) { global $sitepress; /** @var TranslationProxy_Service $service */ $service = self::get_service( $service_id ); if ( $service ) { self::deselect_active_service(); try { $service = self::build_and_store_active_translation_service( $service, $credentials ); $result = $service;// Force authentication if no user input is needed if ( ! self::service_requires_authentication( $service ) ) { try { ( new AuthorizationFactory() )->create()->authorize( new \stdClass() ); } catch ( \Exception $e ) { // Note that we do not unselect the service even though the authentication failed. // It is better to show it to user as selected and let him try to authenticate again. $result = new WP_Error( '1', sprintf( __( 'Authentication failed ( serviceId: %d )', 'sitepress' ), $service_id ) ); } } } catch ( \Exception $e ) { $result = new WP_Error( '2', sprintf( __( 'Failed to select translation service ( serviceId: %d )', 'sitepress' ), $service_id ) ); $sitepress->set_setting( 'translation_service', false, true ); } } else { $result = new WP_Error( '2', sprintf( __( 'Failed to select translation service ( serviceId: %d )', 'sitepress' ), $service_id ) ); $sitepress->set_setting( 'translation_service', false, true ); } $sitepress->save_settings(); return $result; } public static function deselect_active_service() { global $sitepress; $sitepress->set_setting( 'translation_service', false ); $sitepress->set_setting( 'translator_choice', false ); $sitepress->set_setting( 'icl_lang_status', false ); $sitepress->set_setting( 'icl_html_status', false ); $sitepress->set_setting( 'icl_current_session', false ); $sitepress->set_setting( 'last_icl_reminder_fetch', false ); $sitepress->set_setting( 'translators_management_info', false ); $sitepress->set_setting( 'language_pairs', false ); $sitepress->save_settings(); do_action( 'wpml_tp_service_dectivated', self::get_current_service() ); } /** * @param $service * @param bool $custom_fields_data * * @return mixed * @throws \WPMLTranslationProxyApiException */ public static function build_and_store_active_translation_service( $service, $custom_fields_data = false ) { global $sitepress; // set language map $service->languages_map = self::languages_map( $service ); // set information about custom fields $service->custom_fields = self::get_custom_fields( $service->id, true ); $service->custom_fields_data = $custom_fields_data; $service->last_refresh = time(); $sitepress->set_setting( 'translation_service', $service, true ); return $service; } /** * @return TranslationProxy_Project|false */ public static function get_current_project() { $translation_service = self::get_current_service(); if ( $translation_service && ! is_wp_error( $translation_service ) ) { return new TranslationProxy_Project( $translation_service, 'xmlrpc', self::get_tp_client() ); } return false; } public static function get_current_service_info( array $info = array() ) { global $sitepress; if ( ! $sitepress->get_setting( 'translation_service' ) ) { $sitepress->set_setting( 'translation_service', false, true ); } $service = self::get_current_service(); if ( $service ) { $service_info = array(); if ( icl_do_not_promote() ) { $service_info['name'] = __( 'Translation Service', 'wpml-translation-management' ); $service_info['logo'] = false; $service_info['header'] = __( 'Translation Service', 'wpml-translation-management' ); $service_info['description'] = false; $service_info['contact_url'] = false; } else { $service_info['name'] = $service->name; $service_info['logo'] = $service->logo_url; $service_info['header'] = $service->name; $service_info['description'] = $service->description; $service_info['contact_url'] = $service->url; } $service_info['setup_url'] = TranslationProxy_Popup::get_link( '@select-translators;from_replace;to_replace@', array( 'ar' => 1 ), true ); $service_info['has_quote'] = $service->quote_iframe_url !== ''; $service_info['has_translator_selection'] = $service->has_translator_selection; $info[ $service->id ] = $service_info; } return $info; } public static function get_service_promo() { global $sitepress; if ( icl_do_not_promote() ) { return ''; } $cache_key = 'get_service_promo'; $cache_found = false; $output = wp_cache_get( $cache_key, '', false, $cache_found ); if ( $cache_found ) { return $output; } $icl_translation_services = apply_filters( 'icl_translation_services', array() ); $icl_translation_services = array_merge( $icl_translation_services, self::get_current_service_info() ); $output = ''; if ( ! empty( $icl_translation_services ) ) { $sitepress_settings = $sitepress->get_settings(); $icl_dashboard_settings = isset( $sitepress_settings['dashboard'] ) ? $sitepress_settings['dashboard'] : array(); if ( empty( $icl_dashboard_settings['hide_icl_promo'] ) ) { $exp_hidden = ''; $col_hidden = ' hidden'; } else { $exp_hidden = ' hidden'; $col_hidden = ''; } $output .= '<div class="icl-translation-services' . $exp_hidden . '">'; foreach ( $icl_translation_services as $service ) { $output .= '<div class="icl-translation-services-inner">'; $output .= '<p class="icl-translation-services-logo"><span><img src="' . $service['logo'] . '" alt="' . $service['name'] . '" /></span></p>'; $output .= '<h3 class="icl-translation-services-header"> ' . $service['header'] . '</h3>'; $output .= '<div class="icl-translation-desc"> ' . $service['description'] . '</div>'; $output .= '</div>'; $output .= '<p class="icl-translation-links">'; $output .= '<a class="icl-mail-ico" href="' . $service['contact_url'] . '" target="_blank">' . __( 'Contact', 'wpml-translation-management' ) . " {$service['name']}</a>"; $output .= '<a id="icl_hide_promo" href="#">' . __( 'Hide this', 'wpml-translation-management' ) . '</a>'; $output .= '</p>'; } $output .= '</div>'; $output .= '<a class="' . $col_hidden . '" id="icl_show_promo" href="#">' . __( 'Need translators?', 'wpml-translation-management' ) . '</a>'; } wp_cache_set( $cache_key, $output ); return $output; } public static function get_service_dashboard_info() { global $sitepress; return self::get_custom_html( 'dashboard', $sitepress->get_current_language(), array( 'TranslationProxy_Popup', 'get_link', ) ); } public static function get_service_translators_info() { global $sitepress; return self::get_custom_html( 'translators', $sitepress->get_current_language(), array( 'TranslationProxy_Popup', 'get_link', ) ); } /** * @param string $location * @param string $locale * @param callable $popup_link_callback * @param int $max_count * @param bool $paragraph * * @return string */ public static function get_custom_html( $location, $locale, $popup_link_callback, $max_count = 1000, $paragraph = true ) { /** @var $project TranslationProxy_Project */ $project = self::get_current_project(); if ( ! $project ) { return ''; } $cache_key = $project->id . ':' . md5( serialize( array( $location, $locale, serialize( $popup_link_callback ), $max_count, $paragraph, ) ) ); $cache_group = 'get_custom_html'; $cache_found = false; $output = wp_cache_get( $cache_key, $cache_group, false, $cache_found ); if ( $cache_found ) { return $output; } try { $text = $project->custom_text( $location, $locale ); } catch ( Exception $e ) { return 'Error getting custom text from Translation Service: ' . $e->getMessage(); } $count = 0; if ( $text ) { foreach ( $text as $string ) { $format_string = self::sanitize_custom_text( $string->format_string ); if ( $paragraph ) { $format = '<p>' . $format_string . '</p>'; } else { $format = '<div>' . $format_string . '</div>'; } $links = array(); /** @var array $string_links */ $string_links = $string->links; foreach ( $string_links as $link ) { $url = self::sanitize_custom_text( $link->url ); $text = self::sanitize_custom_text( $link->text ); if ( isset( $link->dismiss ) && (int) $link->dismiss === 1 ) { $links[] = '<a href="' . $url . '" class="wpml_tp_custom_dismiss_able">' . $text . '</a>'; } else { $links[] = call_user_func( $popup_link_callback, $url ) . $text . '</a>'; } } $output .= vsprintf( $format, $links ); $count ++; if ( $count >= $max_count ) { break; } } } return $output; } public static function get_current_service_name() { if ( icl_do_not_promote() ) { return __( 'Translation Service', 'wpml-translation-management' ); } $translation_service = self::get_current_service(); if ( $translation_service ) { return $translation_service->name; } return false; } public static function get_current_service_id() { $translation_service = self::get_current_service(); if ( $translation_service ) { return $translation_service->id; } return false; } public static function get_current_service_batch_name_max_length() { $translation_service = self::get_current_service(); if ( $translation_service && isset( $translation_service->batch_name_max_length ) && null !== $translation_service->batch_name_max_length ) { return $translation_service->batch_name_max_length; } return 40; } /** * @param bool|stdClass|TranslationProxy_Service|WP_Error $service * * @return bool * @throws \InvalidArgumentException * @throws \WPMLTranslationProxyApiException */ public static function service_requires_authentication( $service = false ) { if ( ! $service ) { $service = self::get_current_service(); } $custom_fields = false; if ( false !== (bool) $service ) { $custom_fields = self::get_custom_fields( $service->id ); } return $custom_fields && isset( $custom_fields->custom_fields ) && count( $custom_fields->custom_fields ) > 0; } /** * Return true if $service has been successfully authenticated * Services that do not require authentication are by default authenticated * * @param bool|WP_Error|TranslationProxy_Service $service * * @return bool * @throws \InvalidArgumentException */ public static function is_service_authenticated( $service = false ) { if ( ! $service ) { $service = self::get_current_service(); } if ( ! $service ) { return false; } if ( ! self::service_requires_authentication( $service ) ) { return true; } $has_custom_fields = self::has_custom_fields(); $custom_fields_data = self::get_custom_fields_data(); return $has_custom_fields && $custom_fields_data; } /** * @return stdClass|WP_Error|false */ public static function get_current_service() { /** @var SitePress $sitepress */ global $sitepress; /** @var TranslationProxy_Service $ts */ $ts = $sitepress->get_setting( 'translation_service' ); if ( is_array( $ts ) ) { return new WP_Error( 'translation-proxy-service-misconfiguration', 'translation_service is stored as array!', $ts ); } return $ts; } /** * * @return bool * @throws \InvalidArgumentException */ public static function is_current_service_active_and_authenticated() { $active_service = self::get_current_service(); return $active_service && TranslationProxy_Service::is_authenticated( $active_service ); } /** * @return mixed */ public static function get_translation_projects() { global $sitepress; return $sitepress->get_setting( 'icl_translation_projects', null ); } public static function get_service_name( $service_id = false ) { if ( $service_id ) { $name = false; $services = self::services( false ); foreach ( $services as $service ) { if ( $service->id === $service_id ) { $name = $service->name; } } } else { $name = self::get_current_service_name(); } return $name; } public static function has_custom_fields( $service_id = false ) { $custom_fields = self::get_custom_fields( $service_id ); if ( $custom_fields ) { return isset( $custom_fields->custom_fields ) && is_array( $custom_fields->custom_fields ) && count( $custom_fields->custom_fields ); } return false; } /** * @param int|bool $service_id If not given, will use the current service ID (if any) * @param bool $force_reload Force reload custom fields from Translation Service * * @throws WPMLTranslationProxyApiException * @throws InvalidArgumentException * @return array|mixed|null|string */ public static function get_custom_fields( $service_id = false, $force_reload = false ) { if ( ! $service_id ) { $service_id = self::get_current_service_id(); } if ( ! $service_id ) { return false; } $translation_service = self::get_current_service(); if ( $translation_service && ! $force_reload ) { return $translation_service->custom_fields ?: false; } return self::get_tp_client()->services()->get_custom_fields( $service_id ); } /** * @return array */ public static function get_extra_fields_local() { global $sitepress; $service = self::get_current_service(); $icl_translation_projects = $sitepress->get_setting( 'icl_translation_projects' ); if ( isset( $icl_translation_projects[ TranslationProxy_Project::generate_service_index( $service ) ]['extra_fields'] ) && ! empty( $icl_translation_projects[ TranslationProxy_Project::generate_service_index( $service ) ]['extra_fields'] ) ) { return $icl_translation_projects[ TranslationProxy_Project::generate_service_index( $service ) ]['extra_fields']; } return array(); } /** * @param $extra_fields */ public static function save_extra_fields( $extra_fields ) { global $sitepress; $service = self::get_current_service(); $icl_translation_projects = $sitepress->get_setting( 'icl_translation_projects' ); $icl_translation_project_id = TranslationProxy_Project::generate_service_index( $service ); if ( is_array( Obj::prop( $icl_translation_project_id, $icl_translation_projects ) ) ) { $icl_translation_projects[ $icl_translation_project_id ]['extra_fields'] = $extra_fields; $sitepress->set_setting( 'icl_translation_projects', $icl_translation_projects ); $sitepress->save_settings(); } } public static function maybe_convert_extra_fields( $extra_fields ) { $extra_fields_typed = array(); if ( $extra_fields && is_array( $extra_fields ) ) { /** @var array $extra_fields */ /** @var stdClass $extra_field */ foreach ( $extra_fields as $extra_field ) { if ( $extra_field instanceof WPML_TP_Extra_Field ) { $extra_field_typed = $extra_field; } else { $extra_field_typed = new WPML_TP_Extra_Field(); if ( isset( $extra_field->type ) ) { $extra_field_typed->type = $extra_field->type; } if ( isset( $extra_field->label ) ) { $extra_field_typed->label = $extra_field->label; } if ( isset( $extra_field->name ) ) { $extra_field_typed->name = $extra_field->name; } if ( isset( $extra_field->items ) ) { $extra_field_typed->items = $extra_field->items; } } $extra_fields_typed[] = $extra_field_typed; } } return $extra_fields_typed; } public static function get_custom_fields_data() { $service = self::get_current_service(); return null !== $service->custom_fields_data ? $service->custom_fields_data : false; } /** * @return bool true if the current translation service allows selection of specific translators * @throws \InvalidArgumentException */ public static function translator_selection_available() { $res = false; $translation_service = self::get_current_service(); if ( $translation_service && $translation_service->has_translator_selection && self::is_service_authenticated() ) { $res = true; } return $res; } private static function sanitize_custom_text( $text ) { $text = substr( $text, 0, CUSTOM_TEXT_MAX_LENGTH ); $text = esc_html( $text ); // Service sends html tags as [tag] $text = str_replace( array( '[', ']' ), array( '<', '>' ), $text ); return $text; } private static function languages_map( $service ) { $languages_map = array(); $languages = self::get_tp_client()->services()->get_languages_map( $service->id ); if ( ! empty( $languages ) ) { foreach ( $languages as $language ) { $languages_map[ $language->iso_code ] = $language->value; } } return $languages_map; } private static function get_preferred_translation_service() { $tp_default_suid = false; $preferred_translation_service_from_installer = self::get_preferred_translation_service_from_installer(); if ( 'clear' !== $preferred_translation_service_from_installer ) { $tp_default_suid = $preferred_translation_service_from_installer; } return $tp_default_suid; } private static function get_preferred_translation_service_from_installer() { return WP_Installer_API::get_preferred_ts(); } public static function get_tp_client() { if ( ! self::$tp_client ) { $tp_api_factory = new WPML_TP_Client_Factory(); self::$tp_client = $tp_api_factory->create(); } return self::$tp_client; } } translation-proxy/translationproxy-batch.class.php 0000755 00000006475 14720425051 0016634 0 ustar 00 <?php use \WPML\Collect\Support\Traits\Macroable; use function \WPML\FP\curryN; use \WPML\LIB\WP\Cache; use \WPML\FP\Logic; /** * Class TranslationProxy_Batch * * @method static callable|int getBatchId( ...$name ) :: string → int */ class TranslationProxy_Batch { use Macroable; public static function update_translation_batch( $batch_name = false, $tp_id = false ) { $batch_name = $batch_name ? $batch_name : ( ( (bool) $tp_id === false || $tp_id === 'local' ) ? self::get_generic_batch_name() : TranslationProxy_Basket::get_basket_name() ); if ( ! $batch_name ) { return null; } $getBatchId = function( $batch_name, $tp_id ) { $batch_id = self::getBatchId( $batch_name ); return $batch_id ?: self::createBatchRecord( $batch_name, $tp_id ); }; $cache = Cache::memorizeWithCheck( 'update_translation_batch', Logic::isNotNull(), 0, $getBatchId ); return $cache( $batch_name, $tp_id ); } /** * returns the name of a generic batch * name is built based on the current's date * * @param bool $isAuto * * @return string */ public static function get_generic_batch_name( $isAuto = false ) { if ( ! $isAuto && defined( 'WPML_DEBUG_TRANSLATION_PROXY' ) ) \WPML\Utilities\DebugLog::storeBackTrace(); return ( $isAuto ? 'Automatic Translations from ' : 'Manual Translations from ' ) . date( 'F \t\h\e jS\, Y' ); } /** * returns the id of a generic batch * * @return int */ private static function create_generic_batch() { $batch_name = self::get_generic_batch_name(); $batch_id = self::update_translation_batch( $batch_name ); return $batch_id; } public static function maybe_assign_generic_batch( $data ) { global $wpdb; $batch_id = $wpdb->get_var( $wpdb->prepare( "SELECT batch_id FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d", $data['translation_id'] ) ); // if the batch id is smaller than 1 we assign the translation to the generic manual translations batch for today if the translation_service is local if ( ( $batch_id < 1 ) && isset( $data ['translation_service'] ) && $data ['translation_service'] == 'local' ) { // first we retrieve the batch id for today's generic translation batch $batch_id = self::create_generic_batch(); // then we update the entry in the icl_translation_status table accordingly $data_where = array( 'rid' => $data['rid'] ); $wpdb->update( $wpdb->prefix . 'icl_translation_status', array( 'batch_id' => $batch_id ), $data_where ); } } /** * @param $batch_name * @param $tp_id * * @return mixed */ private static function createBatchRecord( $batch_name, $tp_id ) { global $wpdb; $data = [ 'batch_name' => $batch_name, 'last_update' => date( 'Y-m-d H:i:s' ), ]; if ( $tp_id ) { $data['tp_id'] = $tp_id === 'local' ? 0 : $tp_id; } $wpdb->insert( $wpdb->prefix . 'icl_translation_batches', $data ); return $wpdb->insert_id; } } /** * @param $batch_name * * @return mixed */ TranslationProxy_Batch::macro( 'getBatchId', curryN( 1, function( $batch_name ) { global $wpdb; $batch_id_sql = "SELECT id FROM {$wpdb->prefix}icl_translation_batches WHERE batch_name=%s"; $batch_id_prepared = $wpdb->prepare( $batch_id_sql, $batch_name ); return $wpdb->get_var( $batch_id_prepared ); } ) ); translation-proxy/translationproxy-popup.class.php 0000755 00000003436 14720425051 0016710 0 ustar 00 <?php class TranslationProxy_Popup { public static function display() { include_once WPML_TM_PATH . '/inc/translation-proxy/translationproxy-popup.php'; exit; } public static function get_link( $link, $args = array(), $just_url = false ) { $defaults = array( 'title' => null, 'class' => '', 'id' => '', 'ar' => 0, // auto_resize 'unload_cb' => false, // onunload callback ); $args = array_merge($defaults, $args); /** * @var title string */ $title = $args['title']; /** * @var $class string */ $class = $args['class']; /** * @var $id int */ $id = $args['id']; /** * @var $ar int */ $ar = $args['ar']; /** * @var $unload_cb bool */ $unload_cb = $args['unload_cb']; if ( !empty( $ar ) ) { $auto_resize = '&auto_resize=1'; } else { $auto_resize = ''; } $unload_cb = isset( $unload_cb ) ? '&unload_cb=' . $unload_cb : ''; $url_glue = false !== strpos( $link, '?' ) ? '&' : '?'; $link .= $url_glue . 'compact=1'; $nonce_snippet = '&_icl_nonce=' . wp_create_nonce( 'reminder_popup_nonce' ); $action_and_nonce = 'admin.php?page=' . ICL_PLUGIN_FOLDER . "/menu/languages.php&icl_action=reminder_popup{$nonce_snippet}{$auto_resize}{$unload_cb}" . "&target=" . urlencode( $link ); if ( ! empty( $id ) ) { $id = ' id="' . $id . '"'; } if ( ! $just_url ) { return '<a class="icl_thickbox ' . $class . '" title="' . $title . '" href="' . $action_and_nonce . '"' . $id . '>'; } else { if ( ! $just_url ) { return '<a class="icl_thickbox ' . $class . '" href="' . $action_and_nonce . '"' . $id . '>'; } else { return $action_and_nonce; } } } } translation-proxy/translationproxy-basket.class.php 0000755 00000067346 14720425051 0017030 0 ustar 00 <?php /** * @package wpml-core * @subpackage wpml-core */ use WPML\TM\API\Jobs; if ( ! class_exists( 'TranslationProxy_Basket' ) ) { /** * TranslationProxy_basket collects all static methods to operate on * translations basket (cart) */ class TranslationProxy_Basket { private static $messages; private static $dashboard_select; private static $basket; // The name of the option stored in wp_options table and that // stores all the basket items const ICL_TRANSLATION_JOBS_BASKET = 'icl_translation_jobs_basket'; private static $posts_ids; private static $translate_from; private static $translation_action; public static function add_message( $array ) { self::$messages[] = $array; } public static function remove_message( $text ) { if ( is_array( self::$messages ) ) { foreach ( self::$messages as $key => $message ) { if ( array_key_exists( 'text', $message ) && $message['text'] === $text ) { unset( self::$messages[ $key ] ); } } } } public static function get_basket( $force = false ) { if ( ! isset( self::$basket ) || $force ) { self::$basket = get_option( self::ICL_TRANSLATION_JOBS_BASKET, [] ); } return self::$basket; } public static function update_basket( $basket_portion = array() ) { if ( ! empty( $basket_portion ) ) { if ( ! self::$basket || self::get_basket_items_count() == 0 ) { self::$basket = $basket_portion; } else { self::$basket = self::merge_baskets( self::$basket, $basket_portion ); } } if ( self::get_basket_items_count( true ) == 0 ) { self::$basket = []; } self::sync_target_languages(); self::update_basket_option( self::$basket ); self::update_basket_notifications(); } /** * @param array $basket */ private static function update_basket_option( $basket ) { update_option( self::ICL_TRANSLATION_JOBS_BASKET, $basket, false ); } private static function merge_baskets( $from, $to ) { if ( function_exists( 'array_replace_recursive' ) ) { return array_replace_recursive( $from, $to ); } else { return self::array_replace_recursive( $from, $to ); } } /** * Return number of items in translation basket by key * * @param string $type * @param bool $skip_cache * * @return int number of items in translation basket */ public static function get_basket_items_type_count( $type, $skip_cache = false ) { $cache_key = $type; $cache_group = 'get_basket_items_type_count'; $cache_found = false; if ( ! $skip_cache ) { $basket_items_number = (int) wp_cache_get( $cache_key, $cache_group, false, $cache_found ); } else { $basket_items_number = 0; } if ( $cache_found ) { return $basket_items_number; } self::get_basket(); if ( self::$basket ) { if ( isset( self::$basket[ $type ] ) ) { $posts = self::$basket[ $type ]; $basket_items_number += count( $posts ); } } if ( ! $skip_cache ) { wp_cache_set( $cache_key, $basket_items_number, $cache_group ); } return $basket_items_number; } /** * Return number of items in translation basket * * @param bool $skip_cache * * @return int number of items in translation basket */ public static function get_basket_items_count( $skip_cache = false ) { $basket_items_number = 0; $basket_items_types = self::get_basket_items_types(); foreach ( $basket_items_types as $item_type_name => $item_type ) { $basket_items_number += self::get_basket_items_type_count( $item_type_name, $skip_cache ); } return $basket_items_number; } /** * Register notification with number of items in basket and link to basket */ public static function update_basket_notifications() { $positions = self::get_basket_notification_positions(); $basket_link = 'admin.php?page=' . WPML_TM_FOLDER . '/menu/main.php&sm=basket'; foreach ( $positions as $position => $group ) { ICL_AdminNotifier::remove_message_group( $position ); } self::get_basket(); $basket_items_count = self::get_basket_items_count( true ); $limit_to_page = array(); if ( defined( 'WPML_ST_FOLDER' ) ) { $limit_to_page[] = WPML_ST_FOLDER . '/menu/string-translation.php'; } // if we have something in the basket if ( self::is_st_page() && $basket_items_count > 0 && ( ! isset( $_GET['clear_basket'] ) || $_GET['clear_basket'] != 1 ) && ( ! isset( $_GET['action'] ) || $_GET['action'] != 'delete' ) ) { $text = __( 'The items you have selected are now in the translation basket –', 'wpml-translation-management' ); $text .= ' ' . sprintf( __( '<a href="%s">Send to translation »</a>', 'wpml-translation-management' ), $basket_link ); // translation management pages $message_args = array( 'id' => $positions['tm_dashboard_top'], 'text' => $text, 'classes' => 'small', 'type' => 'information small', 'group' => $positions['tm_dashboard_top'], 'admin_notice' => false, 'hide_per_user' => false, 'dismiss_per_user' => false, 'limit_to_page' => $limit_to_page, 'capability' => 'manage_translations', ); ICL_AdminNotifier::add_message( $message_args ); } else { ICL_AdminNotifier::remove_message( $positions['tm_dashboard_top'] ); } $admin_basket_message_id = $positions['admin_notice']; if ( ( self::$messages || $basket_items_count > 0 ) && self::is_st_page() ) { $additional_messages = array(); if ( isset( self::$messages ) && is_array( self::$messages ) ) { foreach ( self::$messages as $message ) { $additional_messages[] = $message['text']; } } $additional_messages_text = ''; if ( count( $additional_messages ) > 0 ) { $additional_messages_text = '<ul><li>' . implode( '</li><li>', $additional_messages ) . '</li></ul>'; } $message_args = array( 'id' => $admin_basket_message_id, 'text' => $additional_messages_text, 'classes' => 'small', 'type' => 'information', 'group' => $admin_basket_message_id, 'admin_notice' => true, 'hide_per_user' => false, 'dismiss_per_user' => false, 'limit_to_page' => $limit_to_page, 'show_once' => true, ); if ( trim( $additional_messages_text ) != '' ) { ICL_AdminNotifier::add_message( $message_args ); } } else { ICL_AdminNotifier::remove_message( $admin_basket_message_id ); } } private static function is_st_page() { return defined( 'WPML_ST_FOLDER' ) && array_key_exists( 'page', $_GET ) && false !== strpos( $_GET['page'], WPML_ST_FOLDER ); } /** * Displays div with number of items in basket and link to basket * Removes notification if basket is empty */ public static function display_basket_items_notification() { ICL_AdminNotifier::display_messages( 'translation-basket-notification' ); } public static function is_in_basket( $post_id, $source_language, $target_language, $item_type = 'post' ) { self::get_basket(); if ( ! self::$basket || ! isset( self::$basket[ $item_type ][ $post_id ] ) ) { return false; } $basket_item = self::$basket[ $item_type ][ $post_id ]; return $basket_item['from_lang'] == $source_language && isset( $basket_item['to_langs'][ $target_language ] ) && $basket_item['to_langs'][ $target_language ]; } /** * Checks if post with ID $post_id is in the basket for any language * * @param int $post_id * @param string $element_type * @param array $check_in_languages * @param bool $original_language_code * * @return bool */ public static function anywhere_in_basket( $post_id, $element_type = 'post', $check_in_languages = array(), $original_language_code = false ) { $basket = self::get_basket(); if ( $post_id && isset( $basket[ $element_type ][ $post_id ] ) ) { if ( $check_in_languages ) { if ( ! $original_language_code ) { $original_language_code = $basket['source_language']; } foreach ( $check_in_languages as $language_code => $language_data ) { if ( $language_code != $original_language_code && isset( $basket[ $element_type ][ $post_id ]['to_langs'][ $language_code ] ) ) { return true; } } return false; } else { return true; } } return false; } public static function is_string_in_basket_anywhere( $string_id ) { return self::anywhere_in_basket( $string_id, 'string' ); } public static function has_any_string() { return self::has_any_item_type( 'string' ); } public static function has_any_item_type( $item_type ) { self::get_basket(); return isset( self::$basket[ $item_type ] ) && count( self::$basket[ $item_type ] ); } /**** adding items to basket ****/ /** * Serves Translation Dashboard form submission and adds posts to basket * * @param array $data data submitted from form * * @return boolean */ public static function add_posts_to_basket( $data ) { self::get_basket(); global $sitepress; extract( $data, EXTR_OVERWRITE ); self::$translation_action = null; if ( isset( $data['tr_action'] ) ) { // adapt new format self::$translation_action = $data['tr_action']; } if ( ! isset( $data['tr_action'] ) && isset( $data['translate_to'] ) ) { // adapt new format $data['tr_action'] = $data['translate_to']; self::$translation_action = $data['tr_action']; unset( $data['translate_to'] ); } self::$posts_ids = self::get_elements_ids( $data, 'post' ); self::$translate_from = $data ['translate_from']; // language of the submitted posts transported by hidden field $data_is_valid = self::validate_data( $data ); if ( ! $data_is_valid ) { return false; } // check tr_action and do what user decided foreach ( self::$translation_action as $language_code => $status ) { $language_name = $sitepress->get_display_language_name( $language_code ); // if he decided duplicate or not to translate for this particular language, // try to remove it from wp_options $basket_item_type = 'post'; if ( $status == 2 ) { // iterate posts ids, check if they are in wp_options // if they are set to translate for this particular language // end then remove it foreach ( self::$posts_ids as $id ) { if ( isset( self::$basket[ $basket_item_type ][ $id ]['to_langs'][ $language_code ] ) ) { unset( self::$basket[ $basket_item_type ][ $id ]['to_langs'][ $language_code ] ); } } } elseif ( $status == 1 ) { foreach ( self::$posts_ids as $id ) { $send_to_basket = true; $post = self::get_post( $id ); $post_type = $post->post_type; $messages = new \WPML\TM\Jobs\Dispatch\Messages(); global $wpdb; $source_language_code = $wpdb->get_var( $wpdb->prepare( " SELECT source_language_code FROM {$wpdb->prefix}icl_translations WHERE element_type LIKE 'post_%%' AND element_id = %d", $post->ID ) ); if ( $source_language_code != $language_code ) { $job = Jobs::getPostJob( $id, $post_type, $language_code ); if ( $job && ICL_TM_IN_PROGRESS === $job->status && ! $job->needs_update ) { self::$messages[] = array( 'type' => 'update', 'text' => $messages->ignoreInProgressPostMessage( $post, $language_name ), ); $send_to_basket = false; } } else { self::$messages[] = array( 'type' => 'update', 'text' => $messages->ignoreOriginalPostMessage( $post, $language_name ), ); $send_to_basket = false; } if ( $send_to_basket ) { self::$basket[ $basket_item_type ][ $id ]['from_lang'] = self::$translate_from; self::$basket[ $basket_item_type ][ $id ]['to_langs'][ $language_code ] = 1; // set basket language if not already set if ( ! isset( self::$basket['source_language'] ) ) { self::$basket['source_language'] = self::$translate_from; } } } } } self::update_basket(); return true; } /** * Serves WPML > String translation form submission and adds strings to basket * * @param array $string_ids identifiers of strings * @param $source_language * @param array $target_languages selected target languages * @return bool * @todo: [WPML 3.3] move to ST and handle with hooks */ public static function add_strings_to_basket( $string_ids, $source_language, $target_languages ) { global $wpdb, $sitepress; self::get_basket(); /* structure of cart in get_option: * [posts] * [element_id] * [to_langs] * [language_code] fr | pl | de ... with value 1 * [strings] * [string_id] * [to_langs] * [language_code] */ // no post selected ? if ( empty( $string_ids ) ) { self::$messages[] = array( 'type' => 'error', 'text' => __( 'Please select at least one document to translate.', 'wpml-translation-management' ), ); self::update_basket(); return false; } // no language selected ? if ( empty( $target_languages ) ) { self::$messages[] = array( 'type' => 'error', 'text' => __( 'Please select at least one language to translate into.', 'wpml-translation-management' ), ); self::update_basket(); return false; } if ( self::get_basket() && self::get_source_language() ) { /* we do not add items that are not in the source language of the current basket we cannot yet set its source language though since update_basket would set the basket to false oso long as we do not have any elements in the basket*/ if ( $source_language != self::get_source_language() ) { self::$messages[] = array( 'type' => 'update', 'text' => __( 'You cannot add strings in this language to the basket since it already contains posts or strings of another source language! Either submit the current basket and then add the post or delete the posts of differing language in the current basket', 'wpml-translation-management' ), ); self::update_basket(); return false; } } foreach ( $target_languages as $target_language => $selected ) { if ( $target_language == $source_language ) { continue; } $target_language_name = $sitepress->get_display_language_name( $target_language ); foreach ( $string_ids as $id ) { $send_to_basket = true; $query = " SELECT {$wpdb->prefix}icl_string_translations.status, {$wpdb->prefix}icl_strings.value FROM {$wpdb->prefix}icl_string_translations INNER JOIN {$wpdb->prefix}icl_strings ON {$wpdb->prefix}icl_string_translations.string_id = {$wpdb->prefix}icl_strings.id WHERE {$wpdb->prefix}icl_string_translations.string_id=%d AND {$wpdb->prefix}icl_string_translations.language=%s"; $string_translation = $wpdb->get_row( $wpdb->prepare( $query, $id, $target_language ) ); if ( ! is_null( $string_translation ) && $string_translation->status == ICL_TM_WAITING_FOR_TRANSLATOR ) { self::$messages[] = array( 'type' => 'update', 'text' => sprintf( __( 'String "%1$s" will be ignored for %2$s, because translation is already waiting for translator.', 'wpml-translation-management' ), $string_translation->value, $target_language_name ), ); $send_to_basket = false; } if ( $send_to_basket ) { self::$basket['string'][ $id ]['from_lang'] = $source_language; self::$basket['string'][ $id ]['to_langs'][ $target_language ] = 1; // set basket language if not already set if ( ! isset( self::$basket['source_language'] ) ) { self::$basket['source_language'] = $source_language; } } } } self::update_basket(); return true; } /** * Serves deletion of items from basket, triggered from WPML TM > Translation * Jobs * * @param array $items Array of items ids, in two separate parts: ['post'] * and ['string'] */ public static function delete_items_from_basket( $items ) { self::get_basket(); $basket_items_types = self::get_basket_items_types(); foreach ( $basket_items_types as $item_type_name => $item_type ) { if ( ! empty( $items[ $item_type_name ] ) ) { foreach ( $items[ $item_type_name ] as $id ) { self::delete_item_from_basket( $id, $item_type_name, false ); } } } self::update_basket(); } /** * Removes one item from basket * * @param int $id Item ID * @param string $type Item type (strings | posts | ...) * @param bool $update_option do update_option('icl_translation_jobs_cart' ? */ public static function delete_item_from_basket( $id, $type = 'post', $update_option = true ) { self::get_basket(); if ( isset( self::$basket[ $type ][ $id ] ) ) { unset( self::$basket[ $type ][ $id ] ); if ( count( self::$basket[ $type ] ) == 0 ) { unset( self::$basket[ $type ] ); } } if ( self::get_basket_items_count( true ) == 0 ) { self::$basket = array(); } if ( $update_option ) { self::update_basket( self::$basket ); } else { self::update_basket_notifications(); } } // TODO: [WPML 3.3] implement this in the troubleshooting page public static function delete_all_items_from_basket() { self::$basket = []; delete_option( self::ICL_TRANSLATION_JOBS_BASKET ); self::update_basket(); } /** * @param WPML_TP_Batch|null $batch */ public static function set_batch_data( $batch ) { self::get_basket(); self::$basket['batch'] = $batch; self::update_basket(); } /** * @return false|null|WPML_TP_Batch */ public static function get_batch_data() { self::get_basket(); return isset( self::$basket['batch'] ) ? self::$basket['batch'] : false; } public static function set_basket_name( $basket_name ) { self::get_basket(); self::$basket['name'] = $basket_name; self::update_basket(); } public static function get_basket_name() { self::get_basket(); return isset( self::$basket['name'] ) ? self::$basket['name'] : false; } public static function set_options( array $options ) { self::$basket['options'] = $options; } /** @return array */ public static function get_options() { return isset( self::$basket['options'] ) ? self::$basket['options'] : array(); } public static function get_basket_extra_fields() { if ( isset( $_REQUEST['extra_fields'] ) ) { $extra_fields_string = urldecode( $_REQUEST['extra_fields'] ); if ( strlen( $extra_fields_string ) > 0 ) { $extra_fields_rows = explode( '|', $extra_fields_string ); $result = array(); foreach ( $extra_fields_rows as $row ) { $row_data = explode( ':', $row ); if ( count( $row_data ) == 2 ) { $result[ $row_data[0] ] = $row_data[1]; } } } } if ( isset( $result ) && count( $result ) > 0 ) { return $result; } return false; } private static function array_replace_recursive( $array, $array1 ) { if ( function_exists( 'array_replace_recursive' ) ) { $array = array_replace_recursive( $array, $array1 ); } else { // handle the arguments, merge one by one $args = func_get_args(); $array = $args[0]; if ( ! is_array( $array ) ) { return $array; } for ( $i = 1; $i < count( $args ); $i ++ ) { if ( is_array( $args[ $i ] ) ) { $array = self::recurse( $array, $args[ $i ] ); } } } return $array; } private static function recurse( $array, $array1 ) { foreach ( $array1 as $key => $value ) { // create new key in $array, if it is empty or not an array if ( ! isset( $array[ $key ] ) || ( isset( $array[ $key ] ) && ! is_array( $array[ $key ] ) ) ) { $array[ $key ] = array(); } // overwrite the value in the base array if ( is_array( $value ) ) { $value = self::recurse( $array[ $key ], $value ); } $array[ $key ] = $value; } return $array; } public static function get_basket_items_types() { return apply_filters( 'wpml_tm_basket_items_types', [ 'string' => 'core', 'post' => 'core', 'package' => 'custom', ] ); } /** * @param $post_id * * @return mixed|null|void|WP_Post */ private static function get_post( $post_id ) { if ( is_string( $post_id ) && strcmp( substr( $post_id, 0, strlen( 'external_' ) ), 'external_' ) === 0 ) { $item = apply_filters( 'wpml_get_translatable_item', null, $post_id ); } else { $item = get_post( $post_id ); } return $item; } /** * @param array $selected_elements * * @param bool|string $type * @return array[]|int[] */ public static function get_elements_ids( $selected_elements, $type = false ) { $element_ids = array(); $legal_item_types = $type ? array( $type ) : array_keys( self::get_basket_items_types() ); foreach ( $legal_item_types as $item_type ) { if ( ! isset( $selected_elements[ $item_type ] ) ) { continue; } $element_ids[ $item_type ] = isset( $element_ids[ $item_type ] ) ? $element_ids[ $item_type ] : array(); $items = $selected_elements[ $item_type ]; foreach ( $items as $element_id => $action_data ) { if ( isset( $action_data['checked'] ) && $action_data['checked'] ) { $element_ids[ $item_type ][] = $element_id; } } } return $type && isset( $element_ids[ $type ] ) ? $element_ids[ $type ] : $element_ids; } public static function get_source_language() { self::get_basket(); return isset( self::$basket['source_language'] ) ? self::$basket['source_language'] : false; } private static function sync_target_languages() { self::get_basket(); if ( ! isset( self::$basket['target_languages'] ) ) { self::$basket['target_languages'] = array(); } $basket_items_types = self::get_basket_items_types(); foreach ( $basket_items_types as $item_type_name => $item_type ) { if ( isset( self::$basket[ $item_type_name ] ) ) { $posts_in_basket = self::$basket[ $item_type_name ]; foreach ( (array) $posts_in_basket as $post_in_basket ) { foreach ( (array) $post_in_basket['to_langs'] as $key => $target_language ) { if ( $target_language && ! in_array( $key, self::$basket['target_languages'] ) ) { self::$basket['target_languages'] [] = $key; } } } } } } /** * @return bool|array */ public static function get_target_languages() { self::get_basket(); self::sync_target_languages(); return isset( self::$basket['target_languages'] ) ? self::$basket['target_languages'] : false; } /** * Sets target languages for remote service * * @param $remote_target_languages */ public static function set_remote_target_languages( $remote_target_languages ) { self::get_basket(); self::$basket['remote_target_languages'] = $remote_target_languages; self::update_basket(); } /** * Get target languages for remote service * * @return array | false */ public static function get_remote_target_languages() { self::get_basket(); if ( isset( self::$basket['remote_target_languages'] ) ) { return self::$basket['remote_target_languages']; } else { return self::get_target_languages(); } } /** * @return array */ public static function get_basket_notification_positions() { return array( 'admin_notice' => 'basket_status_update', 'tm_dashboard_top' => 'translation-basket-notification', 'st_dashboard_top' => 'string-translation-top', 'st_dashboard_bottom' => 'string-translation-under', ); } public static function get_basket_extra_fields_section() { $extra_fields = TranslationProxy::get_extra_fields_local(); $html = ''; if ( $extra_fields ) { $html .= '<h3>3. ' . __( 'Select additional options', 'wpml-translation-management' ) . ' <a href="#" id="basket_extra_fields_refresh">(' . __( 'Refresh', 'wpml-translation-management' ) . ')</a></h3>'; $html .= '<div id="basket_extra_fields_list">'; $html .= self::get_basket_extra_fields_inputs( $extra_fields, false ); $html .= '</div>'; } return $html; } public static function get_basket_extra_fields_inputs( array $extra_fields = array(), $force_refresh = false ) { if ( ! $extra_fields ) { if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { $force_refresh = true; } $extra_fields = self::get_basket_extra_fields_array( $force_refresh ); } return self::extra_fields_build_inputs( $extra_fields ); } public static function get_basket_extra_fields_array( $force_refresh = false ) { if ( $force_refresh ) { $networking = wpml_tm_load_tp_networking(); $project = TranslationProxy::get_current_project(); $extra_fields = $networking->get_extra_fields_remote( $project ); TranslationProxy::save_extra_fields( $extra_fields ); } else { $extra_fields = TranslationProxy::get_extra_fields_local(); } return TranslationProxy::maybe_convert_extra_fields( $extra_fields ); } public static function extra_fields_build_inputs( array $extra_fields ) { if ( ! $extra_fields ) { return ''; } $rows = array(); /** @var WPML_TP_Extra_Field $field */ $field_diplay = new WPML_TP_Extra_Field_Display(); foreach ( $extra_fields as $field ) { $rows[] = $field_diplay->render( $field ); } $rows = array_filter( $rows ); $html = ''; if ( $rows ) { $html = '<table class="form-table">'; $html .= '<tbody>'; $html .= implode( PHP_EOL, $rows ); $html .= '</tbody>'; $html .= '</table>'; } return $html; } /** * @param $data * * @return bool */ private static function validate_data( $data ) { $data_is_valid = true; if ( self::get_basket() && self::get_source_language() ) { /* we do not add items that are not in the source language of the current basket we cannot yet set its source language though since update_basket would set the basket to false as long as we do not have any elements in the basket*/ if ( self::$translate_from != self::get_source_language() ) { self::$messages[] = array( 'type' => 'update', 'text' => __( 'You cannot add posts in this language to the basket since it already contains posts or strings of another source language! Either submit the current basket and then add the post or delete the posts of differing language in the current basket', 'wpml-translation-management' ), ); self::update_basket(); $data_is_valid = false; } } // no language selected ? if ( ! isset( self::$translation_action ) || empty( self::$translation_action ) ) { self::$messages[] = array( 'type' => 'error', 'text' => __( 'Please select at least one language to translate into.', 'wpml-translation-management' ), ); self::$dashboard_select = $data; // pre fill dashboard $data_is_valid = false; } if ( $data_is_valid ) { $data_is_valid = false; $basket_items_types = self::get_basket_items_types(); // nothing selected ? foreach ( $basket_items_types as $basket_items_type => $basket_type ) { if ( isset( $data[ $basket_items_type ] ) && $data[ $basket_items_type ] ) { $data_is_valid = true; break; } } } if ( ! $data_is_valid ) { self::$messages[] = array( 'type' => 'error', 'text' => __( 'Please select at least one document to translate.', 'wpml-translation-management' ), ); self::$dashboard_select = $data; // pre-populate dashboard $data_is_valid = false; return $data_is_valid; } return $data_is_valid; } } } translation-proxy/translationproxy-service.class.php 0000755 00000007021 14720425051 0017177 0 ustar 00 <?php /** * @package wpml-core * @subpackage wpml-core */ require_once dirname( __FILE__ ) . '/translationproxy-api.class.php'; class TranslationProxy_Service { public $id; public $name; public $description; public $default_service; public $has_translator_selection = true; // Todo: read this from service properties public $delivery_method; public $project_details_url; public $custom_text_url; public $has_language_pairs; public $languages_map; public $url; public $logo_url; public $create_project_url; public $add_language_pair_url; public $new_job_url; public $custom_fields; public $custom_fields_data; public $select_translator_iframe_url; public $translator_contact_iframe_url; public $quote_iframe_url; public $batch_name_max_length; public static function is_authenticated( $service ) { // for services that do not require authentication return true by default if ( ! TranslationProxy::service_requires_authentication( $service ) ) { return true; } return isset( $service->custom_fields_data ) && $service->custom_fields_data ? true : false; } public static function list_services() { return TranslationProxy_Api::proxy_request( '/services.json' ); } public static function get_service( $service_id ) { $service = TranslationProxy_Api::proxy_request( "/services/$service_id.json" ); $service->languages_map = self::languages_map( $service ); return $service; } public static function get_service_by_suid( $suid ) { $service = TranslationProxy_Api::proxy_request( "/services/$suid.json" ); $service->languages_map = self::languages_map( $service ); return $service; } public static function languages_map( $service ) { $languages_map = array(); $languages = TranslationProxy_Api::proxy_request( "/services/{$service->id}/language_identifiers.json" ); foreach ( $languages as $language ) { $languages_map[ $language->iso_code ] = $language->value; } return $languages_map; } public static function get_language( $service, $language ) { if ( ! empty( $service->languages_map ) and array_key_exists( $language, $service->languages_map ) ) { $language = $service->languages_map[ $language ]; } return $language; } /** * Returns a WPML readable string that allows to tell translation service and translator id * (typically used for translators dropdowns) * * @param int|float|string|bool $translation_service_id * @param int|float|string|bool $translator_id * * @return string */ public static function get_wpml_translator_id( $translation_service_id = false, $translator_id = false ) { if ( $translation_service_id === false ) { $translation_service_id = TranslationProxy::get_current_service_id(); } $result = 'ts-' . $translation_service_id; if ( $translator_id !== false ) { $result .= '-' . $translator_id; } return $result; } /** * @param string $translator_id * * @return array Returns a two elements array, respectively containing translation_service and translator_id */ public static function get_translator_data_from_wpml( $translator_id ) { $result = array(); if ( is_numeric( $translator_id ) ) { $result['translation_service'] = 'local'; $result['translator_id'] = $translator_id; } else { $translator_data = explode( '-', $translator_id ); $result = array(); $result['translation_service'] = $translator_data[1]; $result['translator_id'] = isset( $translator_data[2] ) ? $translator_data[2] : 0; } return $result; } } translation-proxy/translationproxy-popup.php 0000755 00000004721 14720425051 0015602 0 ustar 00 <?php define( 'ICL_LANGUAGE_NOT_SUPPORTED', 3 ); global $wpdb, $sitepress; $target = filter_input( INPUT_GET, 'target', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $auto_resize = filter_input( INPUT_GET, 'auto_resize', FILTER_VALIDATE_BOOLEAN | FILTER_NULL_ON_FAILURE ); $unload_cb = filter_input( INPUT_GET, 'unload_cb', FILTER_SANITIZE_FULL_SPECIAL_CHARS | FILTER_NULL_ON_FAILURE ); // Adding a translator if ( preg_match( '|^@select-translators;([^;]+);([^;]+)@|', $target, $matches ) ) { $source_language = $matches[1]; $target_language = $matches[2]; $project = TranslationProxy::get_current_project(); try { $lp_setting_index = 'language_pairs'; $language_pairs = $sitepress->get_setting( $lp_setting_index, array() ); if ( ! isset( $language_pairs[ $source_language ][ $target_language ] ) || $language_pairs[ $source_language ][ $target_language ] == 0 ) { $language_pairs[ $source_language ][ $target_language ] = 1; TranslationProxy_Translator::update_language_pairs( $project, $language_pairs ); $sitepress->set_setting( $lp_setting_index, $language_pairs, true ); } $target = $project->select_translator_iframe_url( $source_language, $target_language ); } catch ( Exception $e ) { if ( $e->getCode() == ICL_LANGUAGE_NOT_SUPPORTED ) { printf( __( '<p>Requested languages are not supported by the translation service (%s). Please <a%s>contact us</a> for support. </p>', 'wpml-translation-management' ), $e->getMessage(), ' target="_blank" href="http://wpml.org/?page_id=5255"' ); } else { printf( __( '<p>Could not add the requested languages. Please <a%s>contact us</a> for support. </p><p>Show <a%s>debug information</a>.</p>', 'wpml-translation-management' ), ' target="_blank" href="http://wpml.org/?page_id=5255"', ' a href="admin.php?page=' . ICL_PLUGIN_FOLDER . '/menu/troubleshooting.php&icl_action=icl-connection-test' . '#icl-connection-test"' ); } exit; } } $target .= ( strpos( $target, '?' ) === false ) ? '?' : '&'; $target .= "lc=" . $sitepress->get_admin_language(); ?> <iframe src="<?php echo $target; ?>" style="width:100%; height:92%" onload=" var TB_window = jQuery('#TB_window'); <?php if ( $auto_resize ): ?> TB_window.css('width','90%').css('margin-left', '-45%'); <?php endif; ?> <?php if ( $unload_cb ){ $unload_cb = esc_js($unload_cb); ?> TB_window.unbind('unload').bind('tb_unload', function(){<?php echo $unload_cb; ?>}); <?php } ?> ">