Added approval feature for downloadable files
This commit is contained in:
parent
6e9ed9a83a
commit
59b941b239
|
@ -10,6 +10,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
|
||||
/**
|
||||
* Legacy product contains all deprecated methods for this class and can be
|
||||
|
@ -1224,25 +1225,14 @@ class WC_Product extends WC_Abstract_Legacy_Product {
|
|||
$download_object->set_file( $download['file'] );
|
||||
}
|
||||
|
||||
// Validate the file extension.
|
||||
if ( ! $download_object->is_allowed_filetype() ) {
|
||||
try {
|
||||
$download_object->check_is_valid();
|
||||
$downloads[ $download_object->get_id() ] = $download_object;
|
||||
} catch ( Exception $e ) {
|
||||
if ( $this->get_object_read() ) {
|
||||
/* translators: %1$s: Downloadable file */
|
||||
$errors[] = sprintf( __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), '<code>' . basename( $download_object->get_file() ) . '</code>', '<code>' . implode( ', ', array_keys( $download_object->get_allowed_mime_types() ) ) . '</code>' );
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate the file exists.
|
||||
if ( ! $download_object->file_exists() ) {
|
||||
if ( $this->get_object_read() ) {
|
||||
/* translators: %s: Downloadable file */
|
||||
$errors[] = sprintf( __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), '<code>' . $download_object->get_file() . '</code>' );
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$downloads[ $download_object->get_id() ] = $download_object;
|
||||
}
|
||||
|
||||
if ( $errors ) {
|
||||
|
|
|
@ -193,6 +193,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
|
|||
'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ),
|
||||
'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ),
|
||||
'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ),
|
||||
'i18n_confirm_delete' => __( 'Are you sure you wish to delete this item?', 'woocommerce' ),
|
||||
'decimal_point' => $decimal,
|
||||
'mon_decimal_point' => wc_get_price_decimal_separator(),
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Utilities\Users;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
@ -28,18 +29,19 @@ class WC_Admin_Notices {
|
|||
* @var array
|
||||
*/
|
||||
private static $core_notices = array(
|
||||
'update' => 'update_notice',
|
||||
'template_files' => 'template_file_check_notice',
|
||||
'legacy_shipping' => 'legacy_shipping_notice',
|
||||
'no_shipping_methods' => 'no_shipping_methods_notice',
|
||||
'regenerating_thumbnails' => 'regenerating_thumbnails_notice',
|
||||
'regenerating_lookup_table' => 'regenerating_lookup_table_notice',
|
||||
'no_secure_connection' => 'secure_connection_notice',
|
||||
WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice',
|
||||
'maxmind_license_key' => 'maxmind_missing_license_key_notice',
|
||||
'redirect_download_method' => 'redirect_download_method_notice',
|
||||
'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice',
|
||||
'base_tables_missing' => 'base_tables_missing_notice',
|
||||
'update' => 'update_notice',
|
||||
'template_files' => 'template_file_check_notice',
|
||||
'legacy_shipping' => 'legacy_shipping_notice',
|
||||
'no_shipping_methods' => 'no_shipping_methods_notice',
|
||||
'regenerating_thumbnails' => 'regenerating_thumbnails_notice',
|
||||
'regenerating_lookup_table' => 'regenerating_lookup_table_notice',
|
||||
'no_secure_connection' => 'secure_connection_notice',
|
||||
WC_PHP_MIN_REQUIREMENTS_NOTICE => 'wp_php_min_requirements_notice',
|
||||
'maxmind_license_key' => 'maxmind_missing_license_key_notice',
|
||||
'redirect_download_method' => 'redirect_download_method_notice',
|
||||
'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice',
|
||||
'base_tables_missing' => 'base_tables_missing_notice',
|
||||
'download_directories_sync_complete' => 'download_directories_sync_complete',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -526,6 +528,24 @@ class WC_Admin_Notices {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notice about the completion of the product downloads sync, with further advice for the site operator.
|
||||
*/
|
||||
public static function download_directories_sync_complete() {
|
||||
$notice_dismissed = apply_filters(
|
||||
'woocommerce_hide_download_directories_sync_complete',
|
||||
get_user_meta( get_current_user_id(), 'download_directories_sync_complete', true )
|
||||
);
|
||||
|
||||
if ( $notice_dismissed ) {
|
||||
self::remove_notice( 'download_directories_sync_complete' );
|
||||
}
|
||||
|
||||
if ( Users::is_site_administrator() ) {
|
||||
include __DIR__ . '/views/html-notice-download-dir-sync-complete.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display MaxMind missing license key notice.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
@ -804,6 +805,11 @@ if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) :
|
|||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Is your site connected to WooCommerce.com?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo 'yes' === $settings['woocommerce_com_connected'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-export-label="Enforce Approved Product Download Directories"><?php esc_html_e( 'Enforce Approved Product Download Directories', 'woocommerce' ); ?>:</td>
|
||||
<td class="help"><?php echo wc_help_tip( esc_html__( 'Is your site enforcing the use of Approved Product Download Directories?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
|
||||
<td><?php echo wc_get_container()->get( Download_Directories::class )->get_mode() === Download_Directories::MODE_ENABLED ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">–</mark>'; ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="wc_status_table widefat" cellspacing="0">
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin View: Notice - Product downloads directories sync complete.
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
?>
|
||||
<div id="message" class="updated woocommerce-message">
|
||||
<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'download_directories_sync_complete' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Dismiss', 'woocommerce' ); ?></a>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
$settings_screen_link = '<a href="' . esc_url( get_admin_url( null, 'admin.php?page=wc-settings&tab=products§ion=download_urls' ) ) . '">';
|
||||
$documentation_link = '<a href="https://woocommerce.com/document/approved-download-directories">';
|
||||
$closing_link = '</a>';
|
||||
|
||||
printf(
|
||||
/* translators: %1$s and %3$s are HTML (opening link tags). %2$s is also HTML (closing link tag). */
|
||||
esc_html__( 'The %1$sApproved Product Download Directories list%2$s has been updated. To protect your site, please review the list and make any changes that might be required. For more information, please refer to %3$sthis guide%2$s.', 'woocommerce' ),
|
||||
$settings_screen_link,
|
||||
$closing_link,
|
||||
$documentation_link
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
|
||||
|
||||
|
@ -184,6 +186,7 @@ class WC_Install {
|
|||
),
|
||||
'6.4.0' => array(
|
||||
'wc_update_640_add_primary_key_to_product_attributes_lookup_table',
|
||||
'wc_update_640_approved_download_directories',
|
||||
'wc_update_640_db_version',
|
||||
),
|
||||
);
|
||||
|
@ -681,6 +684,9 @@ class WC_Install {
|
|||
// Define initial tax classes.
|
||||
WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) );
|
||||
WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) );
|
||||
|
||||
// For new installs, setup and enable Approved Product Download Directories.
|
||||
wc_get_container()->get( Download_Directories_Sync::class )->init_feature( false, true );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1084,6 +1090,13 @@ CREATE TABLE {$wpdb->prefix}wc_rate_limits (
|
|||
UNIQUE KEY rate_limit_key (rate_limit_key($max_index_length))
|
||||
) $collate;
|
||||
$product_attributes_lookup_table_creation_sql
|
||||
CREATE TABLE {$wpdb->prefix}wc_product_download_directories (
|
||||
url_id BIGINT UNSIGNED NOT NULL auto_increment,
|
||||
url varchar(256) NOT NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (url_id),
|
||||
KEY `url` (`url`)
|
||||
) $collate;
|
||||
";
|
||||
|
||||
return $tables;
|
||||
|
@ -1100,6 +1113,7 @@ $product_attributes_lookup_table_creation_sql
|
|||
|
||||
$tables = array(
|
||||
"{$wpdb->prefix}wc_download_log",
|
||||
"{$wpdb->prefix}wc_product_download_directories",
|
||||
"{$wpdb->prefix}wc_product_meta_lookup",
|
||||
"{$wpdb->prefix}wc_tax_rate_classes",
|
||||
"{$wpdb->prefix}wc_webhooks",
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
use Automattic\WooCommerce\Internal\Utilities\URL;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
|
@ -91,6 +93,44 @@ class WC_Product_Download implements ArrayAccess {
|
|||
return pathinfo( $parsed_url, PATHINFO_EXTENSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the download is of an allowed filetype, that it exists and that it is
|
||||
* contained within an approved directory. Used before adding to a product's list of
|
||||
* downloads.
|
||||
*
|
||||
* @internal
|
||||
* @throws Exception If the download is determined to be invalid.
|
||||
*
|
||||
* @param bool $auto_add_to_approved_directory_list If the download is not already in the approved directory list, automatically add it if possible.
|
||||
*/
|
||||
public function check_is_valid( bool $auto_add_to_approved_directory_list = true ) {
|
||||
$download_file = $this->get_file();
|
||||
|
||||
if ( ! $this->is_allowed_filetype() ) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
/* translators: %1$s: Downloadable file */
|
||||
__( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ),
|
||||
'<code>' . basename( $download_file ) . '</code>',
|
||||
'<code>' . implode( ', ', array_keys( $this->get_allowed_mime_types() ) ) . '</code>'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the file exists.
|
||||
if ( ! $this->file_exists() ) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
/* translators: %s: Downloadable file */
|
||||
__( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ),
|
||||
'<code>' . $download_file . '</code>'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->approved_directory_checks( $auto_add_to_approved_directory_list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is allowed.
|
||||
*
|
||||
|
@ -143,6 +183,59 @@ class WC_Product_Download implements ArrayAccess {
|
|||
return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the download exists within an approved directory.
|
||||
*
|
||||
* If it is not within an approved directory but the current user has sufficient
|
||||
* capabilities, then the method will try to add the download's directory to the
|
||||
* approved directory list.
|
||||
*
|
||||
* @throws Exception If the download is not in an approved directory.
|
||||
*
|
||||
* @param bool $auto_add_to_approved_directory_list If the download is not already in the approved directory list, automatically add it if possible.
|
||||
*/
|
||||
private function approved_directory_checks( bool $auto_add_to_approved_directory_list = true ) {
|
||||
$download_directories = wc_get_container()->get( Download_Directories::class );
|
||||
|
||||
if ( $download_directories->get_mode() !== Download_Directories::MODE_ENABLED ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$download_file = $this->get_file();
|
||||
$is_site_administrator = is_multisite() ? current_user_can( 'manage_sites' ) : current_user_can( 'manage_options' );
|
||||
$valid_storage_directory = $download_directories->is_valid_path( $download_file );
|
||||
|
||||
if ( ! $valid_storage_directory && $auto_add_to_approved_directory_list ) {
|
||||
try {
|
||||
// Add the parent URL to the approved directories list, but *do not enable it* unless the current user is a site admin.
|
||||
$download_directories->add_approved_directory( ( new URL( $download_file ) )->get_parent_url(), $is_site_administrator );
|
||||
} catch ( Exception $e ) {
|
||||
/* translators: %s: Downloadable file */
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
/* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */
|
||||
__( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator and request their approval. %2$sLearn more.%3$s', 'woocommerce' ),
|
||||
'<code>' . $download_file . '</code>',
|
||||
'<a href="https://woocommerce.com/documentation/approved-download-directories">', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181)
|
||||
'</a>'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $valid_storage_directory && ! $is_site_administrator ) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
/* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */
|
||||
__( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator for help. %2$sLearn more.%3$s', 'woocommerce' ),
|
||||
'<code>' . $download_file . '</code>',
|
||||
'<a href="https://woocommerce.com/documentation/approved-download-directories">', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181)
|
||||
'</a>'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Setters
|
||||
|
|
|
@ -13,6 +13,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableControlle
|
|||
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as ProductDownloadDirectories;
|
||||
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
|
||||
|
@ -212,6 +213,7 @@ final class WooCommerce {
|
|||
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) );
|
||||
|
||||
// These classes set up hooks on instantiation.
|
||||
wc_get_container()->get( ProductDownloadDirectories::class );
|
||||
wc_get_container()->get( DownloadPermissionsAdjuster::class );
|
||||
wc_get_container()->get( AssignDefaultCategory::class );
|
||||
wc_get_container()->get( DataRegenerator::class );
|
||||
|
|
|
@ -21,6 +21,8 @@ defined( 'ABSPATH' ) || exit;
|
|||
use Automattic\WooCommerce\Internal\AssignDefaultCategory;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync;
|
||||
|
||||
/**
|
||||
* Update file paths for 2.0
|
||||
|
@ -2373,6 +2375,15 @@ function wc_update_630_db_version() {
|
|||
WC_Install::update_db_version( '6.3.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the standard WooCommerce upload directories to the Approved Product Download Directories list
|
||||
* and start populating it based on existing product download URLs, but do not enable the feature
|
||||
* (for existing installations, a site admin should review and make a conscious decision to enable).
|
||||
*/
|
||||
function wc_update_640_approved_download_directories() {
|
||||
wc_get_container()->get( Download_Directories_Sync::class )->init_feature( true, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the primary key for the product attributes lookup table if it doesn't exist already.
|
||||
*
|
||||
|
|
|
@ -286,3 +286,17 @@
|
|||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin table-marks() {
|
||||
mark {
|
||||
background: transparent none;
|
||||
}
|
||||
|
||||
mark.yes {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
mark.no {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1086,17 +1086,7 @@ table.wc_status_table {
|
|||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
mark {
|
||||
background: transparent none;
|
||||
}
|
||||
|
||||
mark.yes {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
mark.no {
|
||||
color: #999;
|
||||
}
|
||||
@include table-marks();
|
||||
|
||||
mark.error,
|
||||
.red {
|
||||
|
@ -1113,6 +1103,13 @@ table.wc_status_table {
|
|||
}
|
||||
}
|
||||
|
||||
table.wp-list-table.urls {
|
||||
td,
|
||||
th {
|
||||
@include table-marks();
|
||||
}
|
||||
}
|
||||
|
||||
table.wc_status_table--tools {
|
||||
|
||||
td,
|
||||
|
@ -4293,7 +4290,7 @@ img.help_tip {
|
|||
margin: 1.5em 0 1em;
|
||||
}
|
||||
|
||||
.subsubsub {
|
||||
.subsubsub:not(.list-table-filters) {
|
||||
margin: -8px 0 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -208,7 +208,13 @@
|
|||
'keepAlive': true
|
||||
} ).css( 'cursor', 'help' );
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
.on( 'click', '.wc-confirm-delete', function( event ) {
|
||||
if ( ! window.confirm( woocommerce_admin.i18n_confirm_delete ) ) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
} );
|
||||
|
||||
// Tooltips
|
||||
$( document.body ).trigger( 'init_tooltips' );
|
||||
|
|
|
@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\Downlo
|
|||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductAttributesLookupServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductDownloadsServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\RestockRefundedItemsAdjusterServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsClassesServiceProvider;
|
||||
|
@ -42,6 +43,7 @@ final class Container implements \Psr\Container\ContainerInterface {
|
|||
DownloadPermissionsAdjusterServiceProvider::class,
|
||||
OrdersDataStoreServiceProvider::class,
|
||||
ProductAttributesLookupServiceProvider::class,
|
||||
ProductDownloadsServiceProvider::class,
|
||||
ProxiesServiceProvider::class,
|
||||
RestockRefundedItemsAdjusterServiceProvider::class,
|
||||
UtilsClassesServiceProvider::class,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\SyncUI;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Admin\UI;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;
|
||||
|
||||
/**
|
||||
* Service provider for the Product Downloads-related services.
|
||||
*/
|
||||
class ProductDownloadsServiceProvider extends AbstractServiceProvider {
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
Register::class,
|
||||
Sync::class,
|
||||
SyncUI::class,
|
||||
UI::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( Register::class );
|
||||
$this->share( Synchronize::class )->addArgument( Register::class );
|
||||
$this->share( SyncUI::class )->addArgument( Register::class );
|
||||
$this->share( UI::class )->addArgument( Register::class );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<?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' ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
<?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' ) . '">–</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,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,462 @@
|
|||
<?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' ) );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?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 {
|
||||
const INVALID_URL = 1;
|
||||
const DB_ERROR = 2;
|
||||
}
|
|
@ -0,0 +1,507 @@
|
|||
<?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.
|
||||
*/
|
||||
const MODES = array(
|
||||
self::MODE_DISABLED,
|
||||
self::MODE_ENABLED,
|
||||
);
|
||||
|
||||
const MODE_DISABLED = 'disabled';
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
<?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.
|
||||
*/
|
||||
const SYNC_TASK = 'woocommerce_download_dir_sync';
|
||||
|
||||
/**
|
||||
* The group under which synchronization tasks run (our standard 'woocommerce-db-updates' group).
|
||||
*/
|
||||
const SYNC_TASK_GROUP = 'woocommerce-db-updates';
|
||||
|
||||
/**
|
||||
* Used to track progress throughout the sync process.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
const SYNC_TASK_PROGRESS = 'wc_product_download_dir_sync_progress';
|
||||
|
||||
/**
|
||||
* Number of downloadable products to be processed in each atomic sync task.
|
||||
*/
|
||||
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 update to 6.4.0.', '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 {
|
||||
$parent_url = ( new URL( $downloadable->get_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 ) ) );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Utilities;
|
||||
|
||||
/**
|
||||
* Provides an easy method of assessing URLs, including filepaths (which will be silently
|
||||
* converted to a file:// URL if provided).
|
||||
*/
|
||||
class URL {
|
||||
/**
|
||||
* Components of the URL being assessed.
|
||||
*
|
||||
* The keys match those potentially returned by the parse_url() function, except
|
||||
* that they are always defined and 'drive' (Windows drive letter) has been added.
|
||||
*
|
||||
* @var string|null[]
|
||||
*/
|
||||
private $components = array(
|
||||
'drive' => null,
|
||||
'fragment' => null,
|
||||
'host' => null,
|
||||
'pass' => null,
|
||||
'path' => null,
|
||||
'port' => null,
|
||||
'query' => null,
|
||||
'scheme' => null,
|
||||
'user' => null,
|
||||
);
|
||||
|
||||
/**
|
||||
* If the URL (or filepath) is absolute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_absolute;
|
||||
|
||||
/**
|
||||
* The components of the URL's path.
|
||||
*
|
||||
* For instance, in the case of "file:///srv/www/wp.site" (noting that a file URL has
|
||||
* no host component) this would contain:
|
||||
*
|
||||
* [ "srv", "www", "wp.site" ]
|
||||
*
|
||||
* In the case of a non-file URL such as "https://example.com/foo/bar/baz" (noting the
|
||||
* host is not part of the path) it would contain:
|
||||
*
|
||||
* [ "foo", "bar", "baz" ]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $path_parts = array();
|
||||
|
||||
/**
|
||||
* The URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* Creates and processes the provided URL (or filepath).
|
||||
*
|
||||
* @throws URLException If the URL (or filepath) is seriously malformed.
|
||||
*
|
||||
* @param string $url The URL (or filepath).
|
||||
*/
|
||||
public function __construct( string $url ) {
|
||||
$this->url = $url;
|
||||
$this->preprocess();
|
||||
$this->process_path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes all slashes forward slashes, converts filepaths to file:// URLs, and
|
||||
* other processing to help with comprehension of filepaths.
|
||||
*
|
||||
* @throws URLException If the URL is seriously malformed.
|
||||
*/
|
||||
private function preprocess() {
|
||||
// For consistency, all slashes should be forward slashes.
|
||||
$this->url = str_replace( '\\', '/', $this->url );
|
||||
|
||||
// Windows: capture the drive letter if provided.
|
||||
if ( preg_match( '#^(file://)?([a-z]):/(?!/).*#i', $this->url, $matches ) ) {
|
||||
$this->components['drive'] = $matches[2];
|
||||
}
|
||||
|
||||
// If there is no scheme, assume and prepend "file://".
|
||||
if ( ! preg_match( '#^[a-z]+://#i', $this->url ) ) {
|
||||
$this->url = 'file://' . $this->url;
|
||||
}
|
||||
|
||||
$parsed_components = wp_parse_url( $this->url );
|
||||
|
||||
// If we received a really badly formed URL, let's go no further.
|
||||
if ( false === $parsed_components ) {
|
||||
throw new URLException(
|
||||
sprintf(
|
||||
/* translators: %s is the URL. */
|
||||
__( '%s is not a valid URL.', 'woocommerce' ),
|
||||
$this->url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// File URLs cannot have a host. However, the initial path segment *or* the Windows drive letter
|
||||
// (if present) may be incorrectly be interpreted as the host name.
|
||||
if ( 'file' === $parsed_components['scheme'] && ! empty( $parsed_components['host'] ) ) {
|
||||
// If we do not have a drive letter, then simply merge the host and the path together.
|
||||
if ( null === $this->components['drive'] ) {
|
||||
$parsed_components['path'] = $parsed_components['host'] . ( $parsed_components['path'] ?? '' );
|
||||
}
|
||||
|
||||
// Always unset the host in this situation.
|
||||
unset( $parsed_components['host'] );
|
||||
}
|
||||
|
||||
$this->components = array_merge( $this->components, $parsed_components );
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies the path if possible, by resolving directory traversals to the extent possible
|
||||
* without touching the filesystem.
|
||||
*/
|
||||
private function process_path() {
|
||||
$segments = explode( '/', $this->components['path'] );
|
||||
$this->is_absolute = substr( $this->components['path'], 0, 1 ) === '/';
|
||||
|
||||
// Clean the path.
|
||||
foreach ( $segments as $part ) {
|
||||
// Drop empty segments.
|
||||
if ( strlen( $part ) === 0 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Directory traversals created with percent-encoding syntax should also be detected.
|
||||
$is_traversal = str_ireplace( '%2e', '.', $part ) === '..';
|
||||
|
||||
// Unwind directory traversals.
|
||||
if ( $is_traversal && count( $this->path_parts ) > 0 ) {
|
||||
$this->path_parts = array_slice( $this->path_parts, 0, count( $this->path_parts ) - 1 );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Retain this part of the path.
|
||||
$this->path_parts[] = $part;
|
||||
}
|
||||
|
||||
// Reform the path from the processed segments, appending a leading slash if it is absolute and restoring
|
||||
// the Windows drive letter if we have one.
|
||||
$this->components['path'] = ( $this->is_absolute ? '/' : '' ) . implode( '/', $this->path_parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the processed URL as a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all possible parent URLs for the current URL.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_all_parent_urls(): array {
|
||||
$max_parent = count( $this->path_parts );
|
||||
$parents = array();
|
||||
|
||||
for ( $level = 1; $level <= $max_parent; $level++ ) {
|
||||
$parents[] = $this->get_parent_url( $level );
|
||||
}
|
||||
|
||||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the parent URL.
|
||||
*
|
||||
* For example, if $this->get_url() returns "https://example.com/foo/bar/baz" then
|
||||
* this method will return "https://example.com/foo/bar/".
|
||||
*
|
||||
* When a grand-parent is needed, the optional $level parameter can be used. By default
|
||||
* this is set to 1 (parent). 2 will yield the grand-parent, 3 will yield the great
|
||||
* grand-parent, etc.
|
||||
*
|
||||
* @param int $level Used to indicate the level of parent.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_parent_url( int $level = 1 ): string {
|
||||
if ( $level < 1 ) {
|
||||
$level = 1;
|
||||
}
|
||||
|
||||
$parent_path = implode( '/', array_slice( $this->path_parts, 0, count( $this->path_parts ) - $level ) ) . '/';
|
||||
|
||||
// For absolute paths, apply a leading slash (does not apply if we have a root path).
|
||||
if ( $this->is_absolute && 0 !== strpos( $parent_path, '/' ) ) {
|
||||
$parent_path = '/' . $parent_path;
|
||||
}
|
||||
|
||||
return $this->get_url( $this->get_path( $parent_path ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the processed URL.
|
||||
*
|
||||
* Borrows from https://www.php.net/manual/en/function.parse-url.php#106731
|
||||
*
|
||||
* @param string $path_override If provided this will be used as the URL path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url( string $path_override = null ): string {
|
||||
$scheme = null !== $this->components['scheme'] ? $this->components['scheme'] . '://' : '';
|
||||
$host = null !== $this->components['host'] ? $this->components['host'] : '';
|
||||
$port = null !== $this->components['port'] ? ':' . $this->components['port'] : '';
|
||||
|
||||
$user = null !== $this->components['user'] ? $this->components['user'] : '';
|
||||
$pass = null !== $this->components['pass'] ? ':' . $this->components['pass'] : '';
|
||||
$user_pass = ( ! empty( $user ) || ! empty( $pass ) ) ? $user . $pass . '@' : '';
|
||||
|
||||
$path = $path_override ?? $this->get_path();
|
||||
$query = null !== $this->components['query'] ? '?' . $this->components['query'] : '';
|
||||
$fragment = null !== $this->components['fragment'] ? '#' . $this->components['fragment'] : '';
|
||||
|
||||
return $scheme . $user_pass . $host . $port . $path . $query . $fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the path. Especially useful if it was a a regular filepath that was passed in originally.
|
||||
*
|
||||
* @param string $path_override If provided this will be used as the URL path. Does not impact drive letter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path( string $path_override = null ): string {
|
||||
return ( $this->components['drive'] ? $this->components['drive'] . ':' : '' ) . ( $path_override ?? $this->components['path'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the URL or filepath was absolute.
|
||||
*
|
||||
* @return bool True if absolute, else false.
|
||||
*/
|
||||
public function is_absolute(): bool {
|
||||
return $this->is_absolute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the URL or filepath was relative.
|
||||
*
|
||||
* @return bool True if relative, else false.
|
||||
*/
|
||||
public function is_relative(): bool {
|
||||
return ! $this->is_absolute;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Utilities;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Used to represent a problem encountered when processing a URL.
|
||||
*/
|
||||
class URLException extends Exception {}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Utilities;
|
||||
|
||||
/**
|
||||
* Helper functions for working with users.
|
||||
*/
|
||||
class Users {
|
||||
/**
|
||||
* Indicates if the user qualifies as site administrator.
|
||||
*
|
||||
* In the context of multisite networks, this means that they must have the `manage_sites`
|
||||
* capability. In all other cases, they must have the `manage_options` capability.
|
||||
*
|
||||
* @param int $user_id Optional, used to specify a specific user (otherwise we look at the current user).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_site_administrator( int $user_id = 0 ): bool {
|
||||
$user = 0 === $user_id ? wp_get_current_user() : get_user_by( 'id', $user_id );
|
||||
|
||||
if ( false === $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_multisite() ? $user->has_cap( 'manage_sites' ) : $user->has_cap( 'manage_options' );
|
||||
}
|
||||
}
|
|
@ -58,6 +58,41 @@ class WC_Helper_Product {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a downloadable product.
|
||||
*
|
||||
* @since 6.4.0
|
||||
*
|
||||
* @param array $downloads An array of arrays (each containing a 'name' and 'file' key) or WC_Product_Download objects.
|
||||
* @param bool $save Save or return object.
|
||||
*
|
||||
* @return WC_Product_Simple|false
|
||||
*/
|
||||
public static function create_downloadable_product( array $downloads = array(), $save = true ) {
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_props(
|
||||
array(
|
||||
'name' => 'Downloadable Product',
|
||||
'regular_price' => 10,
|
||||
'price' => 10,
|
||||
'manage_stock' => false,
|
||||
'tax_status' => 'taxable',
|
||||
'downloadable' => true,
|
||||
'virtual' => false,
|
||||
'stock_status' => 'instock',
|
||||
)
|
||||
);
|
||||
|
||||
$product->set_downloads( $downloads );
|
||||
|
||||
if ( $save ) {
|
||||
$product->save();
|
||||
return \wc_get_product( $product->get_id() );
|
||||
} else {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create external product.
|
||||
*
|
||||
|
|
|
@ -5,10 +5,21 @@
|
|||
* @package WooCommerce\Tests\Customer
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
|
||||
/**
|
||||
* WC_Tests_Customer_Functions class.
|
||||
*/
|
||||
class WC_Tests_Customer_Functions extends WC_Unit_Test_Case {
|
||||
/**
|
||||
* Perform any common setup work.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// For these tests, we are not concerned with Approved Download Directory functionality.
|
||||
wc_get_container()->get( Download_Directories::class )->set_mode( Download_Directories::MODE_DISABLED );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set illegal login
|
||||
|
|
|
@ -17,20 +17,29 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
|
|||
*/
|
||||
protected $csv_file = '';
|
||||
|
||||
/**
|
||||
* @var WC_Product_CSV_Importer
|
||||
*/
|
||||
private $sut;
|
||||
|
||||
/**
|
||||
* Load up the importer classes since they aren't loaded by default.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response.
|
||||
$this->http_responder = array( $this, 'mock_http_responses' );
|
||||
|
||||
$this->csv_file = dirname( __FILE__ ) . '/sample.csv';
|
||||
|
||||
$bootstrap = WC_Unit_Tests_Bootstrap::instance();
|
||||
require_once $bootstrap->plugin_dir . '/includes/import/class-wc-product-csv-importer.php';
|
||||
require_once $bootstrap->plugin_dir . '/includes/admin/importers/class-wc-product-csv-importer-controller.php';
|
||||
|
||||
// Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response.
|
||||
$this->http_responder = array( $this, 'mock_http_responses' );
|
||||
$this->csv_file = dirname( __FILE__ ) . '/sample.csv';
|
||||
$this->sut = new WC_Product_CSV_Importer( $this->csv_file, array(
|
||||
'mapping' => $this->get_csv_mapped_items(),
|
||||
'parse' => true,
|
||||
'prevent_timeouts' => false,
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,25 +100,39 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
|
|||
}
|
||||
|
||||
/**
|
||||
* Test import.
|
||||
*
|
||||
* @since 3.1.0
|
||||
* @requires PHP 5.4
|
||||
* @testdox Test import as triggered by an admin user.
|
||||
*/
|
||||
public function test_import() {
|
||||
$args = array(
|
||||
'mapping' => $this->get_csv_mapped_items(),
|
||||
'parse' => true,
|
||||
'prevent_timeouts' => false,
|
||||
);
|
||||
public function test_import_for_admin_users() {
|
||||
// In most cases, an admin user will run the import.
|
||||
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
|
||||
$results = $this->sut->import();
|
||||
|
||||
$importer = new WC_Product_CSV_Importer( $this->csv_file, $args );
|
||||
$results = $importer->import();
|
||||
|
||||
$this->assertEquals( 7, count( $results['imported'] ) );
|
||||
$this->assertEquals( 0, count( $results['failed'] ) );
|
||||
$this->assertEquals( 0, count( $results['updated'] ) );
|
||||
$this->assertEquals( 0, count( $results['skipped'] ) );
|
||||
$this->assertEquals(
|
||||
7,
|
||||
count( $results['imported'] ) ,
|
||||
'One import item references a downloadable file stored in an unapproved location: if the import is triggered by an admin user, that location will be automatically approved.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test import as triggered by a shop manager (or other non-admin user).
|
||||
*/
|
||||
public function test_import_for_shop_managers() {
|
||||
// In some cases, a shop manager may run the import.
|
||||
wp_set_current_user( self::factory()->user->create( array( 'role' => 'shop_manager' ) ) );
|
||||
$results = $this->sut->import();
|
||||
|
||||
$this->assertEquals( 0, count( $results['updated'] ) );
|
||||
$this->assertEquals( 0, count( $results['skipped'] ) );
|
||||
$this->assertEquals( 6, count( $results['imported'] ) );
|
||||
$this->assertEquals(
|
||||
1,
|
||||
count( $results['failed'] ),
|
||||
'One import item references a downloadable file stored in an unapproved location: if the import is triggered by a non-admin, that item cannot be imported.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
|
||||
/**
|
||||
* Tests relating to the WC_Abstract_Product class.
|
||||
*/
|
||||
class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
|
||||
/**
|
||||
* @testdox Ensure that individual Downloadable Products follow the rules regarding Approved Download Directories.
|
||||
*/
|
||||
public function test_fetching_of_approved_downloads() {
|
||||
/**
|
||||
* @var Download_Directories $download_directories
|
||||
*/
|
||||
$download_directories = wc_get_container()->get( Download_Directories::class );
|
||||
$download_directories->set_mode( Download_Directories::MODE_ENABLED );
|
||||
$download_directories->add_approved_directory( 'https://always.trusted/' );
|
||||
$problematic_file_source_id = $download_directories->add_approved_directory( 'https://new.supplier/' );
|
||||
|
||||
$product = WC_Helper_Product::create_downloadable_product( array(
|
||||
array(
|
||||
'name' => 'Book 1',
|
||||
'file' => 'https://always.trusted/123.pdf'
|
||||
),
|
||||
array(
|
||||
'name' => 'Book 2',
|
||||
'file' => 'https://new.supplier/456.pdf'
|
||||
),
|
||||
) );
|
||||
|
||||
$this->assertCount(
|
||||
2,
|
||||
wc_get_product( $product->get_id() )->get_downloads(),
|
||||
'If we load the downloadable product and all of its downloads are stored in trusted directories, we expect to fetch all of them.'
|
||||
);
|
||||
|
||||
$download_directories->disable_by_id( $problematic_file_source_id );
|
||||
|
||||
$this->assertCount(
|
||||
1,
|
||||
wc_get_product( $product->get_id() )->get_downloads(),
|
||||
'If a trusted download directory is disabled, we expect any individual download files from that location will not be listed.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'Book 1',
|
||||
current( wc_get_product( $product->get_id() )->get_downloads() )->get_name(),
|
||||
'Only individual download files that are stored in trusted locations will be fetched.'
|
||||
);
|
||||
|
||||
$download_directories->set_mode( Download_Directories::MODE_DISABLED );
|
||||
|
||||
$this->assertCount(
|
||||
2,
|
||||
wc_get_product( $product->get_id() )->get_downloads(),
|
||||
'If the Approved Download Directories system is completely disabled, we expect all product downloads to be fetched irrespective of where they are stored.'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
/**
|
||||
* Class WC_Product_Download_Test
|
||||
*/
|
||||
class WC_Product_Download_Test extends WC_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* Test for file without extension.
|
||||
*/
|
||||
|
@ -35,4 +35,32 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case {
|
|||
$download->set_file( $file_path_with_period_at_end );
|
||||
$this->assertEquals( false, $download->is_allowed_filetype() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that download URLs are automatically added to the approved directories list (for
|
||||
* "admin"-level users) but that they are not automatically added in other cases.
|
||||
*/
|
||||
public function test_allowed_directory_rules_are_enforced() {
|
||||
/** @var Download_Directories $download_directories */
|
||||
$download_directories = wc_get_container()->get( Download_Directories::class );
|
||||
$download_directories->set_mode( Download_Directories::MODE_ENABLED );
|
||||
|
||||
$non_admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'editor', 'user_pass' => 'x' ) );
|
||||
$admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'administrator', 'user_pass' => 'x' ) );
|
||||
$ebook_url = 'https://external.site/books/ultimate-guide-to-stuff.pdf';
|
||||
$podcast_url = 'https://external.site/podcasts/ultimate-guide-to-stuff.mp3';
|
||||
|
||||
wp_set_current_user( $admin_user );
|
||||
$download = new WC_Product_Download();
|
||||
$download->set_file( $ebook_url );
|
||||
$this->assertFalse( $download_directories->is_valid_path( $ebook_url ), 'Verify ebook path has not been added prior to next test.' );
|
||||
$download->check_is_valid();
|
||||
$this->assertTrue( $download_directories->is_valid_path( $ebook_url ), 'Verify ebook path was automatically added by the last operation.' );
|
||||
|
||||
wp_set_current_user( $non_admin_user );
|
||||
$download = new WC_Product_Download();
|
||||
$download->set_file( $podcast_url );
|
||||
$this->expectExceptionMessage( 'cannot be used: it is not located in an approved directory' );
|
||||
$download->check_is_valid();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
namespace Automattic\WooCommerce\Tests\Internal;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
|
||||
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
|
||||
|
||||
/**
|
||||
|
@ -42,6 +43,9 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
|
|||
10,
|
||||
2
|
||||
);
|
||||
|
||||
// In these tests, we are not directly concerned with Approved Download Directory functionality.
|
||||
wc_get_container()->get( Download_Directories::class )->set_mode( Download_Directories::MODE_DISABLED );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Tests\Internal\ProductDownloads;
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
|
||||
|
||||
use WC_Unit_Test_Case;
|
||||
|
||||
/**
|
||||
* Tests for the Product Downloads Whitelist.
|
||||
*/
|
||||
class RegisterTest extends WC_Unit_Test_Case {
|
||||
/**
|
||||
* @var Register
|
||||
*/
|
||||
private static $sut;
|
||||
|
||||
/**
|
||||
* Create a Whitelist with representative paths including a URL with a port number,
|
||||
* a regular URL and an absolute filepath.
|
||||
*/
|
||||
public static function setUpBeforeClass() {
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
static::$sut = new Register();
|
||||
static::$sut->add_approved_directory( 'http://localhost:5000' );
|
||||
static::$sut->add_approved_directory( 'https://foo.bar/assets/' );
|
||||
static::$sut->add_approved_directory( '/absolute/filepath' );
|
||||
static::$sut->add_approved_directory( '/absolute/disabled/filepath', false );
|
||||
static::$sut->add_approved_directory( 'C:\\Program\\Data\\Assets' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test a range of filepaths and URLs that we expect to be seen as valid.
|
||||
*/
|
||||
public function test_valid_paths() {
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( 'https://foo.bar/assets/my/nested/document.xml' ),
|
||||
'URL validates if it is subordinate to a whitelisted URL path.'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( 'http://localhost:5000/direct-child.doc' ),
|
||||
'URL validates if it is subordinate to a whitelisted URL path (including one with a port number).'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( '/absolute/filepath/my-file.pdf' ),
|
||||
'Filepath validates if it is subordinate to a whitelisted path (where the path being tested does not explicitly use the "file://" scheme).'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( 'file:///absolute/filepath/my-file.pdf' ),
|
||||
'Filepath validates if it is subordinate to a whitelisted path (where the path being tested explicitly uses the "file://" scheme).'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( 'C:\\Program\\Data\\Assets\\downloadable.exe' ),
|
||||
'Filepath validates if it is subordinate to a whitelisted path (Windows-style path complete with drive letter).'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( 'C:\\Program\\Data/Assets/downloadable.exe' ),
|
||||
'Filepath validates if it is subordinate to a whitelisted path (Windows-style path complete with drive letter, but with Unix style separators).'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
static::$sut->is_valid_path( '/absolute/filepath/../filepath/good.xml' ),
|
||||
'Filepaths containing directory traversals validate so long as the resolved path is still subordinate to a whitelisted path.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test a range of filepaths and URLs that we expect to be seen as invalid.
|
||||
*/
|
||||
public function test_invalid_paths() {
|
||||
$this->assertFalse(
|
||||
static::$sut->is_valid_path( 'http://foo.bar/assets/my/nested/document.xml' ),
|
||||
'URL should not validate as the ("http://") scheme means it is different to any of the current whitelisted paths.'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
static::$sut->is_valid_path( 'http://localhost:50001/some-file.pdf' ),
|
||||
'URL should not validate as the port number means it belongs to a distinct, non-whitelisted URL path.'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
static::$sut->is_valid_path( 'file://absolute/filepath/directory/my-file.pdf' ),
|
||||
'Filepath will not validate is it does not belong to a whitelisted path (note that the tested path is relative).'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
static::$sut->is_valid_path( '/absolute/filepath/../../filepath/good.xml' ),
|
||||
'Filepaths containing directory traversals do not validate if the resolved path is no longer subordinate to a whitelisted path.'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
static::$sut->is_valid_path( '/absolute/disabled/filepath/guide.pdf' ),
|
||||
'Filepath will not validate if the corresponding rule has been disabled.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Ensure adding individual paths works as expected.
|
||||
*/
|
||||
public function test_adding_single_path() {
|
||||
$this->assertEquals(
|
||||
static::$sut->get_by_url( 'http://localhost:5000' )->get_id(),
|
||||
static::$sut->add_approved_directory( 'http://localhost:5000' ),
|
||||
'If the path was already added, adding it a second time (without trailing slash) will result in the existing row number (rule ID) being returned.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
static::$sut->get_by_url( 'http://localhost:5000' )->get_id(),
|
||||
static::$sut->add_approved_directory( 'http://localhost:5000/' ),
|
||||
'If the path was already added, adding it a second time (with trailing slash) will result in the existing row number (rule ID) being returned.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Ensure individual approved directories can be enabled and disabled.
|
||||
*/
|
||||
public function test_enabling_disabling_individual_rules() {
|
||||
$approved_directory_rule = static::$sut->get_by_url( 'https://foo.bar/assets/' );
|
||||
$approved_directory_id = $approved_directory_rule->get_id();
|
||||
$this->assertTrue( $approved_directory_rule->is_enabled() );
|
||||
|
||||
static::$sut->disable_by_id( $approved_directory_id );
|
||||
$this->assertFalse( static::$sut->get_by_url( 'https://foo.bar/assets/' )->is_enabled() );
|
||||
|
||||
static::$sut->enable_by_id( $approved_directory_id );
|
||||
$this->assertTrue( static::$sut->get_by_url( 'https://foo.bar/assets/' )->is_enabled() );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Tests\Internal\ProductDownloads;
|
||||
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;
|
||||
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
|
||||
use Automattic\WooCommerce\Testing\Tools\FakeQueue;
|
||||
use WC_Queue_Interface;
|
||||
use WC_Unit_Test_Case;
|
||||
|
||||
/**
|
||||
* Tests for the Product Downloads Allowed Directories synchronization utility.
|
||||
*/
|
||||
class SynchronizeTest extends WC_Unit_Test_Case {
|
||||
/**
|
||||
* @var Synchronize
|
||||
*/
|
||||
private $sut;
|
||||
|
||||
/**
|
||||
* Create subject under test.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$this->sut = wc_get_container()->get( Synchronize::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Ensure basic controls to start and stop synchronization behave as expected.
|
||||
*/
|
||||
public function test_basic_synchronization_controls() {
|
||||
$this->sut->start();
|
||||
$this->assertTrue(
|
||||
$this->sut->in_progress(),
|
||||
'We can successfully start synchronizing and verify it is in progress.'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$this->sut->start(),
|
||||
'If a download directory synchronization process is already in progress, additional concurrent sync processes cannot be created.'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$this->sut->start(),
|
||||
'Synchronization process can be cancelled before it completes.'
|
||||
);
|
||||
|
||||
$this->sut->stop();
|
||||
$this->assertNull(
|
||||
wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Queue_Interface::class )->get_next( Synchronize::SYNC_TASK ),
|
||||
'Once synchronization has been cancelled, any related scheduled actions will also have been cleaned up.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Verify expected logging and clean-up take place during and following synchronization of download directories.
|
||||
*/
|
||||
public function test_sync_process() {
|
||||
$logged_messages = array();
|
||||
|
||||
$log_watcher = function ( string $logged_message ) use ( &$logged_messages ) {
|
||||
$logged_messages[] = $logged_message;
|
||||
};
|
||||
|
||||
add_filter( 'woocommerce_logger_log_message', $log_watcher );
|
||||
|
||||
$this->sut->start();
|
||||
$this->sut->run();
|
||||
|
||||
remove_filter( 'woocommerce_logger_log_message', $log_watcher );
|
||||
|
||||
$this->assertTrue(
|
||||
! get_option( Synchronize::SYNC_TASK_PAGE ) && ! get_option( Synchronize::SYNC_TASK_PROGRESS ),
|
||||
'Once synchronization has completed, any temporary options used to hold state will have been deleted.'
|
||||
);
|
||||
|
||||
$this->assertContains(
|
||||
'Approved Download Directories sync: scan is complete!',
|
||||
$logged_messages,
|
||||
'We expect that completion of the synchronization process will have been recorded in the log.'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Tests\Internal\Utilities;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Utilities\URL;
|
||||
use WC_Unit_Test_Case;
|
||||
|
||||
/**
|
||||
* A collection of tests for the filepath utility class.
|
||||
*/
|
||||
class URLTest extends WC_Unit_Test_Case {
|
||||
public function test_if_absolute_or_relative() {
|
||||
$this->assertTrue(
|
||||
( new URL( '/etc/foo/bar' ) )->is_absolute() ,
|
||||
'Correctly determines if a Unix-style path is absolute.'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
( new URL( 'c:\\Windows\Programs\Item' ) )->is_absolute(),
|
||||
'Correctly determines if a Windows-style path is absolute.'
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
( new URL( 'wp-content/uploads/thing.pdf' ) )->is_relative(),
|
||||
'Correctly determines if a filepath is relative.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_directory_traversal_resolution() {
|
||||
$this->assertEquals(
|
||||
'/var/foo/foobar',
|
||||
( new URL( '/var/foo/bar/baz/../../foobar' ) )->get_path(),
|
||||
'Correctly resolves a path containing a directory traversal.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'/bazbar',
|
||||
( new URL( '/var/foo/../../../../bazbar' ) )->get_path(),
|
||||
'Correctly resolves a path containing a directory traversal, even if the traversals attempt to backtrack beyond the root directory.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'../should/remain/relative',
|
||||
( new URL( 'relative/../../should/remain/relative' ) )->get_path(),
|
||||
'Simplifies a relative path containing directory traversals to the extent possible (without inspecting the filesystem).'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_can_get_normalized_string_representation() {
|
||||
$this->assertEquals(
|
||||
'foo/bar/baz',
|
||||
( new URL( 'foo/bar//baz' ) )->get_path(),
|
||||
'Empty segments are discarded, remains as a relative path.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'/foo/ /bar/ /baz/foobarbaz',
|
||||
( new URL( '///foo/ /bar/ /baz//foobarbaz' ) )->get_path(),
|
||||
'Empty segments are discarded, non-empty segments containing only whitespace are preserved, remains as an absolute path.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'c:/Windows/Server/HTTP/dump.xml',
|
||||
( new URL( 'c:\\Windows\Server\HTTP\dump.xml' ) )->get_path(),
|
||||
'String representations of Windows filepaths have forward slash separators and preserve the drive letter.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_can_get_normalized_url_representation() {
|
||||
$this->assertEquals(
|
||||
'file://relative/path',
|
||||
( new URL( 'relative/path' ) )->get_url(),
|
||||
'Can obtain a URL representation of a relative filepath, even when the initial string was a plain filepath.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'file:///absolute/path',
|
||||
( new URL( '/absolute/path' ) )->get_url(),
|
||||
'Can obtain a URL representation of an absolute filepath, even when the initial string was a plain filepath.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'file:///etc/foo/bar',
|
||||
( new URL( 'file:///etc/foo/bar' ) )->get_url(),
|
||||
'Can obtain a URL representation of a filepath, when the source filepath was also expressed as a URL.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_handling_of_percent_encoded_periods() {
|
||||
$this->assertEquals(
|
||||
'https://foo.bar/asset.txt',
|
||||
( new URL( 'https://foo.bar/parent/.%2e/asset.txt' ) )->get_url(),
|
||||
'Directory traversals expressed using percent-encoding are still resolved (lowercase, one encoded period).'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'https://foo.bar/asset.txt',
|
||||
( new URL( 'https://foo.bar/parent/%2E./asset.txt' ) )->get_url(),
|
||||
'Directory traversals expressed using percent-encoding are still resolved (uppercase, one encoded period).'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'https://foo.bar/asset.txt',
|
||||
( new URL( 'https://foo.bar/parent/%2E%2e/asset.txt' ) )->get_url(),
|
||||
'Directory traversals expressed using percent-encoding are still resolved (mixed case, both periods encoded).'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'https://foo.bar/parent/%2E.%2fasset.txt',
|
||||
( new URL( 'https://foo.bar/parent/%2E.%2fasset.txt' ) )->get_url(),
|
||||
'If the forward slash after a double period is URL encoded, there is no directory traversal (since this means the slash is a part of the segment and is not a separator).'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'file:///var/www/network/%2econfig',
|
||||
( new URL( '/var/www/network/%2econfig' ) )->get_url(),
|
||||
'Use of percent-encoding in URLs is accepted and unnecessary conversion does not take place.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_can_obtain_parent_url() {
|
||||
$this->assertEquals(
|
||||
'file:///',
|
||||
( new URL( '/' ) )->get_parent_url(),
|
||||
'The parent of root directory "/" is "/".'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'file:///var/',
|
||||
( new URL( '/var/dev/' ) )->get_parent_url(),
|
||||
'The parent URL will be trailingslashed.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
'https://example.com/',
|
||||
( new URL( 'https://example.com' ) )->get_parent_url(),
|
||||
'The host name (for non-file URLs) is distinct from the path and will not be removed.'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_can_obtain_all_parent_urls() {
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/',
|
||||
'https://local.web/wp-content/uploads/woocommerce_uploads/',
|
||||
'https://local.web/wp-content/uploads/',
|
||||
'https://local.web/wp-content/',
|
||||
'https://local.web/',
|
||||
),
|
||||
( new URL( 'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/secret-sauce.pdf' ) )->get_all_parent_urls(),
|
||||
'All parent URLs can be derived, but the host name is never stripped.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'file:///srv/websites/my.wp.site/public/',
|
||||
'file:///srv/websites/my.wp.site/',
|
||||
'file:///srv/websites/',
|
||||
'file:///srv/',
|
||||
'file:///',
|
||||
),
|
||||
( new URL( '/srv/websites/my.wp.site/public/test-file.doc' ) )->get_all_parent_urls(),
|
||||
'All parent URLs can be derived for a filepath, up to and including the root directory.'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'file://C:/Documents/Web/TestSite/',
|
||||
'file://C:/Documents/Web/',
|
||||
'file://C:/Documents/',
|
||||
'file://C:/',
|
||||
),
|
||||
( new URL( 'C:\\Documents\\Web\\TestSite\\BackgroundTrack.mp3' ) )->get_all_parent_urls(),
|
||||
'All parent URLs can be derived for a filepath, up to and including the root directory plus drive letter (Windows).'
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue