,

Gemini-Assisted WordPress Plugin: Corrections 3

So, I decided to beautify the code, and the tool told me there were a lot of errors and problems, so I spent today trying to fix those.

Start time: 2:37, end time: 4:50. I still had problems, but…

Some of the errors were not errors. Some of the errors, when fixed, caused minor bugs. Rearranging the code revealed a menu bug. Changing a deprecated function call got rid of an error, but introduced a logic bug.

I’m not sure if the WordPress coding standard is an improvement, but I’ll keep trying to conform to it.

I need to factor out the reCAPTCHA stuff so it can be disabled, for testing purposes.

<?php
/**
 * Plugin Name: Correction Feedback Plugin
 * Description: A very basic plugin example.
 * Version: 1.0
 * Author: Your Name
 *
 * @package johnk/plugin
 */

/**
 * PLUGIN ACTIVATION/DEACTIVATION
 */

/**
 * Plugin Activation Function.
 */
function correction_plugin_activate() {

	$page_title   = 'Submit a Correction';
	$page_content = '[correction_form]';

	$correction_pages = get_posts(
		array(
			'post_type' => 'page',
			'title'     => $page_title,
		)
	);

	if ( count( $correction_pages ) < 1 ) {
		$page_id = wp_insert_post(
			array(
				'post_title'   => $page_title,
				'post_content' => $page_content,
				'post_status'  => 'publish',
				'post_type'    => 'page',
			)
		);
		if ( $page_id ) {
			update_option( 'correction_form_page_id', $page_id );
		}
	} else {
        $page = $correction_pages[0];
		update_option( 'correction_form_page_id', $page->ID );
	}
	flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'correction_plugin_activate' );

/**
 * Plugin Deactivation Function
 */
function correction_plugin_deactivate() {
	unregister_post_type( 'correction' );
	delete_option( 'correction_form_page_id' );
	flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'correction_plugin_deactivate' );

/**
 * CUSTOM POST TYPES
 */

/**
 * Correction post type.
 */
function register_correction_post_type() {
	register_post_type(
		'correction', // $post_type
		array(
			'labels'          => array(
				'name'               => __( 'Corrections', 'textdomain' ),
				'singular_name'      => __( 'Correction', 'textdomain' ),
				'menu_name'          => __( 'Corrections', 'textdomain' ),
				'all_items'          => __( 'All Corrections', 'textdomain' ),
				'add_new'            => __( 'Add New', 'textdomain' ),
				'add_new_item'       => __( 'Add New Correction', 'textdomain' ),
				'edit_item'          => __( 'Edit Correction', 'textdomain' ),
				'new_item'           => __( 'New Correction', 'textdomain' ),
				'view_item'          => __( 'View Correction', 'textdomain' ),
				'search_items'       => __( 'Search Corrections', 'textdomain' ),
				'not_found'          => __( 'No corrections found', 'textdomain' ),
				'not_found_in_trash' => __( 'No corrections found in Trash', 'textdomain' ),
			),
			'public'          => true, // Show in admin menu.
			'show_ui'         => true, // Show admin UI.
			'show_in_menu'    => true, // show in the admin menu.
			'capability_type' => 'correction',
			'hierarchical'    => false,
			'rewrite'         => array( 'slug' => 'correction' ),
			'query_var'       => true,
			'supports'        => array( 'title', 'editor' ),
			'menu_position'   => 30,
			'has_archive'     => false, // No archive page.
		)
	);
}

/**
 * SHORTCODES
 */

/**
 * Shortcode to display the link.
 */
function correction_link_shortcode() {
	global $post;
	if ( ! $post ) {
		return '';
	}
	$form_url  = add_query_arg(
		array(
			'correction_form' => 'true',
			'post_id'         => $post->ID,
		),
		get_permalink( get_option( 'correction_form_page_id' ) )
	);
	$icon      = '[]';
	$link_text = 'Submit a Correction ';
	$link      = '<a href="' . esc_url( $form_url ) . '" target="_blank">' . esc_html( $link_text ) . $icon . '</a>';
	return $link;
}

