HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/ApprovedDirectories.tar
Admin/SyncUI.php000064400000010246151550100640007457 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;
use Automattic\WooCommerce\Internal\Utilities\Users;

/**
 * Adds tools to the Status > Tools page that can be used to (re-)initiate or stop a synchronization process
 * for Approved Download Directories.
 */
class SyncUI {
	/**
	 * The active register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * Sets up UI controls for product download URLs.
	 *
	 * @internal
	 *
	 * @param Register $register Register of approved directories.
	 */
	final public function init( Register $register ) {
		$this->register = $register;
	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system,
	 * except in the case where the current user is not a site administrator, no hooks will
	 * be initialized.
	 */
	final public function init_hooks() {
		if ( ! Users::is_site_administrator() ) {
			return;
		}

		add_filter( 'woocommerce_debug_tools', array( $this, 'add_tools' ) );
	}

	/**
	 * Adds Approved Directory list-related entries to the tools page.
	 *
	 * @param array $tools Admin tool definitions.
	 *
	 * @return array
	 */
	public function add_tools( array $tools ): array {
		$sync = wc_get_container()->get( Synchronize::class );

		if ( ! $sync->in_progress() ) {
			// Provide tools to trigger a fresh scan (migration) and to clear the Approved Directories list.
			$tools['approved_directories_sync'] = array(
				'name'             => __( 'Synchronize approved download directories', 'woocommerce' ),
				'desc'             => __( 'Updates the list of Approved Product Download Directories. Note that triggering this tool does not impact whether the Approved Download Directories list is enabled or not.', 'woocommerce' ),
				'button'           => __( 'Update', 'woocommerce' ),
				'callback'         => array( $this, 'trigger_sync' ),
				'requires_refresh' => true,
			);

			$tools['approved_directories_clear'] = array(
				'name'             => __( 'Empty the approved download directories list', 'woocommerce' ),
				'desc'             => __( 'Removes all existing entries from the Approved Product Download Directories list.', 'woocommerce' ),
				'button'           => __( 'Clear', 'woocommerce' ),
				'callback'         => array( $this, 'clear_existing_entries' ),
				'requires_refresh' => true,
			);
		} else {
			// Or if a scan (migration) is already in progress, offer a means of cancelling it.
			$tools['cancel_directories_scan'] = array(
				'name'     => __( 'Cancel synchronization of approved directories', 'woocommerce' ),
				'desc'     => sprintf(
				/* translators: %d is an integer between 0-100 representing the percentage complete of the current scan. */
					__( 'The Approved Product Download Directories list is currently being synchronized with the product catalog (%d%% complete). If you need to, you can cancel it.', 'woocommerce' ),
					$sync->get_progress()
				),
				'button'   => __( 'Cancel', 'woocommerce' ),
				'callback' => array( $this, 'cancel_sync' ),
			);
		}

		return $tools;
	}

	/**
	 * Triggers a new migration.
	 */
	public function trigger_sync() {
		$this->security_check();
		wc_get_container()->get( Synchronize::class )->start();
	}

	/**
	 * Clears all existing rules from the Approved Directories list.
	 */
	public function clear_existing_entries() {
		$this->security_check();
		$this->register->delete_all();
	}

	/**
	 * If a migration is in progress, this will attempt to cancel it.
	 */
	public function cancel_sync() {
		$this->security_check();
		wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan has been cancelled.', 'woocommerce' ) );
		wc_get_container()->get( Synchronize::class )->stop();
	}

	/**
	 * Makes sure the user has appropriate permissions and that we have a valid nonce.
	 */
	private function security_check() {
		if ( ! Users::is_site_administrator() ) {
			wp_die( esc_html__( 'You do not have permission to modify the list of approved directories for product downloads.', 'woocommerce' ) );
		}
	}
}
Admin/Table.php000064400000023766151550100640007347 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\StoredUrl;
use WP_List_Table;
use WP_Screen;

/**
 * Admin list table used to render our current list of approved directories.
 */
class Table extends WP_List_Table {
	/**
	 * Initialize the webhook table list.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'url',
				'plural'   => 'urls',
				'ajax'     => false,
			)
		);

		add_filter( 'manage_woocommerce_page_wc-settings_columns', array( $this, 'get_columns' ) );
		$this->items_per_page();
		set_screen_options();
	}

	/**
	 * Sets up an items-per-page control.
	 */
	private function items_per_page() {
		add_screen_option(
			'per_page',
			array(
				'default' => 20,
				'option'  => 'edit_approved_directories_per_page',
			)
		);

		add_filter( 'set_screen_option_edit_approved_directories_per_page', array( $this, 'set_items_per_page' ), 10, 3 );
	}

	/**
	 * Saves the items-per-page setting.
	 *
	 * @param mixed  $default The default value.
	 * @param string $option  The option being configured.
	 * @param int    $value   The submitted option value.
	 *
	 * @return mixed
	 */
	public function set_items_per_page( $default, string $option, int $value ) {
		return 'edit_approved_directories_per_page' === $option ? absint( $value ) : $default;
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No approved directory URLs found.', 'woocommerce' );
	}

