Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
86.39% |
127 / 147 |
|
69.23% |
9 / 13 |
CRAP | |
0.00% |
0 / 1 |
| PayPal_Payment_Links_List_Table | |
88.81% |
127 / 143 |
|
69.23% |
9 / 13 |
50.09 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| ajax_user_can | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_columns | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| get_sortable_columns | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| prepare_items | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
11 | |||
| column_name | |
100.00% |
44 / 44 |
|
100.00% |
1 / 1 |
6 | |||
| column_price | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| column_status | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| column_created | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
| column_payment_link | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| column_default | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| no_items | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| get_payment_link | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
10 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Payment Links list table for the WordPress admin dashboard. |
| 4 | * |
| 5 | * Displays all PayPal payment links in a WP_List_Table format |
| 6 | * with pagination, delete actions, and status badges. |
| 7 | * |
| 8 | * @package automattic/jetpack-paypal-payments |
| 9 | * @since 0.9.0 |
| 10 | */ |
| 11 | |
| 12 | namespace Automattic\Jetpack\PaypalPayments; |
| 13 | |
| 14 | if ( ! defined( 'ABSPATH' ) ) { |
| 15 | exit; |
| 16 | } |
| 17 | |
| 18 | if ( ! class_exists( 'WP_List_Table' ) ) { |
| 19 | require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * Class PayPal_Payment_Links_List_Table |
| 24 | * |
| 25 | * WP_List_Table subclass for displaying PayPal payment links. |
| 26 | */ |
| 27 | class PayPal_Payment_Links_List_Table extends \WP_List_Table { |
| 28 | |
| 29 | /** |
| 30 | * Items per page. |
| 31 | * |
| 32 | * @var int |
| 33 | */ |
| 34 | const PER_PAGE = 20; |
| 35 | |
| 36 | /** |
| 37 | * API error from the last prepare_items() call, if any. |
| 38 | * |
| 39 | * @var \WP_Error|null |
| 40 | */ |
| 41 | public $api_error = null; |
| 42 | |
| 43 | /** |
| 44 | * Next page token for cursor-based pagination. |
| 45 | * |
| 46 | * @var string|null |
| 47 | */ |
| 48 | public $next_page_token = null; |
| 49 | |
| 50 | /** |
| 51 | * Published posts embedding each link, keyed by resource id. Loaded once per table. |
| 52 | * |
| 53 | * @var array<string,int>|null |
| 54 | */ |
| 55 | private $embed_counts = null; |
| 56 | |
| 57 | /** |
| 58 | * Constructor. |
| 59 | */ |
| 60 | public function __construct() { |
| 61 | parent::__construct( |
| 62 | array( |
| 63 | 'singular' => 'payment_link', |
| 64 | 'plural' => 'payment_links', |
| 65 | 'ajax' => false, |
| 66 | ) |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Whether the current user can view the list table over AJAX. |
| 72 | * |
| 73 | * The table is registered with 'ajax' => false, so this is never reached |
| 74 | * in practice. It mirrors the capability the admin page itself requires. |
| 75 | * |
| 76 | * @return bool True if the current user may load the list. |
| 77 | */ |
| 78 | public function ajax_user_can() { |
| 79 | return current_user_can( PayPal_Admin_Page::CAPABILITY ); |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Define table columns. |
| 84 | * |
| 85 | * @return array Column slug => label. |
| 86 | */ |
| 87 | public function get_columns() { |
| 88 | return array( |
| 89 | 'name' => __( 'Product', 'jetpack-paypal-payments' ), |
| 90 | 'price' => __( 'Price', 'jetpack-paypal-payments' ), |
| 91 | 'status' => __( 'Status', 'jetpack-paypal-payments' ), |
| 92 | 'created' => __( 'Created', 'jetpack-paypal-payments' ), |
| 93 | 'payment_link' => __( 'Payment Link', 'jetpack-paypal-payments' ), |
| 94 | ); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Columns that should be sortable. |
| 99 | * |
| 100 | * Note: Sorting is client-side only since PayPal's API does not |
| 101 | * support server-side sort parameters. |
| 102 | * |
| 103 | * @return array |
| 104 | */ |
| 105 | public function get_sortable_columns() { |
| 106 | return array(); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Fetch payment links from the PayPal API and populate the table. |
| 111 | */ |
| 112 | public function prepare_items() { |
| 113 | $this->api_error = null; |
| 114 | $this->next_page_token = null; |
| 115 | |
| 116 | $columns = $this->get_columns(); |
| 117 | $hidden = array(); |
| 118 | $sortable = $this->get_sortable_columns(); |
| 119 | |
| 120 | $this->_column_headers = array( $columns, $hidden, $sortable ); |
| 121 | |
| 122 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination parameter. |
| 123 | $page_token = isset( $_GET['page_token'] ) ? sanitize_text_field( wp_unslash( $_GET['page_token'] ) ) : ''; |
| 124 | |
| 125 | $result = PayPal_API_Client::list_resources_cached( self::PER_PAGE, $page_token ); |
| 126 | |
| 127 | if ( is_wp_error( $result ) ) { |
| 128 | $this->api_error = $result; |
| 129 | $this->items = array(); |
| 130 | return; |
| 131 | } |
| 132 | |
| 133 | $this->items = $result['resources'] ?? array(); |
| 134 | |
| 135 | // Extract next page token from HATEOAS links if present. |
| 136 | if ( isset( $result['links'] ) && is_array( $result['links'] ) ) { |
| 137 | foreach ( $result['links'] as $link ) { |
| 138 | if ( isset( $link['rel'] ) && 'next' === $link['rel'] && isset( $link['href'] ) ) { |
| 139 | $parsed = wp_parse_url( $link['href'] ); |
| 140 | if ( isset( $parsed['query'] ) ) { |
| 141 | parse_str( $parsed['query'], $query_params ); |
| 142 | if ( isset( $query_params['page_token'] ) ) { |
| 143 | $this->next_page_token = $query_params['page_token']; |
| 144 | } |
| 145 | } |
| 146 | break; |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | // Fall back to the row count when a response has no total. |
| 152 | // total_pages is 1 because this table pages by cursor: core's numbered |
| 153 | // links navigate by `paged`, which prepare_items() ignores. |
| 154 | $this->set_pagination_args( |
| 155 | array( |
| 156 | 'total_items' => absint( $result['total_items'] ?? count( $this->items ) ), |
| 157 | 'total_pages' => 1, |
| 158 | 'per_page' => self::PER_PAGE, |
| 159 | ) |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Render the product name column with row actions. |
| 165 | * |
| 166 | * @param array $item The payment link data. |
| 167 | * @return string Column HTML. |
| 168 | */ |
| 169 | public function column_name( $item ) { |
| 170 | $name = isset( $item['line_items'][0]['name'] ) ? esc_html( $item['line_items'][0]['name'] ) : '—'; |
| 171 | |
| 172 | $actions = array(); |
| 173 | |
| 174 | // View details link (admin detail view). |
| 175 | if ( isset( $item['id'] ) ) { |
| 176 | $detail_url = add_query_arg( |
| 177 | array( |
| 178 | 'page' => 'paypal-payment-links', |
| 179 | 'action' => 'view', |
| 180 | 'resource_id' => $item['id'], |
| 181 | ), |
| 182 | admin_url( 'admin.php' ) |
| 183 | ); |
| 184 | |
| 185 | $actions['view_details'] = sprintf( |
| 186 | '<a href="%s">%s</a>', |
| 187 | esc_url( $detail_url ), |
| 188 | esc_html__( 'View Details', 'jetpack-paypal-payments' ) |
| 189 | ); |
| 190 | } |
| 191 | |
| 192 | // View on PayPal link. |
| 193 | $payment_link = $this->get_payment_link( $item ); |
| 194 | if ( $payment_link ) { |
| 195 | $actions['view_paypal'] = sprintf( |
| 196 | '<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>', |
| 197 | esc_url( $payment_link ), |
| 198 | esc_html__( 'View on PayPal', 'jetpack-paypal-payments' ) |
| 199 | ); |
| 200 | } |
| 201 | |
| 202 | // Delete action. |
| 203 | if ( isset( $item['id'] ) ) { |
| 204 | $delete_url = wp_nonce_url( |
| 205 | add_query_arg( |
| 206 | array( |
| 207 | 'page' => 'paypal-payment-links', |
| 208 | 'action' => 'delete', |
| 209 | 'resource_id' => $item['id'], |
| 210 | ), |
| 211 | admin_url( 'admin.php' ) |
| 212 | ), |
| 213 | 'delete_payment_link_' . $item['id'] |
| 214 | ); |
| 215 | |
| 216 | if ( null === $this->embed_counts ) { |
| 217 | $this->embed_counts = PayPal_Admin_Page::count_published_embeds(); |
| 218 | } |
| 219 | |
| 220 | $actions['delete'] = sprintf( |
| 221 | '<a href="%s" class="submitdelete paypal-delete-link" data-confirm="%s">%s</a>', |
| 222 | esc_url( $delete_url ), |
| 223 | esc_attr( PayPal_Admin_Page::delete_confirm_text( $this->embed_counts[ $item['id'] ] ?? 0 ) ), |
| 224 | esc_html__( 'Delete', 'jetpack-paypal-payments' ) |
| 225 | ); |
| 226 | } |
| 227 | |
| 228 | return sprintf( '<strong>%s</strong>%s', $name, $this->row_actions( $actions ) ); |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Render the price column. |
| 233 | * |
| 234 | * @param array $item The payment link data. |
| 235 | * @return string Column HTML. |
| 236 | */ |
| 237 | public function column_price( $item ) { |
| 238 | $price = PayPal_Payment_Buttons::resource_price( $item ); |
| 239 | |
| 240 | return '' === $price ? '—' : esc_html( $price ); |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Render the status column with a badge. |
| 245 | * |
| 246 | * @param array $item The payment link data. |
| 247 | * @return string Column HTML. |
| 248 | */ |
| 249 | public function column_status( $item ) { |
| 250 | $status = isset( $item['status'] ) ? strtoupper( $item['status'] ) : 'UNKNOWN'; |
| 251 | |
| 252 | $class = 'ACTIVE' === $status ? 'paypal-status-active' : 'paypal-status-inactive'; |
| 253 | |
| 254 | // Prefix with a text icon so status is conveyed by more than color alone (A11Y-S1). |
| 255 | $label = 'ACTIVE' === $status ? "\xE2\x9C\x93 ACTIVE" : "\xE2\x80\x94 INACTIVE"; |
| 256 | |
| 257 | return sprintf( |
| 258 | '<span class="paypal-status-badge %s">%s</span>', |
| 259 | esc_attr( $class ), |
| 260 | esc_html( $label ) |
| 261 | ); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Render the created date column. |
| 266 | * |
| 267 | * @param array $item The payment link data. |
| 268 | * @return string Column HTML. |
| 269 | */ |
| 270 | public function column_created( $item ) { |
| 271 | if ( ! isset( $item['create_time'] ) ) { |
| 272 | return '—'; |
| 273 | } |
| 274 | |
| 275 | $timestamp = strtotime( $item['create_time'] ); |
| 276 | if ( false === $timestamp ) { |
| 277 | return '—'; |
| 278 | } |
| 279 | |
| 280 | $date_format = get_option( 'date_format', 'F j, Y' ); |
| 281 | $time_format = get_option( 'time_format', 'g:i a' ); |
| 282 | |
| 283 | return esc_html( wp_date( $date_format . ' ' . $time_format, $timestamp ) ); |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Render the payment link column with a copy button. |
| 288 | * |
| 289 | * @param array $item The payment link data. |
| 290 | * @return string Column HTML. |
| 291 | */ |
| 292 | public function column_payment_link( $item ) { |
| 293 | $link = $this->get_payment_link( $item ); |
| 294 | if ( ! $link ) { |
| 295 | return '—'; |
| 296 | } |
| 297 | |
| 298 | $short = strlen( $link ) > 50 ? substr( $link, 0, 47 ) . '...' : $link; |
| 299 | |
| 300 | return sprintf( |
| 301 | '<code class="paypal-payment-url" title="%s">%s</code> <button type="button" class="button button-small paypal-copy-link" data-url="%s">%s</button>', |
| 302 | esc_attr( $link ), |
| 303 | esc_html( $short ), |
| 304 | esc_attr( $link ), |
| 305 | esc_html__( 'Copy', 'jetpack-paypal-payments' ) |
| 306 | ); |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Default column rendering. |
| 311 | * |
| 312 | * @param array $item The payment link data. |
| 313 | * @param string $column_name The column slug. |
| 314 | * @return string Column HTML. |
| 315 | */ |
| 316 | public function column_default( $item, $column_name ) { |
| 317 | return isset( $item[ $column_name ] ) ? esc_html( $item[ $column_name ] ) : '—'; |
| 318 | } |
| 319 | |
| 320 | /** |
| 321 | * Message shown when no items are found. |
| 322 | */ |
| 323 | public function no_items() { |
| 324 | if ( $this->api_error ) { |
| 325 | printf( |
| 326 | '<p>%s</p><p><a href="%s" class="button">%s</a></p>', |
| 327 | esc_html( $this->api_error->get_error_message() ), |
| 328 | esc_url( admin_url( 'admin.php?page=paypal-payment-links' ) ), |
| 329 | esc_html__( 'Try Again', 'jetpack-paypal-payments' ) |
| 330 | ); |
| 331 | return; |
| 332 | } |
| 333 | |
| 334 | printf( |
| 335 | '<p>%s</p><p>%s</p>', |
| 336 | esc_html__( 'No payment links found.', 'jetpack-paypal-payments' ), |
| 337 | esc_html__( 'Create your first payment link by adding a PayPal Payment Buttons block in the editor.', 'jetpack-paypal-payments' ) |
| 338 | ); |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Extract the payment link URL from an item. |
| 343 | * |
| 344 | * @param array $item The payment link data. |
| 345 | * @return string|null The payment link URL, or null. |
| 346 | */ |
| 347 | private function get_payment_link( $item ) { |
| 348 | $link = null; |
| 349 | |
| 350 | // Top-level payment_link field. |
| 351 | if ( ! empty( $item['payment_link'] ) ) { |
| 352 | $link = $item['payment_link']; |
| 353 | } |
| 354 | |
| 355 | // HATEOAS links array. |
| 356 | if ( null === $link && isset( $item['links'] ) && is_array( $item['links'] ) ) { |
| 357 | foreach ( $item['links'] as $hateoas_link ) { |
| 358 | if ( isset( $hateoas_link['rel'] ) && 'payment_link' === $hateoas_link['rel'] && isset( $hateoas_link['href'] ) ) { |
| 359 | $link = $hateoas_link['href']; |
| 360 | break; |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | if ( null === $link ) { |
| 366 | return null; |
| 367 | } |
| 368 | |
| 369 | // Merchants share this link straight out of the admin, so it has to |
| 370 | // carry the same attribution code as the rendered button. |
| 371 | return PayPal_Payment_Buttons::add_partner_attribution( $link ); |
| 372 | } |
| 373 | } |