/**
 * Shortcode to display and handle the correction form.
 */
function correction_form_shortcode() {
	ob_start();

	$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : 0;

	if ( $post_id <= 0 ) {
		echo '<p>Invalid post ID.</p>';
		return ob_get_clean();
	}

	if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' === $_SERVER['REQUEST_METHOD'] ) {
		$recaptcha_site_key = esc_attr( get_option( 'correction_recaptcha_site_key' ) );
		$nonce              = wp_create_nonce( 'correction_form_nonce' );

		echo '<form method="post">';
		echo '<input type="hidden" name="post_id" value="' . esc_attr( $post_id ) . '">';
		echo '<input type="hidden" name="correction_form_nonce" value="' . esc_attr( $nonce ) . '">';
		echo '<label for="original_sentence">Original Sentence (Paste Here):</label><br>';
		echo '<textarea name="original_sentence" id="original_sentence" rows="4" cols="50" required></textarea><br><br>';
		echo '<label for="correction">Your Correction:</label><br>';
		echo '<textarea name="correction-text" id="correction" rows="4" cols="50" required></textarea><br><br>';
		echo '<label for="email">Your Email:</label><br>';
		echo '<input type="email" name="correction-email" id="email"><br><br>';
		echo '<input type="hidden" name="recaptcha_token" id="recaptcha_token">';
		echo '<input type="submit" value="Submit">';
		echo '</form>';

		if ( ! empty( $recaptcha_site_key ) ) {
			echo '<script src="https://www.google.com/recaptcha/api.js?render=' . esc_attr( $recaptcha_site_key ) . '"></script>';
			echo '<script>';
			echo 'grecaptcha.ready(function() {';
			echo 'grecaptcha.execute("' . esc_attr( $recaptcha_site_key ) . '", {action: "correction_form"}).then(function(token) {';
			echo 'document.getElementById("recaptcha_token").value = token;';
			echo '});';
			echo '});';
			echo '</script>';
		}

		return ob_get_clean();
	} else {
		return correction_form_handler();
	}
}

/**
 * Handle the correction form submission.
 */
