diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-admin/includes/class-wp-list-table.php --- a/wp/wp-admin/includes/class-wp-list-table.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-admin/includes/class-wp-list-table.php Fri Sep 05 18:40:08 2025 +0200 @@ -11,8 +11,8 @@ * Base class for displaying a list of items in an ajaxified HTML table. * * @since 3.1.0 - * @access private */ +#[AllowDynamicProperties] class WP_List_Table { /** @@ -173,9 +173,10 @@ } /** - * Make private properties readable for backward compatibility. + * Makes private properties readable for backward compatibility. * * @since 4.0.0 + * @since 6.4.0 Getting a dynamic property is deprecated. * * @param string $name Property to get. * @return mixed Property. @@ -184,27 +185,44 @@ if ( in_array( $name, $this->compat_fields, true ) ) { return $this->$name; } + + wp_trigger_error( + __METHOD__, + "The property `{$name}` is not declared. Getting a dynamic property is " . + 'deprecated since version 6.4.0! Instead, declare the property on the class.', + E_USER_DEPRECATED + ); + return null; } /** - * Make private properties settable for backward compatibility. + * Makes private properties settable for backward compatibility. * * @since 4.0.0 + * @since 6.4.0 Setting a dynamic property is deprecated. * * @param string $name Property to check if set. * @param mixed $value Property value. - * @return mixed Newly-set property. */ public function __set( $name, $value ) { if ( in_array( $name, $this->compat_fields, true ) ) { - return $this->$name = $value; + $this->$name = $value; + return; } + + wp_trigger_error( + __METHOD__, + "The property `{$name}` is not declared. Setting a dynamic property is " . + 'deprecated since version 6.4.0! Instead, declare the property on the class.', + E_USER_DEPRECATED + ); } /** - * Make private properties checkable for backward compatibility. + * Makes private properties checkable for backward compatibility. * * @since 4.0.0 + * @since 6.4.0 Checking a dynamic property is deprecated. * * @param string $name Property to check if set. * @return bool Whether the property is a back-compat property and it is set. @@ -214,24 +232,39 @@ return isset( $this->$name ); } + wp_trigger_error( + __METHOD__, + "The property `{$name}` is not declared. Checking `isset()` on a dynamic property " . + 'is deprecated since version 6.4.0! Instead, declare the property on the class.', + E_USER_DEPRECATED + ); return false; } /** - * Make private properties un-settable for backward compatibility. + * Makes private properties un-settable for backward compatibility. * * @since 4.0.0 + * @since 6.4.0 Unsetting a dynamic property is deprecated. * * @param string $name Property to unset. */ public function __unset( $name ) { if ( in_array( $name, $this->compat_fields, true ) ) { unset( $this->$name ); + return; } + + wp_trigger_error( + __METHOD__, + "A property `{$name}` is not declared. Unsetting a dynamic property is " . + 'deprecated since version 6.4.0! Instead, declare the property on the class.', + E_USER_DEPRECATED + ); } /** - * Make private/protected methods readable for backward compatibility. + * Makes private/protected methods readable for backward compatibility. * * @since 4.0.0 * @@ -269,7 +302,7 @@ } /** - * An internal method that sets all the necessary pagination arguments + * Sets all the necessary pagination arguments. * * @since 3.1.0 * @@ -286,7 +319,7 @@ ); if ( ! $args['total_pages'] && $args['per_page'] > 0 ) { - $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] ); + $args['total_pages'] = (int) ceil( $args['total_items'] / $args['per_page'] ); } // Redirect if page number is invalid and headers are not already sent. @@ -320,7 +353,7 @@ } /** - * Whether the table has items to display or not + * Determines whether the table has items to display or not * * @since 3.1.0 * @@ -355,7 +388,13 @@ $input_id = $input_id . '-search-input'; if ( ! empty( $_REQUEST['orderby'] ) ) { - echo ''; + if ( is_array( $_REQUEST['orderby'] ) ) { + foreach ( $_REQUEST['orderby'] as $key => $value ) { + echo ''; + } + } else { + echo ''; + } } if ( ! empty( $_REQUEST['order'] ) ) { echo ''; @@ -376,6 +415,79 @@ } /** + * Generates views links. + * + * @since 6.1.0 + * + * @param array $link_data { + * An array of link data. + * + * @type string $url The link URL. + * @type string $label The link label. + * @type bool $current Optional. Whether this is the currently selected view. + * } + * @return string[] An array of link markup. Keys match the `$link_data` input array. + */ + protected function get_views_links( $link_data = array() ) { + if ( ! is_array( $link_data ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: The $link_data argument. */ + __( 'The %s argument must be an array.' ), + '$link_data' + ), + '6.1.0' + ); + + return array( '' ); + } + + $views_links = array(); + + foreach ( $link_data as $view => $link ) { + if ( empty( $link['url'] ) || ! is_string( $link['url'] ) || '' === trim( $link['url'] ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %1$s: The argument name. %2$s: The view name. */ + __( 'The %1$s argument must be a non-empty string for %2$s.' ), + 'url', + '' . esc_html( $view ) . '' + ), + '6.1.0' + ); + + continue; + } + + if ( empty( $link['label'] ) || ! is_string( $link['label'] ) || '' === trim( $link['label'] ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %1$s: The argument name. %2$s: The view name. */ + __( 'The %1$s argument must be a non-empty string for %2$s.' ), + 'label', + '' . esc_html( $view ) . '' + ), + '6.1.0' + ); + + continue; + } + + $views_links[ $view ] = sprintf( + '%s', + esc_url( $link['url'] ), + isset( $link['current'] ) && true === $link['current'] ? ' class="current" aria-current="page"' : '', + $link['label'] + ); + } + + return $views_links; + } + + /** * Gets the list of views available on this table. * * The format is an associative array: @@ -458,7 +570,7 @@ * * @since 3.1.0 * - * @param string $which The location of the bulk actions: 'top' or 'bottom'. + * @param string $which The location of the bulk actions: Either 'top' or 'bottom'. * This is designated as optional for backward compatibility. */ protected function bulk_actions( $which = '' ) { @@ -487,7 +599,10 @@ return; } - echo ''; + echo ''; echo '", - '', + '' . + "" . + "", + /* translators: Hidden accessibility text. */ + __( 'Current Page' ), $current, strlen( $total_pages ) ); } + $html_total_pages = sprintf( "%s", number_format_i18n( $total_pages ) ); - $page_links[] = $total_pages_before . sprintf( + + $page_links[] = $total_pages_before . sprintf( /* translators: 1: Current page, 2: Total pages. */ _x( '%1$s of %2$s', 'paging' ), $html_current_page, @@ -960,8 +1133,12 @@ $page_links[] = ''; } else { $page_links[] = sprintf( - "%s", + "" . + "%s" . + "" . + '', esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), + /* translators: Hidden accessibility text. */ __( 'Next page' ), '›' ); @@ -971,8 +1148,12 @@ $page_links[] = ''; } else { $page_links[] = sprintf( - "%s", + "" . + "%s" . + "" . + '', esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), + /* translators: Hidden accessibility text. */ __( 'Last page' ), '»' ); @@ -1014,10 +1195,17 @@ * * The format is: * - `'internal-name' => 'orderby'` + * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` - * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. * + * In the second format, passing true as second parameter will make the initial + * sorting order be descending. Following parameters add a short column name to + * be used as 'abbr' attribute, a translatable string for the current sorting, + * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false). + * * @since 3.1.0 + * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'. * * @return array */ @@ -1040,8 +1228,10 @@ return $column; } - // We need a primary defined so responsive views show something, - // so let's fall back to the first non-checkbox column. + /* + * We need a primary defined so responsive views show something, + * so let's fall back to the first non-checkbox column. + */ foreach ( $columns as $col => $column_name ) { if ( 'cb' === $col ) { continue; @@ -1055,6 +1245,8 @@ } /** + * Gets the name of the primary column. + * * Public wrapper for WP_List_Table::get_default_primary_column_name(). * * @since 4.4.0 @@ -1076,8 +1268,10 @@ $columns = get_column_headers( $this->screen ); $default = $this->get_default_primary_column_name(); - // If the primary column doesn't exist, - // fall back to the first non-checkbox column. + /* + * If the primary column doesn't exist, + * fall back to the first non-checkbox column. + */ if ( ! isset( $columns[ $default ] ) ) { $default = self::get_default_primary_column_name(); } @@ -1108,7 +1302,10 @@ */ protected function get_column_info() { // $_column_headers is already set / cached. - if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) { + if ( + isset( $this->_column_headers ) && + is_array( $this->_column_headers ) + ) { /* * Backward compatibility for `$_column_headers` format prior to WordPress 4.3. * @@ -1116,12 +1313,18 @@ * column headers property. This ensures the primary column name is included * in plugins setting the property directly in the three item format. */ + if ( 4 === count( $this->_column_headers ) ) { + return $this->_column_headers; + } + $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); foreach ( $this->_column_headers as $key => $value ) { $column_headers[ $key ] = $value; } - return $column_headers; + $this->_column_headers = $column_headers; + + return $this->_column_headers; } $columns = get_column_headers( $this->screen ); @@ -1147,9 +1350,22 @@ } $data = (array) $data; + // Descending initial sorting. if ( ! isset( $data[1] ) ) { $data[1] = false; } + // Current sorting translatable string. + if ( ! isset( $data[2] ) ) { + $data[2] = ''; + } + // Initial view sorted column and asc/desc order, default: false. + if ( ! isset( $data[3] ) ) { + $data[3] = false; + } + // Initial order for the initial sorted column, default: false. + if ( ! isset( $data[4] ) ) { + $data[4] = false; + } $sortable[ $id ] = $data; } @@ -1186,27 +1402,39 @@ $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); $current_url = remove_query_arg( 'paged', $current_url ); + // When users click on a column header to sort by other columns. if ( isset( $_GET['orderby'] ) ) { $current_orderby = $_GET['orderby']; + // In the initial view there's no orderby parameter. } else { $current_orderby = ''; } + // Not in the initial view and descending order. if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { $current_order = 'desc'; } else { + // The initial view is not always 'asc', we'll take care of this below. $current_order = 'asc'; } if ( ! empty( $columns['cb'] ) ) { static $cb_counter = 1; - $columns['cb'] = '' - . ''; - $cb_counter++; + $columns['cb'] = ' + '; + ++$cb_counter; } foreach ( $columns as $column_key => $column_display_name ) { - $class = array( 'manage-column', "column-$column_key" ); + $class = array( 'manage-column', "column-$column_key" ); + $aria_sort_attr = ''; + $abbr_attr = ''; + $order_text = ''; if ( in_array( $column_key, $hidden, true ) ) { $class[] = 'hidden'; @@ -1223,14 +1451,41 @@ } if ( isset( $sortable[ $column_key ] ) ) { - list( $orderby, $desc_first ) = $sortable[ $column_key ]; + $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : ''; + $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false; + $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : ''; + $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : ''; + $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : ''; + /* + * We're in the initial view and there's no $_GET['orderby'] then check if the + * initial sorting information is set in the sortable columns and use that. + */ + if ( '' === $current_orderby && $initial_order ) { + // Use the initially sorted column $orderby as current orderby. + $current_orderby = $orderby; + // Use the initially sorted column asc/desc order as initial order. + $current_order = $initial_order; + } + + /* + * True in the initial view when an initial orderby is set via get_sortable_columns() + * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. + */ if ( $current_orderby === $orderby ) { - $order = 'asc' === $current_order ? 'desc' : 'asc'; + // The sorted column. The `aria-sort` attribute must be set only on the sorted column. + if ( 'asc' === $current_order ) { + $order = 'desc'; + $aria_sort_attr = ' aria-sort="ascending"'; + } else { + $order = 'asc'; + $aria_sort_attr = ' aria-sort="descending"'; + } $class[] = 'sorted'; $class[] = $current_order; } else { + // The other sortable columns. $order = strtolower( $desc_first ); if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { @@ -1239,12 +1494,33 @@ $class[] = 'sortable'; $class[] = 'desc' === $order ? 'asc' : 'desc'; + + /* translators: Hidden accessibility text. */ + $asc_text = __( 'Sort ascending.' ); + /* translators: Hidden accessibility text. */ + $desc_text = __( 'Sort descending.' ); + $order_text = 'asc' === $order ? $asc_text : $desc_text; } + if ( '' !== $order_text ) { + $order_text = ' ' . $order_text . ''; + } + + // Print an 'abbr' attribute if a value is provided via get_sortable_columns(). + $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : ''; + $column_display_name = sprintf( - '%s', + '' . + '%2$s' . + '' . + '' . + '' . + '' . + '%3$s' . + '', esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), - $column_display_name + $column_display_name, + $order_text ); } @@ -1256,7 +1532,80 @@ $class = "class='" . implode( ' ', $class ) . "'"; } - echo "<$tag $scope $id $class>$column_display_name"; + echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name"; + } + } + + /** + * Print a table description with information about current sorting and order. + * + * For the table initial view, information about initial orderby and order + * should be provided via get_sortable_columns(). + * + * @since 6.3.0 + * @access public + */ + public function print_table_description() { + list( $columns, $hidden, $sortable ) = $this->get_column_info(); + + if ( empty( $sortable ) ) { + return; + } + + // When users click on a column header to sort by other columns. + if ( isset( $_GET['orderby'] ) ) { + $current_orderby = $_GET['orderby']; + // In the initial view there's no orderby parameter. + } else { + $current_orderby = ''; + } + + // Not in the initial view and descending order. + if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { + $current_order = 'desc'; + } else { + // The initial view is not always 'asc', we'll take care of this below. + $current_order = 'asc'; + } + + foreach ( array_keys( $columns ) as $column_key ) { + + if ( isset( $sortable[ $column_key ] ) ) { + $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : ''; + $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false; + $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : ''; + $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : ''; + $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : ''; + + if ( ! is_string( $orderby_text ) || '' === $orderby_text ) { + return; + } + /* + * We're in the initial view and there's no $_GET['orderby'] then check if the + * initial sorting information is set in the sortable columns and use that. + */ + if ( '' === $current_orderby && $initial_order ) { + // Use the initially sorted column $orderby as current orderby. + $current_orderby = $orderby; + // Use the initially sorted column asc/desc order as initial order. + $current_order = $initial_order; + } + + /* + * True in the initial view when an initial orderby is set via get_sortable_columns() + * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. + */ + if ( $current_orderby === $orderby ) { + /* translators: Hidden accessibility text. */ + $asc_text = __( 'Ascending.' ); + /* translators: Hidden accessibility text. */ + $desc_text = __( 'Descending.' ); + $order_text = 'asc' === $current_order ? $asc_text : $desc_text; + echo '' . $orderby_text . ' ' . $order_text . ''; + + return; + } + } } } @@ -1273,6 +1622,7 @@ $this->screen->render_screen_reader_content( 'heading_list' ); ?> + print_table_description(); ?> print_column_headers(); ?> @@ -1319,7 +1669,7 @@ * Generates the table navigation above or below the table * * @since 3.1.0 - * @param string $which + * @param string $which The location of the navigation: Either 'top' or 'bottom'. */ protected function display_tablenav( $which ) { if ( 'top' === $which ) { @@ -1344,7 +1694,7 @@ } /** - * Extra controls to be displayed between bulk actions and pagination. + * Displays extra controls between bulk actions and pagination. * * @since 3.1.0 * @@ -1422,8 +1772,10 @@ $classes .= ' hidden'; } - // Comments column uses HTML in the display name with screen reader text. - // Strip tags to get closer to a user-friendly string. + /* + * Comments column uses HTML in the display name with screen reader text. + * Strip tags to get closer to a user-friendly string. + */ $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; $attributes = "class='$classes' $data"; @@ -1466,7 +1818,10 @@ * if the current column is not the primary column. */ protected function handle_row_actions( $item, $column_name, $primary ) { - return $column_name === $primary ? '' : ''; + return $column_name === $primary ? '' : ''; } /**