├── css └── style.css ├── includes └── class-bbg-unconfirmed.php ├── languages └── unconfirmed.pot ├── lib ├── boones-pagination.php └── boones-sortable-columns.php ├── readme.txt └── unconfirmed.php /css/style.css: -------------------------------------------------------------------------------- 1 | .unconfirmed-pagination { 2 | height: 30px; 3 | color: #555; 4 | font-size: 11px; 5 | } 6 | .unconfirmed-pagination .currently-viewing { 7 | font-style: italic; 8 | margin-right: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /includes/class-bbg-unconfirmed.php: -------------------------------------------------------------------------------- 1 | load_textdomain(); 64 | 65 | add_filter( 'bbg_cpt_pag_add_args', array( $this, 'add_args' ) ); 66 | 67 | add_filter( 'boones_sortable_columns_keys_to_remove', array( $this, 'sortable_keys_to_remove' ) ); 68 | 69 | add_filter( 'map_meta_cap', array( $this, 'map_moderate_signups_cap' ), 10, 4 ); 70 | 71 | // Multisite behavior? Configurable for plugins 72 | $this->is_multisite = apply_filters( 'unconfirmed_is_multisite', is_multisite() ); 73 | 74 | /** 75 | * Should the Unconfirmed panel appear in the Network admin? 76 | * 77 | * @since 1.3 78 | * 79 | * @param bool $do_network_admin 80 | */ 81 | $do_network_admin = apply_filters( 'unconfirmed_do_network_admin', $this->is_multisite ); 82 | 83 | $this->base_url = add_query_arg( 'page', 'unconfirmed', $do_network_admin ? network_admin_url( 'users.php' ) : admin_url( 'users.php' ) ); 84 | 85 | $admin_hook = apply_filters( 'unconfirmed_admin_hook', $do_network_admin ? 'network_admin_menu' : 'admin_menu' ); 86 | 87 | add_action( $admin_hook, array( $this, 'add_admin_panel' ) ); 88 | } 89 | 90 | /** 91 | * Load textdomain. 92 | * 93 | * @since 1.3.2 94 | */ 95 | public function load_textdomain() { 96 | load_plugin_textdomain( 'unconfirmed', false, basename( dirname( __FILE__ ) ) . '/languages' ); 97 | } 98 | 99 | /** 100 | * Adds the admin panel and detects incoming admin actions 101 | * 102 | * When the admin submits an action like "activate" or "resend activation email", it will 103 | * ultimately result in a redirect. In order to minimize the amount of work done in the 104 | * interim page load (after the link is clicked but before the redirect happens), I check 105 | * for these actions (out of $_REQUEST parameters) before the admin panel is even added to the 106 | * Dashboard. 107 | * 108 | * @package Unconfirmed 109 | * @since 1.0 110 | * 111 | * @uses BBG_Unconfirmed::delete_user() to delete registrations 112 | * @uses BBG_Unconfirmed::activate_user() to process manual activations 113 | * @uses BBG_Unconfirmed::resend_email() to process manual activations 114 | * @uses add_users_page() to add the admin panel underneath user.php 115 | */ 116 | function add_admin_panel() { 117 | $page = add_submenu_page( 'users.php', __( 'Unconfirmed', 'unconfirmed' ), __( 'Unconfirmed', 'unconfirmed' ), 'moderate_signups', 'unconfirmed', array( $this, 'admin_panel_main' ) ); 118 | add_action( "admin_print_styles-$page", array( $this, 'add_admin_styles' ) ); 119 | 120 | if ( isset( $_REQUEST['performed_search'] ) && '1' == $_REQUEST['performed_search'] ) { 121 | return; 122 | } 123 | 124 | // Look for actions first 125 | if ( isset( $_REQUEST['unconfirmed_action'] ) ) { 126 | switch ( $_REQUEST['unconfirmed_action'] ) { 127 | case 'delete': 128 | $this->delete_user(); 129 | break; 130 | 131 | case 'activate': 132 | $this->activate_user(); 133 | break; 134 | 135 | case 'resend': 136 | default: 137 | $this->resend_email(); 138 | break; 139 | } 140 | 141 | $this->do_redirect(); 142 | } 143 | 144 | if ( isset( $_REQUEST['unconfirmed_complete'] ) ) { 145 | $this->setup_get_users(); 146 | } 147 | } 148 | 149 | /** 150 | * Enqueues the Unconfirmed stylesheet 151 | * 152 | * @package Unconfirmed 153 | * @since 1.0.1 154 | * 155 | * @uses wp_enqueue_style() 156 | */ 157 | function add_admin_styles() { 158 | wp_enqueue_style( 'unconfirmed-css', plugins_url( 'css/style.css', __FILE__ ) ); 159 | } 160 | 161 | /** 162 | * Map the 'moderate_signups' cap. 163 | * 164 | * 'moderate_signups' is the custom capability used by Unconfirmed for management of signups. 165 | * By default, we map this to 'create_users', but it is possible to override. 166 | * 167 | * @since 1.3 168 | * 169 | * @param array $caps 170 | * @param string $cap 171 | * @param int $user_id 172 | * @param array $args 173 | */ 174 | public function map_moderate_signups_cap( $caps, $cap, $user_id, $args ) { 175 | if ( 'moderate_signups' === $cap ) { 176 | $caps = array( 'create_users' ); 177 | } 178 | 179 | return $caps; 180 | } 181 | 182 | /** 183 | * Queries and prepares a list of unactivated registrations for use elsewhere in the plugin 184 | * 185 | * This function is only called when such a list is required, i.e. on the admin pane 186 | * itself. See BBG_Unconfirmed::admin_panel_main(). 187 | * 188 | * @package Unconfirmed 189 | * @since 1.0 190 | * 191 | * @uses apply_filters() Filter 'unconfirmed_paged_query' to alter the per-page query 192 | * @uses apply_filters() Filter 'unconfirmed_total_query' to alter the total count query 193 | * 194 | * @param array $args See $defaults below for documentation 195 | */ 196 | function setup_users( $args ) { 197 | global $wpdb; 198 | 199 | /** 200 | * Override the $defaults with the following parameters: 201 | * - 'orderby': Which column should determine the sort? Accepts: 202 | * - 'registered' (MS) / 'user_registered' (non-MS) - These are translated to 203 | * each other accordingly, depending on is_multisite(), so you don't 204 | * have to be too careful about which one you pass 205 | * - 'user_login' 206 | * - 'user_email' 207 | * - 'activation_key' (MS) / 'user_activation_key' (non-MS) - As in the case 208 | * of 'registered', this will be switched to the appropriate version 209 | * automatically 210 | * - 'order': In conjunction with 'orderby', how should users be sorted? Accepts: 211 | * 'desc', 'asc' 212 | * - 'offset': Which user are we starting with? Eg for the third page of 10, use 213 | * 31 214 | * - 'number': How many users to return? 215 | */ 216 | $defaults = array( 217 | 'orderby' => 'registered', 218 | 'order' => 'desc', 219 | 'offset' => 0, 220 | 'number' => 10, 221 | ); 222 | 223 | $r = wp_parse_args( $args, $defaults ); 224 | 225 | $orderby = $r['orderby']; 226 | $order = $r['order']; 227 | $offset = $r['offset']; 228 | $number = $r['number']; 229 | 230 | $search = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : ''; 231 | 232 | // Our query will be different for multisite and for non-multisite 233 | if ( $this->is_multisite ) { 234 | $sql['select'] = "SELECT * FROM $wpdb->signups"; 235 | $sql['where'] = 'WHERE active = 0'; 236 | 237 | if ( ! empty( $search ) ) { 238 | if ( method_exists( $wpdb, 'esc_like' ) ) { // WP >= 4.0.0 239 | $search_text = '%' . $wpdb->esc_like( $search ) . '%'; 240 | } else { 241 | $search_text = '%' . like_escape( $search ) . '%'; 242 | } 243 | $sql['where'] .= $wpdb->prepare( ' AND ( user_login LIKE %s OR user_email LIKE %s )', $search_text, $search_text ); 244 | } 245 | 246 | // Switch the non-MS orderby keys to their MS counterparts 247 | if ( 'user_registered' == $orderby ) { 248 | $orderby = 'registered'; 249 | } elseif ( 'user_activation_key' == $orderby ) { 250 | $orderby = 'activation_key'; 251 | } 252 | 253 | $sql['orderby'] = "ORDER BY $orderby"; 254 | $sql['order'] = strtoupper( $order ); 255 | $sql['limit'] = $wpdb->prepare( 'LIMIT %d, %d', $offset, $number ); 256 | } else { 257 | // Stinky WP_User_Query doesn't allow filtering by user_status, so we must 258 | // query wp_users directly. I should probably send a patch upstream to WP 259 | $sql['select'] = "SELECT u.*, um.meta_value AS activation_key FROM $wpdb->users u INNER JOIN $wpdb->usermeta um ON ( u.ID = um.user_id )"; 260 | 261 | // The convention of using user_status = 2 for an unactivated user comes (I 262 | // think) from BuddyPress. This will probably do nothing if you're not 263 | // running BP. 264 | $sql['where'] = "WHERE u.user_status = 2 AND um.meta_key = 'activation_key'"; 265 | 266 | if ( ! empty( $search ) ) { 267 | if ( method_exists( $wpdb, 'esc_like' ) ) { // WP >= 4.0.0 268 | $search_text = '%' . $wpdb->esc_like( $search ) . '%'; 269 | } else { 270 | $search_text = '%' . like_escape( $search ) . '%'; 271 | } 272 | $sql['where'] .= $wpdb->prepare( ' AND ( u.user_login LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s )', $search_text, $search_text, $search_text ); 273 | } 274 | 275 | // Switch the MS orderby keys to their non-MS counterparts 276 | if ( 'registered' == $orderby ) { 277 | $orderby = 'user_registered'; 278 | } elseif ( 'activation_key' == $orderby ) { 279 | $orderby = 'um.activation_key'; 280 | } 281 | 282 | $sql['orderby'] = $wpdb->prepare( 'ORDER BY %s', $orderby ); 283 | $sql['order'] = strtoupper( $order ); 284 | $sql['limit'] = $wpdb->prepare( 'LIMIT %d, %d', $offset, $number ); 285 | } 286 | 287 | // Get the resent counts 288 | $resent_counts = get_site_option( 'unconfirmed_resent_counts' ); 289 | 290 | $paged_query = apply_filters( 'unconfirmed_paged_query', join( ' ', $sql ), $sql, $args, $r ); 291 | 292 | $users = $wpdb->get_results( $paged_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 293 | 294 | /* 295 | * Now loop through the users and unserialize their metadata for nice display 296 | * Probably only necessary with BuddyPress 297 | * We'll also use this opportunity to add the resent counts to the user objects 298 | */ 299 | foreach ( (array) $users as $key => $user ) { 300 | 301 | $meta = ! empty( $user->meta ) ? maybe_unserialize( $user->meta ) : false; 302 | 303 | foreach ( (array) $meta as $mkey => $mvalue ) { 304 | $user->$mkey = $mvalue; 305 | } 306 | 307 | if ( $this->is_multisite ) { 308 | // Multisite 309 | $akey = $user->activation_key; 310 | } else { 311 | // Non-multisite 312 | $akey = $user->activation_key; 313 | 314 | if ( $user->user_registered ) { 315 | $user->registered = $user->user_registered; 316 | } 317 | } 318 | 319 | $akey = isset( $user->activation_key ) ? $user->activation_key : $user->user_activation_key; 320 | 321 | $user->resent_count = isset( $resent_counts[ $akey ] ) ? $resent_counts[ $akey ] : 0; 322 | 323 | $users[ $key ] = $user; 324 | } 325 | 326 | $this->users = $users; 327 | 328 | // Gotta run a second query to get the overall pagination data 329 | unset( $sql['limit'] ); 330 | $sql['select'] = preg_replace( '/SELECT.*?FROM/', 'SELECT COUNT(*) FROM', $sql['select'] ); 331 | $total_query = apply_filters( 'unconfirmed_total_query', join( ' ', $sql ), $sql, $args, $r ); 332 | 333 | $this->total_users = $wpdb->get_var( $total_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 334 | } 335 | 336 | 337 | function sortable_keys_to_remove( $keys ) { 338 | $unconfirmed_keys = array( 339 | 'unconfirmed_complete', 340 | 'unconfirmed_key', 341 | 'updated_resent', 342 | 'updated_activated', 343 | 'error_couldntactivate', 344 | 'error_nouser', 345 | 'error_nokey', 346 | ); 347 | 348 | $keys = array_merge( $keys, $unconfirmed_keys ); 349 | 350 | return $keys; 351 | } 352 | 353 | /** 354 | * Get userdata from an activation key, when using WP single 355 | * 356 | * For maximum flexibility, this method looks both in the user_activation_key column of 357 | * wp_users (rarely used) and the activation_key usermeta row (used by BP). 358 | * 359 | * Part of the function is borrowed from BP itself. 360 | * 361 | * @package Unconfirmed 362 | * @since 1.2 363 | */ 364 | function get_userdata_from_key( $key ) { 365 | global $wpdb; 366 | 367 | $user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE user_activation_key = %s", $key ) ); 368 | if ( $user ) { 369 | $key_loc = 'users'; 370 | } else { 371 | $user_id = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'activation_key' AND meta_value = %s", $key ) ); 372 | if ( $user_id ) { 373 | $key_loc = 'usermeta'; 374 | $user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE ID = %d", (int) $user_id ) ); 375 | } 376 | } 377 | 378 | return $user; 379 | } 380 | 381 | /** 382 | * Activates a user 383 | * 384 | * Depending on the result, the admin will be redirected back to the main Unconfirmed panel, 385 | * with additional URL params that explain success/failure. 386 | * 387 | * @package Unconfirmed 388 | * @since 1.0 389 | * 390 | * @uses wpmu_activate_signup() WP's core function for user activation on Multisite 391 | */ 392 | function activate_user() { 393 | global $wpdb; 394 | 395 | if ( ! current_user_can( 'edit_users' ) ) { 396 | return; 397 | } 398 | 399 | // Did you mean to do this? HMMM??? 400 | if ( isset( $_REQUEST['unconfirmed_bulk'] ) ) { 401 | check_admin_referer( 'unconfirmed_bulk_action' ); 402 | } else { 403 | check_admin_referer( 'unconfirmed_activate_user' ); 404 | } 405 | 406 | // Get the activation key(s) out of the URL params 407 | if ( ! isset( $_REQUEST['unconfirmed_key'] ) ) { 408 | $this->record_status( 'error_nokey' ); 409 | return; 410 | } 411 | 412 | $keys = $_REQUEST['unconfirmed_key']; 413 | 414 | foreach ( (array) $keys as $key ) { 415 | if ( $this->is_multisite ) { 416 | $result = wpmu_activate_signup( $key ); 417 | $user_id = ! is_wp_error( $result ) && isset( $result['user_id'] ) ? $result['user_id'] : 0; 418 | } else { 419 | $user = $this->get_userdata_from_key( $key ); 420 | 421 | if ( empty( $user->ID ) ) { 422 | $this->record_status( 'error_nouser' ); 423 | return; 424 | } else { 425 | $user_id = $user->ID; 426 | } 427 | 428 | if ( empty( $user_id ) ) { 429 | return new WP_Error( 'invalid_key', __( 'Invalid activation key', 'unconfirmed' ) ); 430 | } 431 | 432 | // Change the user's status so they become active 433 | $result = $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->users SET user_status = 0 WHERE ID = %d", $user_id ) ); 434 | if ( ! $result ) { 435 | return new WP_Error( 'invalid_key', __( 'Invalid activation key', 'unconfirmed' ) ); 436 | } 437 | } 438 | 439 | if ( is_wp_error( $result ) ) { 440 | $this->record_status( 'error_couldntactivate', $key ); 441 | } else { 442 | do_action( 'unconfirmed_user_activated', $user_id, $key ); 443 | $this->record_status( 'updated_activated', $key ); 444 | } 445 | } 446 | } 447 | 448 | /** 449 | * Deletes an unactivated registration 450 | * 451 | * @package Unconfirmed 452 | * @since 1.2 453 | */ 454 | function delete_user() { 455 | global $wpdb; 456 | 457 | if ( ! current_user_can( 'remove_users' ) ) { 458 | return; 459 | } 460 | 461 | // Don't go there 462 | if ( isset( $_REQUEST['unconfirmed_bulk'] ) ) { 463 | check_admin_referer( 'unconfirmed_bulk_action' ); 464 | } else { 465 | check_admin_referer( 'unconfirmed_delete_user' ); 466 | } 467 | 468 | // Get the activation key(s) out of the URL params 469 | if ( ! isset( $_REQUEST['unconfirmed_key'] ) ) { 470 | $this->record_status( 'error_nokey' ); 471 | return; 472 | } 473 | 474 | $keys = $_REQUEST['unconfirmed_key']; 475 | 476 | foreach ( (array) $keys as $key ) { 477 | if ( $this->is_multisite ) { 478 | // Ensure the user exists before deleting, and pass the data along 479 | // to a hook 480 | $check = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s", $key ) ); 481 | 482 | if ( ! $check ) { 483 | $this->record_status( 'error_nouser' ); 484 | return; 485 | } else { 486 | do_action( 'unconfirmed_pre_user_delete', $key, $check ); 487 | } 488 | 489 | $delete_sql = apply_filters( 'unconfirmed_delete_sql', $wpdb->prepare( "DELETE FROM $wpdb->signups WHERE activation_key = %s", $key ), $key, $this->is_multisite ); 490 | $result = $wpdb->query( $delete_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 491 | } else { 492 | // Ensure the user exists before deleting, and pass the data along 493 | // to a hook 494 | $check = $this->get_userdata_from_key( $key ); 495 | 496 | if ( ! $check ) { 497 | $this->record_status( 'error_nouser' ); 498 | return; 499 | } else { 500 | do_action( 'unconfirmed_pre_user_delete', $key, $check ); 501 | } 502 | 503 | $user_id = isset( $check->ID ) ? $check->ID : $check->user_id; 504 | 505 | $result = wp_delete_user( $user_id ); 506 | } 507 | 508 | if ( ! $key ) { 509 | $key = 0; 510 | } 511 | 512 | if ( $result ) { 513 | do_action( 'unconfirmed_user_deleted', $key, $check ); 514 | $this->record_status( 'updated_deleted', $key ); 515 | } else { 516 | $this->record_status( 'error_couldntdelete', $key ); 517 | } 518 | } 519 | } 520 | 521 | /** 522 | * Resends an activation email 523 | * 524 | * This sends exactly the same email the registrant originally got, using data pulled from 525 | * their registration. In the future I may add a UI for customized emails. 526 | * 527 | * @package Unconfirmed 528 | * @since 1.0 529 | * 530 | * @uses wpmu_signup_blog_notification() to notify users who signed up with a blog 531 | * @uses wpmu_signup_user_notification() to notify users who signed up without a blog 532 | */ 533 | function resend_email() { 534 | global $wpdb; 535 | 536 | if ( ! current_user_can( 'edit_users' ) ) { 537 | return; 538 | } 539 | 540 | // Hubba hubba 541 | if ( isset( $_REQUEST['unconfirmed_bulk'] ) ) { 542 | check_admin_referer( 'unconfirmed_bulk_action' ); 543 | } else { 544 | check_admin_referer( 'unconfirmed_resend_email' ); 545 | } 546 | 547 | // Get the user's activation key out of the URL params 548 | if ( ! isset( $_REQUEST['unconfirmed_key'] ) ) { 549 | $this->record_status( 'error_nokey' ); 550 | return; 551 | } 552 | 553 | $resent_counts = get_site_option( 'unconfirmed_resent_counts' ); 554 | 555 | $keys = $_REQUEST['unconfirmed_key']; 556 | 557 | foreach ( (array) $keys as $key ) { 558 | if ( $this->is_multisite ) { 559 | $user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s", $key ) ); 560 | } else { 561 | $user = $this->get_userdata_from_key( $key ); 562 | } 563 | 564 | if ( ! $user ) { 565 | $this->record_status( 'error_nouser', $key ); 566 | continue; 567 | } 568 | 569 | if ( $this->is_multisite ) { 570 | // We use a different email function depending on whether they registered with blog 571 | if ( ! empty( $user->domain ) ) { 572 | wpmu_signup_blog_notification( $user->domain, $user->path, $user->title, $user->user_login, $user->user_email, $user->activation_key, maybe_unserialize( $user->meta ) ); 573 | } else { 574 | wpmu_signup_user_notification( $user->user_login, $user->user_email, $user->activation_key, maybe_unserialize( $user->meta ) ); 575 | } 576 | } else { 577 | // If you're running BP on a non-multisite instance of WP, use the 578 | // BP function to send the email 579 | if ( function_exists( 'bp_core_signup_send_validation_email' ) ) { 580 | bp_core_signup_send_validation_email( (int) $user->ID, $user->user_email, $key ); 581 | } 582 | } 583 | 584 | if ( isset( $resent_counts[ $key ] ) ) { 585 | $resent_counts[ $key ] = $resent_counts[ $key ] + 1; 586 | } else { 587 | $resent_counts[ $key ] = 1; 588 | } 589 | 590 | // I can't do a true/false check on whether the email was sent because of 591 | // the crappy way that WPMU and BP work together to send these messages 592 | // See bp_core_activation_signup_user_notification() 593 | $this->record_status( 'updated_resent', $key ); 594 | } 595 | 596 | update_site_option( 'unconfirmed_resent_counts', $resent_counts ); 597 | } 598 | 599 | /** 600 | * Utility function for recording the status of a resend/activation attempt 601 | * 602 | * @package Unconfirmed 603 | * @since 1.1 604 | * 605 | * @param str $status Something like 'updated_resent' 606 | * @param str $key The activation key of the affected user, if available 607 | */ 608 | function record_status( $status, $key = false ) { 609 | $this->results[ $status ][] = $key; 610 | } 611 | 612 | /** 613 | * Redirects the user after the requestion actions have been performed 614 | * 615 | * The function builds the redirect URL out of the $results array, so that messages can be 616 | * rendered on the redirected page. 617 | * 618 | * @package Unconfirmed 619 | * @since 1.1 620 | */ 621 | function do_redirect() { 622 | $query_vars = array( 'unconfirmed_complete' => '1' ); 623 | 624 | foreach ( (array) $this->results as $status => $keys ) { 625 | $query_vars[ $status ] = implode( ',', $keys ); 626 | } 627 | 628 | $redirect_url = add_query_arg( $query_vars, $this->base_url ); 629 | 630 | wp_redirect( $redirect_url ); 631 | } 632 | 633 | function add_args( $add_args ) { 634 | if ( ! empty( $_REQUEST['s'] ) ) { 635 | $search_text = urlencode( $_REQUEST['s'] ); 636 | $add_args['s'] = $search_text; 637 | } else { 638 | $add_args['s'] = ''; 639 | } 640 | return $add_args; 641 | } 642 | 643 | /** 644 | * Gets user activation keys out of the URL parameters and converts them to email addresses 645 | * 646 | * @package Unconfirmed 647 | * @since 1.1 648 | */ 649 | function setup_get_users() { 650 | global $wpdb; 651 | 652 | foreach ( $_REQUEST as $get_key => $activation_keys ) { 653 | $get_key = explode( '_', $get_key ); 654 | 655 | if ( 'updated' == $get_key[0] || 'error' == $get_key[0] ) { 656 | $activation_keys = explode( ',', $activation_keys ); 657 | 658 | if ( $this->is_multisite ) { 659 | foreach ( (array) $activation_keys as $ak_index => $activation_key ) { 660 | $activation_keys[ $ak_index ] = '"' . sanitize_text_field( $activation_key ) . '"'; 661 | } 662 | $activation_keys = implode( ',', $activation_keys ); 663 | 664 | $registrations = $wpdb->get_results( "SELECT user_email, activation_key FROM $wpdb->signups WHERE activation_key IN ({$activation_keys})" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 665 | } else { 666 | $registrations = array(); 667 | foreach ( (array) $activation_keys as $akey ) { 668 | $user = $this->get_userdata_from_key( $akey ); 669 | 670 | $registration = new stdClass(); 671 | $registration->user_email = isset( $user->user_email ) ? $user->user_email : ''; 672 | $registration->activation_key = isset( $user->user_activation_key ) ? $user->user_activation_key : ''; // todo: usermeta compat 673 | 674 | $registrations[] = $registration; 675 | } 676 | } 677 | 678 | $updated_or_error = $get_key[0]; 679 | $message_type = $get_key[1]; 680 | 681 | $this->results_emails[ $updated_or_error ][ $message_type ] = $registrations; 682 | } 683 | } 684 | } 685 | 686 | /** 687 | * Loops through the results_emails to create success/failure messages 688 | * 689 | * @package Unconfirmed 690 | * @since 1.0 691 | */ 692 | function setup_messages() { 693 | global $wpdb; 694 | 695 | if ( ! empty( $this->results_emails ) ) { 696 | 697 | // Cycle through the successful actions first 698 | if ( ! empty( $this->results_emails['updated'] ) ) { 699 | foreach ( $this->results_emails['updated'] as $message_type => $registrations ) { 700 | if ( ! empty( $registrations ) ) { 701 | $emails = array(); 702 | 703 | foreach ( $registrations as $registration ) { 704 | $emails[] = $registration->user_email; 705 | } 706 | 707 | $emails = implode( ', ', $emails ); 708 | 709 | } 710 | 711 | $message = ''; 712 | 713 | switch ( $message_type ) { 714 | case 'activated': 715 | /* translators: list of email addresses */ 716 | $message = sprintf( __( 'You successfully activated the following users: %s', 'unconfirmed' ), $emails ); 717 | break; 718 | 719 | case 'resent': 720 | /* translators: list of email addresses */ 721 | $message = sprintf( __( 'You successfully resent activation emails to the following users: %s', 'unconfirmed' ), $emails ); 722 | break; 723 | 724 | case 'deleted': 725 | if ( count( $registrations ) > 1 ) { 726 | $message = __( 'Registrations successfully deleted.', 'unconfirmed' ); 727 | } else { 728 | $message = __( 'Registration successfully deleted.', 'unconfirmed' ); 729 | } 730 | break; 731 | 732 | default: 733 | break; 734 | } 735 | } 736 | 737 | $this->message['updated'] = $message; 738 | } 739 | 740 | // Now cycle through the failures 741 | if ( ! empty( $this->results_emails['error'] ) ) { 742 | foreach ( $this->results_emails['error'] as $message_type => $registrations ) { 743 | if ( ! empty( $registrations ) ) { 744 | $emails = array(); 745 | 746 | foreach ( $registrations as $registration ) { 747 | $emails[] = $registration->user_email; 748 | } 749 | 750 | $emails = implode( ', ', $emails ); 751 | } 752 | 753 | switch ( $message_type ) { 754 | case 'nokey': 755 | $message = __( 'You didn\'t provide an activation key.', 'unconfirmed' ); 756 | break; 757 | 758 | case 'couldntactivate': 759 | /* translators: list of email addresses */ 760 | $message = sprintf( __( 'The following users could not be activated: %s', 'unconfirmed' ), $emails ); 761 | break; 762 | 763 | case 'nouser': 764 | $message = __( 'You provided invalid activation keys.', 'unconfirmed' ); 765 | break; 766 | 767 | case 'unsent': 768 | /* translators: list of email addresses */ 769 | $message = sprintf( __( 'Activations emails could not be resent to the following email addresses: %s', 'unconfirmed' ), $emails ); 770 | break; 771 | 772 | default: 773 | break; 774 | } 775 | } 776 | 777 | $this->message['error'] = $message; 778 | } 779 | } 780 | } 781 | 782 | /** 783 | * Echoes the error message to the screen. 784 | * 785 | * Uses the standard WP admin nag markup. 786 | * 787 | * Not sure why I put this in a separate method. I guess, so you can override it easily? 788 | * 789 | * @package Unconfirmed 790 | * @since 1.0 791 | */ 792 | function render_messages() { 793 | $this->setup_messages(); 794 | 795 | if ( ! empty( $this->message ) ) { 796 | ?> 797 | 798 | message as $message_type => $text ) : ?> 799 |
800 |

801 |
802 | 803 | 804 | 'user_login', 833 | 'title' => __( 'User Login', 'unconfirmed' ), 834 | 'css_class' => 'login', 835 | ), 836 | array( 837 | 'name' => 'user_email', 838 | 'title' => __( 'Email Address', 'unconfirmed' ), 839 | 'css_class' => 'email', 840 | ), 841 | array( 842 | 'name' => 'registered', 843 | 'title' => 'Registered', 844 | 'css_class' => 'registered', 845 | 'default_order' => 'desc', 846 | 'is_default' => true, 847 | ), 848 | array( 849 | 'name' => 'activation_key', 850 | 'title' => __( 'Activation Key', 'unconfirmed' ), 851 | 'css_class' => 'activation-key', 852 | ), 853 | array( 854 | 'name' => 'resent_count', 855 | 'title' => __( '# of Times Resent', 'unconfirmed' ), 856 | 'css_class' => 'resent-count', 857 | 'default_order' => 'desc', 858 | 'is_sortable' => false, 859 | ), 860 | ); 861 | 862 | // On non-multisite installations, we have the display name available. Show it. 863 | if ( ! $this->is_multisite ) { 864 | $non_ms_cols = array( 865 | array( 866 | 'name' => 'display_name', 867 | 'title' => __( 'Display Name', 'unconfirmed' ), 868 | ), 869 | ); 870 | 871 | // Can't get array_splice to work right for this multi-d array, so I'm 872 | // hacking around it 873 | $col0 = array( $cols[0] ); 874 | $cols_rest = array_slice( $cols, 1 ); 875 | $cols = array_merge( $col0, $non_ms_cols, $cols_rest ); 876 | } 877 | 878 | $sortable = new BBG_CPT_Sort( $cols ); 879 | 880 | $offset = $pagination->get_per_page * ( $pagination->get_paged - 1 ); 881 | 882 | $args = array( 883 | 'orderby' => $sortable->get_orderby, 884 | 'order' => $sortable->get_order, 885 | 'number' => $pagination->get_per_page, 886 | 'offset' => $offset, 887 | ); 888 | 889 | $this->setup_users( $args ); 890 | 891 | // Setting this up a certain way to make pagination/sorting easier 892 | $query = new stdClass(); 893 | $query->users = $this->users; 894 | 895 | // In order for Boone's Pagination to work, this stuff must be set manually 896 | $query->found_posts = $this->total_users; 897 | $query->max_num_pages = ceil( $query->found_posts / $pagination->get_per_page ); 898 | 899 | // Complete the pagination setup 900 | $pagination->setup_query( $query ); 901 | 902 | $search_value = isset( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : ''; 903 | 904 | ?> 905 |
906 | 907 |

