UI.php
463 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin; |
| 4 | |
| 5 | use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register; |
| 6 | use Automattic\WooCommerce\Internal\Utilities\Users; |
| 7 | use Exception; |
| 8 | use WC_Admin_Settings; |
| 9 | |
| 10 | /** |
| 11 | * Manages user interactions for product download URL safety. |
| 12 | */ |
| 13 | class UI { |
| 14 | /** |
| 15 | * The active register of approved directories. |
| 16 | * |
| 17 | * @var Register |
| 18 | */ |
| 19 | private $register; |
| 20 | |
| 21 | /** |
| 22 | * The WP_List_Table instance used to display approved directories. |
| 23 | * |
| 24 | * @var Table |
| 25 | */ |
| 26 | private $table; |
| 27 | |
| 28 | /** |
| 29 | * Sets up UI controls for product download URLs. |
| 30 | * |
| 31 | * @internal |
| 32 | * |
| 33 | * @param Register $register Register of approved directories. |
| 34 | */ |
| 35 | final public function init( Register $register ) { |
| 36 | $this->register = $register; |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * Performs any work needed to add hooks and otherwise integrate with the wider system, |
| 41 | * except in the case where the current user is not a site administrator, no hooks will |
| 42 | * be initialized. |
| 43 | */ |
| 44 | final public function init_hooks() { |
| 45 | if ( ! Users::is_site_administrator() ) { |
| 46 | return; |
| 47 | } |
| 48 | |
| 49 | add_filter( 'woocommerce_get_sections_products', array( $this, 'add_section' ) ); |
| 50 | add_action( 'load-woocommerce_page_wc-settings', array( $this, 'setup' ) ); |
| 51 | add_action( 'woocommerce_settings_products', array( $this, 'render' ) ); |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Injects our new settings section (when approved directory rules are disabled, it will not show). |
| 56 | * |
| 57 | * @param array $sections Other admin settings sections. |
| 58 | * |
| 59 | * @return array |
| 60 | */ |
| 61 | public function add_section( array $sections ): array { |
| 62 | $sections['download_urls'] = __( 'Approved download directories', 'woocommerce' ); |
| 63 | return $sections; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Sets up the table, renders any notices and processes actions as needed. |
| 68 | */ |
| 69 | public function setup() { |
| 70 | if ( ! $this->is_download_urls_screen() ) { |
| 71 | return; |
| 72 | } |
| 73 | |
| 74 | $this->table = new Table(); |
| 75 | $this->admin_notices(); |
| 76 | $this->handle_search(); |
| 77 | $this->process_actions(); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Renders the UI. |
| 82 | */ |
| 83 | public function render() { |
| 84 | if ( null === $this->table || ! $this->is_download_urls_screen() ) { |
| 85 | return; |
| 86 | } |
| 87 | |
| 88 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 89 | if ( isset( $_REQUEST['action'] ) && 'edit' === $_REQUEST['action'] && isset( $_REQUEST['url'] ) ) { |
| 90 | $this->edit_screen( (int) $_REQUEST['url'] ); |
| 91 | return; |
| 92 | } |
| 93 | // phpcs:enable |
| 94 | |
| 95 | // Show list table. |
| 96 | $this->table->prepare_items(); |
| 97 | wp_nonce_field( 'modify_approved_directories', 'check' ); |
| 98 | $this->display_title(); |
| 99 | $this->table->render_views(); |
| 100 | $this->table->search_box( _x( 'Search', 'Approved Directory URLs', 'woocommerce' ), 'download_url_search' ); |
| 101 | $this->table->display(); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Indicates if we are currently on the download URLs admin screen. |
| 106 | * |
| 107 | * @return bool |
| 108 | */ |
| 109 | private function is_download_urls_screen(): bool { |
| 110 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 111 | return isset( $_GET['tab'] ) |
| 112 | && 'products' === $_GET['tab'] |
| 113 | && isset( $_GET['section'] ) |
| 114 | && 'download_urls' === $_GET['section']; |
| 115 | // phpcs:enable |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Process bulk and single-row actions. |
| 120 | */ |
| 121 | private function process_actions() { |
| 122 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 123 | $ids = isset( $_REQUEST['url'] ) ? array_map( 'absint', (array) $_REQUEST['url'] ) : array(); |
| 124 | |
| 125 | if ( empty( $ids ) || empty( $_REQUEST['action'] ) ) { |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | $this->security_check(); |
| 130 | |
| 131 | $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ); |
| 132 | |
| 133 | switch ( $action ) { |
| 134 | case 'edit': |
| 135 | $this->process_edits( current( $ids ) ); |
| 136 | break; |
| 137 | |
| 138 | case 'delete': |
| 139 | case 'enable': |
| 140 | case 'disable': |
| 141 | $this->process_bulk_actions( $ids, $action ); |
| 142 | break; |
| 143 | |
| 144 | case 'enable-all': |
| 145 | case 'disable-all': |
| 146 | $this->process_all_actions( $action ); |
| 147 | break; |
| 148 | |
| 149 | case 'turn-on': |
| 150 | case 'turn-off': |
| 151 | $this->process_on_off( $action ); |
| 152 | break; |
| 153 | } |
| 154 | // phpcs:enable |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Support pagination across search results. |
| 159 | * |
| 160 | * In the context of the WC settings screen, form data is submitted by the post method: that poses |
| 161 | * a problem for the default WP_List_Table pagination logic which expects the search value to live |
| 162 | * as part of the URL query. This method is a simple shim to bridge the resulting gap. |
| 163 | */ |
| 164 | private function handle_search() { |
| 165 | // phpcs:disable WordPress.Security.NonceVerification.Missing |
| 166 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 167 | |
| 168 | // If a search value has not been POSTed, or if it was POSTed but is already equal to the |
| 169 | // same value in the URL query, we need take no further action. |
| 170 | if ( empty( $_POST['s'] ) || sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ) === $_POST['s'] ) { |
| 171 | return; |
| 172 | } |
| 173 | |
| 174 | wp_safe_redirect( |
| 175 | add_query_arg( |
| 176 | array( |
| 177 | 'paged' => absint( $_GET['paged'] ?? 1 ), |
| 178 | 's' => sanitize_text_field( wp_unslash( $_POST['s'] ) ), |
| 179 | ), |
| 180 | $this->table->get_base_url() |
| 181 | ) |
| 182 | ); |
| 183 | // phpcs:enable |
| 184 | |
| 185 | exit; |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Handles updating or adding a new URL to the list of approved directories. |
| 190 | * |
| 191 | * @param int $url_id The ID of the rule to be edited/created. Zero if we are creating a new entry. |
| 192 | */ |
| 193 | private function process_edits( int $url_id ) { |
| 194 | // phpcs:disable WordPress.Security.NonceVerification.Missing |
| 195 | $url = esc_url_raw( wp_unslash( $_POST['approved_directory_url'] ?? '' ) ); |
| 196 | $enabled = (bool) sanitize_text_field( wp_unslash( $_POST['approved_directory_enabled'] ?? '' ) ); |
| 197 | |
| 198 | if ( empty( $url ) ) { |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | $redirect_url = add_query_arg( 'id', $url_id, $this->table->get_action_url( 'edit', $url_id ) ); |
| 203 | |
| 204 | try { |
| 205 | $upserted = 0 === $url_id |
| 206 | ? $this->register->add_approved_directory( $url, $enabled ) |
| 207 | : $this->register->update_approved_directory( $url_id, $url, $enabled ); |
| 208 | |
| 209 | if ( is_integer( $upserted ) ) { |
| 210 | $redirect_url = add_query_arg( 'url', $upserted, $redirect_url ); |
| 211 | } |
| 212 | |
| 213 | $redirect_url = add_query_arg( 'edit-status', 0 === $url_id ? 'added' : 'updated', $redirect_url ); |
| 214 | } catch ( Exception $e ) { |
| 215 | $redirect_url = add_query_arg( |
| 216 | array( |
| 217 | 'edit-status' => 'failure', |
| 218 | 'submitted-url' => $url, |
| 219 | ), |
| 220 | $redirect_url |
| 221 | ); |
| 222 | } |
| 223 | |
| 224 | wp_safe_redirect( $redirect_url ); |
| 225 | exit; |
| 226 | // phpcs:enable WordPress.Security.NonceVerification.Missing |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Processes actions that can be applied in bulk (requests to delete, enable |
| 231 | * or disable). |
| 232 | * |
| 233 | * @param int[] $ids The ID(s) to be updates. |
| 234 | * @param string $action The action to be applied. |
| 235 | */ |
| 236 | private function process_bulk_actions( array $ids, string $action ) { |
| 237 | $deletes = 0; |
| 238 | $enabled = 0; |
| 239 | $disabled = 0; |
| 240 | $register = wc_get_container()->get( Register::class ); |
| 241 | |
| 242 | foreach ( $ids as $id ) { |
| 243 | if ( 'delete' === $action && $register->delete_by_id( $id ) ) { |
| 244 | $deletes++; |
| 245 | } elseif ( 'enable' === $action && $register->enable_by_id( $id ) ) { |
| 246 | $enabled++; |
| 247 | } elseif ( 'disable' === $action && $register->disable_by_id( $id ) ) { |
| 248 | $disabled ++; |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | $fails = count( $ids ) - $deletes - $enabled - $disabled; |
| 253 | $redirect = $this->table->get_base_url(); |
| 254 | |
| 255 | if ( $deletes ) { |
| 256 | $redirect = add_query_arg( 'deleted-ids', $deletes, $redirect ); |
| 257 | } elseif ( $enabled ) { |
| 258 | $redirect = add_query_arg( 'enabled-ids', $enabled, $redirect ); |
| 259 | } elseif ( $disabled ) { |
| 260 | $redirect = add_query_arg( 'disabled-ids', $disabled, $redirect ); |
| 261 | } |
| 262 | |
| 263 | if ( $fails ) { |
| 264 | $redirect = add_query_arg( 'bulk-fails', $fails, $redirect ); |
| 265 | } |
| 266 | |
| 267 | wp_safe_redirect( $redirect ); |
| 268 | exit; |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Handles the enable/disable-all actions. |
| 273 | * |
| 274 | * @param string $action The action to be applied. |
| 275 | */ |
| 276 | private function process_all_actions( string $action ) { |
| 277 | $register = wc_get_container()->get( Register::class ); |
| 278 | $redirect = $this->table->get_base_url(); |
| 279 | |
| 280 | switch ( $action ) { |
| 281 | case 'enable-all': |
| 282 | $redirect = add_query_arg( 'enabled-all', (int) $register->enable_all(), $redirect ); |
| 283 | break; |
| 284 | |
| 285 | case 'disable-all': |
| 286 | $redirect = add_query_arg( 'disabled-all', (int) $register->disable_all(), $redirect ); |
| 287 | break; |
| 288 | } |
| 289 | |
| 290 | wp_safe_redirect( $redirect ); |
| 291 | exit; |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Handles turning on/off the entire approved download directory system (vs enabling |
| 296 | * and disabling of individual rules). |
| 297 | * |
| 298 | * @param string $action Whether the feature should be turned on or off. |
| 299 | */ |
| 300 | private function process_on_off( string $action ) { |
| 301 | switch ( $action ) { |
| 302 | case 'turn-on': |
| 303 | $this->register->set_mode( Register::MODE_ENABLED ); |
| 304 | break; |
| 305 | |
| 306 | case 'turn-off': |
| 307 | $this->register->set_mode( Register::MODE_DISABLED ); |
| 308 | break; |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Displays the screen title, etc. |
| 314 | */ |
| 315 | private function display_title() { |
| 316 | $turn_on_off = $this->register->get_mode() === Register::MODE_ENABLED |
| 317 | ? '<a href="' . esc_url( $this->table->get_action_url( 'turn-off', 0 ) ) . '" class="page-title-action">' . esc_html_x( 'Stop Enforcing Rules', 'Approved product download directories', 'woocommerce' ) . '</a>' |
| 318 | : '<a href="' . esc_url( $this->table->get_action_url( 'turn-on', 0 ) ) . '" class="page-title-action">' . esc_html_x( 'Start Enforcing Rules', 'Approved product download directories', 'woocommerce' ) . '</a>'; |
| 319 | |
| 320 | ?> |
| 321 | <h2 class='wc-table-list-header'> |
| 322 | <?php esc_html_e( 'Approved Download Directories', 'woocommerce' ); ?> |
| 323 | <a href='<?php echo esc_url( $this->table->get_action_url( 'edit', 0 ) ); ?>' class='page-title-action'><?php esc_html_e( 'Add New', 'woocommerce' ); ?></a> |
| 324 | <?php echo $turn_on_off; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> |
| 325 | </h2> |
| 326 | <?php |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Renders the editor screen for approved directory URLs. |
| 331 | * |
| 332 | * @param int $url_id The ID of the rule to be edited (may be zero for new rules). |
| 333 | */ |
| 334 | private function edit_screen( int $url_id ) { |
| 335 | $this->security_check(); |
| 336 | $existing = $this->register->get_by_id( $url_id ); |
| 337 | |
| 338 | if ( 0 !== $url_id && ! $existing ) { |
| 339 | WC_Admin_Settings::add_error( _x( 'The provided ID was invalid.', 'Approved product download directories', 'woocommerce' ) ); |
| 340 | WC_Admin_Settings::show_messages(); |
| 341 | return; |
| 342 | } |
| 343 | |
| 344 | $title = $existing |
| 345 | ? __( 'Edit Approved Directory', 'woocommerce' ) |
| 346 | : __( 'Add New Approved Directory', 'woocommerce' ); |
| 347 | |
| 348 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 349 | $submitted = sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) ); |
| 350 | $existing_url = $existing ? $existing->get_url() : ''; |
| 351 | $enabled = $existing ? $existing->is_enabled() : true; |
| 352 | // phpcs:enable |
| 353 | |
| 354 | ?> |
| 355 | <h2 class='wc-table-list-header'> |
| 356 | <?php echo esc_html( $title ); ?> |
| 357 | <?php if ( $existing ) : ?> |
| 358 | <a href="<?php echo esc_url( $this->table->get_action_url( 'edit', 0 ) ); ?>" class="page-title-action"><?php esc_html_e( 'Add New', 'woocommerce' ); ?></a> |
| 359 | <?php endif; ?> |
| 360 | <a href="<?php echo esc_url( $this->table->get_base_url() ); ?> " class="page-title-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a> |
| 361 | </h2> |
| 362 | <table class='form-table'> |
| 363 | <tbody> |
| 364 | <tr valign='top'> |
| 365 | <th scope='row' class='titledesc'> |
| 366 | <label for='approved_directory_url'> <?php echo esc_html_x( 'Directory URL', 'Approved product download directories', 'woocommerce' ); ?> </label> |
| 367 | </th> |
| 368 | <td class='forminp'> |
| 369 | <input name='approved_directory_url' id='approved_directory_url' type='text' class='input-text regular-input' value='<?php echo esc_attr( empty( $submitted ) ? $existing_url : $submitted ); ?>'> |
| 370 | </td> |
| 371 | </tr> |
| 372 | <tr valign='top'> |
| 373 | <th scope='row' class='titledesc'> |
| 374 | <label for='approved_directory_enabled'> <?php echo esc_html_x( 'Enabled', 'Approved product download directories', 'woocommerce' ); ?> </label> |
| 375 | </th> |
| 376 | <td class='forminp'> |
| 377 | <input name='approved_directory_enabled' id='approved_directory_enabled' type='checkbox' value='1' <?php checked( true, $enabled ); ?>'> |
| 378 | </td> |
| 379 | </tr> |
| 380 | </tbody> |
| 381 | </table> |
| 382 | <input name='id' id='approved_directory_id' type='hidden' value='{$url_id}'> |
| 383 | <?php |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Displays any admin notices that might be needed. |
| 388 | */ |
| 389 | private function admin_notices() { |
| 390 | // phpcs:disable WordPress.Security.NonceVerification.Recommended |
| 391 | $successfully_deleted = isset( $_GET['deleted-ids'] ) ? (int) $_GET['deleted-ids'] : 0; |
| 392 | $successfully_enabled = isset( $_GET['enabled-ids'] ) ? (int) $_GET['enabled-ids'] : 0; |
| 393 | $successfully_disabled = isset( $_GET['disabled-ids'] ) ? (int) $_GET['disabled-ids'] : 0; |
| 394 | $failed_updates = isset( $_GET['bulk-fails'] ) ? (int) $_GET['bulk-fails'] : 0; |
| 395 | $edit_status = sanitize_text_field( wp_unslash( $_GET['edit-status'] ?? '' ) ); |
| 396 | $edit_url = esc_attr( sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) ) ); |
| 397 | // phpcs:enable |
| 398 | |
| 399 | if ( $successfully_deleted ) { |
| 400 | WC_Admin_Settings::add_message( |
| 401 | sprintf( |
| 402 | /* translators: %d: count */ |
| 403 | _n( '%d approved directory URL deleted.', '%d approved directory URLs deleted.', $successfully_deleted, 'woocommerce' ), |
| 404 | $successfully_deleted |
| 405 | ) |
| 406 | ); |
| 407 | } elseif ( $successfully_enabled ) { |
| 408 | WC_Admin_Settings::add_message( |
| 409 | sprintf( |
| 410 | /* translators: %d: count */ |
| 411 | _n( '%d approved directory URL enabled.', '%d approved directory URLs enabled.', $successfully_enabled, 'woocommerce' ), |
| 412 | $successfully_enabled |
| 413 | ) |
| 414 | ); |
| 415 | } elseif ( $successfully_disabled ) { |
| 416 | WC_Admin_Settings::add_message( |
| 417 | sprintf( |
| 418 | /* translators: %d: count */ |
| 419 | _n( '%d approved directory URL disabled.', '%d approved directory URLs disabled.', $successfully_disabled, 'woocommerce' ), |
| 420 | $successfully_disabled |
| 421 | ) |
| 422 | ); |
| 423 | } |
| 424 | |
| 425 | if ( $failed_updates ) { |
| 426 | WC_Admin_Settings::add_error( |
| 427 | sprintf( |
| 428 | /* translators: %d: count */ |
| 429 | _n( '%d URL could not be updated.', '%d URLs could not be updated.', $failed_updates, 'woocommerce' ), |
| 430 | $failed_updates |
| 431 | ) |
| 432 | ); |
| 433 | } |
| 434 | |
| 435 | if ( 'added' === $edit_status ) { |
| 436 | WC_Admin_Settings::add_message( __( 'URL was successfully added.', 'woocommerce' ) ); |
| 437 | } |
| 438 | |
| 439 | if ( 'updated' === $edit_status ) { |
| 440 | WC_Admin_Settings::add_message( __( 'URL was successfully updated.', 'woocommerce' ) ); |
| 441 | } |
| 442 | |
| 443 | if ( 'failure' === $edit_status && ! empty( $edit_url ) ) { |
| 444 | WC_Admin_Settings::add_error( |
| 445 | sprintf( |
| 446 | /* translators: %s is the submitted URL. */ |
| 447 | __( '"%s" could not be saved. Please review, ensure it is a valid URL and try again.', 'woocommerce' ), |
| 448 | $edit_url |
| 449 | ) |
| 450 | ); |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | /** |
| 455 | * Makes sure the user has appropriate permissions and that we have a valid nonce. |
| 456 | */ |
| 457 | private function security_check() { |
| 458 | if ( ! Users::is_site_administrator() || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['check'] ?? '' ) ), 'modify_approved_directories' ) ) { |
| 459 | wp_die( esc_html__( 'You do not have permission to modify the list of approved directories for product downloads.', 'woocommerce' ) ); |
| 460 | } |
| 461 | } |
| 462 | } |
| 463 |