post_content, 'lovely_notes' ) || has_shortcode( $post->post_content, 'lovely_notes_archive' ) ) ) ) { self::enqueue_assets(); } } /** Avoid losing the first tap when LiteSpeed delays scripts until interaction. */ public function exclude_delayed_script( $excluded ) { $excluded = is_array( $excluded ) ? $excluded : array(); $excluded[] = 'lovely-notes/assets/js/front.js'; $excluded[] = 'LovelyNotesData'; return array_values( array_unique( $excluded ) ); } /** * Render the newest published Lovely Note. * * @param array $atts Shortcode attributes. * @return string */ public function render( $atts = array() ) { $atts = shortcode_atts( array( 'show_date' => 'yes', 'show_media' => 'yes', 'show_name' => 'auto', 'text_lines' => Lovely_Notes_Settings::get()['text_lines'], 'class' => '', ), $atts, 'lovely_notes' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only selection of published, unprotected notes; no data is changed. $initial_offset = isset( $_GET['ln_offset'] ) && is_scalar( $_GET['ln_offset'] ) ? absint( wp_unslash( $_GET['ln_offset'] ) ) : 0; $note = $this->get_note_at_offset( $initial_offset ); if ( ! $note ) { return ''; } self::enqueue_assets(); $classes = array( 'lovely-notes', 'lovely-notes--latest' ); if ( ! empty( $atts['class'] ) ) { $extra_classes = preg_split( '/\s+/', (string) $atts['class'] ); foreach ( $extra_classes as $extra_class ) { $extra_class = sanitize_html_class( $extra_class ); if ( $extra_class ) { $classes[] = $extra_class; } } } $config = array( 'show_date' => $this->is_enabled( $atts['show_date'] ), 'show_media' => $this->is_enabled( $atts['show_media'] ), 'text_lines' => Lovely_Notes_Settings::normalize_lines( $atts['text_lines'] ), ); $name = Lovely_Notes_Settings::get_name(); $show_name = 'auto' === $atts['show_name'] ? $name !== Lovely_Notes_Settings::defaults()['name'] : $this->is_enabled( $atts['show_name'] ); ob_start(); ?>

render_note( $note, $initial_offset, $config ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by renderer. ?>
request_value( 'offset', 0 ) ); $config = array( 'show_date' => '0' !== (string) $this->request_value( 'show_date', '1' ), 'show_media' => '0' !== (string) $this->request_value( 'show_media', '1' ), 'text_lines' => Lovely_Notes_Settings::normalize_lines( $this->request_value( 'text_lines', Lovely_Notes_Settings::get()['text_lines'] ) ), ); $note = $this->get_note_at_offset( $offset ); if ( ! $note ) { wp_send_json_error( array( 'message' => __( 'Cette note est introuvable.', 'lovely-notes' ) ), 404 ); return; } wp_send_json_success( array( 'html' => $this->render_note( $note, $offset, $config ), 'offset' => $offset, ) ); } /** Enqueue the small public stylesheet and navigation script. */ public static function enqueue_assets() { if ( self::$assets_enqueued ) { return; } self::$assets_enqueued = true; wp_enqueue_style( 'lovely-notes-front', LOVELY_NOTES_URL . 'assets/css/front.css', array(), LOVELY_NOTES_VERSION ); wp_enqueue_script( 'lovely-notes-front', LOVELY_NOTES_URL . 'assets/js/front.js', array(), LOVELY_NOTES_VERSION, true ); wp_localize_script( 'lovely-notes-front', 'LovelyNotesData', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'title' => Lovely_Notes_Settings::get_name(), 'textLines' => Lovely_Notes_Settings::get()['text_lines'], 'labels' => array( 'close' => __( 'Fermer la note', 'lovely-notes' ), 'older' => __( 'Note précédente', 'lovely-notes' ), 'newer' => __( 'Note suivante', 'lovely-notes' ), 'loading' => __( 'Chargement de la note…', 'lovely-notes' ), 'error' => __( 'Impossible de charger cette note. Réessayez dans un instant.', 'lovely-notes' ), 'navigation' => __( 'Navigation dans le carnet', 'lovely-notes' ), 'shareLink' => __( 'Lien de cette note', 'lovely-notes' ), 'copy' => __( 'Copier le lien', 'lovely-notes' ), 'copied' => __( 'Lien copié !', 'lovely-notes' ), 'copyError' => __( 'Sélectionnez et copiez le lien ci-dessus.', 'lovely-notes' ), 'readText' => __( 'Texte de la note', 'lovely-notes' ), 'retry' => __( 'Réessayer', 'lovely-notes' ), ), ) ); } /** Fetch one published note according to its position, newest first. */ private function get_note_at_offset( $offset ) { $query = new WP_Query( array( 'post_type' => array( 'lovely_note' ), 'post_status' => 'publish', 'post_password' => '', 'posts_per_page' => 1, 'offset' => max( 0, absint( $offset ) ), 'orderby' => array( 'date' => 'DESC', 'ID' => 'DESC' ), 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_term_cache' => false, ) ); return ! empty( $query->posts[0] ) && $query->posts[0] instanceof WP_Post ? $query->posts[0] : null; } /** Check whether an older note exists after the current offset. */ private function has_older_note( $offset ) { return (bool) $this->get_note_at_offset( absint( $offset ) + 1 ); } /** Render the complete note card. */ private function render_note( WP_Post $note, $offset, array $config ) { $offset = absint( $offset ); $archive_url = Lovely_Notes_Archive::get_archive_url(); $has_older = empty( $config['hide_navigation'] ) && $this->has_older_note( $offset ); $has_newer = empty( $config['hide_navigation'] ) && $offset > 0; $media_html = $config['show_media'] ? $this->render_media( $note->ID ) : ''; $has_media = '' !== trim( $media_html ); $lovely_eye = Lovely_Notes::render_lovely_eye( $note->ID ); $classes = array( 'lovely-notes__note' ); $classes[] = $has_media ? 'lovely-notes__note--has-media' : 'lovely-notes__note--no-media'; if ( '' !== $lovely_eye ) { $classes[] = 'lovely-notes__note--lovely-eye'; } ob_start(); ?>
render_content( $note ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-filtered and locally sanitized content. ?>
true, 'show_media' => true, 'text_lines' => $settings['text_lines'], 'hide_navigation' => true ); return '
' . $this->render_note( $note, 0, $config ) . '
'; } public static function get_public_note( $note_id ) { $note = get_post( absint( $note_id ) ); return $note instanceof WP_Post && 'lovely_note' === $note->post_type && 'publish' === $note->post_status && '' === $note->post_password ? $note : null; } private function request_value( $key, $default ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public read-only endpoints; access is restricted to published, unprotected notes. return isset( $_POST[ $key ] ) && is_scalar( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : $default; } /** Determine whether a yes/no attribute is enabled. */ private function is_enabled( $value ) { return in_array( strtolower( (string) $value ), array( '1', 'true', 'yes', 'oui', 'on' ), true ); } /** Create a localized long date such as Dimanche 12 juillet 2026. */ private function format_date( WP_Post $note ) { $timestamp = get_post_timestamp( $note ); $date = wp_date( 'l j F Y', $timestamp ?: null ); return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( mb_substr( $date, 0, 1 ) ) . mb_substr( $date, 1 ) : ucfirst( $date ); } /** Use WordPress rendering for blocks, pasted video URLs and media shortcodes in every view. */ private function render_content( WP_Post $note ) { $had_post = array_key_exists( 'post', $GLOBALS ); $original_post = $GLOBALS['post'] ?? null; $wp_embed = $GLOBALS['wp_embed'] ?? null; $embed_post_id = $wp_embed instanceof WP_Embed ? $wp_embed->post_ID : null; $GLOBALS['post'] = $note; if ( $wp_embed instanceof WP_Embed ) { $wp_embed->post_ID = $note->ID; } try { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core content-rendering hook, not a plugin-defined hook. return $this->sanitize_rendered_content( apply_filters( 'the_content', $note->post_content ) ); } finally { if ( $wp_embed instanceof WP_Embed ) { $wp_embed->post_ID = $embed_post_id; } if ( $had_post ) { $GLOBALS['post'] = $original_post; } else { unset( $GLOBALS['post'] ); } } } /** Keep video players without changing WordPress's global HTML permissions. */ private function sanitize_rendered_content( $html ) { $allowed = wp_kses_allowed_html( 'post' ); $allowed['iframe'] = array( 'src' => true, 'title' => true, 'width' => true, 'height' => true, 'class' => true, 'frameborder' => true, 'allow' => true, 'allowfullscreen' => true, 'loading' => true, 'referrerpolicy' => true, 'sandbox' => true, ); $allowed['source'] = array( 'src' => true, 'type' => true, 'media' => true ); return wp_kses( $html, $allowed, wp_allowed_protocols() ); } /** Render the media attached through Lovely Notes fields. */ private function render_media( $post_id ) { $attachment_id = absint( get_post_meta( $post_id, Lovely_Notes::META_ATTACHMENT_ID, true ) ); $external = trim( (string) get_post_meta( $post_id, Lovely_Notes::META_EXTERNAL, true ) ); if ( $attachment_id ) { return $this->render_attachment( $attachment_id ); } if ( $external ) { return $this->render_external( $external ); } return ''; } /** Render an image, video, audio, or linked file from the Media Library. */ private function render_attachment( $attachment_id ) { $mime = (string) get_post_mime_type( $attachment_id ); $url = wp_get_attachment_url( $attachment_id ); if ( ! $url ) { return ''; } if ( 0 === strpos( $mime, 'image/' ) ) { $image = wp_get_attachment_image( $attachment_id, 'large', false, array( 'class' => 'lovely-notes__image', 'loading' => 'lazy', ) ); return $image ? '
' . $image . '
' : ''; } if ( 0 === strpos( $mime, 'video/' ) ) { return '
'; } return '

' . esc_html__( 'Ouvrir le média', 'lovely-notes' ) . '

'; } /** Render an external URL or a previously sanitized iframe/video embed. */ private function render_external( $external ) { if ( filter_var( $external, FILTER_VALIDATE_URL ) ) { $embed = wp_oembed_get( $external ); if ( $embed ) { return '
' . $this->sanitize_rendered_content( $embed ) . '
'; } $path = (string) wp_parse_url( $external, PHP_URL_PATH ); $ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ); if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif' ), true ) ) { return '
'; } if ( in_array( $ext, array( 'mp4', 'webm', 'ogv' ), true ) ) { return '
'; } return '

' . esc_html__( 'Ouvrir le média', 'lovely-notes' ) . '

'; } $allowed = array( 'iframe' => array( 'src' => true, 'title' => true, 'width' => true, 'height' => true, 'frameborder' => true, 'allow' => true, 'allowfullscreen' => true, 'loading' => true, 'referrerpolicy' => true, ), 'video' => array( 'src' => true, 'controls' => true, 'poster' => true, 'preload' => true, ), 'source' => array( 'src' => true, 'type' => true, ), ); return '
' . wp_kses( $external, $allowed, array( 'http', 'https' ) ) . '
'; } /** Read a published note by its permanent ID. No mutable offset in sharing URLs. */ public function ajax_read_note() { $note_id = absint( $this->request_value( 'note_id', 0 ) ); $note = $note_id ? self::get_public_note( $note_id ) : null; if ( ! $note ) { wp_send_json_error( array( 'message' => __( 'Cette note n’est pas disponible.', 'lovely-notes' ) ), 404 ); return; } $adjacent_ids = $this->get_archive_adjacent_note_ids( $note ); wp_send_json_success( array( 'id' => $note->ID, 'date' => $this->format_date( $note ), 'datetime' => get_post_time( DATE_W3C, false, $note ), 'content' => $this->render_content( $note ), 'lovely_eye' => Lovely_Notes::render_lovely_eye( $note->ID ), 'media' => $this->render_media( $note->ID ), 'older_id' => $adjacent_ids['older'], 'newer_id' => $adjacent_ids['newer'], 'share_url' => Lovely_Notes_Archive::get_note_url( $note->ID ), ) ); } /** Follow the same date / ID order as the reader, including equal timestamps. */ private function get_archive_adjacent_note_ids( WP_Post $note ) { return array( 'older' => $this->get_adjacent_note_id( $note, true ), 'newer' => $this->get_adjacent_note_id( $note, false ), ); } private function get_adjacent_note_id( WP_Post $note, $older ) { global $wpdb; $order = $older ? 'DESC' : 'ASC'; $where = static function ( $clause, $query ) use ( $wpdb, $note, $older ) { if ( ! $query->get( 'lovely_notes_adjacent' ) ) { return $clause; } if ( $older ) { return $clause . $wpdb->prepare( " AND ({$wpdb->posts}.post_date < %s OR ({$wpdb->posts}.post_date = %s AND {$wpdb->posts}.ID < %d))", $note->post_date, $note->post_date, $note->ID ); } return $clause . $wpdb->prepare( " AND ({$wpdb->posts}.post_date > %s OR ({$wpdb->posts}.post_date = %s AND {$wpdb->posts}.ID > %d))", $note->post_date, $note->post_date, $note->ID ); }; add_filter( 'posts_where', $where, 10, 2 ); try { $query = new WP_Query( array( 'post_type' => 'lovely_note', 'post_status' => 'publish', 'post_password' => '', 'posts_per_page' => 1, 'fields' => 'ids', 'orderby' => array( 'date' => $order, 'ID' => $order ), 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'lovely_notes_adjacent' => true, ) ); } finally { remove_filter( 'posts_where', $where, 10 ); } return ! empty( $query->posts ) ? absint( $query->posts[0] ) : 0; } }