	/**
	 * Displays the list of views available on this table.
	 */
	public function render_views() {
		$register = wc_get_container()->get( Register::class );

		$enabled_count  = $register->count( true );
		$disabled_count = $register->count( false );
		$all_count      = $enabled_count + $disabled_count;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$selected_view = isset( $_REQUEST['view'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['view'] ) ) : 'all';

		$all_url   = esc_url( add_query_arg( 'view', 'all', $this->get_base_url() ) );
		$all_class = 'all' === $selected_view ? 'class="current"' : '';
		$all_text  = sprintf(
			/* translators: %s is the count of approved directory list entries. */
			_nx(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				$all_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$all_count
		);

		$enabled_url   = esc_url( add_query_arg( 'view', 'enabled', $this->get_base_url() ) );
		$enabled_class = 'enabled' === $selected_view ? 'class="current"' : '';
		$enabled_text  = sprintf(
			/* translators: %s is the count of enabled approved directory list entries. */
			_nx(
				'Enabled <span class="count">(%s)</span>',
				'Enabled <span class="count">(%s)</span>',
				$enabled_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$enabled_count
		);

		$disabled_url   = esc_url( add_query_arg( 'view', 'disabled', $this->get_base_url() ) );
		$disabled_class = 'disabled' === $selected_view ? 'class="current"' : '';
		$disabled_text  = sprintf(
			/* translators: %s is the count of disabled directory list entries. */
			_nx(
				'Disabled <span class="count">(%s)</span>',
				'Disabled <span class="count">(%s)</span>',
				$disabled_count,
				'Approved product download directory views',
				'woocommerce'
			),
			$disabled_count
		);

		$views = array(
			'all'      => "<a href='{$all_url}' {$all_class}>{$all_text}</a>",
			'enabled'  => "<a href='{$enabled_url}' {$enabled_class}>{$enabled_text}</a>",
			'disabled' => "<a href='{$disabled_url}' {$disabled_class}>{$disabled_text}</a>",
		);

		$this->screen->render_screen_reader_content( 'heading_views' );

		echo '<ul class="subsubsub list-table-filters">';
		foreach ( $views as $slug => $view ) {
			$views[ $slug ] = "<li class='{$slug}'>{$view}";
		}
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo implode( ' | </li>', $views ) . "</li>\n";
		echo '</ul>';
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'    => '<input type="checkbox" />',
			'title' => _x( 'URL', 'Approved product download directories', 'woocommerce' ),
			'enabled' => _x( 'Enabled', 'Approved product download directories', 'woocommerce' ),
		);
	}

	/**
	 * Checklist column, used for selecting items for processing by a bulk action.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_cb( $item ) {
		return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', esc_attr( $this->_args['singular'] ), esc_attr( $item->get_id() ) );
	}

	/**
	 * URL column.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_title( $item ) {
		$id      = (int) $item->get_id();
		$url     = esc_html( $item->get_url() );
		$enabled = $item->is_enabled();

		$edit_url            = esc_url( $this->get_action_url( 'edit', $id ) );
		$enable_disable_url  = esc_url( $enabled ? $this->get_action_url( 'disable', $id ) : $this->get_action_url( 'enable', $id ) );
		$enable_disable_text = esc_html( $enabled ? __( 'Disable', 'woocommerce' ) : __( 'Enable', 'woocommerce' ) );
		$delete_url          = esc_url( $this->get_action_url( 'delete', $id ) );
		$edit_link           = "<a href='{$edit_url}'>" . esc_html_x( 'Edit', 'Product downloads list', 'woocommerce' ) . '</a>';
		$enable_disable_link = "<a href='{$enable_disable_url}'>{$enable_disable_text}</a>";
		$delete_link         = "<a href='{$delete_url}' class='submitdelete wc-confirm-delete'>" . esc_html_x( 'Delete permanently', 'Product downloads list', 'woocommerce' ) . '</a>';
		$url_link            = "<a href='{$edit_url}'>{$url}</a>";

		return "
			<strong>{$url_link}</strong>
			<div class='row-actions'>
				<span class='id'>ID: {$id}</span> |
				<span class='edit'>{$edit_link}</span> |
				<span class='enable-disable'>{$enable_disable_link}</span> |
				<span class='delete'><a class='submitdelete'>{$delete_link}</a></span>
			</div>
		";
	}

	/**
	 * Rule-is-enabled column.
	 *
	 * @param StoredUrl $item The approved directory information for the current row.
	 *
	 * @return string
	 */
	public function column_enabled( StoredUrl $item ): string {
		return $item->is_enabled()
			? '<mark class="yes" title="' . esc_html__( 'Enabled', 'woocommerce' ) . '"><span class="dashicons dashicons-yes"></span></mark>'
			: '<mark class="no" title="' . esc_html__( 'Disabled', 'woocommerce' ) . '">&ndash;</mark>';
	}

	/**
	 * Get bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array(
			'enable'  => __( 'Enable rule', 'woocommerce' ),
			'disable' => __( 'Disable rule', 'woocommerce' ),
			'delete'  => __( 'Delete permanently', 'woocommerce' ),
		);
	}

	/**
	 * Builds an action URL (ie, to edit or delete a row).
	 *
	 * @param string $action       The action to be created.
	 * @param int    $id           The ID that is the subject of the action.
	 * @param string $nonce_action Action used to add a nonce to the URL.
	 *
	 * @return string
	 */
	public function get_action_url( string $action, int $id, string $nonce_action = 'modify_approved_directories' ): string {
		return add_query_arg(
			array(
				'check'  => wp_create_nonce( $nonce_action ),
				'action' => $action,
				'url'    => $id,
			),
			$this->get_base_url()
		);
	}

	/**
	 * Supplies the 'base' admin URL for this admin table.
	 *
	 * @return string
	 */
	public function get_base_url(): string {
		return add_query_arg(
			array(
				'page'    => 'wc-settings',
				'tab'     => 'products',
				'section' => 'download_urls',
			),
			admin_url( 'admin.php' )
		);
	}

	/**
	 * Generate the table navigation above or below the table.
	 * Included to remove extra nonce input.
	 *
	 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		$directories = wc_get_container()->get( Register::class );
		echo '<div class="tablenav ' . esc_attr( $which ) . '">';

		if ( $this->has_items() ) {
			echo '<div class="alignleft actions bulkactions">';
			$this->bulk_actions( $which );

			if ( $directories->count( false ) > 0 ) {
				echo '<a href="' . esc_url( $this->get_action_url( 'enable-all', 0 ) ) . '" class="wp-core-ui button">' . esc_html_x( 'Enable All', 'Approved product download directories', 'woocommerce' ) . '</a> ';
			}

			if ( $directories->count( true ) > 0 ) {
				echo '<a href="' . esc_url( $this->get_action_url( 'disable-all', 0 ) ) . '" class="wp-core-ui button">' . esc_html_x( 'Disable All', 'Approved product download directories', 'woocommerce' ) . '</a>';
			}

			echo '</div>';
		}

		$this->pagination( $which );
		echo '<br class="clear" />';
		echo '</div>';
	}

	/**
	 * Prepare table list items.
	 */
	public function prepare_items() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$current_page = $this->get_pagenum();
		$per_page     = $this->get_items_per_page( 'edit_approved_directories_per_page' );
		$search       = sanitize_text_field( wp_unslash( $_REQUEST['s'] ?? '' ) );

		switch ( $_REQUEST['view'] ?? '' ) {
			case 'enabled':
				$enabled = true;
				break;

			case 'disabled':
				$enabled = false;
				break;

			default:
				$enabled = null;
				break;
		}
		// phpcs:enable

		$approved_directories = wc_get_container()->get( Register::class )->list(
			array(
				'page'     => $current_page,
				'per_page' => $per_page,
				'search'   => $search,
				'enabled'  => $enabled,
			)
		);

		$this->items = $approved_directories['approved_directories'];

		// Set the pagination.
		$this->set_pagination_args(
			array(
				'total_items' => $approved_directories['total_urls'],
				'total_pages' => $approved_directories['total_pages'],
				'per_page'    => $per_page,
			)
		);
	}
}
Admin/UI.php000064400000035040151550100640006621 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Exception;
use WC_Admin_Settings;

/**
 * Manages user interactions for product download URL safety.
 */
class UI {
	/**
	 * The active register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * The WP_List_Table instance used to display approved directories.
	 *
	 * @var Table
	 */
	private $table;

