Plugin Directory

source: signed-posts/trunk/signed-posts.php

Last change on this file was 3477129, checked in by Marc4, 3 weeks ago

v0.5

File size: 23.5 KB
Line 
1<?php
2/**
3 * Plugin Name: Signed Posts
4 * Plugin URI:  https://wordpress.org/plugins/signed-posts/
5 * Description: Signed Posts allows authors to sign posts, assuring content integrity. Signature verification proves post-signing alteration hasn't occurred.
6 * Version:     0.5
7 * Requires at least: 6.9
8 * Tested up to: 6.9
9 * Requires PHP: 8.2
10 * Tested up to PHP: 8.3
11 * Author:      Marc Armengou
12 * Author URI:  https://www.marcarmengou.com/
13 * Text Domain: signed-posts
14 * License:     GPLv2 or later
15 *
16 * @package SignedPosts
17 */
18
19if ( ! defined( 'ABSPATH' ) ) {
20        exit; // Exit if accessed directly.
21}
22
23// -----------------------------------------------------------
24// Utilities
25// -----------------------------------------------------------
26
27/**
28 * Returns the canonical post content string used for signing and verification.
29 *
30 * Normalises line endings to LF, trims surrounding whitespace, and appends a
31 * trailing newline so the string matches what the author signed.
32 *
33 * @param int $post_id The ID of the post whose content should be normalised.
34 * @return string Normalised post content ready for signature verification.
35 */
36function wppgps_normalize_post_content_for_signing( int $post_id ): string {
37        $signed_message = get_post_field( 'post_content', $post_id );
38        $signed_message = str_replace( "\r\n", "\n", $signed_message );
39        $signed_message = trim( $signed_message );
40        if ( ! empty( $signed_message ) ) {
41                $signed_message .= "\n";
42        }
43        return $signed_message;
44}
45
46/**
47 * Returns cached signature data for a given post.
48 *
49 * Fetches and caches (in a static variable) the signature, method, author
50 * identity meta, and derived flags for the post. This avoids repeated
51 * get_post_meta / get_user_meta calls across wppgps_enqueue_scripts,
52 * wppgps_append_signature_block, and wppgps_should_append_badge within the
53 * same request.
54 *
55 * @param int $post_id The post ID to retrieve signature data for.
56 * @return array{
57 *     signature: string,
58 *     method: string,
59 *     author_id: int,
60 *     pgp_key_url: string,
61 *     did_identifier: string,
62 *     has_identity: bool
63 * } Associative array of signature-related data for the post.
64 */
65function wppgps_get_post_signature_data( int $post_id ): array {
66        static $cache = array();
67
68        if ( isset( $cache[ $post_id ] ) ) {
69                return $cache[ $post_id ];
70        }
71
72        $signature = get_post_meta( $post_id, '_wppgps_signature', true );
73        $method    = get_post_meta( $post_id, '_wppgps_sig_method', true );
74        if ( ! in_array( $method, array( 'openpgp', 'did:key', 'did:web' ), true ) ) {
75                $method = 'openpgp';
76        }
77
78        $author_id      = (int) get_post_field( 'post_author', $post_id );
79        $pgp_key_url    = (string) get_user_meta( $author_id, 'pgp_key_url', true );
80        $did_identifier = (string) get_user_meta( $author_id, 'did_identifier', true );
81        $has_identity   = ( 'openpgp' === $method ) ? ( '' !== $pgp_key_url ) : ( '' !== $did_identifier );
82
83        $cache[ $post_id ] = array(
84                'signature'      => (string) $signature,
85                'method'         => $method,
86                'author_id'      => $author_id,
87                'pgp_key_url'    => $pgp_key_url,
88                'did_identifier' => $did_identifier,
89                'has_identity'   => $has_identity,
90        );
91
92        return $cache[ $post_id ];
93}
94
95// -----------------------------------------------------------
96// Activation
97// -----------------------------------------------------------
98
99/**
100 * Registers default plugin options when the plugin is activated.
101 *
102 * Sets all cleanup-on-uninstall preferences to '0' (preserve data) if they
103 * have not been set before. Uses add_option so existing values are never
104 * overwritten on re-activation.
105 *
106 * @return void
107 */
108function wppgps_activate(): void {
109        if ( false === get_option( 'wppgps_delete_post_signatures', false ) ) {
110                add_option( 'wppgps_delete_post_signatures', '0' );
111        }
112        if ( false === get_option( 'wppgps_delete_user_key_urls', false ) ) {
113                add_option( 'wppgps_delete_user_key_urls', '0' );
114        }
115        if ( false === get_option( 'wppgps_delete_user_did_identifiers', false ) ) {
116                add_option( 'wppgps_delete_user_did_identifiers', '0' );
117        }
118}
119register_activation_hook( __FILE__, 'wppgps_activate' );
120
121// -----------------------------------------------------------
122// 1. BACKEND: Author profile fields (OpenPGP URL + DID)
123// -----------------------------------------------------------
124
125/**
126 * Renders the OpenPGP public key URL field in the user profile screen.
127 *
128 * Only the profile owner can edit the field; other users see it as read-only.
129 *
130 * @param WP_User $user The user object whose profile is being displayed.
131 * @return void
132 */
133function wppgps_add_user_profile_fields( WP_User $user ): void {
134        $pgp_key_url   = (string) get_user_meta( $user->ID, 'pgp_key_url', true );
135        $readonly_attr = ( (int) $user->ID === get_current_user_id() ) ? '' : 'readonly';
136        ?>
137        <h2><?php esc_html_e( 'Signed Posts Settings', 'signed-posts' ); ?></h2>
138        <h3><?php esc_html_e( 'OpenPGP', 'signed-posts' ); ?></h3>
139        <table class="form-table" role="presentation">
140                <tr>
141                        <th><label for="pgp_key_url"><?php esc_html_e( 'URL of your OpenPGP Public Key', 'signed-posts' ); ?></label></th>
142                        <td>
143                                <input type="url" name="pgp_key_url" id="pgp_key_url" value="<?php echo esc_attr( $pgp_key_url ); ?>" class="regular-text" <?php echo esc_attr( $readonly_attr ); ?> />
144                                <p class="description">
145                                        <?php esc_html_e( 'Full URL to your OpenPGP public key. Must be hosted externally with CORS enabled. This URL is the trusted source for verification.', 'signed-posts' ); ?>
146                                </p>
147                        </td>
148                </tr>
149        </table>
150        <?php
151}
152add_action( 'show_user_profile', 'wppgps_add_user_profile_fields' );
153add_action( 'edit_user_profile', 'wppgps_add_user_profile_fields' );
154
155/**
156 * Renders the Decentralized Identifier (DID) field in the user profile screen.
157 *
158 * The profile owner and administrators can edit the field; other users see it
159 * as read-only.
160 *
161 * @param WP_User $user The user object whose profile is being displayed.
162 * @return void
163 */
164function wppgps_add_user_profile_fields_did( WP_User $user ): void {
165    $did           = (string) get_user_meta( $user->ID, 'did_identifier', true );
166    $readonly_attr = ( (int) $user->ID === get_current_user_id() || current_user_can( 'manage_options' ) ) ? '' : 'readonly';
167    ?>
168    <h3><?php esc_html_e( 'Decentralized Identifiers (DID)', 'signed-posts' ); ?></h3>
169    <table class="form-table" role="presentation">
170        <tr>
171            <th><label for="did_identifier"><?php esc_html_e( 'Your DID (did:key or did:web)', 'signed-posts' ); ?></label></th>
172            <td>
173                <input type="text" name="did_identifier" id="did_identifier" value="<?php echo esc_attr( $did ); ?>" class="regular-text" <?php echo esc_attr( $readonly_attr ); ?> placeholder="did:key:z6Mk... or did:web:example.com" />
174                <p class="description">
175                    <?php esc_html_e( 'Used for verifying detached JWS (Ed25519). For did:web, ensure a valid https://<host>/.well-known/did.json exists.', 'signed-posts' ); ?>
176                </p>
177            </td>
178        </tr>
179    </table>
180    <?php
181} // <--- LLAVE CORREGIDA AQUÍ
182add_action( 'show_user_profile', 'wppgps_add_user_profile_fields_did', 11 );
183add_action( 'edit_user_profile', 'wppgps_add_user_profile_fields_did', 11 );
184
185/**
186 * Saves the OpenPGP key URL and DID identifier from the user profile form.
187 *
188 * The OpenPGP URL can only be changed by the profile owner. The DID can be
189 * changed by the profile owner or a site administrator.
190 *
191 * @param int $user_id The ID of the user whose profile is being saved.
192 * @return void
193 */
194function wppgps_save_user_profile_fields( int $user_id ): void { // <--- TIPO CORREGIDO A VOID
195    if ( ! current_user_can( 'edit_user', $user_id ) ) {
196        return;
197    }
198    if ( ! isset( $_POST['_wpnonce'] ) ) {
199        return;
200    }
201    $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) );
202    if ( ! wp_verify_nonce( $nonce, 'update-user_' . $user_id ) ) {
203        return;
204    }
205
206    // OpenPGP URL: only the profile owner may change it.
207    if ( (int) $user_id === get_current_user_id() && isset( $_POST['pgp_key_url'] ) ) {
208        $url = esc_url_raw( wp_unslash( $_POST['pgp_key_url'] ) );
209        if ( $url && wp_is_valid_url( $url ) ) {
210            update_user_meta( $user_id, 'pgp_key_url', $url );
211        }
212    }
213
214    // DID: profile owner or administrators may change it.
215    if ( isset( $_POST['did_identifier'] ) && ( (int) $user_id === get_current_user_id() || current_user_can( 'manage_options' ) ) ) {
216        $val = sanitize_text_field( wp_unslash( $_POST['did_identifier'] ) );
217        if ( '' === $val || preg_match( '#^did:(key|web):#', $val ) ) {
218            update_user_meta( $user_id, 'did_identifier', $val );
219        }
220    }
221}
222add_action( 'personal_options_update', 'wppgps_save_user_profile_fields' );
223add_action( 'edit_user_profile_update', 'wppgps_save_user_profile_fields' );
224
225// -----------------------------------------------------------
226// 1.1 ADMIN: Uninstall cleanup preferences UI
227// -----------------------------------------------------------
228
229/**
230 * Renders the uninstall cleanup preference checkboxes in the user profile screen.
231 *
232 * Only visible to administrators (manage_options capability). The checkboxes
233 * control which plugin data is deleted when the plugin is uninstalled.
234 *
235 * @param WP_User $user The user object whose profile is being displayed.
236 * @return void
237 */
238function wppgps_render_cleanup_checkboxes( WP_User $user ): void {
239        if ( ! current_user_can( 'manage_options' ) ) {
240                return;
241        }
242        $del_sigs = get_option( 'wppgps_delete_post_signatures', '0' ) === '1';
243        $del_urls = get_option( 'wppgps_delete_user_key_urls', '0' ) === '1';
244        $del_dids = get_option( 'wppgps_delete_user_did_identifiers', '0' ) === '1';
245        ?>
246        <h2><?php esc_html_e( 'Cleanup preferences for Signed Posts', 'signed-posts' ); ?></h2>
247        <table class="form-table" role="presentation">
248                <tr>
249                        <th scope="row"><?php esc_html_e( 'On plugin uninstall', 'signed-posts' ); ?></th>
250                        <td>
251                                <fieldset>
252                                        <?php wp_nonce_field( 'wppgps_cleanup_save', 'wppgps_cleanup_nonce' ); ?>
253                                        <p>
254                                                <label for="wppgps_delete_user_key_urls">
255                                                <input type="checkbox" name="wppgps_delete_user_key_urls" id="wppgps_delete_user_key_urls" value="1" <?php checked( $del_urls ); ?> />
256                                                <?php esc_html_e( 'Delete all public key URLs stored in user meta', 'signed-posts' ); ?>
257                                                </label>
258                                        </p>
259                                        <p>
260                                                <label for="wppgps_delete_user_did_identifiers" style="margin-left: 12px;">
261                                                <input type="checkbox" name="wppgps_delete_user_did_identifiers" id="wppgps_delete_user_did_identifiers" value="1" <?php checked( $del_dids ); ?> />
262                                                <?php esc_html_e( 'Delete all DID identifiers stored in user meta', 'signed-posts' ); ?>
263                                                </label>
264                                        </p>
265                                        <p>
266                                                <label for="wppgps_delete_post_signatures" style="margin-left: 12px;">
267                                                <input type="checkbox" name="wppgps_delete_post_signatures" id="wppgps_delete_post_signatures" value="1" <?php checked( $del_sigs ); ?> />
268                                                <?php esc_html_e( 'Delete all signatures stored in post meta. You will need to re-sign and paste the signature on all posts if you check this box.', 'signed-posts' ); ?>
269                                                </label>
270                                        </p><br />
271                                </fieldset>
272                        </td>
273                </tr>
274        </table>
275        <?php
276}
277add_action( 'show_user_profile', 'wppgps_render_cleanup_checkboxes', 20 );
278add_action( 'edit_user_profile', 'wppgps_render_cleanup_checkboxes', 20 );
279
280/**
281 * Saves the uninstall cleanup preferences submitted from the user profile form.
282 *
283 * Requires the manage_options capability and a valid nonce. Each preference
284 * defaults to '0' (preserve data) when its checkbox is not present in the
285 * POST data.
286 *
287 * @param int $user_id The ID of the user whose profile form was submitted.
288 * @return void
289 */
290function wppgps_save_cleanup_checkboxes( int $user_id ): void {
291        if ( ! current_user_can( 'manage_options' ) ) {
292                return;
293        }
294        $nonce = isset( $_POST['wppgps_cleanup_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['wppgps_cleanup_nonce'] ) ) : '';
295        if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wppgps_cleanup_save' ) ) {
296                return;
297        }
298        update_option( 'wppgps_delete_post_signatures', isset( $_POST['wppgps_delete_post_signatures'] ) ? '1' : '0' );
299        update_option( 'wppgps_delete_user_key_urls', isset( $_POST['wppgps_delete_user_key_urls'] ) ? '1' : '0' );
300        update_option( 'wppgps_delete_user_did_identifiers', isset( $_POST['wppgps_delete_user_did_identifiers'] ) ? '1' : '0' );
301}
302add_action( 'personal_options_update', 'wppgps_save_cleanup_checkboxes' );
303add_action( 'edit_user_profile_update', 'wppgps_save_cleanup_checkboxes' );
304
305// -----------------------------------------------------------
306// 2. BACKEND: Metabox (signature + method selector)
307// -----------------------------------------------------------
308
309/**
310 * Registers the Signed Posts metabox on the post edit screen.
311 *
312 * @return void
313 */
314function wppgps_add_metabox(): void {
315        add_meta_box(
316                'wppgps_signature_box',
317                esc_html__( 'Signed Posts', 'signed-posts' ),
318                'wppgps_metabox_callback',
319                'post',
320                'normal',
321                'high'
322        );
323}
324add_action( 'add_meta_boxes', 'wppgps_add_metabox' );
325
326/**
327 * Renders the content of the Signed Posts metabox.
328 *
329 * Displays the signature method selector, the current verification source,
330 * and the signature textarea. Only the post author can edit the signature
331 * field; other users see it as read-only.
332 *
333 * @param WP_Post $post The post object being edited.
334 * @return void
335 */
336function wppgps_metabox_callback( WP_Post $post ): void {
337        wp_nonce_field( 'wppgps_save_data', 'wppgps_nonce' );
338
339        $sig    = (string) get_post_meta( $post->ID, '_wppgps_signature', true );
340        $method = (string) get_post_meta( $post->ID, '_wppgps_sig_method', true );
341        if ( ! in_array( $method, array( 'openpgp', 'did:key', 'did:web' ), true ) ) {
342                $method = 'openpgp';
343        }
344
345        $post_author_id  = (int) $post->post_author;
346        $current_user_id = get_current_user_id();
347        $is_author       = ( $post_author_id === $current_user_id );
348        $pgp_key_url     = (string) get_user_meta( $post_author_id, 'pgp_key_url', true );
349        $did_id          = (string) get_user_meta( $post_author_id, 'did_identifier', true );
350        ?>
351        <p>
352                <label for="wppgps_sig_method"><strong><?php esc_html_e( 'Signature method', 'signed-posts' ); ?></strong></label><br/>
353                <select id="wppgps_sig_method" name="wppgps_sig_method">
354                        <option value="openpgp" <?php selected( $method, 'openpgp' ); ?>>OpenPGP</option>
355                        <option value="did:key" <?php selected( $method, 'did:key' ); ?>>DID (did:key)</option>
356                        <option value="did:web" <?php selected( $method, 'did:web' ); ?>>DID (did:web)</option>
357                </select>
358        </p>
359        <?php
360        if ( 'openpgp' === $method && empty( $pgp_key_url ) ) {
361                echo '<p style="color: red; font-weight: bold;">⚠️ ' . esc_html__( 'ERROR: The post author has NOT configured their OpenPGP Public Key URL.', 'signed-posts' ) . '</p>';
362                echo '<p>' . esc_html__( 'Please edit your', 'signed-posts' ) . ' <a href="' . esc_url( admin_url( 'profile.php' ) ) . '">' . esc_html__( 'User Profile', 'signed-posts' ) . '</a> ' . esc_html__( 'to add your key URL before signing.', 'signed-posts' ) . '</p>';
363        }
364        if ( ( 'did:key' === $method || 'did:web' === $method ) && empty( $did_id ) ) {
365                echo '<p style="color: #d98300; font-weight: bold;">⚠️ ' . esc_html__( 'WARNING: The post author has NOT configured a DID in their profile.', 'signed-posts' ) . '</p>';
366        }
367        $readonly = $is_author ? '' : 'readonly';
368        ?>
369        <p>
370                <strong><?php esc_html_e( 'Verification source:', 'signed-posts' ); ?></strong>
371                <code><?php echo esc_html( ( 'openpgp' === $method ? $pgp_key_url : $did_id ) ?: '—' ); ?></code>
372        </p>
373        <label for="wppgps_signature"><?php esc_html_e( 'Content signature (OpenPGP ASCII armor OR JWS compact detached, depending on method):', 'signed-posts' ); ?></label>
374        <textarea id="wppgps_signature" name="wppgps_signature" rows="8" style="width:100%;" placeholder="<?php echo ( 'openpgp' === $method ) ? esc_attr__( '-----BEGIN PGP SIGNATURE-----...', 'signed-posts' ) : esc_attr__( 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImtpZCI6ImRpZDp...<detached JWS>', 'signed-posts' ); ?>" <?php echo esc_attr( $readonly ); ?>><?php echo esc_textarea( $sig ); ?></textarea>
375        <p style="font-size: 0.9em; color: #555;">
376                *<?php esc_html_e( 'Only the original author can save or modify the signature. This prevents manipulation by Editors or Administrators.', 'signed-posts' ); ?>*
377        </p>
378        <?php
379}
380
381/**
382 * Saves the signature method and signature value from the post edit screen.
383 *
384 * Only the original post author can save or modify signature data. Runs on
385 * save_post but skips autosaves.
386 *
387 * @param int $post_id The ID of the post being saved.
388 * @return int The post ID, unchanged.
389 */
390function wppgps_save_post_data( int $post_id ): int {
391        if ( ! isset( $_POST['wppgps_nonce'] ) ) {
392                return $post_id;
393        }
394        $nonce = sanitize_text_field( wp_unslash( $_POST['wppgps_nonce'] ) );
395        if ( ! wp_verify_nonce( $nonce, 'wppgps_save_data' ) ) {
396                return $post_id;
397        }
398        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
399                return $post_id;
400        }
401        $post_author_id = (int) get_post_field( 'post_author', $post_id );
402        if ( get_current_user_id() !== $post_author_id ) {
403                return $post_id;
404        }
405        if ( isset( $_POST['wppgps_sig_method'] ) ) {
406                $method = sanitize_text_field( wp_unslash( $_POST['wppgps_sig_method'] ) );
407                if ( in_array( $method, array( 'openpgp', 'did:key', 'did:web' ), true ) ) {
408                        update_post_meta( $post_id, '_wppgps_sig_method', $method );
409                }
410        }
411        if ( isset( $_POST['wppgps_signature'] ) ) {
412                update_post_meta(
413                        $post_id,
414                        '_wppgps_signature',
415                        sanitize_textarea_field( wp_unslash( $_POST['wppgps_signature'] ) )
416                );
417        }
418        return $post_id;
419}
420add_action( 'save_post', 'wppgps_save_post_data' );
421
422// -----------------------------------------------------------
423// 3. FRONTEND: Enqueue, Signature block & Author badge
424// -----------------------------------------------------------
425
426/**
427 * Enqueues front-end styles and scripts for signature verification.
428 *
429 * Only runs on singular post views. Loads the appropriate verification script
430 * (OpenPGP.js or DID/WebCrypto) based on the post's signature method, and
431 * passes the required data to JavaScript via wp_localize_script.
432 *
433 * @return void
434 */
435function wppgps_enqueue_scripts(): void {
436        if ( ! is_singular( 'post' ) ) {
437                return;
438        }
439        global $post;
440        $data = wppgps_get_post_signature_data( (int) $post->ID );
441
442        if ( empty( $data['signature'] ) || ! $data['has_identity'] ) {
443                return;
444        }
445
446        wp_enqueue_style( 'wppgps-styles', plugin_dir_url( __FILE__ ) . 'signed-posts.css', array(), '0.5', 'all' );
447        if ( ! wp_style_is( 'dashicons', 'enqueued' ) ) {
448                wp_enqueue_style( 'dashicons' );
449        }
450
451        $message = wppgps_normalize_post_content_for_signing( (int) $post->ID );
452
453        if ( 'openpgp' === $data['method'] ) {
454                wp_enqueue_script( 'openpgp-js', plugin_dir_url( __FILE__ ) . 'openpgp.min.js', array(), '6.2.2', true );
455                wp_enqueue_script( 'wppgps-verifier', plugin_dir_url( __FILE__ ) . 'signed-posts.js', array( 'openpgp-js' ), '0.5', true );
456                wp_localize_script(
457                        'wppgps-verifier',
458                        'wppgpsData',
459                        array(
460                                'postId'       => (int) $post->ID,
461                                'method'       => 'openpgp',
462                                'message'      => $message,
463                                'signature'    => $data['signature'],
464                                'publicKeyUrl' => $data['pgp_key_url'],
465                                'badgeToken'   => '[[WPPGPS_BADGE:' . (int) $post->ID . ']]',
466                        )
467                );
468        } else {
469                wp_enqueue_script( 'wppgps-did', plugin_dir_url( __FILE__ ) . 'signed-posts.did.js', array(), '0.5', true );
470                wp_localize_script(
471                        'wppgps-did',
472                        'wppgpsData',
473                        array(
474                                'postId'     => (int) $post->ID,
475                                'method'     => $data['method'],
476                                'message'    => $message,
477                                'signature'  => $data['signature'],
478                                'did'        => $data['did_identifier'],
479                                'badgeToken' => '[[WPPGPS_BADGE:' . (int) $post->ID . ']]',
480                        )
481                );
482        }
483}
484add_action( 'wp_enqueue_scripts', 'wppgps_enqueue_scripts' );
485
486/**
487 * Appends the signature verification status block to the post content.
488 *
489 * Only runs on singular post views, within the main loop. The block is
490 * populated client-side by the verification script.
491 *
492 * @param string $content The original post content.
493 * @return string The post content with the signature block appended, or the
494 *                original content if the post is not signed.
495 */
496function wppgps_append_signature_block( string $content ): string {
497        if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
498                return $content;
499        }
500        global $post;
501        $data = wppgps_get_post_signature_data( (int) $post->ID );
502
503        if ( empty( $data['signature'] ) || ! $data['has_identity'] ) {
504                return $content;
505        }
506
507        $signature_block = '
508                <div id="pgp-signature-block" class="pgp-signature-block">
509                        <div class="pgp-header">
510                                <h3 class="pgp-title">' . esc_html__( 'Status of the Signature', 'signed-posts' ) . '</h3>
511                                <span class="pgp-icon">🔒</span>
512                        </div>
513                        <div class="pgp-content">
514                                <div id="pgp-verification-result" class="pgp-status-container">
515                                        <span class="pgp-status-text pgp-status-pending">' . esc_html__( 'Starting verification...', 'signed-posts' ) . '</span>
516                                </div>
517                                <div class="pgp-details">
518                                        <p>
519                                                <span class="pgp-detail-label">' . esc_html__( 'Method:', 'signed-posts' ) . '</span>
520                                                <span id="pgp-method-value" class="pgp-detail-value">' . ( 'openpgp' === $data['method'] ? esc_html__( 'Verified in your browser with OpenPGP.js', 'signed-posts' ) : esc_html__( 'Verified in your browser with DID (Ed25519 JWS)', 'signed-posts' ) ) . '</span>
521                                        </p>
522                                        <p>
523                                                <span class="pgp-detail-label">' . esc_html__( 'Source:', 'signed-posts' ) . '</span>
524                                                <span id="pgp-key-url-link" class="pgp-detail-value pgp-link" style="word-break: break-all;"></span>
525                                        </p>
526                                        <p class="pgp-result-message">
527                                                <span class="pgp-detail-label">' . esc_html__( 'Result:', 'signed-posts' ) . '</span>
528                                                <span id="pgp-result-details" class="pgp-detail-value"></span>
529                                        </p>
530                                </div>
531                        </div>
532                </div>
533        ';
534
535        return $content . $signature_block;
536}
537add_filter( 'the_content', 'wppgps_append_signature_block' );
538
539// -----------------------------------------------------------
540// 3.1 AUTHOR BADGE
541// -----------------------------------------------------------
542
543/**
544 * Determines whether the author badge should be appended to author output.
545 *
546 * Returns true only on singular post views where the post has a signature and
547 * the author has the required identity configured (OpenPGP URL or DID).
548 *
549 * @return bool True if the badge should be appended; false otherwise.
550 */
551function wppgps_should_append_badge(): bool {
552        if ( ! is_singular( 'post' ) ) {
553                return false;
554        }
555        global $post;
556        if ( ! $post ) {
557                return false;
558        }
559        $data = wppgps_get_post_signature_data( (int) $post->ID );
560        return ( ! empty( $data['signature'] ) && $data['has_identity'] );
561}
562
563/**
564 * Returns the placeholder token string used to locate the badge insertion point.
565 *
566 * @param int $post_id The post ID the badge belongs to.
567 * @return string The badge placeholder token.
568 */
569function wppgps_get_author_badge_token( int $post_id ): string {
570        return '[[WPPGPS_BADGE:' . $post_id . ']]';
571}
572
573/**
574 * Appends the author badge placeholder token to author display output.
575 *
576 * The token is later replaced with the actual badge HTML by the front-end
577 * verification script. Skips appending if a token is already present in the
578 * output to avoid duplication.
579 *
580 * @param string $output The current author display string (name or link).
581 * @return string The output with the badge token appended, or unchanged.
582 */
583function wppgps_append_badge_to_output( string $output ): string {
584        if ( ! wppgps_should_append_badge() ) {
585                return $output;
586        }
587        if ( false !== strpos( $output, '[[WPPGPS_BADGE:' ) ) {
588                return $output;
589        }
590        global $post;
591        return $output . wppgps_get_author_badge_token( (int) $post->ID );
592}
593
594add_filter( 'the_author_posts_link', 'wppgps_append_badge_to_output', 20 );
595add_filter( 'the_author', 'wppgps_append_badge_to_output', 20 );
596add_filter( 'get_the_author', 'wppgps_append_badge_to_output', 20 );
597add_filter( 'get_the_author_display_name', 'wppgps_append_badge_to_output', 20 );
Note: See TracBrowser for help on using the repository browser.