Filigrane), live preview and AJAX tools. */ if ( ! defined( 'ABSPATH' ) ) { exit; } function wmguru_admin_menu() { add_media_page( __( 'Filigrane', 'filigrane' ), __( 'Filigrane', 'filigrane' ), 'manage_options', 'wmguru', 'wmguru_render_settings_page' ); } add_action( 'admin_menu', 'wmguru_admin_menu' ); function wmguru_register_settings() { register_setting( 'wmguru', WMGURU_OPTION, array( 'type' => 'array', 'sanitize_callback' => 'wmguru_sanitize_settings', 'default' => wmguru_default_settings(), ) ); } add_action( 'admin_init', 'wmguru_register_settings' ); /** * @param array $links Plugin action links. * @return array */ function wmguru_plugin_action_links( $links ) { $url = admin_url( 'upload.php?page=wmguru' ); array_unshift( $links, '' . esc_html__( 'Settings', 'filigrane' ) . '' ); return $links; } add_filter( 'plugin_action_links_' . plugin_basename( WMGURU_PLUGIN_FILE ), 'wmguru_plugin_action_links' ); /** * @param string $hook Admin page hook suffix. */ function wmguru_admin_assets( $hook ) { if ( 'media_page_wmguru' !== $hook ) { return; } wp_enqueue_media(); wp_enqueue_style( 'wmguru-admin', WMGURU_PLUGIN_URL . 'assets/css/admin.css', array(), WMGURU_VERSION ); wp_enqueue_script( 'wmguru-admin', WMGURU_PLUGIN_URL . 'assets/js/admin.js', array( 'media-views' ), WMGURU_VERSION, true ); wp_localize_script( 'wmguru-admin', 'wmguruAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wmguru_admin' ), 'i18n' => array( 'chooseImage' => __( 'Choose an image', 'filigrane' ), 'useImage' => __( 'Use this image', 'filigrane' ), 'rendering' => __( 'Rendering preview…', 'filigrane' ), 'previewFailed' => __( 'Preview failed.', 'filigrane' ), 'working' => __( 'Working…', 'filigrane' ), 'requestFailed' => __( 'The request failed.', 'filigrane' ), 'sampleImage' => __( 'Built-in sample image', 'filigrane' ), ), ) ); } add_action( 'admin_enqueue_scripts', 'wmguru_admin_assets' ); /** * 3x3 placement picker. * * @param string $name Input name. * @param string $value Selected position. */ function wmguru_position_picker( $name, $value ) { echo '
'; foreach ( wmguru_positions() as $key => $label ) { printf( '', esc_attr( $label ), esc_attr( $name ), esc_attr( $key ), checked( $value, $key, false ) ); } echo '
'; } /** * Tags and attributes the settings rows are built from. * * @return array wp_kses() allowed-HTML list. */ function wmguru_field_allowed_html() { $common = array( 'class' => true, 'id' => true, 'title' => true, 'role' => true, 'aria-live' => true, 'aria-label' => true, 'hidden' => true, ); return array( 'div' => $common, 'span' => $common, 'p' => $common, 'ul' => $common, 'li' => $common, 'code' => $common, 'strong' => $common, 'br' => array(), 'label' => $common + array( 'for' => true ), 'img' => $common + array( 'src' => true, 'alt' => true, ), 'input' => $common + array( 'type' => true, 'name' => true, 'value' => true, 'checked' => true, 'disabled' => true, 'min' => true, 'max' => true, 'step' => true, 'maxlength' => true, ), 'select' => $common + array( 'name' => true ), 'option' => array( 'value' => true, 'selected' => true, ), 'button' => $common + array( 'type' => true, 'data-wmguru-action' => true, 'data-reload' => true, ), ); } /** * One table row. * * @param string $label Label. * @param string $html Field HTML (already escaped). * @param string $desc Optional description. */ function wmguru_row( $label, $html, $desc = '' ) { echo '' . esc_html( $label ) . '' . wp_kses( $html, wmguru_field_allowed_html() ); if ( '' !== $desc ) { echo '

' . esc_html( $desc ) . '

'; } echo ''; } /** * @param string $name Input name. * @param mixed $value Value. * @param array $attrs min / max / step. * @return string */ function wmguru_number_input( $name, $value, $attrs ) { return sprintf( '', esc_attr( $name ), esc_attr( $value ), esc_attr( $attrs['min'] ), esc_attr( $attrs['max'] ), esc_attr( isset( $attrs['step'] ) ? $attrs['step'] : 1 ) ); } /** * Fields shared by the text and image layer cards. * * @param string $base Input name prefix, e.g. wmguru_settings[layers][0]. * @param array $layer Layer. * @param bool $is_text True for a text layer. */ function wmguru_render_common_layer_fields( $base, $layer, $is_text ) { wmguru_row( $is_text ? __( 'Text size', 'filigrane' ) : __( 'Logo width', 'filigrane' ), wmguru_number_input( $base . '[size]', $layer['size'], array( 'min' => 0.5, 'max' => 100, 'step' => 0.1, ) ) . ' % ' . esc_html__( 'of the image width', 'filigrane' ) . '   ' . esc_html__( 'min', 'filigrane' ) . ' ' . wmguru_number_input( $base . '[min_px]', $layer['min_px'], array( 'min' => 1, 'max' => 4000, ) ) . ' px   ' . esc_html__( 'max', 'filigrane' ) . ' ' . wmguru_number_input( $base . '[max_px]', $layer['max_px'], array( 'min' => 1, 'max' => 8000, ) ) . ' px', __( 'The watermark scales with each image, within the min/max limits.', 'filigrane' ) ); ob_start(); wmguru_position_picker( $base . '[position]', $layer['position'] ); wmguru_row( __( 'Position', 'filigrane' ), ob_get_clean() ); wmguru_row( __( 'Margin', 'filigrane' ), wmguru_number_input( $base . '[margin]', $layer['margin'], array( 'min' => 0, 'max' => 30, 'step' => 0.1, ) ) . ' % ' . esc_html__( 'of the shorter side', 'filigrane' ) ); wmguru_row( __( 'Opacity', 'filigrane' ), wmguru_number_input( $base . '[opacity]', $layer['opacity'], array( 'min' => 1, 'max' => 100, ) ) . ' %' ); wmguru_row( __( 'Rotation', 'filigrane' ), wmguru_number_input( $base . '[rotation]', $layer['rotation'], array( 'min' => -180, 'max' => 180, ) ) . '° ' . esc_html__( 'clockwise', 'filigrane' ) ); } /** * @param int $index Layer slot. * @param array $layer Layer. */ function wmguru_render_layer_card( $index, $layer ) { $base = WMGURU_OPTION . '[layers][' . (int) $index . ']'; $is_text = 'text' === $layer['type']; echo '

' . ( $is_text ? esc_html__( 'Text watermark', 'filigrane' ) : esc_html__( 'Image watermark (logo)', 'filigrane' ) ) . '

'; echo ''; echo ''; wmguru_row( __( 'Enabled', 'filigrane' ), '' ); if ( $is_text ) { wmguru_row( __( 'Text', 'filigrane' ), '', __( 'Tokens: {site} site title, {year} current year, {url} site domain.', 'filigrane' ) ); $options = ''; foreach ( wmguru_font_choices() as $key => $font ) { $options .= ''; } wmguru_row( __( 'Font', 'filigrane' ), '', __( 'Bundled fonts cover Latin, Cyrillic and Greek.', 'filigrane' ) ); wmguru_row( __( 'Color', 'filigrane' ), '' ); } else { $attachment_id = (int) $layer['attachment_id']; $thumb = $attachment_id ? wp_get_attachment_image_url( $attachment_id, 'medium' ) : ''; $html = '
' . '' . '' . ' ' . '' . '
'; wmguru_row( __( 'Logo', 'filigrane' ), $html, __( 'A PNG with a transparent background works best.', 'filigrane' ) ); } wmguru_render_common_layer_fields( $base, $layer, $is_text ); echo '
'; } function wmguru_render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $settings = wmguru_get_settings(); $slots = array( 'text' => wmguru_default_layer( 'text' ), 'image' => wmguru_default_layer( 'image' ), ); $seen = array(); foreach ( $settings['layers'] as $layer ) { if ( isset( $slots[ $layer['type'] ] ) && ! isset( $seen[ $layer['type'] ] ) ) { $slots[ $layer['type'] ] = array_merge( $slots[ $layer['type'] ], $layer ); $seen[ $layer['type'] ] = true; } } $support = wmguru_image_support(); $status = get_option( 'wmguru_delivery_status', array() ); $stats = wmguru_cache_stats(); $log = get_option( 'wmguru_error_log', array() ); echo '

' . esc_html__( 'Filigrane', 'filigrane' ) . '

'; settings_errors(); if ( empty( $settings['enabled'] ) ) { echo '

' . esc_html__( 'Watermarks are switched off. Set up your watermark below, check the preview, then enable it under “General”. Your original files are never modified either way.', 'filigrane' ) . '

'; } echo '
'; settings_fields( 'wmguru' ); // General. echo '

' . esc_html__( 'General', 'filigrane' ) . '

'; wmguru_row( __( 'Watermarks', 'filigrane' ), '', __( 'Copies are generated on first view and cached. Turn this off at any time and visitors get the untouched originals immediately.', 'filigrane' ) ); $boxes = ''; foreach ( wmguru_available_sizes() as $size => $label ) { $boxes .= ''; } wmguru_row( __( 'Apply to sizes', 'filigrane' ), $boxes, __( '“Full size” also covers scaled and original uploads.', 'filigrane' ) ); wmguru_row( __( 'Skip small images', 'filigrane' ), esc_html__( 'narrower than', 'filigrane' ) . ' ' . wmguru_number_input( WMGURU_OPTION . '[min_width]', $settings['min_width'], array( 'min' => 0, 'max' => 10000, ) ) . ' px ' . esc_html__( 'or shorter than', 'filigrane' ) . ' ' . wmguru_number_input( WMGURU_OPTION . '[min_height]', $settings['min_height'], array( 'min' => 0, 'max' => 10000, ) ) . ' px' ); wmguru_row( __( 'Output quality', 'filigrane' ), wmguru_number_input( WMGURU_OPTION . '[quality]', $settings['quality'], array( 'min' => 30, 'max' => 100, ) ) . ' ' . esc_html__( 'for JPEG, WebP and AVIF copies (originals are not re-encoded)', 'filigrane' ) ); $drivers = array( 'auto' => __( 'Automatic (Imagick if available, otherwise GD)', 'filigrane' ), 'imagick' => __( 'Imagick', 'filigrane' ), 'gd' => __( 'GD', 'filigrane' ), ); $options = ''; foreach ( $drivers as $key => $label ) { $options .= ''; } $formats = array(); foreach ( array_keys( $support['gd'] ) as $mime ) { $libs = array(); if ( ! empty( $support['imagick'][ $mime ] ) ) { $libs[] = 'Imagick'; } if ( ! empty( $support['gd'][ $mime ] ) ) { $libs[] = 'GD'; } $formats[] = strtoupper( str_replace( 'image/', '', $mime ) ) . ': ' . ( $libs ? implode( ' + ', $libs ) : __( 'not supported', 'filigrane' ) ); } wmguru_row( __( 'Image library', 'filigrane' ), '', implode( ' · ', $formats ) ); wmguru_row( __( 'On uninstall', 'filigrane' ), '' ); echo '
'; wmguru_render_layer_card( 0, $slots['text'] ); wmguru_render_layer_card( 1, $slots['image'] ); // Delivery. $modes = array( 'auto' => __( 'Automatic', 'filigrane' ), 'pretty' => __( 'Direct file URLs', 'filigrane' ), 'query' => __( 'Signed query URLs (fallback)', 'filigrane' ), ); $options = ''; foreach ( $modes as $key => $label ) { $options .= ''; } if ( is_array( $status ) && isset( $status['pretty'] ) && true === $status['pretty'] ) { $state = __( 'Last test: direct file URLs work.', 'filigrane' ); } elseif ( is_array( $status ) && isset( $status['pretty'] ) && false === $status['pretty'] ) { /* translators: %s: error detail */ $state = sprintf( __( 'Last test: direct URLs do not work here (%s). The fallback is used automatically.', 'filigrane' ), isset( $status['error'] ) ? $status['error'] : '' ); } elseif ( ! empty( $status['error'] ) ) { /* translators: %s: error detail */ $state = sprintf( __( 'Last test could not run (%s). Direct file URLs are assumed.', 'filigrane' ), $status['error'] ); } else { $state = __( 'Not tested yet.', 'filigrane' ); } echo '

' . esc_html__( 'Delivery & cache', 'filigrane' ) . '

'; wmguru_row( __( 'URL mode', 'filigrane' ), '', $state ); wmguru_row( __( 'Cache', 'filigrane' ), sprintf( /* translators: 1: number of files, 2: size */ esc_html__( '%1$s files, %2$s', 'filigrane' ), esc_html( number_format_i18n( $stats['files'] ) . ( $stats['capped'] ? '+' : '' ) ), esc_html( size_format( $stats['bytes'] ) ) ) . ' ', __( 'Clearing the cache is always safe: copies are regenerated on demand.', 'filigrane' ) ); if ( is_array( $log ) && $log ) { $items = ''; foreach ( array_reverse( array_slice( $log, -5 ) ) as $entry ) { $items .= '
  • ' . esc_html( wp_date( 'Y-m-d H:i', $entry['time'] ) ) . ' ' . esc_html( $entry['message'] ) . '
  • '; } wmguru_row( __( 'Recent problems', 'filigrane' ), '
      ' . $items . '
    ' ); } echo '
    '; submit_button(); echo '
    '; // Preview column. echo '
    '; do_action( 'wmguru_settings_page_after' ); echo '
    '; } /** * Source file for previews: one of the user's images, or the bundled sample. * * @param int $attachment_id Attachment id, 0 for the sample. * @return array array( path, mime ) */ function wmguru_preview_source( $attachment_id ) { if ( $attachment_id && wp_attachment_is_image( $attachment_id ) ) { $mime = get_post_mime_type( $attachment_id ); $file = get_attached_file( $attachment_id ); $size = image_get_intermediate_size( $attachment_id, 'large' ); $uploads = wmguru_uploads(); if ( $size && ! empty( $size['path'] ) && $uploads && is_readable( $uploads['basedir'] . '/' . $size['path'] ) ) { $file = $uploads['basedir'] . '/' . $size['path']; } if ( $file && is_readable( $file ) && wmguru_get_file_handler( $mime ) ) { return array( $file, $mime ); } } return array( WMGURU_PLUGIN_DIR . 'assets/images/sample.jpg', 'image/jpeg' ); } /** * Render a profile onto a preview image and return it as a data URI. * * @param array $profile Profile. * @param int $attachment_id Image to preview on, 0 for the sample. * @param array $overrides 'quality' / 'driver'. * @return array|WP_Error array( 'src' => data URI, 'notice' => string ) */ function wmguru_render_preview( $profile, $attachment_id = 0, $overrides = array() ) { list( $src, $mime ) = wmguru_preview_source( (int) $attachment_id ); $notice = ''; $tmp = wp_tempnam( 'wmguru-preview' ); if ( wmguru_effective_layers( $profile ) ) { $result = wmguru_is_animated( $src, $mime ) ? new WP_Error( 'wmguru_animated', __( 'Animated images are shown and served unchanged.', 'filigrane' ) ) : wmguru_render_image( $src, $tmp, $profile, $mime, $overrides ); if ( is_wp_error( $result ) ) { wp_delete_file( $tmp ); return $result; } $file = $tmp; } else { $notice = __( 'Nothing to draw yet: enable a layer and give it content.', 'filigrane' ); $file = $src; } $data = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents wp_delete_file( $tmp ); if ( false === $data ) { return new WP_Error( 'wmguru_preview_read', __( 'Could not read the rendered preview.', 'filigrane' ) ); } return array( 'src' => 'data:' . $mime . ';base64,' . base64_encode( $data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'notice' => $notice, ); } /** * Shared guard for the AJAX endpoints. */ function wmguru_ajax_guard() { check_ajax_referer( 'wmguru_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'filigrane' ) ), 403 ); } } function wmguru_ajax_preview() { wmguru_ajax_guard(); $raw = isset( $_POST['wmguru_settings'] ) ? wp_unslash( $_POST['wmguru_settings'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce checked in wmguru_ajax_guard(); sanitized by wmguru_sanitize_settings(). $settings = wmguru_sanitize_settings( $raw ); $profile = array( 'id' => 'default', 'label' => '', 'layers' => $settings['layers'], ); $attachment_id = isset( $_POST['preview_id'] ) ? absint( $_POST['preview_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = wmguru_render_preview( $profile, $attachment_id, array( 'quality' => $settings['quality'], 'driver' => $settings['driver'], ) ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( $result ); } add_action( 'wp_ajax_wmguru_preview', 'wmguru_ajax_preview' ); function wmguru_ajax_test_delivery() { wmguru_ajax_guard(); $status = wmguru_run_delivery_test(); if ( true === $status['pretty'] ) { wp_send_json_success( array( 'message' => __( 'Direct file URLs work.', 'filigrane' ) ) ); } if ( false === $status['pretty'] ) { wp_send_json_success( array( 'message' => __( 'Direct URLs do not work here; the fallback will be used.', 'filigrane' ) ) ); } wp_send_json_error( array( 'message' => $status['error'] ? $status['error'] : __( 'The test could not run.', 'filigrane' ) ) ); } add_action( 'wp_ajax_wmguru_test_delivery', 'wmguru_ajax_test_delivery' ); function wmguru_ajax_purge() { wmguru_ajax_guard(); wmguru_purge_all(); wp_send_json_success( array( 'message' => __( 'Cache cleared.', 'filigrane' ) ) ); } add_action( 'wp_ajax_wmguru_purge', 'wmguru_ajax_purge' );