	/**
	 * Sets up UI controls for product download URLs.
	 *
	 * @internal
	 *
	 * @param Register $register Register of approved directories.
	 */
	final public function init( Register $register ) {
		$this->register = $register;
	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system,
	 * except in the case where the current user is not a site administrator, no hooks will
	 * be initialized.
	 */
	final public function init_hooks() {
		if ( ! Users::is_site_administrator() ) {
			return;
		}

		add_filter( 'woocommerce_get_sections_products', array( $this, 'add_section' ) );
		add_action( 'load-woocommerce_page_wc-settings', array( $this, 'setup' ) );
		add_action( 'woocommerce_settings_products', array( $this, 'render' ) );
	}

	/**
	 * Injects our new settings section (when approved directory rules are disabled, it will not show).
	 *
	 * @param array $sections Other admin settings sections.
	 *
	 * @return array
	 */
	public function add_section( array $sections ): array {
		$sections['download_urls'] = __( 'Approved download directories', 'woocommerce' );
		return $sections;
	}

	/**
	 * Sets up the table, renders any notices and processes actions as needed.
	 */
	public function setup() {
		if ( ! $this->is_download_urls_screen() ) {
			return;
		}

		$this->table = new Table();
		$this->admin_notices();
		$this->handle_search();
		$this->process_actions();
	}

	/**
	 * Renders the UI.
	 */
	public function render() {
		if ( null === $this->table || ! $this->is_download_urls_screen() ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_REQUEST['action'] ) && 'edit' === $_REQUEST['action'] && isset( $_REQUEST['url'] ) ) {
			$this->edit_screen( (int) $_REQUEST['url'] );
			return;
		}
		// phpcs:enable

		// Show list table.
		$this->table->prepare_items();
		wp_nonce_field( 'modify_approved_directories', 'check' );
		$this->display_title();
		$this->table->render_views();
		$this->table->search_box( _x( 'Search', 'Approved Directory URLs', 'woocommerce' ), 'download_url_search' );
		$this->table->display();
	}

	/**
	 * Indicates if we are currently on the download URLs admin screen.
	 *
	 * @return bool
	 */
	private function is_download_urls_screen(): bool {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		return isset( $_GET['tab'] )
			&& 'products' === $_GET['tab']
			&& isset( $_GET['section'] )
			&& 'download_urls' === $_GET['section'];
		// phpcs:enable
	}

	/**
	 * Process bulk and single-row actions.
	 */
	private function process_actions() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$ids = isset( $_REQUEST['url'] ) ? array_map( 'absint', (array) $_REQUEST['url'] ) : array();

		if ( empty( $ids ) || empty( $_REQUEST['action'] ) ) {
			return;
		}

		$this->security_check();

		$action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );

