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 '';
}
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 'Review the issue name and affected content line. Use Highlight issue to scroll to and mark the exact line inside Code Editor. Review the highlighted code. Use Fix & save to correct only that occurrence. The plugin creates a revision backup and reloads the editor. 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 '
In this guide Getting started Actions explained Issues detected Safe workflow Limitations ';
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.
1 Open the checker Go to Tools → Content Quality Checker.
2 Choose content Find an item marked Needs Review.
3 Review issues Select Review to see issue details and locations.
4 Edit 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 paragraphs Duplicate paragraphs Nested formatting Heading structure Duplicate IDs Broken anchors Missing image alt text Unsafe 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 '';
echo '';
echo '
Page / Post Type Total Issue Summary Status Action ';
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 '' . 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 '
';
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 '';
foreach ( $issues as $key => $count ) { echo '' . esc_html( $this->scanner->rules()->label( $key ) ) . ': ' . absint( $count ) . ' '; }
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 '' . esc_html( $item['message'] ) . ' Content line ' . absint( $item['line'] ) . ' ';
if ( $in_editor ) {
echo 'Highlight issue ';
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 ' ';
}
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 );
}
}