908 | 909 | render_messages(); ?> 910 | 911 |
912 | 913 | 919 | 920 | users ) ) : ?> 921 |
922 |
923 | 928 | 929 | 930 | 931 | 932 | 933 |
934 | 935 |
936 |
937 | currently_viewing_text(); ?> 938 |
939 | 940 | 943 |
944 |
945 | 946 | 947 | 948 | 949 | 950 | 953 | 954 | have_columns() ) { 956 | while ( $sortable->have_columns() ) { 957 | $sortable->the_column(); 958 | $sortable->the_column_th(); 959 | } 960 | } 961 | ?> 962 | 963 | 964 | 965 | 966 | 967 | users as $user ) : ?> 968 | 969 | 972 | 973 | 1023 | 1024 | is_multisite ) : ?> 1025 | 1028 | 1029 | 1030 | 1033 | 1034 | 1037 | 1038 | 1041 | 1042 | 1045 | 1046 | 1047 | 1048 | 1049 |
951 | 952 |
970 | 971 | 1026 | display_name ); ?> 1027 | 1035 | registered ); ?> 1036 | 1039 | activation_key ); ?> 1040 | 1043 | resent_count ); ?> 1044 |
1050 | 1051 |
1052 |
1053 |
1054 | currently_viewing_text(); ?> 1055 |
1056 | 1057 | 1060 |
1061 |
1062 | 1063 | 1064 | 1065 |