		switch ( $action ) {
			case 'edit':
				$this->process_edits( current( $ids ) );
				break;

			case 'delete':
			case 'enable':
			case 'disable':
				$this->process_bulk_actions( $ids, $action );
				break;

			case 'enable-all':
			case 'disable-all':
				$this->process_all_actions( $action );
				break;

			case 'turn-on':
			case 'turn-off':
				$this->process_on_off( $action );
				break;
		}
		// phpcs:enable
	}

	/**
	 * Support pagination across search results.
	 *
	 * In the context of the WC settings screen, form data is submitted by the post method: that poses
	 * a problem for the default WP_List_Table pagination logic which expects the search value to live
	 * as part of the URL query. This method is a simple shim to bridge the resulting gap.
	 */
	private function handle_search() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// phpcs:disable WordPress.Security.NonceVerification.Recommended

		// If a search value has not been POSTed, or if it was POSTed but is already equal to the
		// same value in the URL query, we need take no further action.
		if ( empty( $_POST['s'] ) || sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ) === $_POST['s'] ) {
			return;
		}

		wp_safe_redirect(
			add_query_arg(
				array(
					'paged' => absint( $_GET['paged'] ?? 1 ),
					's'     => sanitize_text_field( wp_unslash( $_POST['s'] ) ),
				),
				$this->table->get_base_url()
			)
		);
		// phpcs:enable

		exit;
	}

	/**
	 * Handles updating or adding a new URL to the list of approved directories.
	 *
	 * @param int $url_id The ID of the rule to be edited/created. Zero if we are creating a new entry.
	 */
	private function process_edits( int $url_id ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$url     = esc_url_raw( wp_unslash( $_POST['approved_directory_url'] ?? '' ) );
		$enabled = (bool) sanitize_text_field( wp_unslash( $_POST['approved_directory_enabled'] ?? '' ) );

		if ( empty( $url ) ) {
			return;
		}

		$redirect_url = add_query_arg( 'id', $url_id, $this->table->get_action_url( 'edit', $url_id ) );

		try {
			$upserted = 0 === $url_id
				? $this->register->add_approved_directory( $url, $enabled )
				: $this->register->update_approved_directory( $url_id, $url, $enabled );

			if ( is_integer( $upserted ) ) {
				$redirect_url = add_query_arg( 'url', $upserted, $redirect_url );
			}

			$redirect_url = add_query_arg( 'edit-status', 0 === $url_id ? 'added' : 'updated', $redirect_url );
		} catch ( Exception $e ) {
			$redirect_url = add_query_arg(
				array(
					'edit-status'   => 'failure',
					'submitted-url' => $url,
				),
				$redirect_url
			);
		}

		wp_safe_redirect( $redirect_url );
		exit;
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Processes actions that can be applied in bulk (requests to delete, enable
	 * or disable).
	 *
	 * @param int[]  $ids    The ID(s) to be updates.
	 * @param string $action The action to be applied.
	 */
	private function process_bulk_actions( array $ids, string $action ) {
		$deletes  = 0;
		$enabled  = 0;
		$disabled = 0;
		$register = wc_get_container()->get( Register::class );

		foreach ( $ids as $id ) {
			if ( 'delete' === $action && $register->delete_by_id( $id ) ) {
				$deletes++;
			} elseif ( 'enable' === $action && $register->enable_by_id( $id ) ) {
				$enabled++;
			} elseif ( 'disable' === $action && $register->disable_by_id( $id ) ) {
				$disabled ++;
			}
		}

		$fails    = count( $ids ) - $deletes - $enabled - $disabled;
		$redirect = $this->table->get_base_url();

		if ( $deletes ) {
			$redirect = add_query_arg( 'deleted-ids', $deletes, $redirect );
		} elseif ( $enabled ) {
			$redirect = add_query_arg( 'enabled-ids', $enabled, $redirect );
		} elseif ( $disabled ) {
			$redirect = add_query_arg( 'disabled-ids', $disabled, $redirect );
		}

		if ( $fails ) {
			$redirect = add_query_arg( 'bulk-fails', $fails, $redirect );
		}

		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Handles the enable/disable-all actions.
	 *
	 * @param string $action The action to be applied.
	 */
	private function process_all_actions( string $action ) {
		$register = wc_get_container()->get( Register::class );
		$redirect = $this->table->get_base_url();

		switch ( $action ) {
			case 'enable-all':
				$redirect = add_query_arg( 'enabled-all', (int) $register->enable_all(), $redirect );
				break;

			case 'disable-all':
				$redirect = add_query_arg( 'disabled-all', (int) $register->disable_all(), $redirect );
				break;
		}

		wp_safe_redirect( $redirect );
			exit;
}

	/**
	 * Handles turning on/off the entire approved download directory system (vs enabling
	 * and disabling of individual rules).
	 *
	 * @param string $action Whether the feature should be turned on or off.
	 */
	private function process_on_off( string $action ) {
		switch ( $action ) {
				case 'turn-on':
					$this->register->set_mode( Register::MODE_ENABLED );
					break;

			case 'turn-off':
				$this->register->set_mode( Register::MODE_DISABLED );
				break;
		}
	}

	/**
	 * Displays the screen title, etc.
	 */
	private function display_title() {
		$turn_on_off = $this->register->get_mode() === Register::MODE_ENABLED
			? '<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>'
			: '<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>';

		?>
			<h2 class='wc-table-list-header'>
				<?php esc_html_e( 'Approved Download Directories', 'woocommerce' ); ?>
				<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>
				<?php echo $turn_on_off; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</h2>
		<?php
	}

	/**
	 * Renders the editor screen for approved directory URLs.
	 *
	 * @param int $url_id The ID of the rule to be edited (may be zero for new rules).
	 */
	private function edit_screen( int $url_id ) {
		$this->security_check();
		$existing = $this->register->get_by_id( $url_id );

		if ( 0 !== $url_id && ! $existing ) {
			WC_Admin_Settings::add_error( _x( 'The provided ID was invalid.', 'Approved product download directories', 'woocommerce' ) );
			WC_Admin_Settings::show_messages();
			return;
		}

		$title = $existing
			? __( 'Edit Approved Directory', 'woocommerce' )
			: __( 'Add New Approved Directory', 'woocommerce' );

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$submitted    = sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) );
		$existing_url = $existing ? $existing->get_url() : '';
		$enabled      = $existing ? $existing->is_enabled() : true;
		// phpcs:enable

		?>
			<h2 class='wc-table-list-header'>
				<?php echo esc_html( $title ); ?>
				<?php if ( $existing ) : ?>
					<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>
				<?php endif; ?>
				<a href="<?php echo esc_url( $this->table->get_base_url() ); ?> " class="page-title-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
			</h2>
			<table class='form-table'>
				<tbody>
					<tr valign='top'>
						<th scope='row' class='titledesc'>
							<label for='approved_directory_url'> <?php echo esc_html_x( 'Directory URL', 'Approved product download directories', 'woocommerce' ); ?> </label>
						</th>
						<td class='forminp'>
							<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 ); ?>'>
						</td>
					</tr>
					<tr valign='top'>
						<th scope='row' class='titledesc'>
							<label for='approved_directory_enabled'> <?php echo esc_html_x( 'Enabled', 'Approved product download directories', 'woocommerce' ); ?> </label>
						</th>
						<td class='forminp'>
							<input name='approved_directory_enabled' id='approved_directory_enabled' type='checkbox' value='1' <?php checked( true, $enabled ); ?>'>
						</td>
					</tr>
				</tbody>
			</table>
			<input name='id' id='approved_directory_id' type='hidden' value='{$url_id}'>
		<?php
	}

	/**
	 * Displays any admin notices that might be needed.
	 */
	private function admin_notices() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$successfully_deleted  = isset( $_GET['deleted-ids'] ) ? (int) $_GET['deleted-ids'] : 0;
		$successfully_enabled  = isset( $_GET['enabled-ids'] ) ? (int) $_GET['enabled-ids'] : 0;
		$successfully_disabled = isset( $_GET['disabled-ids'] ) ? (int) $_GET['disabled-ids'] : 0;
		$failed_updates        = isset( $_GET['bulk-fails'] ) ? (int) $_GET['bulk-fails'] : 0;
		$edit_status           = sanitize_text_field( wp_unslash( $_GET['edit-status'] ?? '' ) );
		$edit_url              = esc_attr( sanitize_text_field( wp_unslash( $_GET['submitted-url'] ?? '' ) ) );
		// phpcs:enable

		if ( $successfully_deleted ) {
			WC_Admin_Settings::add_message(
				sprintf(
					/* translators: %d: count */
					_n( '%d approved directory URL deleted.', '%d approved directory URLs deleted.', $successfully_deleted, 'woocommerce' ),
					$successfully_deleted
				)
			);
		} elseif ( $successfully_enabled ) {
			WC_Admin_Settings::add_message(
				sprintf(
				/* translators: %d: count */
					_n( '%d approved directory URL enabled.', '%d approved directory URLs enabled.', $successfully_enabled, 'woocommerce' ),
					$successfully_enabled
				)
			);
		} elseif ( $successfully_disabled ) {
			WC_Admin_Settings::add_message(
				sprintf(
				/* translators: %d: count */
					_n( '%d approved directory URL disabled.', '%d approved directory URLs disabled.', $successfully_disabled, 'woocommerce' ),
					$successfully_disabled
				)
			);
		}

		if ( $failed_updates ) {
			WC_Admin_Settings::add_error(
				sprintf(
					/* translators: %d: count */
					_n( '%d URL could not be updated.', '%d URLs could not be updated.', $failed_updates, 'woocommerce' ),
					$failed_updates
				)
			);
		}

		if ( 'added' === $edit_status ) {
			WC_Admin_Settings::add_message( __( 'URL was successfully added.', 'woocommerce' ) );
		}

		if ( 'updated' === $edit_status ) {
			WC_Admin_Settings::add_message( __( 'URL was successfully updated.', 'woocommerce' ) );
		}

		if ( 'failure' === $edit_status && ! empty( $edit_url ) ) {
			WC_Admin_Settings::add_error(
				sprintf(
					/* translators: %s is the submitted URL. */
					__( '"%s" could not be saved. Please review, ensure it is a valid URL and try again.', 'woocommerce' ),
					$edit_url
				)
			);
		}
	}

	/**
	 * Makes sure the user has appropriate permissions and that we have a valid nonce.
	 */
	private function security_check() {
		if ( ! Users::is_site_administrator() || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['check'] ?? '' ) ), 'modify_approved_directories' ) ) {
			wp_die( esc_html__( 'You do not have permission to modify the list of approved directories for product downloads.', 'woocommerce' ) );
		}
	}
}
ApprovedDirectoriesException.php000064400000000523151550100640013106 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Exception;

