├── 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 |
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 |
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 column header element out of the column data
489 | *
490 | * The item returned will look something like this:
491 | * |
492 | *
493 | * Restaurant Name
494 | *
495 | *
496 | * |
497 | *
498 | * This is intended for use in 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 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 | ', $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 | |