function correction_form_handler() {
	if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
		ob_start();

		if ( ! isset( $_POST['correction_form_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['correction_form_nonce'] ) ), 'correction_form_nonce' ) ) {
			echo '<p>Security check failed.</p>';
			return ob_get_clean();
		}

		$original_sentence = isset( $_POST['original_sentence'] ) ? sanitize_textarea_field( wp_unslash( $_POST['original_sentence'] ) ) : '';
		$correction        = isset( $_POST['correction-text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['correction-text'] ) ) : '';
		$email             = isset( $_POST['correction-email'] ) ? sanitize_email( wp_unslash( $_POST['correction-email'] ) ) : '';
		$post_id           = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
		$recaptcha_token   = isset( $_POST['recaptcha_token'] ) ? sanitize_text_field( wp_unslash( $_POST['recaptcha_token'] ) ) : '';

		if ( empty( $original_sentence ) || empty( $correction ) || empty( $email ) ) {
			echo '<p>Please fill in all required fields.</p>';
			return ob_get_clean();
		}

		if ( ! correction_verify_recaptcha( $recaptcha_token ) ) {
			echo '<p>reCAPTCHA verification failed. You might be a bot.</p>';
			return ob_get_clean();
		}

		$correction_post = array(
			'post_type'    => 'correction',
			'post_status'  => 'pending',
			'post_title'   => 'Correction for Post ID: ' . $post_id,
			'post_content' => "Original Sentence:\n" . $original_sentence . "\n\nCorrection:\n" . $correction . "\n\nEmail: " . $email,
			'meta_input'   => array(
				'original_post_id'  => $post_id,
				'original_sentence' => $original_sentence,
				'correction'        => $correction,
				'email'             => $email,
			),
		);

		$result = wp_insert_post( $correction_post, true );

		$admin_email = get_option( 'admin_email' );
		$subject     = 'New Correction Submitted';
		$message     = "A new correction has been submitted for post ID: {$post_id}\n\n";
		$message    .= "Original Sentence:\n{$original_sentence}\n\n";
		$message    .= "Correction:\n{$correction}\n\n";
		$message    .= "Email: {$email}\n";

		wp_mail( $admin_email, $subject, $message );

		echo '<p>Thank you for your correction! It has been submitted.</p>';
		return ob_get_clean();
	}
}

/**
 * Verifies the reCAPTCHA token.
 *
 * @param string $recaptcha_token The reCAPTCHA token.
 *
 * @return bool True if verification succeeds, false otherwise.
 */
function correction_verify_recaptcha( $recaptcha_token ) {
	$recaptcha_secret_key = esc_attr( get_option( 'correction_recaptcha_secret_key' ) );

	if ( empty( $recaptcha_secret_key ) ) {
		return false;
	}

	$recaptcha_url      = 'https://www.google.com/recaptcha/api/siteverify';
	$recaptcha_response = wp_remote_post(
		$recaptcha_url,
		array(
			'body' => array(
				'secret'   => $recaptcha_secret_key,
				'response' => $recaptcha_token,
			),
		)
	);

	if ( is_wp_error( $recaptcha_response ) ) {
		return false;
	}

	$recaptcha_body = json_decode( wp_remote_retrieve_body( $recaptcha_response ) );

	return $recaptcha_body->success && $recaptcha_body->score >= 0.5;
}

/**
 * ADMIN
 */

/**
 * Add the Correction menu.
 */
function correction_admin_menu() {
	add_menu_page(
		'Corrections', // Page title.
		'Corrections', // Menu title.
		'edit_posts', // Capability.
		'correction', // Menu slug.
		'correction_admin_page', // Callback function.
		'dashicons-edit',
		30
	);
}

/**
 * Add settings submenu.
 */
function correction_add_settings_submenu() {
	add_submenu_page(
		'correction', // Parent slug (the main Corrections menu slug).
		'Correction Settings', // Page title.
		'Settings', // Menu title.
		'manage_options', // Capability.
		'options-general.php?page=correction-settings', // Menu slug.
		'correction_settings_page' // Callback function.
	);
}

/**
 * Admin page content.
 */
function correction_admin_page() {
	?>
	<div class="wrap">
		<h2>Corrections</h2>
		<table class="wp-list-table widefat fixed striped posts">
			<thead>
				<tr>
					<th>Post ID</th>
					<th>Original Sentence</th>
					<th>Correction</th>
					<th>Email</th>
					<th>Actions</th>
				</tr>
			</thead>
			<tbody>
				<?php
				$corrections = get_posts(
					array(
						'post_type'   => 'correction',
						'post_status' => 'any',
						'numberposts' => 10,
					)
				);

				foreach ( $corrections as $correction ) {
					$original_post_id   = get_post_meta( $correction->ID, 'original_post_id', true );
					$original_sentence  = get_post_meta( $correction->ID, 'original_sentence', true );
					$corrected_sentence = get_post_meta( $correction->ID, 'correction', true );
					$email              = get_post_meta( $correction->ID, 'email', true );
					$status             = $correction->post_status;

					// Check if the original post exists.
					$original_post = get_post( $original_post_id );
					if ( $original_post ) {
						$original_post_link = '<a href="' . esc_url( get_edit_post_link( $original_post_id ) ) . '">' . esc_html( $original_post->post_title ) . '</a>';
					} else {
						$original_post_link = 'Post ID: ' . esc_html( $original_post_id ) . ' (Not Found)';
					}
					?>
					<tr>
						<td><?php echo $original_post_link; ?></td>					 
						<td><?php echo esc_html( $original_sentence ); ?></td>
						<td><?php echo esc_html( $corrected_sentence ); ?></td>
						<td><?php echo esc_html( $email ); ?></td>
						<td>
							<a href="
							<?php
							echo esc_url(
								add_query_arg(
									array(
										'action'        => 'delete_correction',
										'correction_id' => $correction->ID,
									)
								)
							);
							?>
										">Delete</a>
						</td>
					</tr>
				<?php } ?>
			</tbody>
		</table>
	</div>
	<?php
}

/**
 * Handle delete actions.
 */
function handle_correction_actions() {
	if ( isset( $_GET['action'] ) && 'delete_correction ' === $_GET['action'] && isset( $_GET['correction_id'] ) ) {
		$correction_id = intval( $_GET['correction_id'] );

		if ( $correction_id ) {
			wp_delete_post( $correction_id, true ); // true = force delete.

			// Redirect back to the corrections admin page.
			wp_safe_redirect( admin_url( 'admin.php?page=correction' ) ); // Ensure to use admin.php?page=correction for menu slug.
			exit();
		}
	}
}

/**
 * Add settings page and menu.
 */
function correction_add_settings_page() {
	add_options_page(
		'Correction Settings',
		'Correction',
		'manage_options',
		'correction-settings',
		'correction_settings_page'
	);
}

/**
 * Settings page content.
 */
function correction_settings_page() {
	?>
	<div class="wrap">
		<h2>Correction Settings</h2>
		<form method="post" action="options.php">
			<?php
			settings_fields( 'correction_settings_group' );
			do_settings_sections( 'correction-settings' );
			submit_button();
			?>
		</form>
	</div>
	<?php
}

/**
 * Register settings.
 */
function correction_register_settings() {
	register_setting( 'correction_settings_group', 'correction_recaptcha_site_key' );
	register_setting( 'correction_settings_group', 'correction_recaptcha_secret_key' );

	add_settings_section(
		'correction_recaptcha_settings',
		'reCAPTCHA Settings',
		'correction_recaptcha_settings_callback',
		'correction-settings'
	);

	add_settings_field(
		'correction_recaptcha_site_key',
		'reCAPTCHA Site Key',
		'correction_recaptcha_site_key_callback',
		'correction-settings',
		'correction_recaptcha_settings'
	);

	add_settings_field(
		'correction_recaptcha_secret_key',
		'reCAPTCHA Secret Key',
		'correction_recaptcha_secret_key_callback',
		'correction-settings',
		'correction_recaptcha_settings'
	);
}

/**
 * Settings heading.
 */
function correction_recaptcha_settings_callback() {
	echo 'Enter your reCAPTCHA v3 site key and secret key.';
}

/**
 * Show the site key field.
 */
function correction_recaptcha_site_key_callback() {
	$site_key = get_option( 'correction_recaptcha_site_key' );
	echo "<input type='text' name='correction_recaptcha_site_key' value='" . esc_attr( $site_key ) . "' size='50' />";
}

/**
 * Show the secret key field.
 */
function correction_recaptcha_secret_key_callback() {
	$secret_key = get_option( 'correction_recaptcha_secret_key' );
	echo "<input type='text' name='correction_recaptcha_secret_key' value='" . esc_attr( $secret_key ) . "' size='50' />";
}

/**
 * INIT SECTION
 */

/**
 * Admin notice error shown when permalinks are off.
 */
function correction_permalink_notice() {
	?>
	<div class="notice notice-error">
		<p><?php esc_html_e( 'The Correction Plugin requires using permalinks. Please enable permalinks in your WordPress settings.', 'textdomain' ); ?></p>
	</div>
	<?php
}

/**
 * I like to keep the callbacks together.
 */
function correction_init() {
	global $wp_rewrite;
	if ( ! $wp_rewrite->using_permalinks() ) {
		add_action( 'admin_notices', 'correction_permalink_notice' );
	} else {
		add_shortcode( 'correction_link', 'correction_link_shortcode' );
		add_shortcode( 'correction_form', 'correction_form_shortcode' );
		add_action( 'init', 'register_correction_post_type' );
		add_action( 'admin_init', 'handle_correction_actions' );
		add_action( 'admin_init', 'correction_register_settings' );
		add_action( 'admin_menu', 'correction_admin_menu' );
		add_action( 'admin_menu', 'correction_add_settings_page' );
		add_action( 'admin_menu', 'correction_add_settings_submenu' );
	}
}
add_action( 'init', 'correction_init', 5 );