'',
'route' => \esc_html__( 'Endpoint Route', '0-day-analytics' ),
'methods' => \esc_html__( 'Methods', '0-day-analytics' ),
'namespace' => \esc_html__( 'Namespace', '0-day-analytics' ),
'show_in_index' => \esc_html__( 'In Index', '0-day-analytics' ),
'is_public' => \esc_html__( 'Public', '0-day-analytics' ),
'status' => \esc_html__( 'Status', '0-day-analytics' ),
);
static::$columns = $admin_columns;
return $admin_columns;
}
/**
* Get sortable columns.
*
* @return array
*
* @since 5.0.0
*/
public function get_sortable_columns() {
return array(
'route' => array( 'route', true ),
'namespace' => array( 'namespace', false ),
'status' => array( 'status', false ),
);
}
/**
* Get bulk actions.
*
* @return array
*
* @since 5.0.0
*/
public function get_bulk_actions() {
return array(
'disable' => \esc_html__( 'Disable', '0-day-analytics' ),
'enable' => \esc_html__( 'Enable', '0-day-analytics' ),
);
}
/**
* Column default handler.
*
* @param array $item The item data.
* @param string $column_name The column name.
*
* @return string
*
* @since 5.0.0
*/
protected function column_default( $item, $column_name ) {
return self::format_column_value( $item, $column_name );
}
/**
* Route column with row actions.
*
* @param array $item The item data.
*
* @return string
*
* @since 5.0.0
*/
protected function column_route( $item ) {
$rules = Rest_Api_Helper::get_endpoint_rules();
$route = \esc_html( $item['route'] );
$is_obfuscated = isset( $rules[ $item['route'] ]['obfuscate'] ) && $rules[ $item['route'] ]['obfuscate'];
$obfuscated_badge = $is_obfuscated ? ' ' : '';
$route_hash = \esc_attr( $item['route_hash'] );
$is_disabled = isset( $rules[ $item['route'] ]['disabled'] ) && $rules[ $item['route'] ]['disabled'];
$list_params = self::get_preserved_list_params();
$edit_url = \add_query_arg(
array_merge(
$list_params,
array(
'page' => self::REST_API_MENU_SLUG,
'action' => 'edit_route',
'route_hash' => $route_hash,
'_wpnonce' => \wp_create_nonce( 'bulk-rest-api-endpoints' ),
)
),
\admin_url( 'admin.php' )
);
$toggle_label = $is_disabled ? \esc_html__( 'Enable', '0-day-analytics' ) : \esc_html__( 'Disable', '0-day-analytics' );
$toggle_url = \wp_nonce_url(
\add_query_arg(
array_merge(
$list_params,
array(
'action' => self::TOGGLE_ACTION,
'route_hash' => $route_hash,
'toggle' => $is_disabled ? 'enable' : 'disable',
)
),
\admin_url( 'admin-post.php' )
),
self::NONCE_NAME
);
$actions = array(
'edit' => sprintf( '%s', \esc_url( $edit_url ), \esc_html__( 'Edit', '0-day-analytics' ) ),
'toggle' => sprintf( '%s', \esc_url( $toggle_url ), $toggle_label ),
);
return '' . $route . '' . $obfuscated_badge . $this->row_actions( $actions );
}
/**
* Checkbox column.
*
* @param array $item The item data.
*
* @return string
*
* @since 5.0.0
*/
protected function column_cb( $item ) {
return sprintf(
'',
\esc_attr( $item['route_hash'] )
);
}
/**
* Format column value.
*
* @param array $item The item data.
* @param string $column_name The column name.
*
* @return string
*
* @since 5.0.0
*/
public static function format_column_value( $item, $column_name ): string {
$rules = Rest_Api_Helper::get_endpoint_rules();
switch ( $column_name ) {
case 'route':
$route = \esc_html( $item['route'] );
$is_obfuscated = isset( $rules[ $item['route'] ]['obfuscate'] ) && $rules[ $item['route'] ]['obfuscate'];
$obfuscated_badge = $is_obfuscated ? ' ' : '';
return '' . $route . '' . $obfuscated_badge;
case 'methods':
$methods = $item['methods'];
$disabled_methods = isset( $rules[ $item['route'] ]['disabled_methods'] ) ? $rules[ $item['route'] ]['disabled_methods'] : array();
$output = '';
foreach ( $methods as $method ) {
$is_disabled = in_array( $method, $disabled_methods, true );
$color = $is_disabled ? '#999; text-decoration: line-through' : self::get_method_color( $method );
$output .= sprintf(
'%s',
\esc_attr( $color ),
\esc_html( $method )
);
}
return $output;
case 'namespace':
return '' . \esc_html( $item['namespace'] ) . '';
case 'show_in_index':
if ( ! empty( $item['show_in_index'] ) ) {
return '';
}
return '';
case 'is_public':
if ( ! empty( $item['is_public'] ) ) {
return '⚠ ' . \esc_html__( 'Public', '0-day-analytics' ) . '';
}
return ' ' . \esc_html__( 'Protected', '0-day-analytics' ) . '';
case 'status':
$is_disabled = isset( $rules[ $item['route'] ]['disabled'] ) && $rules[ $item['route'] ]['disabled'];
if ( $is_disabled ) {
return '' . \esc_html__( 'Disabled', '0-day-analytics' ) . '';
}
$disabled_methods = isset( $rules[ $item['route'] ]['disabled_methods'] ) ? $rules[ $item['route'] ]['disabled_methods'] : array();
if ( ! empty( $disabled_methods ) ) {
return '' . \esc_html__( 'Partial', '0-day-analytics' ) . '';
}
return '' . \esc_html__( 'Active', '0-day-analytics' ) . '';
default:
return '';
}
}
/**
* Get the color for an HTTP method badge.
*
* @param string $method HTTP method.
*
* @return string Hex color code.
*
* @since 5.0.0
*/
private static function get_method_color( string $method ): string {
$colors = array(
'GET' => '#00a32a',
'POST' => '#2271b1',
'PUT' => '#dba617',
'PATCH' => '#dba617',
'DELETE' => '#d63638',
'OPTIONS' => '#8c8f94',
'HEAD' => '#8c8f94',
);
return $colors[ strtoupper( $method ) ] ?? '#8c8f94';
}
/**
* Prepares items for display.
*
* @return void
*
* @since 5.0.0
*/
public function prepare_items() {
$columns = $this->get_columns();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, array(), $sortable );
$endpoints = Rest_Api_Helper::get_all_endpoints();
$search = self::escaped_search_input();
// Filter by search.
if ( ! empty( $search ) ) {
$endpoints = array_filter(
$endpoints,
function ( $item ) use ( $search ) {
return false !== stripos( $item['route'], $search ) || false !== stripos( $item['namespace'], $search );
}
);
}
// Filter by namespace if set.
$ns_filter = isset( $_REQUEST['namespace_filter'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['namespace_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $ns_filter ) ) {
$endpoints = array_filter(
$endpoints,
function ( $item ) use ( $ns_filter ) {
return $item['namespace'] === $ns_filter;
}
);
}
// Filter by status.
$status_filter = isset( $_REQUEST['status_filter'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['status_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $status_filter ) ) {
$rules = Rest_Api_Helper::get_endpoint_rules();
$endpoints = array_filter(
$endpoints,
function ( $item ) use ( $status_filter, $rules ) {
$is_disabled = isset( $rules[ $item['route'] ]['disabled'] ) && $rules[ $item['route'] ]['disabled'];
$disabled_methods = isset( $rules[ $item['route'] ]['disabled_methods'] ) ? $rules[ $item['route'] ]['disabled_methods'] : array();
$is_obfuscated = isset( $rules[ $item['route'] ]['obfuscate'] ) && $rules[ $item['route'] ]['obfuscate'];
if ( 'disabled' === $status_filter ) {
return $is_disabled;
}
if ( 'active' === $status_filter ) {
return ! $is_disabled && empty( $disabled_methods );
}
if ( 'partial' === $status_filter ) {
return ! $is_disabled && ! empty( $disabled_methods );
}
if ( 'obfuscated' === $status_filter ) {
return $is_obfuscated;
}
return true;
}
);
}
// Filter by method.
$method_filter = isset( $_REQUEST['method_filter'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['method_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $method_filter ) ) {
$method_filter_upper = strtoupper( $method_filter );
$endpoints = array_filter(
$endpoints,
function ( $item ) use ( $method_filter_upper ) {
return in_array( $method_filter_upper, $item['methods'], true );
}
);
}
// Filter by access (public/protected).
$access_filter = isset( $_REQUEST['access_filter'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['access_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $access_filter ) ) {
$endpoints = array_filter(
$endpoints,
function ( $item ) use ( $access_filter ) {
if ( 'public' === $access_filter ) {
return ! empty( $item['is_public'] );
}
if ( 'protected' === $access_filter ) {
return empty( $item['is_public'] );
}
return true;
}
);
}
// Compute real status based on rules so sorting works.
$rules = Rest_Api_Helper::get_endpoint_rules();
foreach ( $endpoints as &$ep ) {
$is_disabled = isset( $rules[ $ep['route'] ]['disabled'] ) && $rules[ $ep['route'] ]['disabled'];
if ( $is_disabled ) {
$ep['status'] = 'disabled';
} elseif ( ! empty( $rules[ $ep['route'] ]['disabled_methods'] ) ) {
$ep['status'] = 'partial';
} else {
$ep['status'] = 'active';
}
}
unset( $ep );
// Sort.
$orderby = isset( $_REQUEST['orderby'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['orderby'] ) ) : 'route'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = isset( $_REQUEST['order'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['order'] ) ) : 'asc'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( in_array( $orderby, array( 'route', 'namespace', 'status' ), true ) ) {
usort(
$endpoints,
function ( $a, $b ) use ( $orderby, $order ) {
$result = strcmp( $a[ $orderby ] ?? '', $b[ $orderby ] ?? '' );
return 'desc' === $order ? -$result : $result;
}
);
}
// Pagination.
$per_page = $this->get_items_per_page( self::SCREEN_OPTIONS_SLUG . '_per_page', static::$rows_per_page );
$current_page = $this->get_pagenum();
$total_items = count( $endpoints );
$this->items = array_slice( $endpoints, ( $current_page - 1 ) * $per_page, $per_page );
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => (int) ceil( $total_items / $per_page ),
)
);
}
/**
* Process bulk actions early (on load-{page} hook) before any output.
*
* @return void
*
* @since 5.1.0
*/
public static function process_bulk_actions(): void {
if ( ! \current_user_can( 'manage_options' ) ) {
return;
}
// Determine the current action from the bulk action dropdowns.
$action = '';
if ( ! empty( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = \sanitize_key( \wp_unslash( $_REQUEST['action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} elseif ( ! empty( $_REQUEST['action2'] ) && '-1' !== $_REQUEST['action2'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = \sanitize_key( \wp_unslash( $_REQUEST['action2'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( ! in_array( $action, array( 'disable', 'enable' ), true ) ) {
return;
}
WP_Helper::verify_admin_nonce( 'bulk-items' );
$routes = isset( $_REQUEST['rest_routes'] ) ? array_map( 'sanitize_text_field', (array) $_REQUEST['rest_routes'] ) : array();
if ( empty( $routes ) ) {
return;
}
$all_endpoints = Rest_Api_Helper::get_all_endpoints();
$rules = Rest_Api_Helper::get_endpoint_rules();
foreach ( $routes as $route_hash ) {
foreach ( $all_endpoints as $endpoint ) {
if ( $endpoint['route_hash'] === $route_hash ) {
if ( 'disable' === $action ) {
$rules[ $endpoint['route'] ]['disabled'] = true;
} elseif ( 'enable' === $action ) {
unset( $rules[ $endpoint['route'] ]['disabled'] );
if ( empty( $rules[ $endpoint['route'] ] ) ) {
unset( $rules[ $endpoint['route'] ] );
}
}
break;
}
}
}
Rest_Api_Helper::save_endpoint_rules( $rules );
$redirect_params = self::get_preserved_list_params();
$redirect_params['page'] = self::REST_API_MENU_SLUG;
$redirect_params['updated'] = 'true';
\wp_safe_redirect(
\add_query_arg( $redirect_params, \admin_url( 'admin.php' ) )
);
exit;
}
/**
* Handle table actions (bulk actions).
*
* @return void
*
* @since 5.0.0
*/
public function handle_table_actions() {
if ( ! \current_user_can( 'manage_options' ) ) {
return;
}
$action = $this->current_action();
if ( ! $action ) {
return;
}
if ( ! in_array( $action, array( 'disable', 'enable' ), true ) ) {
return;
}
WP_Helper::verify_admin_nonce( 'bulk-' . $this->_args['plural'] );
$routes = isset( $_REQUEST['rest_routes'] ) ? array_map( 'sanitize_text_field', (array) $_REQUEST['rest_routes'] ) : array();
if ( empty( $routes ) ) {
return;
}
$all_endpoints = Rest_Api_Helper::get_all_endpoints();
$rules = Rest_Api_Helper::get_endpoint_rules();
foreach ( $routes as $route_hash ) {
// Find the route by hash.
foreach ( $all_endpoints as $endpoint ) {
if ( $endpoint['route_hash'] === $route_hash ) {
if ( 'disable' === $action ) {
$rules[ $endpoint['route'] ]['disabled'] = true;
} elseif ( 'enable' === $action ) {
unset( $rules[ $endpoint['route'] ]['disabled'] );
// Clean up empty entries.
if ( empty( $rules[ $endpoint['route'] ] ) ) {
unset( $rules[ $endpoint['route'] ] );
}
}
break;
}
}
}
Rest_Api_Helper::save_endpoint_rules( $rules );
$redirect_params = self::get_preserved_list_params();
$redirect_params['page'] = self::REST_API_MENU_SLUG;
$redirect_params['updated'] = 'true';
\wp_safe_redirect(
\add_query_arg( $redirect_params, \admin_url( 'admin.php' ) )
);
exit;
}
/**
* Display extra table navigation (filters).
*
* @param string $which Top or bottom.
*
* @return void
*
* @since 5.0.0
*/
public function extra_tablenav( $which ) {
if ( 'top' !== $which ) {
return;
}
$namespaces = Rest_Api_Helper::get_all_namespaces();
$all_methods = Rest_Api_Helper::get_all_methods();
$current_ns = isset( $_REQUEST['namespace_filter'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['namespace_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_status = isset( $_REQUEST['status_filter'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['status_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_method = isset( $_REQUEST['method_filter'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['method_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_access = isset( $_REQUEST['access_filter'] ) ? \sanitize_key( \wp_unslash( $_REQUEST['access_filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
echo '