/**
 * Encapsulates a problem encountered while an operation relating to approved directories
 * was performed.
 */
class ApprovedDirectoriesException extends Exception {
	public const INVALID_URL = 1;
	public const DB_ERROR    = 2;
}
Register.php000064400000033654151550100640007051 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\SyncUI;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\UI;
use Automattic\WooCommerce\Internal\Utilities\URL;
use Automattic\WooCommerce\Internal\Utilities\URLException;

/**
 * Maintains and manages the list of approved directories, within which product downloads can
 * be stored.
 */
class Register {
	/**
	 * Used to indicate the current mode.
	 */
	private const MODES = array(
		self::MODE_DISABLED,
		self::MODE_ENABLED,
	);

	public const MODE_DISABLED  = 'disabled';
	public const MODE_ENABLED   = 'enabled';

	/**
	 * Name of the option used to store the current mode. See self::MODES for a
	 * list of acceptable values for the actual option.
	 *
	 * @var string
	 */
	private $mode_option = 'wc_downloads_approved_directories_mode';

	/**
	 * Sets up the approved directories sub-system.
	 *
	 * @internal
	 */
	final public function init() {
		add_action(
			'admin_init',
			function () {
				wc_get_container()->get( SyncUI::class )->init_hooks();
				wc_get_container()->get( UI::class )->init_hooks();
			}
		);

		add_action(
			'before_woocommerce_init',
			function() {
				if ( get_option( Synchronize::SYNC_TASK_PAGE ) > 0 ) {
					wc_get_container()->get( Synchronize::class )->init_hooks();
				}
			}
		);
	}

	/**
	 * Supplies the name of the database table used to store approved directories.
	 *
	 * @return string
	 */
	public function get_table(): string {
		global $wpdb;
		return $wpdb->prefix . 'wc_product_download_directories';
	}

	/**
	 * Returns a string indicating the current mode.
	 *
	 * May be one of: 'disabled', 'enabled', 'migrating'.
	 *
	 * @return string
	 */
	public function get_mode(): string {
		$current_mode = get_option( $this->mode_option, self::MODE_DISABLED );
		return in_array( $current_mode, self::MODES, true ) ? $current_mode : self::MODE_DISABLED;
	}

	/**
	 * Sets the mode. This effectively controls if approved directories are enforced or not.
	 *
	 * May be one of: 'disabled', 'enabled', 'migrating'.
	 *
	 * @param string $mode One of the values contained within self::MODES.
	 *
	 * @return bool
	 */
	public function set_mode( string $mode ): bool {
		if ( ! in_array( $mode, self::MODES, true ) ) {
			return false;
		}

		update_option( $this->mode_option, $mode );
		return get_option( $this->mode_option ) === $mode;
	}

	/**
	 * Adds a new URL path.
	 *
	 * On success (or if the URL was already added) returns the URL ID, or else
	 * returns boolean false.
	 *
	 * @throws URLException                 If the URL was invalid.
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 *
	 * @param string $url     The URL of the approved directory.
	 * @param bool   $enabled If the rule is enabled.
	 *
	 * @return int
	 */
	public function add_approved_directory( string $url, bool $enabled = true ): int {
		$url      = $this->prepare_url_for_upsert( $url );
		$existing = $this->get_by_url( $url );

		if ( $existing ) {
			return $existing->get_id();
		}

		global $wpdb;
		$insert_fields = array(
			'url'     => $url,
			'enabled' => (int) $enabled,
		);

		if ( false !== $wpdb->insert( $this->get_table(), $insert_fields ) ) {
			return $wpdb->insert_id;
		}

		throw new ApprovedDirectoriesException( __( 'URL could not be added (probable database error).', 'woocommerce' ), ApprovedDirectoriesException::DB_ERROR );
	}

