PluginProbe
Groups – Memberships and Access Control / 4.7.1
Groups – Memberships and Access Control v4.7.1
4.7.1 4.7.0 4.6.0 4.5.0 4.4.0 4.3.0 trunk 1.0.0-beta-1 1.0.0-beta-2 1.0.0-beta-3 1.0.0-beta-3b 1.0.0-beta-3c 1.0.0-beta-3d 1.1.4 1.1.5 1.10.0 1.10.1 1.10.2 1.10.3 1.11.0 1.11.1 1.11.2 1.11.3 1.12.0 1.13.0 All 131 releases
← All changes | lib/admin/groups-admin-groups.php +448 -209 1.11.14.7.1 View file →
@@ -22,19 +22,19 @@
22 22 if ( !defined( 'ABSPATH' ) ) {
23 23 exit;
24 24 }
25 25
26 +// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
27 +
26 28 // admin defines
27 29 define( 'GROUPS_GROUPS_PER_PAGE', 10 );
28 -define( 'GROUPS_ADMIN_GROUPS_NONCE_1', 'groups-nonce-1');
29 -define( 'GROUPS_ADMIN_GROUPS_NONCE_2', 'groups-nonce-2');
30 -define( 'GROUPS_ADMIN_GROUPS_ACTION_NONCE', 'groups-action-nonce');
30 +define( 'GROUPS_ADMIN_GROUPS_ACTION_NONCE', 'groups-action-nonce' );
31 31 define( 'GROUPS_ADMIN_GROUPS_FILTER_NONCE', 'groups-filter-nonce' );
32 32
33 -require_once( GROUPS_CORE_LIB . '/class-groups-pagination.php' );
34 -require_once( GROUPS_ADMIN_LIB . '/groups-admin-groups-add.php');
35 -require_once( GROUPS_ADMIN_LIB . '/groups-admin-groups-edit.php');
36 -require_once( GROUPS_ADMIN_LIB . '/groups-admin-groups-remove.php');
33 +require_once GROUPS_CORE_LIB . '/class-groups-pagination.php';
34 +require_once GROUPS_ADMIN_LIB . '/groups-admin-groups-add.php';
35 +require_once GROUPS_ADMIN_LIB . '/groups-admin-groups-edit.php';
36 +require_once GROUPS_ADMIN_LIB . '/groups-admin-groups-remove.php';
37 37
38 38 /**
39 39 * Manage Groups: table of groups and add, edit, remove actions.
40 40 */
@@ -42,55 +42,60 @@
42 42
43 43 global $wpdb;
44 44
45 45 $output = '';
46 - $today = date( 'Y-m-d', time() );
46 + // $today = date( 'Y-m-d', time() ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
47 47
48 - if ( !current_user_can( GROUPS_ADMINISTER_GROUPS ) ) {
49 - wp_die( __( 'Access denied.', GROUPS_PLUGIN_DOMAIN ) );
48 + if ( !Groups_User::current_user_can( GROUPS_ADMINISTER_GROUPS ) ) {
49 + wp_die( esc_html__( 'Access denied.', 'groups' ) );
50 50 }
51 51
52 52 //
53 53 // handle actions
54 54 //
55 - if ( isset( $_POST['action'] ) ) {
55 + if ( isset( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
56 56 // handle action submit - do it
57 - switch( $_POST['action'] ) {
57 + switch ( groups_sanitize_post( 'action' ) ) {
58 58 case 'add' :
59 59 if ( !( $group_id = groups_admin_groups_add_submit() ) ) {
60 60 return groups_admin_groups_add();
61 61 } else {
62 62 $group = Groups_Group::read( $group_id );
63 - Groups_Admin::add_message( sprintf( __( "The <em>%s</em> group has been created.", GROUPS_PLUGIN_DOMAIN ), stripslashes( wp_filter_nohtml_kses( $group->name ) ) ) );
63 + Groups_Admin::add_message( sprintf(
64 + /* translators: group name */
65 + __( 'The <em>%s</em> group has been created.', 'groups' ),
66 + $group->name ? stripslashes( wp_filter_nohtml_kses( $group->name ) ) : ''
67 + ) );
64 68 }
65 69 break;
66 70 case 'edit' :
67 71 if ( !( $group_id = groups_admin_groups_edit_submit() ) ) {
68 - return groups_admin_groups_edit( $_POST['group-id-field'] );
72 + return groups_admin_groups_edit( groups_sanitize_post( 'group-id-field' ) );
69 73 } else {
70 74 $group = Groups_Group::read( $group_id );
71 - Groups_Admin::add_message( sprintf( __( 'The <em>%s</em> group has been updated.', GROUPS_PLUGIN_DOMAIN ), stripslashes( wp_filter_nohtml_kses( $group->name ) ) ) );
75 + Groups_Admin::add_message( sprintf(
76 + /* translators: group name */
77 + __( 'The <em>%s</em> group has been updated.', 'groups' ),
78 + $group->name ? stripslashes( wp_filter_nohtml_kses( $group->name ) ) : ''
79 + ) );
72 80 }
73 81 break;
74 82 case 'remove' :
75 83 if ( $group_id = groups_admin_groups_remove_submit() ) {
76 - Groups_Admin::add_message( __( 'The group has been deleted.', GROUPS_PLUGIN_DOMAIN ) );
84 + Groups_Admin::add_message( __( 'The group has been deleted.', 'groups' ) );
77 85 }
78 86 break;
79 87 // bulk actions on groups: add capabilities, remove capabilities, remove groups
80 88 case 'groups-action' :
81 - if ( wp_verify_nonce( $_POST[GROUPS_ADMIN_GROUPS_ACTION_NONCE], 'admin' ) ) {
82 - $group_ids = isset( $_POST['group_ids'] ) ? $_POST['group_ids'] : null;
83 - $bulk_action = null;
84 - if ( isset( $_POST['bulk'] ) ) {
85 - $bulk_action = $_POST['bulk-action'];
86 - }
89 + if ( groups_verify_post_nonce( GROUPS_ADMIN_GROUPS_ACTION_NONCE, 'admin' ) ) {
90 + $group_ids = groups_sanitize_post( 'group_ids' );
91 + $bulk_action = groups_sanitize_post( 'bulk-action' );
87 92 if ( is_array( $group_ids ) && ( $bulk_action !== null ) ) {
88 93 foreach ( $group_ids as $group_id ) {
89 94 switch ( $bulk_action ) {
90 95 case 'add-capability' :
91 - $capabilities_id = isset( $_POST['capability_id'] ) ? $_POST['capability_id'] : null;
92 - if ( $capabilities_id !== null ) {
96 + $capabilities_id = groups_sanitize_post( 'capability_id' );
97 + if ( is_array( $capabilities_id ) ) {
93 98 foreach ( $capabilities_id as $capability_id ) {
94 99 Groups_Group_Capability::create( array( 'group_id' => $group_id, 'capability_id' => $capability_id ) );
95 100 }
96 101 }
@@ -95,10 +100,10 @@
95 100 }
96 101 }
97 102 break;
98 103 case 'remove-capability' :
99 - $capabilities_id = isset( $_POST['capability_id'] ) ? $_POST['capability_id'] : null;
100 - if ( $capabilities_id !== null ) {
104 + $capabilities_id = groups_sanitize_post( 'capability_id' );
105 + if ( is_array( $capabilities_id ) ) {
101 106 foreach ( $capabilities_id as $capability_id ) {
102 107 Groups_Group_Capability::delete( $group_id, $capability_id );
103 108 }
104 109 }
@@ -103,9 +108,9 @@
103 108 }
104 109 }
105 110 break;
106 111 case 'remove-group' :
107 - $bulk_confirm = isset( $_POST['confirm'] ) ? true : false;
112 + $bulk_confirm = isset( $_POST['confirm'] ) ? true : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing
108 113 if ( $bulk_confirm ) {
109 114 groups_admin_groups_bulk_remove_submit();
110 115 } else {
111 116 return groups_admin_groups_bulk_remove();
@@ -110,30 +115,90 @@
110 115 } else {
111 116 return groups_admin_groups_bulk_remove();
112 117 }
113 118 break;
119 + default:
120 + if ( has_action( 'groups_admin_groups_handle_bulk_action' ) ) {
121 + /**
122 + * Handle the requested bulk action.
123 + *
124 + * @param string $bulk_action the requested bulk action
125 + * @param string|int $group_id the requested group ID
126 + */
127 + do_action( 'groups_admin_groups_handle_bulk_action', $bulk_action, $group_id );
128 + }
114 129 }
115 130 }
116 131 }
117 132 }
118 133 break;
134 + default:
135 + if ( has_filter( 'groups_admin_groups_handle_action_submit' ) ) {
136 + /**
137 + * Handle a requested action after $_POST.
138 + *
139 + * @since 3.7.0
140 + *
141 + * @param boolean $handle whether to handle the posted action
142 + * @param string $action the requested action
143 + *
144 + * @return boolean whether the posted data was accepted and action was taken
145 + */
146 + if ( apply_filters( 'groups_admin_groups_handle_action_submit', false, groups_sanitize_post( 'action' ) ) ) {
147 + /**
148 + * Fires after the posted data for an action was accepted.
149 + *
150 + * Should produce output to provide feedback to the user.
151 + *
152 + * @since 3.7.0
153 + *
154 + * @param string $action the requested action
155 + */
156 + do_action( 'groups_admin_groups_handle_action_confirm', groups_sanitize_post( 'action' ) );
157 + } else {
158 + /**
159 + * Fires after the posted data for an action was rejected.
160 + *
161 + * Should produce output to provide feedback to the user.
162 + *
163 + * @since 3.7.0
164 + *
165 + * @param string $action the requested action
166 + */
167 + do_action( 'groups_admin_groups_handle_action_reject', groups_sanitize_post( 'action' ) );
168 + return;
169 + }
170 + }
119 171 }
120 - } else if ( isset ( $_GET['action'] ) ) {
172 + } else if ( isset( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
121 173 // handle action request - show form
122 - switch( $_GET['action'] ) {
174 + switch ( groups_sanitize_get( 'action' ) ) {
123 175 case 'add' :
124 176 return groups_admin_groups_add();
125 177 break;
126 178 case 'edit' :
127 - if ( isset( $_GET['group_id'] ) ) {
128 - return groups_admin_groups_edit( $_GET['group_id'] );
179 + if ( isset( $_GET['group_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
180 + return groups_admin_groups_edit( groups_sanitize_get( 'group_id' ) );
129 181 }
130 182 break;
131 183 case 'remove' :
132 - if ( isset( $_GET['group_id'] ) ) {
133 - return groups_admin_groups_remove( $_GET['group_id'] );
184 + if ( isset( $_GET['group_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
185 + return groups_admin_groups_remove( groups_sanitize_get( 'group_id' ) );
134 186 }
135 187 break;
188 + default:
189 + if ( isset( $_GET['group_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
190 + if ( has_action( 'groups_admin_groups_handle_action' ) ) {
191 + /**
192 + * Handle the requested action and produce the corresponding output.
193 + *
194 + * @param string $action the requested action
195 + * @param string|int $group_id the requested group ID
196 + */
197 + do_action( 'groups_admin_groups_handle_action', groups_sanitize_get( 'action' ), groups_sanitize_get( 'group_id' ) );
198 + return;
199 + }
200 + }
136 201 }
137 202 }
138 203
139 204 //
@@ -139,78 +204,80 @@
139 204 //
140 205 // group table
141 206 //
142 207 if (
143 - isset( $_POST['clear_filters'] ) ||
144 - isset( $_POST['group_id'] ) ||
145 - isset( $_POST['group_name'] )
208 + isset( $_POST['clear_filters'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Missing
209 + isset( $_POST['group_id'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Missing
210 + isset( $_POST['group_name'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
146 211 ) {
147 - if ( !wp_verify_nonce( $_POST[GROUPS_ADMIN_GROUPS_FILTER_NONCE], 'admin' ) ) {
148 - wp_die( __( 'Access denied.', GROUPS_PLUGIN_DOMAIN ) );
212 + if ( !groups_verify_post_nonce( GROUPS_ADMIN_GROUPS_FILTER_NONCE, 'admin' ) ) {
213 + wp_die( esc_html__( 'Access denied.', 'groups' ) );
149 214 }
150 215 }
151 216
152 217 // filters
153 - $group_id = Groups_Options::get_user_option( 'groups_group_id', null );
154 - $group_name = Groups_Options::get_user_option( 'groups_group_name', null );
218 + $group_id = Groups_Options::get_user_option( 'groups_group_id', null );
219 + $group_name = Groups_Options::get_user_option( 'groups_group_name', null );
155 220
156 - if ( isset( $_POST['clear_filters'] ) ) {
221 + if ( isset( $_POST['clear_filters'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
157 222 Groups_Options::delete_user_option( 'groups_group_id' );
158 223 Groups_Options::delete_user_option( 'groups_group_name' );
159 224 $group_id = null;
160 225 $group_name = null;
161 - } else if ( isset( $_POST['submitted'] ) ) {
226 + } else if ( isset( $_POST['submitted'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
162 227 // filter by name
163 - if ( !empty( $_POST['group_name'] ) ) {
164 - $group_name = $_POST['group_name'];
228 + if ( !empty( $_POST['group_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
229 + $group_name = groups_sanitize_post( 'group_name' );
165 230 Groups_Options::update_user_option( 'groups_group_name', $group_name );
166 231 }
167 232 // filter by group id
168 - if ( !empty( $_POST['group_id'] ) ) {
169 - $group_id = intval( $_POST['group_id'] );
233 + if ( !empty( $_POST['group_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
234 + $group_id = intval( groups_sanitize_post( 'group_id' ) );
170 235 Groups_Options::update_user_option( 'groups_group_id', $group_id );
171 - } else if ( isset( $_POST['group_id'] ) ) { // empty && isset => '' => all
236 + } else if ( isset( $_POST['group_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
237 + // empty && isset => '' => all
172 238 $group_id = null;
173 239 Groups_Options::delete_user_option( 'groups_group_id' );
174 240 }
175 241 }
176 242
177 - if ( isset( $_POST['row_count'] ) ) {
178 - if ( !wp_verify_nonce( $_POST[GROUPS_ADMIN_GROUPS_NONCE_1], 'admin' ) ) {
179 - wp_die( __( 'Access denied.', GROUPS_PLUGIN_DOMAIN ) );
243 + if ( isset( $_POST['row_count'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
244 + if ( !groups_verify_post_nonce( GROUPS_ADMIN_GROUPS_NONCE, 'admin' ) ) {
245 + wp_die( esc_html__( 'Access denied.', 'groups' ) );
180 246 }
181 247 }
182 248
183 - if ( isset( $_POST['paged'] ) ) {
184 - if ( !wp_verify_nonce( $_POST[GROUPS_ADMIN_GROUPS_NONCE_2], 'admin' ) ) {
185 - wp_die( __( 'Access denied.', GROUPS_PLUGIN_DOMAIN ) );
249 + if ( isset( $_POST['paged'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
250 + if ( !groups_verify_post_nonce( GROUPS_ADMIN_GROUPS_NONCE, 'admin' ) ) {
251 + wp_die( esc_html__( 'Access denied.', 'groups' ) );
186 252 }
187 253 }
188 254
189 - $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
255 + $current_url = groups_get_current_url();
190 256 $current_url = remove_query_arg( 'paged', $current_url );
191 257 $current_url = remove_query_arg( 'action', $current_url );
192 258 $current_url = remove_query_arg( 'group_id', $current_url );
193 259
194 260 $group_table = _groups_get_tablename( 'group' );
261 + $user_group_table = _groups_get_tablename( 'user_group' );
195 262
196 263 $output .=
197 264 '<div class="manage-groups wrap">' .
198 265 '<h1>' .
199 - _x( 'Groups', 'page-title', GROUPS_PLUGIN_DOMAIN ) .
266 + _x( 'Groups', 'page-title', 'groups' ) .
200 267 sprintf(
201 268 '<a title="%s" class="add page-title-action" href="%s">',
202 - esc_attr( __( 'Click to add a new group', GROUPS_PLUGIN_DOMAIN ) ),
269 + esc_attr__( 'Click to add a new group', 'groups' ),
203 270 esc_url( $current_url . '&action=add' )
204 271 ) .
205 272 sprintf(
206 273 '<img class="icon" alt="%s" src="%s" />',
207 - esc_attr( __( 'Add', GROUPS_PLUGIN_DOMAIN ) ),
274 + esc_attr__( 'Add', 'groups' ),
208 275 esc_url( GROUPS_PLUGIN_URL . 'images/add.png' )
209 276 ) .
210 277 sprintf(
211 278 '<span class="label">%s</span>',
212 - stripslashes( wp_filter_nohtml_kses( __( 'New Group', GROUPS_PLUGIN_DOMAIN ) ) )
279 + esc_html__( 'New Group', 'groups' )
213 280 ) .
214 281 '</a>' .
215 282 '</h1>';
216 283
@@ -215,9 +282,9 @@
215 282 '</h1>';
216 283
217 284 $output .= Groups_Admin::render_messages();
218 285
219 - $row_count = isset( $_POST['row_count'] ) ? intval( $_POST['row_count'] ) : 0;
286 + $row_count = intval( groups_sanitize_post( 'row_count' ) ?? 0 );
220 287
221 288 if ($row_count <= 0) {
222 289 $row_count = Groups_Options::get_user_option( 'groups_per_page', GROUPS_GROUPS_PER_PAGE );
223 290 } else {
@@ -222,28 +289,29 @@
222 289 $row_count = Groups_Options::get_user_option( 'groups_per_page', GROUPS_GROUPS_PER_PAGE );
223 290 } else {
224 291 Groups_Options::update_user_option('groups_per_page', $row_count );
225 292 }
226 - $offset = isset( $_GET['offset'] ) ? intval( $_GET['offset'] ) : 0;
293 + $offset = intval( groups_sanitize_get( 'offset' ) ?? 0 );
227 294 if ( $offset < 0 ) {
228 295 $offset = 0;
229 296 }
230 - $paged = isset( $_REQUEST['paged'] ) ? intval( $_REQUEST['paged'] ) : 0;
297 + $paged = intval( groups_sanitize_request( 'paged' ) ?? 0 );
231 298 if ( $paged < 0 ) {
232 299 $paged = 0;
233 300 }
234 301
235 - $orderby = isset( $_GET['orderby'] ) ? $_GET['orderby'] : null;
302 + $orderby = groups_sanitize_get( 'orderby' );
236 303 switch ( $orderby ) {
237 304 case 'group_id' :
238 305 case 'name' :
239 306 case 'description' :
307 + case 'members':
240 308 break;
241 309 default:
242 310 $orderby = 'name';
243 311 }
244 312
245 - $order = isset( $_GET['order'] ) ? $_GET['order'] : null;
313 + $order = groups_sanitize_get( 'order' );
246 314 switch ( $order ) {
247 315 case 'asc' :
248 316 case 'ASC' :
249 317 $switch_order = 'DESC';
@@ -263,20 +331,20 @@
263 331 $filters[] = " $group_table.group_id = %d ";
264 332 $filter_params[] = $group_id;
265 333 }
266 334 if ( $group_name ) {
267 - $filters[] = " $group_table.name LIKE '%%%s%%' ";
268 - $filter_params[] = $group_name;
335 + $filters[] = " $group_table.name LIKE %s ";
336 + $filter_params[] = '%' . $wpdb->esc_like( $group_name ) . '%';
269 337 }
270 338
271 - if ( !empty( $filters ) ) {
339 + if ( !empty( $filters ) ) { // @phpstan-ignore empty.variable
272 340 $filters = " WHERE " . implode( " AND ", $filters );
273 341 } else {
274 342 $filters = '';
275 343 }
276 344
277 - $count_query = $wpdb->prepare( "SELECT COUNT(*) FROM $group_table $filters", $filter_params );
278 - $count = $wpdb->get_var( $count_query );
345 + $count_query = $wpdb->prepare( "SELECT COUNT(*) FROM $group_table $filters", $filter_params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
346 + $count = $wpdb->get_var( $count_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
279 347 if ( $count > $row_count ) {
280 348 $paginate = true;
281 349 } else {
282 350 $paginate = false;
@@ -288,82 +356,139 @@
288 356 if ( $paged != 0 ) {
289 357 $offset = ( $paged - 1 ) * $row_count;
290 358 }
291 359
292 - $query = $wpdb->prepare(
293 - "SELECT * FROM $group_table
294 - $filters
295 - ORDER BY $orderby $order
296 - LIMIT $row_count OFFSET $offset",
297 - $filter_params
360 + switch ( $orderby ) {
361 + case 'members':
362 + $query = $wpdb->prepare(
363 + // nosemgrep: audit.php.wp.security.sqli.input-in-sinks
364 + "SELECT $group_table.*, COUNT($user_group_table.user_id) AS members " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
365 + "FROM $group_table LEFT JOIN $user_group_table ON $group_table.group_id = $user_group_table.group_id " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
366 + "$filters " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
367 + "GROUP BY $group_table.group_id " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
368 + "ORDER BY COUNT($user_group_table.user_id) $order " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
369 + "LIMIT $row_count OFFSET $offset", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
370 + $filter_params
371 + );
372 + break;
373 + default:
374 + $query = $wpdb->prepare(
375 + // nosemgrep: audit.php.wp.security.sqli.input-in-sinks
376 + "SELECT * FROM $group_table $filters ORDER BY $orderby $order LIMIT $row_count OFFSET $offset", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
377 + $filter_params
378 + );
379 + }
380 +
381 + /**
382 + * Allows to modify the query for the groups table.
383 + *
384 + * @since 3.7.0
385 + *
386 + * @param string $query the query
387 + *
388 + * @return string
389 + */
390 + $query = apply_filters( 'groups_admin_groups_query', $query );
391 +
392 + // nosemgrep: audit.php.wp.security.sqli.input-in-sinks
393 + $results = $wpdb->get_results( $query, OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
394 +
395 + /**
396 + * Allows to modify the results for the groups table.
397 + *
398 + * @since 3.7.0
399 + *
400 + * @param object[] $results result to show
401 + *
402 + * @return object[]
403 + */
404 + $results = apply_filters( 'groups_admin_groups_results', $results );
405 +
406 + $columns = array(
407 + 'group_id' => array( 'label' => __( 'ID', 'groups' ), 'sortable' => true ),
408 + 'name' => array( 'label' => __( 'Group', 'groups' ), 'sortable' => true ),
409 + 'members' => array( 'label' => __( 'Members', 'groups' ), 'sortable' => true ),
410 + 'description' => array( 'label' => __( 'Description', 'groups' ), 'sortable' => true ),
411 + 'capabilities' => array( 'label' => __( 'Capabilities', 'groups' ), 'sortable' => false )
298 412 );
299 413
300 - $results = $wpdb->get_results( $query, OBJECT );
414 + /**
415 + * Allows to modify the columns of the groups table.
416 + *
417 + * @since 3.7.0
418 + *
419 + * @param array $columns maps column keys to column details; keys must be alphanumeric allowing also for underscores '_' and dashes '-', columns with invalid keys are removed; 'checkbox' is a reserved column key and must not be used
420 + *
421 + * @return array
422 + */
423 + $columns = apply_filters( 'groups_admin_groups_columns', $columns );
424 + unset( $columns['checkbox'] );
425 + foreach ( $columns as $key => $column ) {
426 + if ( preg_replace( '/[^a-zA-Z0-9_-]/', '', $key ) !== $key ) {
427 + unset( $columns[$key] );
428 + }
429 + }
301 430
302 - $column_display_names = array(
303 - 'group_id' => __( 'ID', GROUPS_PLUGIN_DOMAIN ),
304 - 'name' => __( 'Group', GROUPS_PLUGIN_DOMAIN ),
305 - 'description' => __( 'Description', GROUPS_PLUGIN_DOMAIN ),
306 - 'capabilities' => __( 'Capabilities', GROUPS_PLUGIN_DOMAIN )
307 - );
431 + $column_count = count( $columns ) + 1;
308 432
309 433 $output .= '<div class="groups-overview">';
310 434
311 - $output .=
312 - '<div class="filters">' .
313 - '<form id="setfilters" action="" method="post">' .
314 - '<fieldset>' .
315 - '<legend>' . __( 'Filters', GROUPS_PLUGIN_DOMAIN ) . '</legend>' .
316 - '<label class="group-id-filter">' . __( 'Group ID', GROUPS_PLUGIN_DOMAIN ) . ' ' .
317 - '<input class="group-id-filter" name="group_id" type="text" value="' . esc_attr( $group_id ) . '"/>' .
318 - '</label>' . ' ' .
319 - '<label class="group-name-filter">' . __( 'Group Name', GROUPS_PLUGIN_DOMAIN ) . ' ' .
320 - '<input class="group-name-filter" name="group_name" type="text" value="' . $group_name . '"/>' .
321 - '</label>' . ' ' .
322 - wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_FILTER_NONCE, true, false ) .
323 - '<input class="button" type="submit" value="' . __( 'Apply', GROUPS_PLUGIN_DOMAIN ) . '"/>' . ' ' .
324 - '<input class="button" type="submit" name="clear_filters" value="' . __( 'Clear', GROUPS_PLUGIN_DOMAIN ) . '"/>' .
325 - '<input type="hidden" value="submitted" name="submitted"/>' .
326 - '</fieldset>' .
327 - '</form>' .
328 - '</div>';
435 + $filters_html = '<div class="filters">';
436 + $filters_html .= '<form id="setfilters" action="" method="post">';
437 + $filters_html .= '<fieldset>';
438 + $filters_html .= '<legend>' . esc_html__( 'Filters', 'groups' ) . '</legend>';
439 + $filters_html .= '<label class="group-id-filter">' . esc_html__( 'Group ID', 'groups' ) . ' ';
440 + $filters_html .= '<input class="group-id-filter" name="group_id" type="text" value="' . esc_attr( $group_id ) . '"/>';
441 + $filters_html .= '</label>' . ' ';
442 + $filters_html .= '<label class="group-name-filter">' . esc_html__( 'Group Name', 'groups' ) . ' ';
443 + $filters_html .= '<input class="group-name-filter" name="group_name" type="text" value="' . esc_attr( stripslashes( $group_name !== null ? $group_name : '' ) ) . '"/>';
444 + $filters_html .= '</label>' . ' ';
445 + /**
446 + * Allows to add markup after the standard filter fields of the groups table.
447 + *
448 + * @since 3.7.0
449 + *
450 + * @param string $markup additional markup
451 + *
452 + * @return string
453 + */
454 + $filters_html .= apply_filters( 'groups_admin_groups_filters_fields_epilogue', '' );
455 + $filters_html .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_FILTER_NONCE, true, false );
456 + $filters_html .= '<input class="button" type="submit" value="' . esc_attr__( 'Apply', 'groups' ) . '"/>' . ' ';
457 + $filters_html .= '<input class="button" type="submit" name="clear_filters" value="' . esc_attr__( 'Clear', 'groups' ) . '"/>';
458 + $filters_html .= '<input type="hidden" value="submitted" name="submitted"/>';
459 + $filters_html .= '</fieldset>';
460 + $filters_html .= '</form>';
461 + $filters_html .= '</div>'; // .filters
329 462
330 - if ( $paginate ) {
331 - require_once( GROUPS_CORE_LIB . '/class-groups-pagination.php' );
332 - $pagination = new Groups_Pagination( $count, null, $row_count );
333 - $output .= '<form id="posts-filter" method="post" action="">';
334 - $output .= '<div>';
335 - $output .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_NONCE_2, true, false );
336 - $output .= '</div>';
337 - $output .= '<div class="tablenav top">';
338 - $output .= $pagination->pagination( 'top' );
339 - $output .= '</div>';
340 - $output .= '</form>';
341 - }
463 + /**
464 + * Allows to process the HTML of the filters section of the groups table.
465 + *
466 + * @since 3.7.0
467 + *
468 + * @param string $filters_html markup
469 + *
470 + * @return string
471 + */
472 + $output .= apply_filters( 'groups_admin_groups_filters_html', $filters_html );
342 473
343 - $output .= '<div class="page-options right">';
344 - $output .= '<form id="setrowcount" action="" method="post">';
345 - $output .= '<div>';
346 - $output .= '<label for="row_count">' . __('Results per page', GROUPS_PLUGIN_DOMAIN ) . '</label>';
347 - $output .= '<input name="row_count" type="text" size="2" value="' . esc_attr( $row_count ) .'" />';
348 - $output .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_NONCE_1, true, false );
349 - $output .= '<input class="button" type="submit" value="' . __( 'Apply', GROUPS_PLUGIN_DOMAIN ) . '"/>';
350 - $output .= '</div>';
351 - $output .= '</form>';
352 - $output .= '</div>';
353 -
354 474 $capability_table = _groups_get_tablename( "capability" );
355 - $group_capability_table = _groups_get_tablename( "group_capability" );
475 + // $group_capability_table = _groups_get_tablename( "group_capability" );
356 476
357 477 // capabilities select
478 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
358 479 $capabilities = $wpdb->get_results( "SELECT * FROM $capability_table ORDER BY capability" );
359 480 $capabilities_select = sprintf(
360 481 '<select class="select capability" name="capability_id[]" multiple="multiple" placeholder="%s" data-placeholder="%s">',
361 - esc_attr( __( 'Capabilities &hellip;', GROUPS_PLUGIN_DOMAIN ) ) ,
362 - esc_attr( __( 'Capabilities &hellip;', GROUPS_PLUGIN_DOMAIN ) )
482 + esc_attr__( 'Capabilities &hellip;', 'groups' ),
483 + esc_attr__( 'Capabilities &hellip;', 'groups' )
363 484 );
364 - foreach( $capabilities as $capability ) {
365 - $capabilities_select .= sprintf( '<option value="%s">%s</option>', esc_attr( $capability->capability_id ), wp_filter_nohtml_kses( $capability->capability ) );
485 + foreach ( $capabilities as $capability ) {
486 + $capabilities_select .= sprintf(
487 + '<option value="%s">%s</option>',
488 + esc_attr( $capability->capability_id ),
489 + $capability->capability ? stripslashes( wp_filter_nohtml_kses( $capability->capability ) ) : ''
490 + );
366 491 }
367 492 $capabilities_select .= '</select>';
368 493 $capabilities_select .= Groups_UIE::render_select( '.select.capability' );
369 494
@@ -370,24 +495,60 @@
370 495 $output .= '<form id="groups-action" method="post" action="">';
371 496
372 497 $output .= '<div class="tablenav top">';
373 498
374 - $output .= '<div class="groups-bulk-container">';
375 - $output .= '<div class="capabilities-select-container">';
376 - $output .= $capabilities_select;
377 - $output .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_ACTION_NONCE, true, false );
378 - $output .= '</div>';
379 - $output .= '<select class="bulk-action" name="bulk-action">';
380 - $output .= '<option selected="selected" value="-1">' . esc_html( __( 'Bulk Actions', GROUPS_PLUGIN_DOMAIN ) ) . '</option>';
381 - $output .= '<option value="remove-group">' . esc_html( __( 'Remove group', GROUPS_PLUGIN_DOMAIN ) ) . '</option>';
382 - $output .= '<option value="add-capability">' . esc_html( __( 'Add capability', GROUPS_PLUGIN_DOMAIN ) ) . '</option>';
383 - $output .= '<option value="remove-capability">' . esc_html( __( 'Remove capability', GROUPS_PLUGIN_DOMAIN ) ) . '</option>';
384 - $output .= '</select>';
385 - $output .= sprintf( '<input class="button" type="submit" name="bulk" value="%s" />', esc_attr( __( 'Apply', GROUPS_PLUGIN_DOMAIN ) ) );
386 - $output .= '<input type="hidden" name="action" value="groups-action"/>';
387 - $output .= '</div>';
388 - $output .= '</div>';
499 + $bulk_html = '<div class="groups-bulk-container">';
500 + $bulk_html .= '<div class="capabilities-select-container">';
501 + $bulk_html .= $capabilities_select;
502 + $bulk_html .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_ACTION_NONCE, true, false );
503 + $bulk_html .= '</div>';
504 + $bulk_html .= '<select class="bulk-action" name="bulk-action">';
505 + $bulk_html .= '<option selected="selected" value="-1">' . esc_html__( 'Bulk Actions', 'groups' ) . '</option>';
506 + $bulk_html .= '<option value="remove-group">' . esc_html__( 'Remove group', 'groups' ) . '</option>';
507 + $bulk_html .= '<option value="add-capability">' . esc_html__( 'Add capability', 'groups' ) . '</option>';
508 + $bulk_html .= '<option value="remove-capability">' . esc_html__( 'Remove capability', 'groups' ) . '</option>';
509 + $bulk_html .= '</select>';
510 + /**
511 + * Allows to add markup after the standard bulk actions fields of the groups table.
512 + *
513 + * @since 3.7.0
514 + *
515 + * @param string $markup additional markup
516 + *
517 + * @return string
518 + */
519 + $filters_html .= apply_filters( 'groups_admin_groups_bulk_actions_fields_epilogue', '' );
520 + $bulk_html .= sprintf( '<input class="button" type="submit" name="bulk" value="%s" />', esc_attr__( 'Apply', 'groups' ) );
521 + $bulk_html .= '<input type="hidden" name="action" value="groups-action"/>';
522 + $bulk_html .= '</div>';
389 523
524 + /**
525 + * Allows to process the HTML of the bulk actions section of the groups table.
526 + *
527 + * @since 3.7.0
528 + *
529 + * @param string $bulk_html markup
530 + *
531 + * @return string
532 + */
533 + $output .= apply_filters( 'groups_admin_groups_bulk_actions_html', $bulk_html );
534 +
535 + if ( $paginate ) {
536 + require_once GROUPS_CORE_LIB . '/class-groups-pagination.php';
537 + $pagination = new Groups_Pagination( $count, null, $row_count );
538 + $output .= $pagination->pagination( 'top' );
539 + }
540 +
541 + $output .= '<div class="page-options right">';
542 + $output .= '<label for="row_count">' . esc_html__( 'Results per page', 'groups' ) . '</label>';
543 + $output .= '<input name="row_count" type="text" size="2" value="' . esc_attr( $row_count ) .'" />';
544 + $output .= '<input class="button" type="submit" value="' . esc_attr__( 'Apply', 'groups' ) . '"/>';
545 + $output .= '</div>'; // .page-options.right
546 +
547 + $output .= '</div>'; // .tablenav.top
548 +
549 + $output .= wp_nonce_field( 'admin', GROUPS_ADMIN_GROUPS_NONCE, true, false );
550 +
390 551 $output .= '<table id="" class="wp-list-table widefat fixed" cellspacing="0">';
391 552 $output .= '<thead>';
392 553 $output .= '<tr>';
393 554
@@ -392,15 +553,15 @@
392 553 $output .= '<tr>';
393 554
394 555 $output .= '<th id="cb" class="manage-column column-cb check-column" scope="col"><input type="checkbox"></th>';
395 556
396 - foreach ( $column_display_names as $key => $column_display_name ) {
557 + foreach ( $columns as $key => $column ) {
397 558 $options = array(
398 559 'orderby' => $key,
399 560 'order' => $switch_order
400 561 );
401 562 $class = $key;
402 - if ( !in_array( $key, array( 'capabilities' ) ) ) {
563 + if ( isset( $column['sortable'] ) && $column['sortable'] ) {
403 564 if ( strcmp( $key, $orderby ) == 0 ) {
404 565 $lorder = strtolower( $order );
405 566 $class = "$key manage-column sorted $lorder";
406 567 } else {
@@ -405,21 +566,26 @@
405 566 $class = "$key manage-column sorted $lorder";
406 567 } else {
407 568 $class = "$key manage-column sortable";
408 569 }
409 - $column_display_name =
570 + $heading =
410 571 sprintf(
411 - '<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>',
572 + '<a href="%s"><span>%s</span>'.
573 + '<span class="sorting-indicators">' .
574 + '<span class="sorting-indicator asc" aria-hidden="true"></span>'.
575 + '<span class="sorting-indicator desc" aria-hidden="true"></span>'.
576 + '</span>' . // .sorting-indicators
577 + '</a>',
412 578 esc_url( add_query_arg( $options, $current_url ) ),
413 - esc_html( $column_display_name )
579 + esc_html( $column['label'] )
414 580 );
415 581 } else {
416 - $column_display_name = esc_html( $column_display_name );
582 + $heading = esc_html( $column['label'] );
417 583 }
418 584 $output .= sprintf(
419 585 '<th scope="col" class="%s">%s</th>',
420 586 esc_attr( $class ),
421 - $column_display_name
587 + $heading
422 588 );
423 589 }
424 590
425 591 $output .= '</tr>';
@@ -430,8 +596,13 @@
430 596 for ( $i = 0; $i < count( $results ); $i++ ) {
431 597
432 598 $result = $results[$i];
433 599
600 + /**
601 + * @var Groups_Group
602 + */
603 + $group = new Groups_Group( $result->group_id );
604 +
434 605 // Construct the "edit" URL.
435 606 $edit_url = add_query_arg(
436 607 array(
437 608 'group_id' => intval( $result->group_id ),
@@ -450,78 +621,146 @@
450 621 ),
451 622 $current_url
452 623 );
453 624
625 + $users_url = add_query_arg(
626 + array( 'filter_group_ids[0]' => intval( $result->group_id ) ),
627 + admin_url( 'users.php' )
628 + );
629 +
454 630 // Construct row actions for this group.
455 - $row_actions =
456 - '<div class="row-actions">' .
457 - '<span class="edit">' .
458 - '<a href="' . esc_url( $edit_url ) . '">' .
459 - '<img src="' . GROUPS_PLUGIN_URL . 'images/edit.png"/>' .
460 - __( 'Edit', GROUPS_PLUGIN_DOMAIN ) .
461 - '</a>';
631 + $row_actions = array(
632 + 'edit' => sprintf( '<a href="%s"><img src="%s"/>&nbsp;%s</a>', esc_url( $edit_url ), esc_url( GROUPS_PLUGIN_URL . 'images/edit.png' ), esc_html__( 'Edit', 'groups' ) )
633 + );
462 634 if ( $result->name !== Groups_Registered::REGISTERED_GROUP_NAME ) {
463 - $row_actions .=
464 - ' | ' .
465 - '</span>' .
466 - '<span class="remove trash">' .
467 - '<a href="' . esc_url( $delete_url ) . '" class="submitdelete">' .
468 - '<img src="' . GROUPS_PLUGIN_URL . 'images/remove.png"/>' .
469 - __( 'Remove', GROUPS_PLUGIN_DOMAIN ) .
470 - '</a>' .
471 - '</span>';
472 - }
473 - $row_actions .= '</div>'; // .row-actions
635 + $row_actions['remove trash'] = sprintf( '<a href="%s" class="submitdelete"><img src="%s"/>&nbsp;%s</a>', esc_url( $delete_url ), esc_url( GROUPS_PLUGIN_URL . 'images/remove.png' ), esc_html__( 'Remove', 'groups' ) );
636 + }
474 637
475 - $output .= '<tr class="' . ( $i % 2 == 0 ? 'even' : 'odd' ) . '">';
638 + /**
639 + * Allows to alter the row actions for a group in the groups table.
640 + *
641 + * @since 3.7.0
642 + *
643 + * @param array $row_actions row actions as HTML
644 + * @param int $group_id ID of the group
645 + *
646 + * @return array
647 + */
648 + $row_actions = apply_filters( 'groups_admin_groups_row_actions', $row_actions, intval( $result->group_id ) );
476 649
477 - $output .= '<th class="check-column">';
478 - $output .= '<input type="checkbox" value="' . esc_attr( $result->group_id ) . '" name="group_ids[]"/>';
479 - $output .= '</th>';
650 + $n = 1;
651 + $row_actions_html = '<div class="row-actions">';
652 + foreach ( $row_actions as $row_action_key => $row_action ) {
653 + $row_actions_html .= sprintf( '<span class="%s">', esc_attr( $row_action_key ) );
654 + $row_actions_html .= $row_action;
655 + $row_actions_html .= '</span>';
656 + if ( $n < count( $row_actions ) ) {
657 + $row_actions_html .= '&emsp;|&emsp;';
658 + }
659 + $n++;
660 + }
661 + $row_actions_html .= '</div>'; // .row-actions
480 662
481 - $output .= '<td class="group-id">';
482 - $output .= $result->group_id;
483 - $output .= '</td>';
484 - $output .= '<td class="group-name">';
485 - $output .= sprintf( '<a href="%s">%s</a>', esc_url( $edit_url ), stripslashes( wp_filter_nohtml_kses( $result->name ) ) );
486 - $output .= $row_actions;
487 - $output .= '</td>';
488 - $output .= '<td class="group-description">';
489 - $output .= stripslashes( wp_filter_nohtml_kses( $result->description ) );
490 - $output .= '</td>';
663 + /**
664 + * Allows to process the HTML of the row actions for a group in the groups table.
665 + *
666 + * @since 3.7.0
667 + *
668 + * @param string $row_actions_html markup
669 + * @param int $group_id ID of the group
670 + *
671 + * @return string
672 + */
673 + $row_actions_html = apply_filters( 'groups_admin_groups_row_actions_html', $row_actions_html, intval( $result->group_id ) );
491 674
492 - $output .= '<td class="capabilities">';
675 + $output .= '<tr class="' . ( $i % 2 == 0 ? 'even' : 'odd' ) . '">';
493 676
494 - $group = new Groups_Group( $result->group_id );
495 - $group_capabilities = $group->capabilities;
496 - $group_capabilities_deep = $group->capabilities_deep;
497 - usort( $group_capabilities_deep, array( 'Groups_Utility', 'cmp' ) );
498 -
499 - if ( count( $group_capabilities_deep ) > 0 ) {
500 - $output .= '<ul>';
501 - foreach ( $group_capabilities_deep as $group_capability ) {
502 - $output .= '<li>';
503 - $class = '';
504 - if ( empty( $group_capabilities ) || !in_array( $group_capability, $group_capabilities ) ) {
505 - $class = 'inherited';
506 - }
507 - $output .= sprintf( '<span class="%s">', $class );
508 - if ( isset( $group_capability->capability ) && isset( $group_capability->capability->capability ) ) {
509 - $output .= wp_filter_nohtml_kses( $group_capability->capability->capability );
510 - }
511 - $output .= '</span>';
512 - $output .= '</li>';
677 + $columns = array( 'checkbox' => array() ) + $columns;
678 + foreach ( $columns as $key => $column ) {
679 + switch ( $key ) {
680 + case 'checkbox':
681 + $output .= '<th class="check-column">';
682 + $output .= '<input type="checkbox" value="' . esc_attr( $result->group_id ) . '" name="group_ids[]"/>';
683 + $output .= '</th>';
684 + break;
685 + case 'group_id':
686 + $output .= '<td class="group-id">';
687 + $output .= $result->group_id;
688 + $output .= '</td>';
689 + break;
690 + case 'name':
691 + $output .= '<td class="group-name">';
692 + $output .= sprintf(
693 + '<a href="%s">%s</a>',
694 + esc_url( $edit_url ),
695 + $result->name ? stripslashes( wp_filter_nohtml_kses( $result->name ) ) : ''
696 + );
697 + $output .= $row_actions_html;
698 + $output .= '</td>';
699 + break;
700 + case 'members':
701 + $output .= '<td class="group-members">';
702 + $user_ids = $group->get_user_ids();
703 + $user_count = is_array( $user_ids ) ? count( $user_ids ) : 0; // guard against null when there are no users
704 + $output .= sprintf(
705 + '<a href="%s">%s</a>',
706 + esc_url( $users_url ),
707 + $user_count
708 + );
709 + $output .= '</td>';
710 + break;
711 + case 'description':
712 + $output .= '<td class="group-description">';
713 + $output .= $result->description ? stripslashes( wp_filter_nohtml_kses( $result->description ) ) : '';
714 + $output .= '</td>';
715 + break;
716 + case 'capabilities':
717 + $output .= '<td class="capabilities">';
718 + $group_capabilities = $group->get_capabilities();
719 + $group_capabilities_deep = $group->get_capabilities_deep();
720 + usort( $group_capabilities_deep, array( 'Groups_Utility', 'cmp' ) );
721 + if ( count( $group_capabilities_deep ) > 0 ) {
722 + $output .= '<ul>';
723 + foreach ( $group_capabilities_deep as $group_capability ) {
724 + $output .= '<li>';
725 + $class = '';
726 + if ( empty( $group_capabilities ) || !in_array( $group_capability, $group_capabilities ) ) {
727 + $class = 'inherited';
728 + }
729 + $output .= sprintf( '<span class="%s">', $class );
730 + $output .= stripslashes( wp_filter_nohtml_kses( $group_capability->get_capability() ) );
731 + $output .= '</span>';
732 + $output .= '</li>';
733 + }
734 + $output .= '</ul>';
735 + } else {
736 + $output .= esc_html__( 'This group has no capabilities.', 'groups' );
737 + }
738 + $output .= '</td>';
739 + break;
740 + default:
741 + $output .= sprintf( '<td class="custom-column %s">', esc_attr( $key ) );
742 + /**
743 + * Provide the row's output for the column identified by $key for the group given by its ID.
744 + *
745 + * @param string $content column content
746 + * @param string $key the column key
747 + * @param int $group_id the group's ID
748 + *
749 + * @return string content HTML
750 + */
751 + $output .= apply_filters( 'groups_admin_groups_column_content', '', $key, $group->get_group_id() );
752 + $output .= '</td>'; // .custom-column ...
513 753 }
514 - $output .= '</ul>';
515 - } else {
516 - $output .= __( 'This group has no capabilities.', GROUPS_PLUGIN_DOMAIN );
517 754 }
518 - $output .= '</td>';
519 -
520 755 $output .= '</tr>';
521 756 }
522 757 } else {
523 - $output .= '<tr><td colspan="4">' . __( 'There are no results.', GROUPS_PLUGIN_DOMAIN ) . '</td></tr>';
758 + $output .= '<tr>';
759 + $output .= sprintf( '<td colspan="%d">', esc_attr( $column_count ) );
760 + $output .= esc_html__( 'There are no results.', 'groups' );
761 + $output .= '</td>';
762 + $output .= '</tr>';
524 763 }
525 764
526 765 $output .= '</tbody>';
527 766 $output .= '</table>';
@@ -530,9 +769,9 @@
530 769
531 770 $output .= '</form>'; // #groups-action
532 771
533 772 if ( $paginate ) {
534 - require_once( GROUPS_CORE_LIB . '/class-groups-pagination.php' );
773 + require_once GROUPS_CORE_LIB . '/class-groups-pagination.php';
535 774 $pagination = new Groups_Pagination($count, null, $row_count);
536 775 $output .= '<div class="tablenav bottom">';
537 776 $output .= $pagination->pagination( 'bottom' );
538 777 $output .= '</div>';
@@ -540,6 +779,6 @@
540 779
541 780 $output .= '</div>'; // .groups-overview
542 781 $output .= '</div>'; // .manage-groups
543 782
544 - echo $output;
783 + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
545 784 } // function groups_admin_groups()