scanner = $scanner; $this->fixer = $fixer; } public function register_hooks() { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_filter( 'plugin_action_links_' . plugin_basename( OWWCQC_DIR . '1webworld-content-quality-checker.php' ), array( $this, 'plugin_action_links' ) ); add_action( 'add_meta_boxes', array( $this, 'meta_boxes' ) ); add_action( 'admin_post_owwcqc_safe_fix', array( $this, 'handle_safe_fix' ) ); add_action( 'admin_post_owwcqc_fix_one', array( $this, 'handle_fix_one' ) ); foreach ( owwcqc_post_types() as $type ) { add_filter( "manage_{$type}_posts_columns", array( $this, 'column' ) ); add_action( "manage_{$type}_posts_custom_column", array( $this, 'column_value' ), 10, 2 ); } } public function menu() { add_management_page( '1WebWorld Content Quality Checker', 'Content Quality Checker', 'edit_others_posts', '1webworld-content-quality-checker', array( $this, 'page' ) ); add_submenu_page( 'options.php', '1WebWorld Content Checker Guide', 'Content Checker Guide', 'edit_others_posts', '1webworld-content-quality-checker-guide', array( $this, 'documentation_page' ) ); } public function plugin_action_links( $links ) { $review_url = admin_url( 'tools.php?page=1webworld-content-quality-checker' ); array_unshift( $links, 'Open checker' ); return $links; } public function assets( $hook ) { global $post; $screen = get_current_screen(); $plugin_pages = array( 'tools_page_1webworld-content-quality-checker', 'admin_page_1webworld-content-quality-checker-guide' ); if ( ! in_array( $hook, $plugin_pages, true ) && ( ! $screen || ! in_array( $screen->post_type, owwcqc_post_types(), true ) ) ) { return; } wp_enqueue_style( 'owwcqc-admin', OWWCQC_URL . 'admin/css/admin.css', array(), OWWCQC_VERSION ); wp_enqueue_style( 'owwcqc-compat', OWWCQC_URL . 'admin/css/compat.css', array( 'owwcqc-admin' ), OWWCQC_VERSION ); wp_enqueue_script( 'owwcqc-admin', OWWCQC_URL . 'admin/js/admin.js', array(), OWWCQC_VERSION, true ); if ( $screen && in_array( $screen->post_type, owwcqc_post_types(), true ) ) { $post_id = $post instanceof WP_Post ? $post->ID : 0; $nonce = isset( $_GET['_owwcqc_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_owwcqc_nonce'] ) ) : ''; $highlight_allowed = $post_id && wp_verify_nonce( $nonce, 'owwcqc_highlight_' . $post_id ); wp_localize_script( 'owwcqc-admin', 'owwcqcData', array( 'highlight' => $highlight_allowed && isset( $_GET['owwcqc_highlight'] ) && 1 === absint( wp_unslash( $_GET['owwcqc_highlight'] ) ) ? 1 : 0, 'rule' => $highlight_allowed && isset( $_GET['owwcqc_rule'] ) ? sanitize_key( wp_unslash( $_GET['owwcqc_rule'] ) ) : '', 'index' => $highlight_allowed && isset( $_GET['owwcqc_index'] ) ? absint( wp_unslash( $_GET['owwcqc_index'] ) ) : null, 'details' => $post_id ? $this->scanner->details_post( $post_id ) : array(), 'lineCount' => $post_id ? substr_count( (string) get_post_field( 'post_content', $post_id ), "\n" ) + 1 : 1, ) ); } } public function column( $columns ) { $columns['owwcqc_issues'] = 'Content Issues'; return $columns; } public function column_value( $column, $post_id ) { if ( 'owwcqc_issues' !== $column ) { return; } $total = $this->scanner->total( $this->scanner->scan_post( $post_id ) ); echo '' . esc_html( $total . ( 1 === $total ? ' issue' : ' issues' ) ) . ''; } public function meta_boxes() { foreach ( owwcqc_post_types() as $type ) { add_meta_box( 'owwcqc-editor-review', '1WebWorld Content Review', array( $this, 'meta_box' ), $type, 'side', 'high' ); } } public function meta_box( $post ) { $issues = $this->scanner->scan( $post->post_content ); $details = $this->scanner->details( $post->post_content ); $this->summary( $issues ); echo '

Based on the last saved version. Update the page to refresh.

'; $this->issue_list( $issues ); $this->detail_list( $details, $post, true ); $url = $this->review_url( $post->ID ); echo '

Open full review

'; $this->revision_link( $post ); $this->editor_documentation(); } public function page() { if ( ! current_user_can( 'edit_others_posts' ) ) { wp_die( esc_html__( 'You do not have permission to view this report.', '1webworld-content-quality-checker' ) ); } $requested_review_id = isset( $_GET['owwcqc_review_post'] ) ? absint( wp_unslash( $_GET['owwcqc_review_post'] ) ) : 0; $review_nonce = isset( $_GET['_owwcqc_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_owwcqc_nonce'] ) ) : ''; $review_id = $requested_review_id && wp_verify_nonce( $review_nonce, 'owwcqc_review_' . $requested_review_id ) ? $requested_review_id : 0; $notice_nonce = isset( $_GET['_owwcqc_notice_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_owwcqc_notice_nonce'] ) ) : ''; $notice_allowed = $review_id && wp_verify_nonce( $notice_nonce, 'owwcqc_notice_' . $review_id ); echo '
'; $this->page_header( '1WebWorld Content Quality Checker', 'Review saved content, locate common issues, and apply only the fixes you approve.', 'View Documentation', admin_url( 'admin.php?page=1webworld-content-quality-checker-guide' ) ); if ( $notice_allowed && isset( $_GET['owwcqc_fixed'] ) ) { echo '

' . ( 'one' === sanitize_key( wp_unslash( $_GET['owwcqc_fixed'] ) ) ? 'One issue was safely fixed.' : 'Safe fixes were applied.' ) . ' A revision backup was created first.

'; } if ( $review_id ) { $this->single_review( $review_id ); } else { $this->all_content(); } echo '
'; } private function page_header( $title, $description, $action_label = '', $action_url = '' ) { echo '
'; echo ''; echo '

' . esc_html( $title ) . '

v' . esc_html( OWWCQC_VERSION ) . '

' . esc_html( $description ) . '

'; if ( $action_label && $action_url ) { echo '' . esc_html( $action_label ) . ''; } echo '
'; } public function documentation_page() { if ( ! current_user_can( 'edit_others_posts' ) ) { wp_die( esc_html__( 'You do not have permission to view this guide.', '1webworld-content-quality-checker' ) ); } echo '
'; $this->page_header( 'Content Quality Checker Documentation', 'A simple guide for reviewing, highlighting, fixing, and restoring WordPress content.', 'Back to Checker', admin_url( 'tools.php?page=1webworld-content-quality-checker' ) ); $this->documentation(); echo '
'; } private function editor_documentation() { $documentation_url = admin_url( 'admin.php?page=1webworld-content-quality-checker-guide' ); echo '
How to use this checker'; echo '
  1. Review the issue name and affected content line.
  2. Use Highlight issue to scroll to and mark the exact line inside Code Editor.
  3. Review the highlighted code.
  4. Use Fix & save to correct only that occurrence.
  5. The plugin creates a revision backup and reloads the editor.
  6. Check the public page after the fix.
'; echo '

Important: Highlighting never changes content. A saved fix can be restored from WordPress revisions when revisions are enabled.

'; echo '

Open complete plugin documentation

'; } private function documentation() { echo '
'; echo ''; echo '
'; echo '
GETTING STARTED

Review content in four steps

The checker reviews the last saved version of supported posts and pages. It reports formatting, accessibility and block-structure issues without silently changing content.

1Open the checker

Go to Tools → Content Quality Checker.

2Choose content

Find an item marked Needs Review.

3Review issues

Select Review to see issue details and locations.

4Edit carefully

Open the editor, highlight the issue and review any fix.

'; echo '
EDITOR ACTIONS

What each action does

Highlight issue

Scrolls the Code Editor to the exact current line and marks it in yellow. It never modifies content.

Fix & save

Corrects only the selected occurrence, creates a revision backup and reloads the editor.

Duplicate ID fix

Keeps the first ID and gives each later duplicate a unique numeric suffix.

Restore

Use WordPress Revisions to restore the previous saved version if required.

'; echo '
ISSUE TYPES

What the checker can detect

Empty paragraphsDuplicate paragraphsNested formattingHeading structureDuplicate IDsBroken anchorsMissing image alt textUnsafe new-tab links
'; echo '
SAFE WORKFLOW

Before saving a fix

  • Review the affected text and proposed action.
  • Use Undo if an editor-only change is not correct.
  • Save or update only after checking the result.
  • Check the public page after saving.
  • Use WordPress Revisions to restore a previous version when available.
  • Create a site backup before changing important production content.
'; echo '
IMPORTANT

Plugin limitations

The checker supports editorial review. It does not guarantee SEO rankings or accessibility compliance, replace human review, send content to an external service, or automatically publish editor-only changes.

'; echo '
'; } private function single_review( $post_id ) { $post = get_post( $post_id ); if ( ! $post || ! in_array( $post->post_type, owwcqc_post_types(), true ) || ! current_user_can( 'edit_post', $post_id ) ) { wp_die( esc_html__( 'This content cannot be reviewed.', '1webworld-content-quality-checker' ) ); } $issues = $this->scanner->scan( $post->post_content ); $details = $this->scanner->details( $post->post_content ); echo '

← Back to all content

' . esc_html( get_the_title( $post ) ?: '(no title)' ) . '

'; $this->summary( $issues ); $this->issue_list( $issues ); $this->detail_list( $details, $post ); $highlight_url = $this->highlight_url( $post_id ); echo '

Edit & highlight issues '; $this->fix_button( $post, $issues ); echo '

'; $this->revision_link( $post ); echo '
'; } private function all_content() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only report pagination; no data is changed. $paged = isset( $_GET['owwcqc_paged'] ) ? max( 1, absint( wp_unslash( $_GET['owwcqc_paged'] ) ) ) : 1; $query = new WP_Query( array( 'post_type' => owwcqc_post_types(), 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ), 'posts_per_page' => 50, 'paged' => $paged, 'orderby' => 'title', 'order' => 'ASC', ) ); $posts = $query->posts; $clean_count = 0; $review_count = 0; $issue_count = 0; $rows = array(); foreach ( $posts as $post ) { if ( ! current_user_can( 'edit_post', $post->ID ) ) { continue; } $issues = $this->scanner->scan( $post->post_content ); $total = $this->scanner->total( $issues ); $issue_count += $total; if ( $total ) { $review_count++; } else { $clean_count++; } $rows[] = array( 'post' => $post, 'issues' => $issues, 'total' => $total, ); } echo '
'; $this->overview_card( 'Content checked', count( $rows ), 'Items on this page', 'neutral' ); $this->overview_card( 'Needs review', $review_count, 'Items with detected issues', 'warning' ); $this->overview_card( 'Clean', $clean_count, 'Items with no detected issues', 'success' ); $this->overview_card( 'Total issues', $issue_count, 'Issues on this page', 'danger' ); echo '
'; echo '

Content report

Review the latest saved version of every supported post and page.

'; echo '
'; echo ''; foreach ( $rows as $row ) { $post = $row['post']; $issues = $row['issues']; $total = $row['total']; $url = $this->review_url( $post->ID ); $search_text = strtolower( ( get_the_title( $post ) ?: '(no title)' ) . ' ' . $post->post_type . ' ' . ( $total ? 'needs review' : 'clean' ) ); echo ''; } echo '
Page / PostTypeTotalIssue SummaryStatusAction
' . esc_html( get_the_title( $post ) ?: '(no title)' ) . '
ID: ' . absint( $post->ID ) . '
' . esc_html( ucfirst( $post->post_type ) ) . '' . absint( $total ) . ''; $this->issue_list( $issues ); echo '' . ( $total ? 'Needs Review' : 'Clean' ) . 'Review Edit '; $this->fix_button( $post, $issues ); echo '
'; echo '
'; if ( $query->max_num_pages > 1 ) { $base_url = admin_url( 'tools.php?page=1webworld-content-quality-checker' ); echo '
' . wp_kses_post( paginate_links( array( 'base' => add_query_arg( 'owwcqc_paged', '%#%', $base_url ), 'format' => '', 'current' => $paged, 'total' => (int) $query->max_num_pages, 'prev_text' => '«', 'next_text' => '»', ) ) ) . '
'; } } private function overview_card( $label, $value, $description, $state ) { echo '
'; echo '' . esc_html( $label ) . ''; echo '' . absint( $value ) . ''; echo '' . esc_html( $description ) . ''; echo '
'; } private function summary( $issues ) { $total = $this->scanner->total( $issues ); echo '

' . esc_html( $total . ( 1 === $total ? ' issue' : ' issues' ) ) . '

'; } private function issue_list( $issues ) { if ( empty( $issues ) ) { echo '

No issues found in the saved content.

'; return; } echo ''; } private function detail_list( $details, $post, $in_editor = false ) { if ( empty( $details ) ) { return; } echo '

Where the issues are

Expand an issue to see its exact content line. Highlighting never changes content. Fix & save creates a revision backup first.

'; foreach ( $details as $key => $items ) { echo '
' . esc_html( $this->scanner->rules()->label( $key ) ) . ' (' . absint( count( $items ) ) . ')
    '; foreach ( $items as $index => $item ) { echo '
  1. ' . esc_html( $item['message'] ) . ' Content line ' . absint( $item['line'] ) . ''; if ( $in_editor ) { echo ''; if ( $this->fixer->can_fix_one( $key ) && current_user_can( 'edit_post', $post->ID ) ) { $fix_url = wp_nonce_url( add_query_arg( array( 'action' => 'owwcqc_fix_one', 'owwcqc_post_id' => $post->ID, 'owwcqc_rule' => $key, 'owwcqc_occurrence' => $index, 'owwcqc_return_editor' => 1 ), admin_url( 'admin-post.php' ) ), 'owwcqc_fix_one_' . $post->ID . '_' . $key . '_' . $index ); echo 'Fix & save'; } } elseif ( $this->fixer->can_fix_one( $key ) && current_user_can( 'edit_post', $post->ID ) ) { $fix_url = wp_nonce_url( add_query_arg( array( 'action' => 'owwcqc_fix_one', 'owwcqc_post_id' => $post->ID, 'owwcqc_rule' => $key, 'owwcqc_occurrence' => $index ), admin_url( 'admin-post.php' ) ), 'owwcqc_fix_one_' . $post->ID . '_' . $key . '_' . $index ); echo 'Fix this issue'; } else { $highlight_url = $this->highlight_url( $post->ID, $key, $index ); echo 'Highlight in editor'; } echo '
  2. '; } echo '
'; } echo '
'; } private function revision_link( $post ) { if ( ! wp_revisions_enabled( $post ) ) { return; } $revisions = wp_get_post_revisions( $post->ID, array( 'posts_per_page' => 1 ) ); if ( $revisions ) { $latest = reset( $revisions ); echo '

View revisions / restore backup

'; } else { echo '

Backup revisions appear after the page is updated.

'; } } private function fix_button( $post, $issues ) { $count = $this->fixer->fixable_total( $issues ); if ( ! $count || ! current_user_can( 'edit_post', $post->ID ) ) { return; } $url = wp_nonce_url( admin_url( 'admin-post.php?action=owwcqc_safe_fix&owwcqc_post_id=' . absint( $post->ID ) ), 'owwcqc_safe_fix_' . $post->ID ); echo 'Safe Fix (' . absint( $count ) . ')'; } public function handle_safe_fix() { $post_id = isset( $_GET['owwcqc_post_id'] ) ? absint( wp_unslash( $_GET['owwcqc_post_id'] ) ) : 0; check_admin_referer( 'owwcqc_safe_fix_' . $post_id ); $post = get_post( $post_id ); if ( ! $post || ! in_array( $post->post_type, owwcqc_post_types(), true ) || ! current_user_can( 'edit_post', $post_id ) ) { wp_die( esc_html__( 'This content cannot be fixed.', '1webworld-content-quality-checker' ) ); } $result = $this->fixer->fix( $post->post_content ); if ( $result['content'] !== $post->post_content ) { if ( wp_revisions_enabled( $post ) ) { wp_save_post_revision( $post_id ); } $updated = wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => $result['content'] ) ), true ); if ( is_wp_error( $updated ) ) { wp_die( esc_html( $updated->get_error_message() ) ); } } $redirect = add_query_arg( 'owwcqc_fixed', 1, $this->review_url( $post_id ) ); $redirect = add_query_arg( '_owwcqc_notice_nonce', wp_create_nonce( 'owwcqc_notice_' . $post_id ), $redirect ); wp_safe_redirect( $redirect ); exit; } public function handle_fix_one() { $post_id = isset( $_GET['owwcqc_post_id'] ) ? absint( wp_unslash( $_GET['owwcqc_post_id'] ) ) : 0; $rule = isset( $_GET['owwcqc_rule'] ) ? sanitize_key( wp_unslash( $_GET['owwcqc_rule'] ) ) : ''; $occurrence = isset( $_GET['owwcqc_occurrence'] ) ? absint( wp_unslash( $_GET['owwcqc_occurrence'] ) ) : 0; $return_editor = isset( $_GET['owwcqc_return_editor'] ) && 1 === absint( wp_unslash( $_GET['owwcqc_return_editor'] ) ); check_admin_referer( 'owwcqc_fix_one_' . $post_id . '_' . $rule . '_' . $occurrence ); $post = get_post( $post_id ); if ( ! $post || ! in_array( $post->post_type, owwcqc_post_types(), true ) || ! current_user_can( 'edit_post', $post_id ) ) { wp_die( esc_html__( 'This content cannot be fixed.', '1webworld-content-quality-checker' ) ); } $result = $this->fixer->fix_one( $post->post_content, $rule, $occurrence ); if ( ! $result['changed'] ) { wp_die( esc_html__( 'The selected issue was not found. Refresh the review and try again.', '1webworld-content-quality-checker' ) ); } if ( wp_revisions_enabled( $post ) ) { wp_save_post_revision( $post_id ); } $updated = wp_update_post( wp_slash( array( 'ID' => $post_id, 'post_content' => $result['content'] ) ), true ); if ( is_wp_error( $updated ) ) { wp_die( esc_html( $updated->get_error_message() ) ); } $redirect = $return_editor ? get_edit_post_link( $post_id, 'raw' ) : $this->review_url( $post_id ); $redirect = add_query_arg( 'owwcqc_fixed', 'one', $redirect ); $redirect = add_query_arg( '_owwcqc_notice_nonce', wp_create_nonce( 'owwcqc_notice_' . $post_id ), $redirect ); wp_safe_redirect( $redirect ); exit; } private function review_url( $post_id ) { $url = add_query_arg( array( 'page' => '1webworld-content-quality-checker', 'owwcqc_review_post' => absint( $post_id ), ), admin_url( 'tools.php' ) ); return add_query_arg( '_owwcqc_nonce', wp_create_nonce( 'owwcqc_review_' . absint( $post_id ) ), $url ); } private function highlight_url( $post_id, $rule = '', $index = null ) { $args = array( 'owwcqc_highlight' => 1 ); if ( $rule ) { $args['owwcqc_rule'] = sanitize_key( $rule ); } if ( null !== $index ) { $args['owwcqc_index'] = absint( $index ); } $url = add_query_arg( $args, get_edit_post_link( $post_id ) ); return add_query_arg( '_owwcqc_nonce', wp_create_nonce( 'owwcqc_highlight_' . absint( $post_id ) ), $url ); } }