	/**
	 * Updates an existing approved directory.
	 *
	 * On success or if there is an existing entry for the same URL, returns true.
	 *
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 * @throws URLException                 If the URL was invalid.
	 *
	 * @param int    $id      The ID of the approved directory to be updated.
	 * @param string $url     The new URL for the specified option.
	 * @param bool   $enabled If the rule is enabled.
	 *
	 * @return bool
	 */
	public function update_approved_directory( int $id, string $url, bool $enabled = true ): bool {
		$url           = $this->prepare_url_for_upsert( $url );
		$existing_path = $this->get_by_url( $url );

		// No need to go any further if the URL is already listed and nothing has changed.
		if ( $existing_path && $existing_path->get_url() === $url && $enabled === $existing_path->is_enabled() ) {
			return true;
		}

		global $wpdb;
		$fields = array(
			'url'     => $url,
			'enabled' => (int) $enabled,
		);

		if ( false === $wpdb->update( $this->get_table(), $fields, array( 'url_id' => $id ) ) ) {
			throw new ApprovedDirectoriesException( __( 'URL could not be updated (probable database error).', 'woocommerce' ), ApprovedDirectoriesException::DB_ERROR );
		}

		return true;
	}

	/**
	 * Indicates if the specified URL is already an approved directory.
	 *
	 * @param string $url The URL to check.
	 *
	 * @return bool
	 */
	public function approved_directory_exists( string $url ): bool {
		return (bool) $this->get_by_url( $url );
	}

	/**
	 * Returns the path identified by $id, or false if it does not exist.
	 *
	 * @param int $id The ID of the rule we are looking for.
	 *
	 * @return StoredUrl|false
	 */
	public function get_by_id( int $id ) {
		global $wpdb;

		$table = $this->get_table();

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE url_id = %d", array( $id ) ) );

		if ( ! $result ) {
			return false;
		}

		return new StoredUrl( $result->url_id, $result->url, $result->enabled );
	}

	/**
	 * Returns the path identified by $url, or false if it does not exist.
	 *
	 * @param string $url The URL of the rule we are looking for.
	 *
	 * @return StoredUrl|false
	 */
	public function get_by_url( string $url ) {
		global $wpdb;

		$table = $this->get_table();
		$url   = trailingslashit( $url );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE url = %s", array( $url ) ) );

		if ( ! $result ) {
			return false;
		}

		return new StoredUrl( $result->url_id, $result->url, $result->enabled );
	}

	/**
	 * Indicates if the URL is within an approved directory. The approved directory must be enabled
	 * (it is possible for individual approved directories to be disabled).
	 *
	 * For instance, for 'https://storage.king/12345/ebook.pdf' to be valid then 'https://storage.king/12345'
	 * would need to be within our register.
	 *
	 * If the provided URL is a filepath it can be passed in without the 'file://' scheme.
	 *
	 * @throws URLException If the provided URL is badly formed.
	 *
	 * @param string $download_url The URL to check.
	 *
	 * @return bool
	 */
	public function is_valid_path( string $download_url ): bool {
		global $wpdb;

		$parent_directories = array();

		foreach ( ( new URL( $this->normalize_url( $download_url ) ) )->get_all_parent_urls() as $parent ) {
			$parent_directories[] = "'" . esc_sql( $parent ) . "'";
		}

		if ( empty( $parent_directories ) ) {
			return false;
		}

		$parent_directories = join( ',', $parent_directories );
		$table              = $this->get_table();

		// Look for a rule that matches the start of the download URL being tested. Since rules describe parent
		// directories, we also ensure it ends with a trailing slash.
		//
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$matches = (int) $wpdb->get_var(
			"
				SELECT COUNT(*)
				FROM   {$table}
				WHERE  enabled = 1
				       AND url IN ( {$parent_directories} )
			"
		);
		// phpcs:enable

		return $matches > 0;
	}

	/**
	 * Used when a URL string is prepared before potentially adding it to the database.
	 *
	 * It will be normalized and trailing-slashed; a length check will also be performed.
	 *
	 * @throws ApprovedDirectoriesException If the operation could not be performed.
	 * @throws URLException                 If the URL was invalid.
	 *
	 * @param string $url The string URL to be normalized and trailing-slashed.
	 *
	 * @return string
	 */
	private function prepare_url_for_upsert( string $url ): string {
		$url = trailingslashit( $this->normalize_url( $url ) );

		if ( mb_strlen( $url ) > 256 ) {
			throw new ApprovedDirectoriesException( __( 'Approved directory URLs cannot be longer than 256 characters.', 'woocommerce' ), ApprovedDirectoriesException::INVALID_URL );
		}

		return $url;
	}

	/**
	 * Normalizes the provided URL, by trimming whitespace per normal PHP conventions
	 * and removing any trailing slashes. If it lacks a scheme, the file scheme is
	 * assumed and prepended.
	 *
	 * @throws URLException If the URL is badly formed.
	 *
	 * @param string $url The URL to be normalized.
	 *
	 * @return string
	 */
	private function normalize_url( string $url ): string {
		$url = untrailingslashit( trim( $url ) );
		return ( new URL( $url ) )->get_url();
	}

	/**
	 * Lists currently approved directories.
	 *
	 * Returned array will have the following structure:
	 *
	 *     [
	 *         'total_urls'  => 12345,
	 *         'total_pages' => 123,
	 *         'urls'        => [],  # StoredUrl[]
	 *     ]
	 *
	 * @param array $args {
	 *     Controls pagination and ordering.
	 *
	 *     @type null|bool $enabled  Controls if only enabled (true), disabled (false) or all rules (null) should be listed.
	 *     @type string    $order    Ordering ('ASC' for ascending, 'DESC' for descending).
	 *     @type string    $order_by Field to order by (one of 'url_id' or 'url').
	 *     @type int       $page     The page of results to retrieve.
	 *     @type int       $per_page The number of results to retrieve per page.
	 *     @type string    $search   Term to search for.
	 * }
	 *
	 * @return array
	 */
	public function list( array $args ): array {
		global $wpdb;

		$args = array_merge(
			array(
				'enabled'  => null,
				'order'    => 'ASC',
				'order_by' => 'url',
				'page'     => 1,
				'per_page' => 20,
				'search'   => '',
			),
			$args
		);

		$table    = $this->get_table();
		$paths    = array();
		$order    = in_array( $args['order'], array( 'ASC', 'DESC' ), true ) ? $args['order'] : 'ASC';
		$order_by = in_array( $args['order_by'], array( 'url_id', 'url' ), true ) ? $args['order_by'] : 'url';
		$page     = absint( $args['page'] );
		$per_page = absint( $args['per_page'] );
		$enabled  = is_bool( $args['enabled'] ) ? $args['enabled'] : null;
		$search   = '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%';

		if ( $page < 1 ) {
			$page = 1;
		}

		if ( $per_page < 1 ) {
			$per_page = 1;
		}

		$where     = array();
		$where_sql = '';

		if ( ! empty( $search ) ) {
			$where[] = $wpdb->prepare( 'url LIKE %s', $search );
		}

		if ( is_bool( $enabled ) ) {
			$where[] = 'enabled = ' . (int) $enabled;
		}

		if ( ! empty( $where ) ) {
			$where_sql = 'WHERE ' . join( ' AND ', $where );
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"
					SELECT   url_id, url, enabled
					FROM     {$table}
					{$where_sql}
					ORDER BY {$order_by} {$order}
					LIMIT    %d, %d
				",
				( $page - 1 ) * $per_page,
				$per_page
			)
		);