1066 | 1067 | 1068 | 1069 |
1070 | 1071 |
1072 | \n" 13 | "Language-Team: LANGUAGE \n" 14 | 15 | #. translators: 1. start number, 2. end number, 3. total user count 16 | #: lib/boones-pagination.php:201 17 | msgid "Viewing %1$d - %2$d of a total of %3$d" 18 | msgstr "" 19 | 20 | #: lib/boones-pagination.php:225 21 | msgid "«" 22 | msgstr "" 23 | 24 | #: lib/boones-pagination.php:226 25 | msgid "»" 26 | msgstr "" 27 | 28 | #. #-#-#-#-# unconfirmed.pot (Unconfirmed 1.3.2) #-#-#-#-# 29 | #. Plugin Name of the plugin/theme 30 | #: unconfirmed.php:100 unconfirmed.php:875 31 | msgid "Unconfirmed" 32 | msgstr "" 33 | 34 | #: unconfirmed.php:406 unconfirmed.php:411 35 | msgid "Invalid activation key" 36 | msgstr "" 37 | 38 | #. translators: list of email addresses 39 | #: unconfirmed.php:684 40 | msgid "You successfully activated the following users: %s" 41 | msgstr "" 42 | 43 | #. translators: list of email addresses 44 | #: unconfirmed.php:689 45 | msgid "You successfully resent activation emails to the following users: %s" 46 | msgstr "" 47 | 48 | #: unconfirmed.php:694 49 | msgid "Registrations successfully deleted." 50 | msgstr "" 51 | 52 | #: unconfirmed.php:696 53 | msgid "Registration successfully deleted." 54 | msgstr "" 55 | 56 | #: unconfirmed.php:723 57 | msgid "You didn't provide an activation key." 58 | msgstr "" 59 | 60 | #. translators: list of email addresses 61 | #: unconfirmed.php:728 62 | msgid "The following users could not be activated: %s" 63 | msgstr "" 64 | 65 | #: unconfirmed.php:732 66 | msgid "You provided invalid activation keys." 67 | msgstr "" 68 | 69 | #. translators: list of email addresses 70 | #: unconfirmed.php:737 71 | msgid "" 72 | "Activations emails could not be resent to the following email addresses: %s" 73 | msgstr "" 74 | 75 | #: unconfirmed.php:801 76 | msgid "User Login" 77 | msgstr "" 78 | 79 | #: unconfirmed.php:806 80 | msgid "Email Address" 81 | msgstr "" 82 | 83 | #: unconfirmed.php:818 84 | msgid "Activation Key" 85 | msgstr "" 86 | 87 | #: unconfirmed.php:823 88 | msgid "# of Times Resent" 89 | msgstr "" 90 | 91 | #: unconfirmed.php:835 92 | msgid "Display Name" 93 | msgstr "" 94 | 95 | #: unconfirmed.php:882 96 | msgid "Search:" 97 | msgstr "" 98 | 99 | #: unconfirmed.php:892 unconfirmed.php:955 100 | msgid "Resend Activation Email" 101 | msgstr "" 102 | 103 | #: unconfirmed.php:893 unconfirmed.php:968 104 | msgid "Activate" 105 | msgstr "" 106 | 107 | #: unconfirmed.php:894 unconfirmed.php:983 108 | msgid "Delete" 109 | msgstr "" 110 | 111 | #: unconfirmed.php:897 112 | msgid "Apply" 113 | msgstr "" 114 | 115 | #: unconfirmed.php:983 116 | msgid "" 117 | "Deleting a registration means that it will be removed from the database, and " 118 | "the user will be unable to activate his account. Proceed with caution!" 119 | msgstr "" 120 | 121 | #: unconfirmed.php:1029 122 | msgid "No unactivated members were found." 123 | msgstr "" 124 | 125 | #. Plugin URI of the plugin/theme 126 | msgid "http://github.com/boonebgorges/unconfirmed" 127 | msgstr "" 128 | 129 | #. Description of the plugin/theme 130 | msgid "" 131 | "Allows admins on a WordPress Multisite network to manage unactivated users, " 132 | "by either activating them manually or resending the activation email." 133 | msgstr "" 134 | 135 | #. Author of the plugin/theme 136 | msgid "Boone B Gorges" 137 | msgstr "" 138 | 139 | #. Author URI of the plugin/theme 140 | msgid "https://boone.gorg.es" 141 | msgstr "" 142 | -------------------------------------------------------------------------------- /lib/boones-pagination.php: -------------------------------------------------------------------------------- 1 | setup_get_keys(); 38 | 39 | // Get the pagination parameters out of $_GET 40 | $this->setup_get_params(); 41 | } 42 | 43 | /** 44 | * Sets up query vars. 45 | * 46 | * I recommend that you instantiate this class right away when you start rendering the page, 47 | * so that it can do some of the $_GET argument parsing for you, which you can use to 48 | * construct your CPT query (query_posts() or new WP_Query). Then, after you have made the 49 | * query, call this function manually, in order to populate the class with query-specific 50 | * data. 51 | * 52 | * If you use query_posts() to construct the query, there's no need to pass along a $query 53 | * parameter - the function will simply look inside of the $wp_query global. However, if 54 | * you use WP_Query to run your query (so that the data is not in $wp_query), you should 55 | * pass your query object along to setup_query(). 56 | * 57 | * @package Boone's Pagination 58 | * @since 1.0 59 | */ 60 | function setup_query( $query = false ) { 61 | global $wp_query; 62 | 63 | if ( ! $query ) { 64 | $query =& $wp_query; 65 | } 66 | 67 | $this->query = $query; 68 | 69 | // Get the total number of items 70 | $this->setup_total_items(); 71 | 72 | // Get the total number of pages 73 | $this->setup_total_pages(); 74 | } 75 | 76 | /** 77 | * Sets up the $_GET param keys. 78 | * 79 | * You can either override this function in your own extended class, or filter the default 80 | * values. I have provided both options because I love you so very much. 81 | * 82 | * @package Boone's Pagination 83 | * @since 1.0 84 | */ 85 | function setup_get_keys() { 86 | $this->get_per_page_key = apply_filters( 'bbg_cpt_pag_per_page_key', 'per_page' ); 87 | 88 | /** 89 | * I chose 'paged' as the default not because I like it - I don't - but because 90 | * other choices threatened to interfere with native WP functions. In particular, 91 | * 'page' is already used in the Dashboard area to signify a plugin settings page. 92 | */ 93 | $this->get_paged_key = apply_filters( 'bbg_cpt_pag_paged_key', 'paged' ); 94 | } 95 | 96 | /** 97 | * Gets params out of $_GET global 98 | * 99 | * Does some basic checks to ensure that the values are integers and that they are non-empty 100 | * 101 | * @package Boone's Pagination 102 | * @since 1.0 103 | */ 104 | function setup_get_params() { 105 | // Per page 106 | $per_page = isset( $_GET[ $this->get_per_page_key ] ) ? $_GET[ $this->get_per_page_key ] : 10; 107 | 108 | // Basic per_page sanity and security 109 | if ( ! (int) $per_page ) { 110 | $per_page = 10; 111 | } 112 | 113 | $this->get_per_page = $per_page; 114 | 115 | // Page number 116 | $paged = isset( $_GET[ $this->get_paged_key ] ) ? $_GET[ $this->get_paged_key ] : 1; 117 | 118 | // Basic paged sanity and security 119 | if ( ! (int) $paged ) { 120 | $paged = 1; 121 | } 122 | 123 | $this->get_paged = $paged; 124 | } 125 | 126 | /** 127 | * Get the total number of items out of the query 128 | * 129 | * @package Boone's Pagination 130 | * @since 1.0 131 | */ 132 | function setup_total_items() { 133 | $this->total_items = $this->query->found_posts; 134 | } 135 | 136 | /** 137 | * Get the total number of pages out of the query 138 | * 139 | * @package Boone's Pagination 140 | * @since 1.0 141 | */ 142 | function setup_total_pages() { 143 | $this->total_pages = $this->query->max_num_pages; 144 | } 145 | 146 | /** 147 | * Get the start number for the current view (ie "Viewing *5* - 8 of 12") 148 | * 149 | * Here's the math: Subtract one from the current page number; multiply times posts_per_page 150 | * to get the last post on the previous page; add one to get the start for this page. 151 | * 152 | * @package Boone's Pagination 153 | * @since 1.0 154 | * 155 | * @return int $start The start number 156 | */ 157 | function get_start_number() { 158 | $start = ( ( $this->get_paged - 1 ) * $this->get_per_page ) + 1; 159 | 160 | return $start; 161 | } 162 | 163 | /** 164 | * Get the end number for the current view (ie "Viewing 5 - *8* of 12") 165 | * 166 | * Here's the math: Multiply the posts_per_page by the current page number. If it's the last 167 | * page (ie if the result is greater than the total number of docs), just use the total doc 168 | * count 169 | * 170 | * @package Boone's Pagination 171 | * @since 1.0 172 | * 173 | * @return int $end The start number 174 | */ 175 | function get_end_number() { 176 | global $wp_query; 177 | 178 | $end = $this->get_paged * $this->get_per_page; 179 | 180 | if ( $end > $this->total_items ) { 181 | $end = $this->total_items; 182 | } 183 | 184 | return $end; 185 | } 186 | 187 | /** 188 | * Return or echo the "Viewing x-y of z" message 189 | * 190 | * @package Boone's Pagination 191 | * @since 1.0 192 | * 193 | * @param str $type Optional. 'echo' will echo the results, anything else will return them 194 | * @return str $page_links The "viewing" text 195 | */ 196 | function currently_viewing_text( $type = 'echo' ) { 197 | $start = $this->get_start_number(); 198 | $end = $this->get_end_number(); 199 | 200 | /* translators: 1. start number, 2. end number, 3. total user count */ 201 | $string = sprintf( __( 'Viewing %1$d - %2$d of a total of %3$d', 'unconfirmed' ), $start, $end, $this->total_items ); 202 | 203 | if ( 'echo' == $type ) { 204 | echo esc_html( $string ); 205 | } else { 206 | return $string; 207 | } 208 | } 209 | 210 | /** 211 | * Return or echo the pagination links 212 | * 213 | * @package Boone's Pagination 214 | * @since 1.0 215 | * 216 | * @param str $type Optional. 'echo' will echo the results, anything else will return them 217 | * @return str $page_links The pagination links 218 | */ 219 | function paginate_links( $type = 'echo' ) { 220 | $add_args = apply_filters( 'bbg_cpt_pag_add_args', array( $this->get_per_page_key => $this->get_per_page ) ); 221 | $page_links = paginate_links( 222 | array( 223 | 'base' => add_query_arg( $this->get_paged_key, '%#%' ), 224 | 'format' => '', 225 | 'prev_text' => __( '«', 'unconfirmed' ), 226 | 'next_text' => __( '»', 'unconfirmed' ), 227 | 'total' => $this->total_pages, 228 | 'current' => $this->get_paged, 229 | 'add_args' => $add_args, 230 | ) 231 | ); 232 | 233 | if ( 'echo' == $type ) { 234 | echo $page_links; 235 | } else { 236 | return $page_links; 237 | } 238 | } 239 | } 240 | 241 | endif; 242 | 243 | 244 | -------------------------------------------------------------------------------- /lib/boones-sortable-columns.php: -------------------------------------------------------------------------------- 1 | elements of WP Dashboard 'widefat' tables. That complex CSS 68 | * selector will automatically contain classes like 'sortable' and 69 | * 'asc', depending on the parameters and on your current page. 70 | * css_class is an additional class that will be added to the selector 71 | * so that you can do column-specific styling. If you don't provide 72 | * a css_class, it'll default to the 'name' parameter. 73 | * - 'default_order' Accepts 'asc' or 'desc'. Usually you'll want 'asc', except 74 | * for date-based columns, when it generally makes sense for the 75 | * most recent columns to be listed first. 76 | * - 'posts_column' Right now this does nothing :) 77 | * - 'is_default' True if you want the given column to be the default sort order. 78 | * If more than one of your columns have 'is_default' set to true, the 79 | * last one will take precedence. 80 | */ 81 | $defaults = array( 82 | 'name' => false, 83 | 'title' => false, 84 | 'is_sortable' => true, 85 | 'css_class' => false, 86 | 'default_order' => 'asc', 87 | 'posts_column' => false, 88 | 'is_default' => false, 89 | ); 90 | 91 | $this->columns = array(); 92 | $this->sortable_keys = array(); 93 | 94 | foreach ( $cols as $col ) { 95 | // You need at least a name and a title to continue 96 | if ( empty( $col['name'] ) || empty( $col['title'] ) ) { 97 | continue; 98 | } 99 | 100 | $r = wp_parse_args( $col, $defaults ); 101 | 102 | // If the css_class is not set, just use the name param 103 | if ( empty( $r['css_class'] ) ) { 104 | $r['css_class'] = $r['name']; 105 | } 106 | 107 | // Check to see whether this is a default. Providing more than one default 108 | // will mean that the last one overrides the others 109 | if ( ! empty( $r['is_default'] ) ) { 110 | $this->default_orderby = $r['name']; 111 | } 112 | 113 | // Compare the default order against a whitelist of 'asc' and 'desc' 114 | if ( 'asc' == strtolower( $r['default_order'] ) || 'desc' == strtolower( $r['default_order'] ) ) { 115 | $r['default_order'] = strtolower( $r['default_order'] ); 116 | } else { 117 | $r['default_order'] = 'asc'; 118 | } 119 | 120 | // If it's sortable, add the name to the $sortable_keys array 121 | if ( $r['is_sortable'] ) { 122 | $this->sortable_keys[] = $r['name']; 123 | } 124 | 125 | // Convert to an object for maximum prettiness 126 | $col_obj = new stdClass(); 127 | 128 | foreach ( $r as $key => $value ) { 129 | $col_obj->$key = $value; 130 | } 131 | 132 | $this->columns[] = $col_obj; 133 | } 134 | 135 | // Now, set up some values for the loop 136 | $this->column_count = count( $this->columns ); 137 | $this->current_column = -1; 138 | 139 | // If a default orderby was not found, just choose the first item in the array 140 | if ( empty( $this->default_orderby ) && ! empty( $cols[0]['name'] ) ) { 141 | $this->default_orderby = $cols[0]['name']; 142 | } 143 | 144 | // Set up the $_GET keys (which are customizable) 145 | $this->setup_get_keys(); 146 | 147 | // Get the pagination parameters out of $_GET 148 | $this->setup_get_params(); 149 | 150 | // Set up the next orders (asc or desc) depending on current state 151 | $this->setup_next_orders(); 152 | 153 | // Set up the URL to be used as a base for href links 154 | $this->setup_base_url(); 155 | } 156 | 157 | /** 158 | * Sets up the $_GET param keys. 159 | * 160 | * You can either override this function in your own extended class, or filter the default 161 | * values. I have provided both options because I love you so very much. 162 | * 163 | * @package Boone's Sortable Columns 164 | * @since 1.0 165 | */ 166 | function setup_get_keys() { 167 | $this->get_orderby_key = apply_filters( 'bbg_cpt_sort_orderby_key', 'orderby' ); 168 | $this->get_order_key = apply_filters( 'bbg_cpt_sort_order_key', 'order' ); 169 | } 170 | 171 | /** 172 | * Gets params out of $_GET global 173 | * 174 | * Does some basic sanity checks on the orderby and order parameters, ensuring that the 175 | * 'order' param is either 'asc' or 'desc', and that the 'orderby' param actually matches 176 | * one of the columns fed to the constructor. 177 | * 178 | * @package Boone's Sortable Columns 179 | * @since 1.0 180 | */ 181 | function setup_get_params() { 182 | // Orderby 183 | $orderby = isset( $_GET[ $this->get_orderby_key ] ) ? $_GET[ $this->get_orderby_key ] : false; 184 | 185 | // If an orderby param is provided, check to see that it's permitted. 186 | // Otherwise set the current orderby to the default 187 | if ( $orderby && in_array( $orderby, $this->sortable_keys ) ) { 188 | $this->get_orderby = $orderby; 189 | } else { 190 | $this->get_orderby = $this->default_orderby; 191 | } 192 | 193 | // Order 194 | $order = isset( $_GET[ $this->get_order_key ] ) ? $_GET[ $this->get_order_key ] : false; 195 | 196 | // If an order is provided, make sure it's either 'desc' or 'asc' 197 | // Otherwise set current order to the orderby's default order 198 | if ( $order && ( 'desc' == strtolower( $order ) || 'asc' == strtolower( $order ) ) ) { 199 | $order = strtolower( $order ); 200 | } else { 201 | // Loop through to find the default order for this bad boy 202 | // This is not optimized because of the way the array is keyed 203 | // Cry me a river why don't you 204 | foreach ( $this->columns as $col ) { 205 | if ( $col->name == $this->get_orderby ) { 206 | $order = $col->default_order; 207 | break; 208 | } 209 | } 210 | } 211 | 212 | // There should only be two options, 'asc' and 'desc'. We'll cut some slack for 213 | // uppercase variants 214 | $order = 'desc' == strtolower( $order ) ? 'desc' : 'asc'; 215 | 216 | $this->get_order = $order; 217 | } 218 | 219 | /** 220 | * Loops through the columns and determines what the next_order should be 221 | * 222 | * In other words: when you are currently sorting by (for example) post_date ASC, the 223 | * next_order for the post_date column should be DESC. For all columns that are not the 224 | * current sort order, the next_order should be the default_order of that column. 225 | * 226 | * The next_order values are used to create the href of the column header links, as well 227 | * as the CSS selectors 'asc' and 'desc' that the WP admin CSS/JS need to do fancy schmancy 228 | * mouseovers. 229 | * 230 | * @package Boone's Sortable Columns 231 | * @since 1.0 232 | */ 233 | function setup_next_orders() { 234 | foreach ( $this->columns as $name => $col ) { 235 | if ( $col->name == $this->get_orderby ) { 236 | $current_order = $this->get_order; 237 | $next_order = 'asc' == $current_order ? 'desc' : 'asc'; 238 | } else { 239 | $next_order = $col->default_order; 240 | } 241 | 242 | $this->columns[ $name ]->next_order = $next_order; 243 | } 244 | } 245 | 246 | /** 247 | * Set the base url that will be used for creating links 248 | * 249 | * By default, Boone's Sortable Columns will use your current URL as the base for creating 250 | * the clickable headers. (To be more specific, it uses add_query_arg() with a null value 251 | * for the query/url param, so that it defaults to $_SERVER['REQUEST_URI']. See 252 | * add_query_arg() for more details.) 253 | * 254 | * In some cases, you may want to use a special URL for this purpose. For instance, you may 255 | * want to remove certain query argument. In this function, I assume that you *always* want 256 | * to remove _wpnonce, since that should be generated on the fly. I also assume that when 257 | * a column is resorted, pagination should be reset (thus the presence of 'paged' and 258 | * 'per_page' on the blacklist). If you want to remove additional query arguments (such as 259 | * those used to generate success messages, etc), filter 260 | * boones_sortable_columns_keys_to_remove. 261 | * 262 | * You can also override this behavior by feeding your own custom value to the method, 263 | * immediately after instantiating the class. For example, 264 | * $sortable = new BBG_CPT_Sort( $cols ); 265 | * $sortable->setup_base_url( 'http://example.com' ); 266 | * Or, of course, you can override the method in your own class. 267 | * 268 | * @package Boone's Sortable Columns 269 | * @since 1.0.1 270 | * 271 | * @param str $url The base URL. Optional. Defaults to $_SERVER['REQUEST_URI']. 272 | */ 273 | function setup_base_url( $url = false ) { 274 | if ( ! $url ) { 275 | $current_keys = array_keys( $_GET ); 276 | 277 | // These are keys that will always be removed from the base url 278 | $keys_to_remove = apply_filters( 279 | 'boones_sortable_columns_keys_to_remove', array( 280 | '_wpnonce', 281 | 'paged', 282 | ) 283 | ); 284 | 285 | foreach ( $keys_to_remove as $key ) { 286 | $url = remove_query_arg( $key, $url ); 287 | } 288 | } 289 | 290 | $this->base_url = $url; 291 | } 292 | 293 | /** 294 | * Part of the Loop 295 | * 296 | * As in the regular WP post Loop, you can loop through the columns like so: 297 | * $sortable = new BBG_CPT_Sort( $cols ); 298 | * if ( $sortable->have_columns() ) { 299 | * while ( $sortable->have_columns() ) { 300 | * $sortable->the_column(); 301 | * } 302 | * } 303 | * 304 | * @package Boone's Sortable Columns 305 | * @since 1.0 306 | */ 307 | function have_columns() { 308 | // Compare against the column_count - 1 to account for the 0 array index shift 309 | if ( $this->column_count && $this->current_column < $this->column_count - 1 ) { 310 | return true; 311 | } 312 | 313 | return false; 314 | } 315 | 316 | /** 317 | * Part of the Loop 318 | * 319 | * @package Boone's Sortable Columns 320 | * @since 1.0 321 | */ 322 | function next_column() { 323 | $this->current_column++; 324 | $this->column = $this->columns[ $this->current_column ]; 325 | 326 | return $this->column; 327 | } 328 | 329 | /** 330 | * Part of the Loop 331 | * 332 | * @package Boone's Sortable Columns 333 | * @since 1.0 334 | */ 335 | function rewind_columns() { 336 | $this->current_column = -1; 337 | if ( $this->column_count > 0 ) { 338 | $this->column = $this->columns[0]; 339 | } 340 | } 341 | 342 | /** 343 | * Part of the Loop 344 | * 345 | * @package Boone's Sortable Columns 346 | * @since 1.0 347 | */ 348 | function the_column() { 349 | $this->in_the_loop = true; 350 | $this->column = $this->next_column(); 351 | 352 | if ( 0 == $this->current_column ) { // loop has just started 353 | do_action( 'loop_start', $GLOBALS['wp_query']); 354 | } 355 | } 356 | 357 | /** 358 | * Constructs a complex CSS selector for the column header 359 | * 360 | * This set of CSS classes is designed to work seamlessly with WP's admin CSS and JS for 361 | * elements inside of . With just a bit of custom CSS and JS, 362 | * though, the same class can work well on the front end as well. 363 | * 364 | * The following classes are created, as appropriate: 365 | * - 'sortable' / 'sorted' 366 | * - 'asc' / 'desc' 367 | * - the custom css_class fed to BBG_CPT_Sort::__construct() 368 | * 369 | * @package Boone's Sortable Columns 370 | * @since 1.0 371 | * 372 | * @param str $type 'echo' if you want the result echo, 'return' if you want it returned 373 | * @return str $class The CSS classes, separated by spaces 374 | */ 375 | function the_column_css_class( $type = 'echo' ) { 376 | // The column-identifying class 377 | $class = array( $this->column->css_class ); 378 | 379 | // Sortable logic 380 | if ( $this->column->is_sortable ) { 381 | // Add the sorted/sortable class, based on whether this is the current sort 382 | if ( $this->column->name == $this->get_orderby ) { 383 | $class[] = 'sorted'; 384 | $class[] = $this->get_order; 385 | } else { 386 | $class[] = 'sortable'; 387 | $class[] = 'asc' == $this->column->default_order ? 'desc' : 'asc'; 388 | } 389 | } 390 | 391 | $class = implode( ' ', $class ); 392 | 393 | if ( 'echo' == $type ) { 394 | echo $class; 395 | } else { 396 | return $class; 397 | } 398 | } 399 | 400 | /** 401 | * Constructs the href URL for the column header 402 | * 403 | * This method is really the raison d'être of Boone's Sortable Columns, so make sure you 404 | * read this docblock carefully. 405 | * 406 | * Sortable columns work by turning each column header into a link that, when clicked, will 407 | * return new results that are sorted based on your requests. Making the URLs for those 408 | * links can be complex, however. This method will do all the heavy lifting for you, 409 | * producing a URL or an entire anchor tag that you can use as the column header. 410 | * 411 | * For example, let's say your current URL is http://example.com/restaurants, which displays 412 | * a list of restaurants which are, by default, sorted by restaurant name, in ascending 413 | * alphabetical order. The column header for the Restaurant Name column should be a link 414 | * to sort the list by restaurant name in *descending* alphabetical order, while, for 415 | * example, the Cuisine column should be a link to sort the list by cuisine type, in 416 | * ascending order. Accordingly (assuming you've instantiated the class properly; see 417 | * readme.txt for more instructions), the following lines of code 418 | * 419 | * have_columns() ) : ?> 420 | * have_columns() ) : $sortable->the_column() ?> 421 | * the_column_next_link() ?> 422 | * 423 | * 424 | * 425 | * will output the following HTML: 426 | * 427 | * Restaurant 428 | * Name 429 | * Cuisine 430 | * Type 431 | * 432 | * @package Boone's Sortable Columns 433 | * @since 1.0 434 | * 435 | * @param str $type 'echo' if you want the result echo, 'return' if you want it returned 436 | * @param str $html_or_url 'html' if you want the entire anchor HTML, or 'url' if you just 437 | * want the URL for the href 438 | * @return str $link The link URL or the HTML anchor object, depending on the $html_or_url 439 | * param 440 | */ 441 | function the_column_next_link( $type = 'echo', $html_or_url = 'html' ) { 442 | $args = array( 443 | $this->get_orderby_key => $this->column->name, 444 | $this->get_order_key => $this->column->next_order, 445 | ); 446 | 447 | $url = add_query_arg( $args, $this->base_url ); 448 | 449 | // Assemble the html link, if necessary 450 | if ( 'html' == $html_or_url ) { 451 | $html = sprintf( '%3$s', $this->column->name, $url, $this->the_column_title( 'return' ) ); 452 | 453 | $link = $html; 454 | } else { 455 | $link = $url; 456 | } 457 | 458 | if ( 'echo' == $type ) { 459 | echo $link; 460 | } else { 461 | return $link; 462 | } 463 | } 464 | 465 | /** 466 | * Gets the title text for the column header 467 | * 468 | * Essentially, this just returns the value of 'title' fed to $cols. See 469 | * BBG_CPT_Sort::__construct() for more information 470 | * 471 | * @package Boone's Sortable Columns 472 | * @since 1.0 473 | * 474 | * @param str $type 'echo' if you want the result echo, 'return' if you want it returned 475 | * @return str $class The title 476 | */ 477 | function the_column_title( $type = 'echo' ) { 478 | $name = $this->column->title; 479 | 480 | if ( 'echo' == $type ) { 481 | echo $name; 482 | } else { 483 | return $name; 484 | } 485 | } 486 | 487 | /** 488 | * Constructs a 497 | * 498 | * This is intended for use in
column header element out of the column data 489 | * 490 | * The item returned will look something like this: 491 | *
on the WordPress Dashboard, and will 499 | * take advantage of WordPress's nice CSS and JavaScript governing the appearance and 500 | * behavior of these column headers. You can also use these ', $this->the_column_css_class( 'return' ), $td_content ); 519 | 520 | if ( 'echo' == $type ) { 521 | echo $html; 522 | } else { 523 | return $html; 524 | } 525 | } 526 | } 527 | 528 | endif; 529 | 530 | 531 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Unconfirmed === 2 | Contributors: boonebgorges, cuny-academic-commons 3 | Donate link: http://teleogistic.net/donate 4 | Tags: multisite, network, activate, activation, email 5 | Requires at least: 3.1 6 | Tested up to: 5.4 7 | Stable tag: 1.3.5 8 | 9 | Allows WordPress admins to manage unactivated users, by activating them manually, deleting their pending registrations, or resending the activation email. 10 | 11 | == Description == 12 | 13 | If you run a WordPress or BuddyPress installation, you probably know that some of the biggest administrative headaches come from the activation process. Activation emails may be caught by spam filters, deleted unwillingly, or simply not understood. Yet WordPress itself has no UI for viewing and managing unactivated members. 14 | 15 | Unconfirmed creates a Dashboard panel under the Users menu (Network Admin > Users on Multisite) that shows a list of unactivated user registrations. For each registration, you have the option of resending the original activation email, or manually activating the user. 16 | 17 | Note that the plugin works for the following configurations: 18 | 1. Multisite, with or without BuddyPress 19 | 2. Single site, with BuddyPress used for user registration 20 | 21 | There is currently no support for single-site WP registration without BuddyPress. 22 | 23 | == Installation == 24 | 25 | 1. Install 26 | 1. Activate 27 | 1. Navigate to Network Admin > Users > Unconfirmed 28 | 29 | == Changelog == 30 | 31 | = 1.3.5 = 32 | * Fix compatibility with FacetWP 33 | 34 | = 1.3.4 = 35 | * Security hardening 36 | * PHPCS improvements 37 | 38 | = 1.3.3 = 39 | * Internationalization improvements 40 | 41 | = 1.3.2 = 42 | * Internationalization improvements 43 | * Coding standards fixes 44 | 45 | = 1.3.1 = 46 | * Fix bug that causes email resend to fail on BP 2.5+ 47 | 48 | = 1.3 = 49 | * Use custom 'moderate_signups' cap instead of 'create_users' when adding Unconfirmed panel 50 | * Add fine-grained filter for whether to use the Network Admin 51 | * Fix ordering in Multisite 52 | 53 | = 1.2.7 = 54 | * Better loading of assets over SSL 55 | 56 | = 1.2.6 = 57 | * Removed PHP4 constructors from boones-* libraries, to avoid PHP notices 58 | * Enable search 59 | 60 | = 1.2.5 = 61 | * Improved protection against XSS 62 | 63 | = 1.2.4 = 64 | * Improved sanitization 65 | * Improved bootstrap for loading in various environments 66 | * Removed some error warnings 67 | 68 | = 1.2.3 = 69 | * Allows searching 70 | * Better support for WP 3.5+ 71 | 72 | = 1.2.2 = 73 | * Fixes pagination count for non-MS installations 74 | 75 | = 1.2.1 = 76 | * Better support for WP 3.5 77 | 78 | = 1.2 = 79 | * Adds 'Delete' buttons to remove registrations 80 | * Adds support for non-MS WordPress + BuddyPress 81 | 82 | = 1.1 = 83 | * Adds bulk resend/activate options 84 | * Adds a Resent Count column, to keep track of how many times an activation email has been resent to a given user 85 | * Refines the success/failure messages to contain better information 86 | * Updates Boone's Pagination and Boone's Sortable Columns 87 | 88 | = 1.0.3 = 89 | * Removes Boone's Sortable Columns plugin header to ensure no conflicts during WP plugin activation 90 | 91 | = 1.0.2 = 92 | * Adds language file 93 | * Fixes problem with email resending feedback related to BuddyPress 94 | 95 | = 1.0.1 = 96 | * Adds pagination styling 97 | 98 | = 1.0 = 99 | * Initial release 100 | -------------------------------------------------------------------------------- /unconfirmed.php: -------------------------------------------------------------------------------- 1 |
s in other tables (say, on 501 | * the front-end of your WordPress site), but you'll have to duplicate some of WP's core 502 | * CSS and JS if you want it to be all pretty-like. 503 | * 504 | * @package Boone's Sortable Columns 505 | * @since 1.0 506 | * 507 | * @param str $type 'echo' if you want the result echo, 'return' if you want it returned 508 | * @return str $class The element 509 | */ 510 | function the_column_th( $type = 'echo' ) { 511 | if ( $this->column->is_sortable ) { 512 | $td_content = sprintf( '%2$s', $this->the_column_next_link( 'return', 'url' ), $this->the_column_title( 'return' ) ); 513 | } else { 514 | $td_content = $this->the_column_title( 'return' ); 515 | 516 | } 517 | 518 | $html = sprintf( '%2$s