settings = $settings; $this->tokens = $tokens; $this->mailer = $mailer; add_filter( 'manage_users_columns', array( $this, 'register_column' ) ); add_filter( 'manage_users_custom_column', array( $this, 'render_column' ), 10, 3 ); add_filter( 'manage_users_sortable_columns', array( $this, 'mark_sortable' ) ); add_action( 'pre_get_users', array( $this, 'apply_query' ) ); add_action( 'restrict_manage_users', array( $this, 'render_filter' ) ); add_filter( 'bulk_actions-users', array( $this, 'register_bulk_actions' ) ); add_filter( 'handle_bulk_actions-users', array( $this, 'handle_bulk_actions' ), 10, 3 ); add_action( 'admin_notices', array( $this, 'render_bulk_notices' ) ); add_filter( 'user_row_actions', array( $this, 'register_row_actions' ), 10, 2 ); add_action( 'admin_post_wpcev_user_action', array( $this, 'handle_row_action' ) ); // Priority 20: after WooCommerce 11's own "Email confirmation" box (10), which // is the one switch when it exists; see render_profile_widget(). add_action( 'show_user_profile', array( $this, 'render_profile_widget' ), 20 ); add_action( 'edit_user_profile', array( $this, 'render_profile_widget' ), 20 ); // WooCommerce's box ticked or cleared on the profile: follow it. add_action( 'profile_update', array( $this, 'remember_wc_state' ), 5 ); add_action( 'profile_update', array( $this, 'follow_wc_box' ), 20 ); // Pending-count badge on the Users menu (mirrors the "Comments // awaiting moderation" badge WP itself ships). add_action( 'admin_menu', array( $this, 'inject_users_menu_badge' ), 999 ); } /* ─── Pending-count menu badge ───────────────────────────────────────── */ public function inject_users_menu_badge() { if ( ! current_user_can( 'list_users' ) ) { return; } if ( ! class_exists( 'WPCEV_Stats' ) ) { return; } $counts = WPCEV_Stats::counts(); $pending = (int) $counts['unverified']; if ( $pending < 1 ) { return; } global $menu, $submenu; $badge = sprintf( ' %2$s', $pending, number_format_i18n( $pending ) ); // Top-level "Users" entry. Mutating $menu / $submenu in-place is // the documented WP pattern for menu badges (same as core does for // comments awaiting moderation). if ( is_array( $menu ) ) { foreach ( $menu as $key => $item ) { if ( isset( $item[2] ) && 'users.php' === $item[2] ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- documented pattern: append to $menu[$key][0]. $menu[ $key ][0] .= $badge; break; } } } // Submenu "All Users" entry, so the badge also appears when the // Users menu is expanded. if ( isset( $submenu['users.php'] ) && is_array( $submenu['users.php'] ) ) { foreach ( $submenu['users.php'] as $key => $sub ) { if ( isset( $sub[2] ) && 'users.php' === $sub[2] ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- documented pattern: append to $submenu[$parent][$key][0]. $submenu['users.php'][ $key ][0] .= $badge; break; } } } } /* ─── Column ─────────────────────────────────────────────────────────── */ public function register_column( $cols ) { $cols[ self::COLUMN_ID ] = __( 'Verified', 'wpcoretools-email-verify' ); return $cols; } public function render_column( $value, $column_name, $user_id ) { if ( self::COLUMN_ID !== $column_name ) { return $value; } return $this->badge_html( $this->tokens->status( $user_id ) ); } /** * Status badge markup for a status from WPCEV_Tokens::status(). Shared by * the Users list, the profile widget and the WooCommerce Orders list so the * labels cannot drift. Returns escaped HTML. * * @param string $status verified|unverified|exempt|grandfathered. * @return string */ public function badge_html( $status ) { switch ( $status ) { case 'verified': return '' . esc_html__( 'Verified', 'wpcoretools-email-verify' ) . ''; case 'unverified': return '' . esc_html__( 'Pending', 'wpcoretools-email-verify' ) . ''; case 'exempt': return '' . esc_html__( 'Exempt', 'wpcoretools-email-verify' ) . ''; case 'grandfathered': default: return '—'; } } public function mark_sortable( $cols ) { $cols[ self::COLUMN_ID ] = WPCEV_Tokens::META_VERIFIED; return $cols; } public function apply_query( $query ) { if ( ! is_admin() ) { return; } $meta_query = $query->get( 'meta_query' ); if ( ! is_array( $meta_query ) ) { $meta_query = array(); } /* * Sorting by the Verified column. * * Setting `meta_key` would make WP_User_Query INNER JOIN usermeta, which * silently drops every grandfathered user — they have no wpcev_verified * row at all, so sorting the column hid exactly the users an admin sorts * in order to find. Instead order by a named meta_query clause whose * relation is OR'd with NOT EXISTS: that produces a LEFT JOIN, so * grandfathered users are kept and sort together on the NULL side. */ $orderby = $query->get( 'orderby' ); if ( WPCEV_Tokens::META_VERIFIED === $orderby ) { $meta_query['wpcev_sort'] = array( 'relation' => 'OR', 'wpcev_has' => array( 'key' => WPCEV_Tokens::META_VERIFIED, 'compare' => 'EXISTS', ), 'wpcev_none' => array( 'key' => WPCEV_Tokens::META_VERIFIED, 'compare' => 'NOT EXISTS', ), ); $query->set( 'orderby', 'wpcev_has' ); } // Filter dropdown. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter form. $filter = isset( $_GET['wpcev_filter'] ) ? sanitize_key( wp_unslash( $_GET['wpcev_filter'] ) ) : ''; switch ( $filter ) { case 'unverified': $meta_query[] = array( 'key' => WPCEV_Tokens::META_VERIFIED, 'value' => '0', ); break; case 'verified': $meta_query[] = array( 'key' => WPCEV_Tokens::META_VERIFIED, 'value' => '1', ); break; case 'grandfathered': $meta_query[] = array( 'key' => WPCEV_Tokens::META_VERIFIED, 'compare' => 'NOT EXISTS', ); break; } if ( $meta_query ) { // Top-level AND so a filter still narrows the sorted set. if ( ! isset( $meta_query['relation'] ) ) { $meta_query['relation'] = 'AND'; } $query->set( 'meta_query', $meta_query ); } } public function render_filter( $which ) { if ( 'top' !== $which ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $current = isset( $_GET['wpcev_filter'] ) ? sanitize_key( wp_unslash( $_GET['wpcev_filter'] ) ) : ''; // Nonce for our bulk actions (see handle_bulk_actions()): it is in the list form // whichever Apply button is used. wp_nonce_field( self::BULK_NONCE_ACTION, self::BULK_NONCE_FIELD, false ); ?> 403 ) ); } $counts = array( 'verify' => 0, 'unverify' => 0, 'resend' => 0, 'fail' => 0, ); foreach ( (array) $user_ids as $uid ) { $uid = (int) $uid; if ( ! $uid ) { continue; } switch ( $action ) { case 'wpcev_verify': $this->tokens->mark_verified( $uid, 'admin' ); ++$counts['verify']; break; case 'wpcev_unverify': $this->tokens->unmark_verified( $uid, 'admin' ); $result = $this->mailer->send_verification( $uid, false ); if ( 'sent' === $result ) { ++$counts['unverify']; } else { ++$counts['fail']; } break; case 'wpcev_resend': $result = $this->mailer->send_verification( $uid, true ); if ( 'sent' === $result ) { ++$counts['resend']; } elseif ( 'throttled' !== $result ) { ++$counts['fail']; } break; } } return add_query_arg( array( 'wpcev_bulk_verify' => $counts['verify'], 'wpcev_bulk_unverify' => $counts['unverify'], 'wpcev_bulk_resend' => $counts['resend'], 'wpcev_bulk_fail' => $counts['fail'], ), $redirect ); } public function render_bulk_notices() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only flags from our own redirect. $verify = isset( $_GET['wpcev_bulk_verify'] ) ? (int) $_GET['wpcev_bulk_verify'] : 0; $unverify = isset( $_GET['wpcev_bulk_unverify'] ) ? (int) $_GET['wpcev_bulk_unverify'] : 0; $resend = isset( $_GET['wpcev_bulk_resend'] ) ? (int) $_GET['wpcev_bulk_resend'] : 0; $fail = isset( $_GET['wpcev_bulk_fail'] ) ? (int) $_GET['wpcev_bulk_fail'] : 0; // phpcs:enable WordPress.Security.NonceVerification.Recommended $messages = array(); if ( $verify ) { /* translators: %d: number of users */ $messages[] = sprintf( _n( '%d user marked verified.', '%d users marked verified.', $verify, 'wpcoretools-email-verify' ), $verify ); } if ( $unverify ) { /* translators: %d: number of users */ $messages[] = sprintf( _n( '%d user reset and re-emailed.', '%d users reset and re-emailed.', $unverify, 'wpcoretools-email-verify' ), $unverify ); } if ( $resend ) { /* translators: %d: number of users */ $messages[] = sprintf( _n( '%d verification email sent.', '%d verification emails sent.', $resend, 'wpcoretools-email-verify' ), $resend ); } if ( $messages ) { echo '
' . esc_html( implode( ' ', $messages ) ) . '
' . esc_html( sprintf( _n( '%d email failed to send.', '%d emails failed to send.', $fail, 'wpcoretools-email-verify' ), $fail ) ) . '
| render_column( '', self::COLUMN_ID, $user->ID ) ); ?> | |
| ID ) ); ?> | |