		$total_rows = (int) $wpdb->get_var( "SELECT COUNT( * ) FROM {$table} {$where_sql}" );
		// phpcs:enable

		foreach ( $results as $single_result ) {
			$paths[] = new StoredUrl( $single_result->url_id, $single_result->url, $single_result->enabled );
		}

		return array(
			'total_urls'           => $total_rows,
			'total_pages'          => (int) ceil( $total_rows / $per_page ),
			'approved_directories' => $paths,
		);
	}

	/**
	 * Delete the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function delete_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();

		return (bool) $wpdb->delete( $table, array( 'url_id' => $id ) );
	}

	/**
	 * Delete the entirev approved directory list.
	 *
	 * @return bool
	 */
	public function delete_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "DELETE FROM $table" );
	}

	/**
	 * Enable the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function enable_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();
		return (bool) $wpdb->update( $table, array( 'enabled' => 1 ), array( 'url_id' => $id ) );
	}

	/**
	 * Disable the approved directory identitied by the supplied ID.
	 *
	 * @param int $id The ID of the rule to be deleted.
	 *
	 * @return bool
	 */
	public function disable_by_id( int $id ): bool {
		global $wpdb;
		$table = $this->get_table();
		return (bool) $wpdb->update( $table, array( 'enabled' => 0 ), array( 'url_id' => $id ) );
	}

	/**
	 * Enables all Approved Download Directory rules in a single operation.
	 *
	 * @return bool
	 */
	public function enable_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "UPDATE {$table} SET enabled = 1" );
	}

	/**
	 * Disables all Approved Download Directory rules in a single operation.
	 *
	 * @return bool
	 */
	public function disable_all(): bool {
		global $wpdb;
		$table = $this->get_table();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return (bool) $wpdb->query( "UPDATE {$table} SET enabled = 0" );
	}

	/**
	 * Indicates the number of approved directories that are enabled (or disabled, if optional
	 * param $enabled is set to false).
	 *
	 * @param bool $enabled Controls whether enabled or disabled directory rules are counted.
	 *
	 * @return int
	 */
	public function count( bool $enabled = true ): int {
		global $wpdb;
		$table = $this->get_table();

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT COUNT(*) FROM {$table} WHERE enabled = %d",
				$enabled ? 1 : 0
			)
		);
	}
}
StoredUrl.php000064400000002427151550100640007202 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

/**
 * Representation of an approved directory URL, bundling the ID and URL in a single entity.
 */
class StoredUrl {
	/**
	 * The approved directory ID.
	 *
	 * @var int
	 */
	private $id;

	/**
	 * The approved directory URL.
	 *
	 * @var string
	 */
	private $url;

	/**
	 * If the individual rule is enabled or disabled.
	 *
	 * @var bool
	 */
	private $enabled;

	/**
	 * Sets up the approved directory rule.
	 *
	 * @param int    $id      The approved directory ID.
	 * @param string $url     The approved directory URL.
	 * @param bool   $enabled Indicates if the approved directory rule is enabled.
	 */
	public function __construct( int $id, string $url, bool $enabled ) {
		$this->id      = $id;
		$this->url     = $url;
		$this->enabled = $enabled;
	}

	/**
	 * Supplies the ID of the approved directory.
	 *
	 * @return int
	 */
	public function get_id(): int {
		return $this->id;
	}

	/**
	 * Supplies the approved directory URL.
	 *
	 * @return string
	 */
	public function get_url(): string {
		return $this->url;
	}

	/**
	 * Indicates if this rule is enabled or not (rules can be temporarily disabled).
	 *
	 * @return bool
	 */
	public function is_enabled(): bool {
		return $this->enabled;
	}
}
Synchronize.php000064400000020374151550100640007573 0ustar00<?php

namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;

use Exception;
use Automattic\WooCommerce\Internal\Utilities\URL;
use WC_Admin_Notices;
use WC_Product;
use WC_Queue_Interface;

/**
 * Ensures that any downloadable files have a corresponding entry in the Approved Product
 * Download Directories list.
 */
class Synchronize {
	/**
	 * Scheduled action hook used to facilitate scanning the product catalog for downloadable products.
	 */
	public const SYNC_TASK = 'woocommerce_download_dir_sync';

	/**
	 * The group under which synchronization tasks run (our standard 'woocommerce-db-updates' group).
	 */
	public const SYNC_TASK_GROUP = 'woocommerce-db-updates';

	/**
	 * Used to track progress throughout the sync process.
	 */
	public const SYNC_TASK_PAGE = 'wc_product_download_dir_sync_page';

	/**
	 * Used to record an estimation of progress on the current synchronization process. 0 means 0%,
	 * 100 means 100%.
	 *
	 * @param int
	 */
	public const SYNC_TASK_PROGRESS = 'wc_product_download_dir_sync_progress';

	/**
	 * Number of downloadable products to be processed in each atomic sync task.
	 */
	public const SYNC_TASK_BATCH_SIZE = 20;

	/**
	 * WC Queue.
	 *
	 * @var WC_Queue_Interface
	 */
	private $queue;

	/**
	 * Register of approved directories.
	 *
	 * @var Register
	 */
	private $register;

	/**
	 * Sets up our checks and controls for downloadable asset URLs, as appropriate for
	 * the current approved download directory mode.
	 *
	 * @internal
	 * @throws Exception If the WC_Queue instance cannot be obtained.
	 *
	 * @param Register $register The active approved download directories instance in use.
	 */
	final public function init( Register $register ) {
		$this->queue    = WC()->get_instance_of( WC_Queue_Interface::class );
		$this->register = $register;

	}

	/**
	 * Performs any work needed to add hooks and otherwise integrate with the wider system.
	 */
	final public function init_hooks() {
		add_action( self::SYNC_TASK, array( $this, 'run' ) );
	}

	/**
	 * Initializes the Approved Download Directories feature, typically following an update or
	 * during initial installation.
	 *
	 * @param bool $synchronize    Synchronize with existing product downloads. Not needed in a fresh installation.
	 * @param bool $enable_feature Enable (default) or disable the feature.
	 */
	public function init_feature( bool $synchronize = true, bool $enable_feature = true ) {
		try {
			$this->add_default_directories();

			if ( $synchronize ) {
				$this->start();
			}
		} catch ( Exception $e ) {
			wc_get_logger()->log( 'warning', __( 'It was not possible to synchronize download directories following the most recent update.', 'woocommerce' ) );
		}

		$this->register->set_mode(
			$enable_feature ? Register::MODE_ENABLED : Register::MODE_DISABLED
		);
	}

	/**
	 * By default we add the woocommerce_uploads directory (file path plus web URL) to the list
	 * of approved download directories.
	 *
	 * @throws Exception If the default directories cannot be added to the Approved List.
	 */
	public function add_default_directories() {
		$upload_dir = wp_get_upload_dir();
		$this->register->add_approved_directory( $upload_dir['basedir'] . '/woocommerce_uploads' );
		$this->register->add_approved_directory( $upload_dir['baseurl'] . '/woocommerce_uploads' );
	}

	/**
	 * Starts the synchronization process.
	 *
	 * @return bool
	 */
	public function start(): bool {
		if ( null !== $this->queue->get_next( self::SYNC_TASK ) ) {
			wc_get_logger()->log( 'warning', __( 'Synchronization of approved product download directories is already in progress.', 'woocommerce' ) );
			return false;
		}

		update_option( self::SYNC_TASK_PAGE, 1 );
		$this->queue->schedule_single( time(), self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
		wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: new scan scheduled.', 'woocommerce' ) );
		return true;
	}

	/**
	 * Runs the syncronization task.
	 */
	public function run() {
		$products = $this->get_next_set_of_downloadable_products();

		foreach ( $products as $product ) {
			$this->process_product( $product );
		}

		// Detect if we have reached the end of the task.
		if ( count( $products ) < self::SYNC_TASK_BATCH_SIZE ) {
			wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan is complete!', 'woocommerce' ) );
			$this->stop();
		} else {
			wc_get_logger()->log(
				'info',
				sprintf(
				/* translators: %1$d is the current batch in the synchronization task, %2$d is the percent complete. */
					__( 'Approved Download Directories sync: completed batch %1$d (%2$d%% complete).', 'woocommerce' ),
					(int) get_option( self::SYNC_TASK_PAGE, 2 ) - 1,
					$this->get_progress()
				)
			);
			$this->queue->schedule_single( time() + 1, self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
		}
	}

	/**
	 * Stops/cancels the current synchronization task.
	 */
	public function stop() {
		WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
		delete_option( self::SYNC_TASK_PAGE );
		delete_option( self::SYNC_TASK_PROGRESS );
		$this->queue->cancel( self::SYNC_TASK );
	}

	/**
	 * Queries for the next batch of downloadable products, applying logic to ensure we only fetch those that actually
	 * have downloadable files (a downloadable product can be created that does not have downloadable files and/or
	 * downloadable files can be removed from existing downloadable products).
	 *
	 * @return array
	 */
	private function get_next_set_of_downloadable_products(): array {
		$query_filter = function ( array $query ): array {
			$query['meta_query'][] = array(
				'key'     => '_downloadable_files',
				'compare' => 'EXISTS',
			);

			return $query;
		};

		$page = (int) get_option( self::SYNC_TASK_PAGE, 1 );
		add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );

		$products = wc_get_products(
			array(
				'limit'    => self::SYNC_TASK_BATCH_SIZE,
				'page'     => $page,
				'paginate' => true,
			)
		);

		remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );
		$progress = $products->max_num_pages > 0 ? (int) ( ( $page / $products->max_num_pages ) * 100 ) : 1;
		update_option( self::SYNC_TASK_PAGE, $page + 1 );
		update_option( self::SYNC_TASK_PROGRESS, $progress );

		return $products->products;
	}

	/**
	 * Processes an individual downloadable product, adding the parent paths for any downloadable files to the
	 * Approved Download Directories list.
	 *
	 * Any such paths will be added with the disabled flag set, because we want a site administrator to review
	 * and approve first.
	 *
	 * @param WC_Product $product The product we wish to examine for downloadable file paths.
	 */
	private function process_product( WC_Product $product ) {
		$downloads = $product->get_downloads();

		foreach ( $downloads as $downloadable ) {
			$parent_url = _x( 'invalid URL', 'Approved product download URLs migration', 'woocommerce' );

			try {
				$download_file = $downloadable->get_file();

				/**
				 * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature.
				 *
				 * @param bool $should_validate
				 */
				if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $downloadable->get_type_of_file_path() ) {
					$download_file = do_shortcode( $download_file );
				}

				$parent_url = ( new URL( $download_file ) )->get_parent_url();
				$this->register->add_approved_directory( $parent_url, false );
			} catch ( Exception $e ) {
				wc_get_logger()->log(
					'error',
					sprintf(
					/* translators: %s is a URL, %d is a product ID. */
						__( 'Product download migration: %1$s (for product %1$d) could not be added to the list of approved download directories.', 'woocommerce' ),
						$parent_url,
						$product->get_id()
					)
				);
			}
		}
	}

	/**
	 * Indicates if a synchronization of product download directories is in progress.
	 *
	 * @return bool
	 */
	public function in_progress(): bool {
		return (bool) get_option( self::SYNC_TASK_PAGE, false );
	}

	/**
	 * Returns a value between 0 and 100 representing the percentage complete of the current sync.
	 *
	 * @return int
	 */
	public function get_progress(): int {
		return min( 100, max( 0, (int) get_option( self::SYNC_TASK_PROGRESS, 0 ) ) );
	}
}