updated plugin Easy Digital Downloads version 3.1.1.2

This commit is contained in:
2023-03-17 22:34:04 +00:00
committed by Gitium
parent e8a66564bd
commit 19e086d1c4
647 changed files with 20986 additions and 27305 deletions

View File

@ -0,0 +1,82 @@
<?php
/**
* Handles meta related filters/actions for downloads.
*
* @since 3.1.0.5
*/
namespace EDD\Admin\Downloads;
use EDD\EventManagement\SubscriberInterface;
class Meta implements SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_metabox_save_edd_variable_prices' => 'variable_prices_value',
'edd_metabox_save_edd_download_files' => 'download_files_value',
'edd_save_download' => array( 'bundled_conditions', 10, 2 ),
);
}
/**
* Checks the variable prices array to weed out empty prices.
*
* @since 3.1.0.5
* @param array $prices
* @return array
*/
public function variable_prices_value( $prices ) {
if ( empty( $prices ) ) {
return false;
}
foreach ( $prices as $id => $price ) {
if ( empty( $price['amount'] ) && empty( $price['name'] ) ) {
unset( $prices[ $id ] );
continue;
}
}
return $prices;
}
/**
* Checks the download files array to weed out empty file options.
*
* @since 3.1.0.5
* @param array $files
* @return array
*/
public function download_files_value( $files ) {
if ( empty( $files ) ) {
return false;
}
foreach ( $files as $id => $file ) {
if ( empty( $file['name'] ) && empty( $file['file'] ) ) {
unset( $files[ $id ] );
continue;
}
}
return $files;
}
/**
* Once the download is saved, if it's not a bundle, delete the product conditions meta.
*
* @since 3.1.0.5
* @param int $post_id
* @param WP_Post $post
* @return void
*/
public function bundled_conditions( $post_id, $post ) {
if ( ! get_post_meta( $post_id, '_edd_product_type', true ) ) {
delete_post_meta( $post_id, '_edd_bundled_products_conditions' );
}
}
}

View File

@ -0,0 +1,373 @@
<?php
/**
* Extension Card builder.
*
* @package EDD
* @subpackage Extensions
* @copyright 2022 Easy Digital Downloads
*/
namespace EDD\Admin\Extensions;
class Card {
use Traits\Buttons;
/**
* The product data.
*
* @var \EDD\Admin\Extensions\ProductData
*/
private $product;
/**
* The parameters if the plugin is not active.
*
* @var array
*/
private $inactive_parameters = array();
/**
* The parameters if the plugin is active.
*
* @var array
*/
private $active_parameters = array();
/**
* Whether the current plugin is active.
*
* @var bool
*/
private $is_plugin_active = false;
/**
* Whether the current plugin is installed.
*
* @var bool
*/
private $is_plugin_installed = false;
/**
* The plugin version.
*
* @since 3.1.1
* @var bool|string
*/
private $version = false;
public function __construct( ProductData $product, $args ) {
$this->product = $product;
$this->inactive_parameters = $args['inactive_parameters'];
$this->active_parameters = $args['active_parameters'];
$this->required_pass_id = $args['required_pass_id'];
$this->is_plugin_active = $args['is_plugin_active'];
$this->is_plugin_installed = $args['is_plugin_installed'];
$this->version = $args['version'];
if ( ! empty( $this->product->style ) && 'installer' === $this->product->style ) {
$this->do_card_extension_installer();
} else {
$this->do_card_product_education();
}
}
/**
* Outputs the card with the product education style markup.
*
* @since 3.1.1
* @return void
*/
private function do_card_product_education() {
?>
<div class="<?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $this->get_card_classes() ) ) ); ?>">
<?php $this->do_title(); ?>
<div class="edd-extension-manager__body">
<?php
$this->do_image();
$this->do_description();
$this->do_features();
$this->do_actions();
?>
</div>
</div>
<?php
}
/**
* Outputs the product card with the extension installer style markup.
*
* @since 3.1.1
* @return void
*/
private function do_card_extension_installer() {
$filter_terms = $this->get_filter_terms();
?>
<div
class="<?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $this->get_card_classes() ) ) ); ?>"
<?php if ( $filter_terms ) : ?>
data-filter="<?php echo esc_attr( $this->get_filter_terms() ); ?>"
<?php endif; ?>
>
<div class="edd-extension-manager__body">
<?php
$this->do_icon();
echo '<div class="edd-extension-manager__content">';
$this->do_title( true );
$this->do_description();
echo '</div>';
$this->do_settings_link( $this->product );
?>
</div>
<div class="edd-extension-manager__actions">
<?php
$this->do_version();
$this->do_installer_action();
?>
</div>
</div>
<?php
}
/**
* Gets the settings link.
*
* @since 3.1.1
* @param ProductData $product_data The product data.
* @return void
*/
protected function do_settings_link( $product_data ) {}
/**
* Outputs the extension title.
*
* @since 3.1.1
* @param bool $link Whether the title should be linked.
* @return void
*/
private function do_title( $link = false ) {
$title = ! empty( $this->product->heading ) ? $this->product->heading : $this->product->title;
$url = false;
if ( $link && ! empty( $this->product->slug ) ) {
$url = edd_link_helper(
'https://easydigitaldownloads.com/downloads/' . esc_attr( $this->product->slug ),
array(
'utm_content' => esc_attr( $this->product->slug ),
'utm_medium' => 'extensions-page',
),
false
);
}
?>
<h3 class="edd-extension-manager__title">
<?php
if ( $url ) {
printf(
'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
esc_url( $url ),
esc_html( $title )
);
} else {
echo esc_html( $title );
}
?>
</h3>
<?php
}
/**
* Outputs the extension image.
*
* @since 3.1.1
* @return void
*/
private function do_image() {
if ( empty( $this->product->image ) ) {
return;
}
?>
<div class="edd-extension-manager__image">
<img alt="" src="<?php echo esc_url( $this->product->image ); ?>" />
</div>
<?php
}
/**
* Outputs the extension icon.
*
* @since 3.1.1
* @return void
*/
private function do_icon() {
if ( empty( $this->product->icon ) ) {
return;
}
?>
<div class="edd-extension-manager__icon">
<img alt="" src="<?php echo esc_url( $this->product->icon ); ?>" />
</div>
<?php
}
/**
* Outputs the extension description.
*
* @since 3.1.1
* @return void
*/
private function do_description() {
if ( empty( $this->product->description ) ) {
return;
}
?>
<div class="edd-extension-manager__description"><?php echo wp_kses_post( wpautop( $this->product->description ) ); ?></div>
<?php
}
/**
* Outputs the extension features.
*
* @since 3.1.1
* @return void
*/
private function do_features() {
if ( empty( $this->product->features ) || ! is_array( $this->product->features ) ) {
return;
}
?>
<div class="edd-extension-manager__features">
<ul>
<?php foreach ( $this->product->features as $feature ) : ?>
<li><span class="dashicons dashicons-yes"></span><?php echo esc_html( $feature ); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php
}
/**
* Outputs the extension actions.
*
* @since 3.1.1
* @return void
*/
private function do_actions() {
?>
<div class="edd-extension-manager__group edd-extension-manager__actions">
<?php
if ( ! $this->is_plugin_active && ! empty( $this->inactive_parameters['button_text'] ) ) {
?>
<div class="edd-extension-manager__step">
<?php $this->button( $this->inactive_parameters ); ?>
</div>
<?php
}
?>
<div class="edd-extension-manager__step">
<?php $this->link( $this->active_parameters ); ?>
</div>
</div>
<?php
}
/**
* If the plugin version is known, output it on the card.
*
* @since 3.1.1
* @return void
*/
private function do_version() {
if ( ! $this->version ) {
return;
}
?>
<div class="edd-plugin__version">
<?php
/* translators: the plugin version */
printf( esc_html__( 'Version: %s', 'easy-digital-downloads' ), esc_html( $this->version ) );
?>
</div>
<?php
}
/**
* Installer cards have custom actions to output: activate/deactivate button; install button; upgrade link.
*
* @since 3.1.1
* @return void
*/
protected function do_installer_action() {
$args = $this->active_parameters;
if ( ! $this->is_plugin_active && ! empty( $this->inactive_parameters['button_text'] ) ) {
$args = $this->inactive_parameters;
}
?>
<div class="edd-extension-manager__control">
<?php $this->select_installer_action( $args ); ?>
</div>
<?php
}
/**
* Selects which action button should show.
*
* @since 3.1.1
* @param array $args
* @return void
*/
protected function select_installer_action( $args ) {
if ( ! $this->is_plugin_active && ! empty( $this->inactive_parameters['button_text'] ) ) {
$this->button( $this->inactive_parameters );
return;
}
$this->link( $this->active_parameters );
}
/**
* Gets the CSS classes for the single extension card.
*
* @since 2.11.4
* @return array The array of CSS classes.
*/
private function get_card_classes() {
$base_class = 'edd-extension-manager__card';
$card_classes = array(
$base_class,
);
if ( $this->is_plugin_installed ) {
$card_classes[] = 'edd-plugin__installed';
if ( $this->is_plugin_active ) {
$card_classes[] = 'edd-plugin__active';
} else {
$card_classes[] = 'edd-plugin__inactive';
}
}
$variation = 'stacked';
if ( ! empty( $this->product->style ) ) {
$variation = $this->product->style;
}
if ( 'detailed-2col' === $variation && ( empty( $this->product->features ) || ! is_array( $this->product->features ) ) ) {
$variation = 'detailed';
}
$card_classes[] = "{$base_class}--{$variation}";
return $card_classes;
}
/**
* Gets the data-filter terms for a card.
*
* @since 3.1.1
* @return string
*/
private function get_filter_terms() {
$terms = array();
if ( ! empty( $this->product->terms ) ) {
$terms = array_keys( (array) $this->product->terms );
}
if ( ! empty( $this->product->tab ) ) {
$terms[] = $this->product->tab;
}
return implode( ',', array_map( 'strtolower', array_filter( $terms ) ) );
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace EDD\Admin\Extensions;
class DownloadURL {
/**
* Potentially the provided .zip file source.
*
* @since 3.1.1
* @var bool|string
*/
protected $plugin = false;
/**
* The license key.
*
* @since 3.1.1
* @var bool|string
*/
protected $license_key = false;
public function __construct( $license_key ) {
if ( ! empty( $_POST['plugin'] ) ) {
$this->plugin = sanitize_text_field( $_POST['plugin'] );
}
$this->license_key = $license_key;
}
/**
* Gets the download URL.
*
* @since 3.1.1
* @return bool|string
*/
public function get_url() {
return false !== strpos( $this->plugin, 'https://downloads.wordpress.org/plugin' ) ? $this->plugin : false;
}
}

View File

@ -0,0 +1,403 @@
<?php
namespace EDD\Admin\Extensions;
use \EDD\Admin\Pass_Manager;
abstract class Extension {
/**
* The product ID. This only needs to be set if the extending class is
* for a single product.
*
* @since 2.11.4
* @var int
*/
protected $item_id;
/**
* The settings tab where this item will show.
*
* @since 2.11.4
* @var string
*/
protected $settings_tab = '';
/**
* The required AA pass level.
*/
const PASS_LEVEL = Pass_Manager::PERSONAL_PASS_ID;
/**
* The Extension Manager
*
* @var \EDD\Admin\Extensions\Extension_Manager
*/
protected $manager;
/**
* The settings section for this item.
*
* @since 2.11.5
* @var string
*/
protected $settings_section = 'general';
/**
* The pass manager.
*
* @var \EDD\Admin\Pass_Manager
*/
protected $pass_manager;
public function __construct() {
$this->manager = new \EDD\Admin\Extensions\Extension_Manager( static::PASS_LEVEL );
$this->pass_manager = new Pass_Manager();
}
/**
* Whether the extension is activated.
*
* @since 2.11.4
* @return bool
*/
abstract protected function is_activated();
/**
* Output the settings field (installation helper).
*
* @return void
*/
public function settings_field() {
if ( $this->is_activated() ) {
return;
}
$this->do_single_extension_card();
}
/**
* Outputs a single extension card.
*
* @since 2.11.4
* @param false|int $item_id Optional: the individual extension product ID.
* @return void
*/
public function do_single_extension_card( $item_id = false ) {
if ( empty( $item_id ) && empty( $this->item_id ) ) {
return;
}
$product_data = $this->get_product_data( $item_id );
if ( ! $product_data || empty( $product_data->title ) ) {
return;
}
$configuration = $this->get_configuration( $product_data );
if ( ! empty( $configuration ) ) {
$product_data = $product_data->mergeConfig( $configuration );
}
$this->manager->do_extension_card(
$product_data,
$this->get_inactive_parameters( $product_data, $item_id ),
$this->get_active_parameters( $product_data, $item_id )
);
}
/**
* Gets the parameters for an inactive plugin.
*
* @since 3.1.1
* @param ProductData $product_data The extension data returned from the Products API.
* @param int $item_id The individual extension product ID.
* @return array
*/
protected function get_inactive_parameters( $product_data, $item_id ) {
return $this->get_button_parameters( $product_data, $item_id );
}
/**
* Gets the parameters for an active plugin.
*
* @since 3.1.1
* @param ProductData $product_data The extension data returned from the Products API.
* @param int $item_id The individual extension product ID.
* @return array
*/
protected function get_active_parameters( $product_data, $item_id ) {
return $this->get_link_parameters( $product_data );
}
/**
* Gets the product data for a specific extension.
*
* @param false|int $item_id
* @return bool|ProductData|array False if there is no data; product data object if there is, or possibly an array of arrays.
*/
public function get_product_data( $item_id = false ) {
$api = new ExtensionsAPI();
$body = $this->get_api_body();
$api_item_id = $item_id ?: $this->item_id;
$product_data = $api->get_product_data( $body, $api_item_id );
if ( ! $product_data ) {
return false;
}
if ( $api_item_id ) {
return $product_data;
}
if ( $item_id && ! empty( $product_data[ $item_id ] ) ) {
return $product_data[ $item_id ];
}
return $product_data;
}
/**
* Gets the custom configuration for the extension.
*
* @since 2.11.4
* @param ProductData $product_data Optionally allows the product data to be parsed in the configuration.
* @return array
*/
protected function get_configuration( ProductData $product_data ) {
return array();
}
/**
* Formats a custom description array by running wpautop and converting it to a string.
*
* @since 2.11.4
* @param array $description The custom product description.
* @return string
*/
protected function format_description( array $description ) {
return implode( '', array_map( 'wpautop', $description ) );
}
/**
* Whether the current screen is an EDD setings screen.
*
* @since 2.11.4
* @return bool
*/
protected function is_edd_settings_screen() {
return edd_is_admin_page( 'settings', $this->settings_tab );
}
/**
* Whether the current screen is a download new/edit screen.
*
* @since 2.11.4
* @return bool
*/
protected function is_download_edit_screen() {
return edd_is_admin_page( 'download', 'edit' ) || edd_is_admin_page( 'download', 'new' );
}
/**
* Whether the section for an individual product can be registered/shown.
*
* @since 2.11.4
* @return bool
*/
protected function can_show_product_section() {
if ( ! $this->is_edd_settings_screen() ) {
return false;
}
if ( $this->is_activated() ) {
return false;
}
if ( ! $this->get_product_data() ) {
return false;
}
return true;
}
/**
* Gets the array for the body of the API request.
* Classes may need to override this (for example, to query a specific tag).
* Note that the first array key/value pair are used to create the option name.
*
* @return array
*/
protected function get_api_body() {
return array();
}
/**
* Gets the type for the button data-type attribute.
* This is intended to sync with the Products API request.
* Default is product.
*
* Really a shim for array_key_first.
*
* @param array $array
* @return string
*/
private function get_type( array $array ) {
$type = 'product';
if ( empty( $array ) ) {
return $type;
}
if ( function_exists( 'array_key_first' ) ) {
return array_key_first( $array );
}
foreach ( $array as $key => $unused ) {
return $key;
}
return $type;
}
/**
* Gets the button parameters.
* Classes should not need to replace this method.
*
* @param ProductData $product_data The extension data returned from the Products API.
* @param int|false $item_id Optional: the item ID.
* @return array
*/
protected function get_button_parameters( ProductData $product_data, $item_id = false ) {
if ( empty( $item_id ) ) {
$item_id = $this->item_id;
}
$body = $this->get_api_body();
$type = $this->get_type( $body );
$id = ! empty( $body[ $type ] ) ? $body[ $type ] : $this->item_id;
$button = array(
'type' => $type,
'id' => $id,
'product' => $item_id,
);
// If the extension is not installed, the button will prompt to install and activate it.
if ( empty( $product_data->basename ) || ! $this->current_user_can() || ! $this->manager->is_plugin_installed( $product_data->basename ) ) {
$required_pass_id = ! empty( $product_data->pass_id ) ? $product_data->pass_id : static::PASS_LEVEL;
if ( $this->manager->pass_can_download( $required_pass_id ) ) {
$button = array(
/* translators: The extension name. */
'button_text' => sprintf( __( 'Log In to Your Account to Download %s', 'easy-digital-downloads' ), $product_data->title ),
'href' => $this->get_upgrade_url( $product_data, $item_id, true ),
'new_tab' => true,
'type' => $type,
);
} else {
$button = array(
/* translators: The extension name. */
'button_text' => sprintf( __( 'Upgrade Today to Access %s!', 'easy-digital-downloads' ), $product_data->title ),
'href' => $this->get_upgrade_url( $product_data, $item_id ),
'new_tab' => true,
'type' => $type,
);
}
return $button;
}
if ( ! empty( $product_data->basename ) && $this->current_user_can() ) {
$button['plugin'] = $product_data->basename;
// If the extension is installed, but not activated, the button will prompt to activate it.
if ( ! $this->manager->is_plugin_active( $product_data->basename ) ) {
$button['action'] = 'activate';
/* translators: The extension name. */
$button['button_text'] = sprintf( __( 'Activate %s', 'easy-digital-downloads' ), $product_data->title );
} elseif ( ! empty( $product_data->style ) && 'installer' === $product_data->style ) {
$button['action'] = 'deactivate';
/* translators: The extension name. */
$button['button_text'] = sprintf( __( 'Deactivate %s', 'easy-digital-downloads' ), $product_data->title );
}
}
return $button;
}
/**
* Gets the upgrade URL for the button.
*
* @since 2.11.4
* @param ProductData $product_data The product data object.
* @param int $item_id The item/product ID.
* @param bool $has_access Whether the user already has access to the extension (based on pass level).
* @return string
*/
protected function get_upgrade_url( ProductData $product_data, $item_id, $has_access = false ) {
if ( $has_access ) {
$url = 'https://easydigitaldownloads.com/your-account/your-downloads/';
} else {
$url = 'https://easydigitaldownloads.com/lite-upgrade';
}
$utm_parameters = array(
'utm_medium' => $this->settings_section,
'utm_content' => $product_data->slug,
);
return edd_link_helper(
$url,
$utm_parameters
);
}
/**
* Gets the array of parameters for the link to configure the extension.
*
* @since 2.11.4
* @param ProductData $product_data The product data object.
* @return array
*/
protected function get_link_parameters( ProductData $product_data ) {
$configuration = $this->get_configuration( $product_data );
$tab = ! empty( $configuration['tab'] ) ? $configuration['tab'] : $product_data->tab;
$section = ! empty( $configuration['section'] ) ? $configuration['section'] : $product_data->section;
if ( ! empty( $tab ) && ! empty( $section ) && ! empty( $product_data->basename ) && $this->current_user_can() ) {
return array(
/* translators: The extension name. */
'button_text' => sprintf( __( 'Configure %s', 'easy-digital-downloads' ), $product_data->title ),
'href' => edd_get_admin_url(
array(
'page' => 'edd-settings',
'tab' => urlencode( $tab ),
'section' => urlencode( $section ),
)
),
);
}
return array(
/* translators: the plural Downloads label. */
'button_text' => sprintf( __( 'View %s', 'easy-digital-downloads' ), edd_get_label_plural() ),
'href' => add_query_arg(
array(
'post_type' => 'download',
),
admin_url( 'edit.php' )
),
);
}
/**
* Optionally hides the submit button on screens where it's not needed.
*
* @since 2.11.4
* @return void
*/
public function hide_submit_button() {
if ( ! $this->can_show_product_section() ) {
return;
}
?>
<style>p.submit{display:none;}</style>
<?php
}
/**
* Checks the current user's capability level.
*
* @since 3.1.1
* @param string $capability
* @return bool
*/
protected function current_user_can( $capability = 'activate_plugins' ) {
return current_user_can( $capability );
}
}

View File

@ -0,0 +1,258 @@
<?php
/**
* Extensions
*
* Manages automatic installation/activation for all extensions.
*
* @package EDD
* @subpackage Extensions
* @copyright Copyright (c) 2022, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Extensions;
use EDD\Admin\Extensions\Extension;
class ExtensionPage extends Extension {
/**
* The required AA pass level.
*/
const PASS_LEVEL = null;
/**
* Renders the extensions page content.
*
* @return void
*/
public function init() {
?>
<div class="wrap edd-extension-manager__wrap">
<div class="wp-header-end"></div>
<div class="edd-extension-manager__bar">
<div class="edd-extension-manager__bar-description">
<div class="edd-extension-manager__bar-heading">
<h2><?php echo esc_html( $this->get_heading_text() ); ?></h2>
<?php $this->refresh(); ?>
</div>
</div>
<div class="edd-extension-manager__bar-control">
<label for="edd-extension-manager__bar-search" class="screen-reader-text"><?php esc_html_e( 'Search Extensions', 'easy-digital-downloads' ); ?></label>
<input
type="search"
class="regular-text"
id="edd-extension-manager__bar-search"
placeholder="<?php esc_html_e( 'Search Extensions', 'easy-digital-downloads' ); ?>"
>
</div>
</div>
<p>
<?php echo wp_kses_post( $this->get_intro_text() ); ?><br />
<?php $this->show_missing_key_question(); ?>
</p>
<?php $this->do_cards(); ?>
</div>
<?php
}
/**
* @inheritDoc
* @since 3.1.1
* @return array
*/
protected function get_active_parameters( $product_data, $item_id ) {
return $this->get_button_parameters( $product_data, $item_id );
}
/**
* Outputs the cards.
*
* @since 3.1.1
* @return void
*/
protected function do_cards() {
?>
<div class="edd-extension-manager__card-group">
<?php
foreach ( $this->get_product_data() as $item_id => $extension ) {
$this->do_single_extension_card( $item_id );
}
?>
</div>
<?php
}
/**
* Gets the heading text for the extensions page.
*
* @since 3.1.1
* @return string
*/
protected function get_heading_text() {
return __( 'Available Extensions', 'easy-digital-downloads' );
}
/**
* Shows a button to perform a pass "refresh" or check.
*
* @since 3.1.1
* @return void
*/
protected function refresh() {
if ( ! $this->can_show_pass_refresh() ) {
return;
}
?>
<a class="button button-primary" href="<?php echo esc_url( add_query_arg( 'edd_action', 'refresh_pass_status' ) ); ?>"><?php esc_html_e( 'Refresh Extensions', 'easy-digital-downloads' ); ?></a>
<?php
}
/**
* Whether the pass refresh messaging should show.
*
* @since 3.1.1
* @return bool
*/
protected function can_show_pass_refresh() {
if ( get_transient( 'edd_pass_refreshed' ) ) {
return false;
}
return ! empty( get_site_option( 'edd_pro_license_key' ) );
}
/**
* Gets the intro text for the extensions page.
*
* @since 3.1.1
* @return string
*/
protected function get_intro_text() {
if ( ! empty( $this->pass_manager->highest_pass_id ) ) {
/* translators: the active pass name */
return sprintf( __( 'Add functionality to your Easy Digital Downloads powered store with your <strong>%s</strong>.', 'easy-digital-downloads' ), $this->pass_manager->get_pass_name() );
}
return __( 'Add functionality to your Easy Digital Downloads powered store.', 'easy-digital-downloads' );
}
/**
* If a pass hasn't been saved, show the text offering to add it.
*
* @since 3.1.1
* @return void
*/
protected function show_missing_key_question() {
if ( ! empty( $this->pass_manager->highest_pass_id ) && edd_is_pro() ) {
return;
}
$url = edd_get_admin_url(
array(
'page' => 'edd-settings',
)
);
printf(
wp_kses_post(
/* translators: 1. opening anchor tag; 2. closing anchor tag. */
__( 'Missing access to an extension? %1$sAdd your license key now%2$s.', 'easy-digital-downloads' )
),
'<a href="' . esc_url( $url ) . '">',
'</a>'
);
}
/**
* Updates the card configuration.
*
* @since 3.1.1
* @param ProductData $product_data The extension data returned from the Products API.
* @return array
*/
protected function get_configuration( ProductData $product_data ) {
return array(
'style' => 'installer',
'pass_id' => $this->pass_manager->can_access_categories( $product_data->categories ),
);
}
/**
* Update the button parameters.
*
* @since 3.1.1
* @param ProductData $product_data The extension data returned from the Products API.
* @param bool|int $item_id
* @return array
*/
protected function get_button_parameters( ProductData $product_data, $item_id = false ) {
$button = parent::get_button_parameters( $product_data, $item_id );
// Lite always shows "Upgrade Now".
$button['button_text'] = __( 'Upgrade Now', 'easy-digital-downloads' );
$button['button_class'] = 'button-primary edd-promo-notice__trigger';
$button['type'] = 'extension';
return $button;
}
/**
* Overrides the body array sent to the Products API.
* Download category 1592 is "extensions".
*
* @since 3.1.1
* @return array
*/
protected function get_api_body() {
return array( 'category' => 1592 );
}
/**
* Whether the current extension is activated.
* Not used here for now but required since it's an abstract method.
*
* @since 3.1.1
* @return bool
*/
protected function is_activated() {
return false;
}
/**
* Checks the current user's capability level.
*
* @since 3.1.1
* @param string $capability
* @return bool
*/
protected function current_user_can( $capability = 'activate_plugins' ) {
return false;
}
/**
* Gets the upgrade URL for the button.
*
* @since 3.1.1
* @param ProductData $product_data The product data object.
* @param int $item_id The item/product ID.
* @param bool $has_access Whether the user already has access to the extension (based on pass level).
* @return string
*/
protected function get_upgrade_url( ProductData $product_data, $item_id, $has_access = false ) {
if ( $has_access ) {
$url = 'https://easydigitaldownloads.com/your-account/your-downloads/';
} else {
$url = 'https://easydigitaldownloads.com/lite-upgrade';
}
$utm_parameters = array(
'utm_medium' => 'extensions-page',
'utm_content' => $product_data->slug,
);
return edd_link_helper(
$url,
$utm_parameters
);
}
}

View File

@ -0,0 +1,517 @@
<?php
namespace EDD\Admin\Extensions;
use EDD\Admin\Pass_Manager;
use EDD\Admin\Extensions\Card;
use EDD\EventManagement\SubscriberInterface;
class Extension_Manager implements SubscriberInterface {
use \EDD\Admin\Extensions\Traits\Buttons;
/**
* All of the installed plugins on the site.
*
* @since 2.11.4
* @var array
*/
public $all_plugins;
/**
* The minimum pass ID required to install the extension.
*
* @since 3.1.1
*/
private $required_pass_id;
/**
* Pass Manager class
*
* @var Pass_Manager
*/
protected $pass_manager;
public function __construct( $required_pass_id = null ) {
if ( $required_pass_id ) {
$this->required_pass_id = $required_pass_id;
}
$this->pass_manager = new Pass_Manager();
}
/**
* Gets the subscribed events for this class.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'wp_ajax_edd_activate_extension' => 'activate',
'wp_ajax_edd_install_extension' => 'install',
'wp_ajax_edd_deactivate_extension' => 'deactivate',
'admin_enqueue_scripts' => 'register_assets',
'edd_after_ajax_activate_extension' => 'post_extension_activation',
);
}
/**
* Registers the extension manager script and style.
*
* @since 2.11.4
* @return void
*/
public function register_assets() {
if ( wp_script_is( 'edd-extension-manager', 'registered' ) ) {
return;
}
$minify = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_register_style( 'edd-extension-manager', EDD_PLUGIN_URL . 'assets/css/edd-admin-extension-manager.min.css', array(), EDD_VERSION );
wp_register_script( 'edd-extension-manager', EDD_PLUGIN_URL . 'assets/js/edd-admin-extension-manager.js', array( 'jquery' ), EDD_VERSION, true );
wp_localize_script(
'edd-extension-manager',
'EDDExtensionManager',
array(
'activating' => __( 'Activating', 'easy-digital-downloads' ),
'installing' => __( 'Installing', 'easy-digital-downloads' ),
'plugin_install_failed' => __( 'Could not install the plugin. Please download and install it manually via Plugins > Add New.', 'easy-digital-downloads' ),
'extension_install_failed' => sprintf(
/* translators: 1. opening anchor tag, do not translate; 2. closing anchor tag, do not translate */
__( 'Could not install the extension. Please %1$sdownload it from your account%2$s and install it manually.', 'easy-digital-downloads' ),
'<a href="https://easydigitaldownloads.com/your-account/" target="_blank" rel="noopener noreferrer">',
'</a>'
),
'extension_manager_nonce' => wp_create_nonce( 'edd_extensionmanager' ),
'results' => __( 'extensions found', 'easy-digital-downloads' ),
'deactivating' => __( 'Deactivating', 'easy-digital-downloads' ),
'debug' => edd_doing_script_debug(),
)
);
}
/**
* Enqueues the extension manager script/style.
*
* @since 2.11.4
* @return void
*/
public function enqueue() {
wp_enqueue_style( 'edd-extension-manager' );
wp_enqueue_script( 'edd-extension-manager' );
}
/**
* Outputs a standard extension card.
*
* @since 2.11.4
* @param ProductData $product The product data object.
* @param array $inactive_parameters The array of information to build the button for an inactive/not installed plugin.
* @param array $active_parameters The array of information needed to build the link to configure an active plugin.
* @param array $configuration The optional array of data to override the product data retrieved from the API.
* @return void
*/
public function do_extension_card( ProductData $product, $inactive_parameters, $active_parameters, $configuration = array() ) {
$this->enqueue();
$parameters = array(
'inactive_parameters' => $inactive_parameters,
'active_parameters' => $active_parameters,
'required_pass_id' => ! empty( $product->pass_id ) ? $product->pass_id : $this->required_pass_id,
'is_plugin_installed' => false,
'is_plugin_active' => false,
'version' => ! empty( $product->version ) ? $product->version : false,
);
if ( ! empty( $product->basename ) ) {
$parameters['is_plugin_installed'] = $this->is_plugin_installed( $product->basename );
$parameters['is_plugin_active'] = $this->is_plugin_active( $product->basename );
$parameters['version'] = $this->get_plugin_version( $product->basename );
}
$card_class = edd_get_namespace( 'Admin\\Extensions\\Card' );
$card = new $card_class(
$product,
$parameters
);
}
/**
* Installs and maybe activates a plugin or extension.
*
* @since 2.11.4
*/
public function install() {
// Run a security check.
check_ajax_referer( 'edd_extensionmanager', 'nonce', true );
$generic_error = esc_html__( 'There was an error while performing your request.', 'easy-digital-downloads' );
$type = ! empty( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : '';
$required_pass = ! empty( $_POST['pass'] ) ? sanitize_text_field( $_POST['pass'] ) : '';
$result = array(
'message' => $generic_error,
'is_activated' => false,
'type' => $type,
);
if ( ! $type ) {
wp_send_json_error( $result );
}
// Check if new installations are allowed.
if ( ! $this->can_install( $type, $required_pass ) ) {
$result['message'] = __( 'Plugin installation is not available for you on this site.', 'easy-digital-downloads' );
if ( 'extension' === $type ) {
$result['highest_pass'] = $this->pass_manager->highest_pass_id;
}
wp_send_json_error( $result );
}
$result['message'] = 'plugin' === $type
? __( 'Could not install the plugin. Please download and install it manually via Plugins > Add New.', 'easy-digital-downloads' )
: sprintf(
/* translators: 1. opening anchor tag, do not translate; 2. closing anchor tag, do not translate */
__( 'Could not install the extension. Please %1$sdownload it from your account%2$s and install it manually.', 'easy-digital-downloads' ),
'<a href="https://easydigitaldownloads.com/your-account/" target="_blank" rel="noopener noreferrer">',
'</a>'
);
$download_url_classname = \edd_get_namespace( 'Admin\\Extensions\\DownloadURL' );
$download_url_class = new $download_url_classname( $this->pass_manager->highest_license_key );
$plugin = $download_url_class->get_url();
if ( empty( $plugin ) ) {
wp_send_json_error( $result );
}
// Set the current screen to avoid undefined notices.
set_current_screen( 'download_page_edd-settings' );
// Prepare variables.
$url = esc_url_raw(
edd_get_admin_url(
array(
'page' => 'edd-addons',
)
)
);
ob_start();
$creds = request_filesystem_credentials( $url, '', false, false, null );
// Hide the filesystem credentials form.
ob_end_clean();
// Check for file system permissions.
if ( ! $creds ) {
wp_send_json_error( $result );
}
if ( ! WP_Filesystem( $creds ) ) {
wp_send_json_error( $result );
}
/*
* We do not need any extra credentials if we have gotten this far, so let's install the plugin.
*/
// Do not allow WordPress to search/download translations, as this will break JS output.
remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
// Create the plugin upgrader with our custom skin.
$installer = new \EDD\Admin\Installers\PluginSilentUpgrader( new \EDD\Admin\Installers\Install_Skin() );
// Error check.
if ( ! method_exists( $installer, 'install' ) || empty( $plugin ) ) {
wp_send_json_error( $result );
}
$installer->install( $plugin ); // phpcs:ignore
// Flush the cache and return the newly installed plugin basename.
wp_cache_flush();
$plugin_basename = $installer->plugin_info();
// Check for permissions.
if ( ! current_user_can( 'activate_plugins' ) ) {
$result['message'] = 'plugin' === $type ? esc_html__( 'Plugin installed.', 'easy-digital-downloads' ) : esc_html__( 'Extension installed.', 'easy-digital-downloads' );
wp_send_json_error( $result );
}
$this->activate( $plugin_basename );
}
/**
* Activates an existing extension.
*
* @since 2.11.4
* @param string $plugin_basename Optional: the plugin basename.
*/
public function activate( $plugin_basename = '' ) {
$result = array(
'message' => __( 'There was an error while performing your request.', 'easy-digital-downloads' ),
'is_activated' => false,
);
// Check for permissions.
if ( ! check_ajax_referer( 'edd_extensionmanager', 'nonce', false ) || ! current_user_can( 'activate_plugins' ) ) {
$result['message'] = __( 'Plugin activation is not available for you on this site.', 'easy-digital-downloads' );
wp_send_json_error( $result );
}
$already_installed = false;
if ( empty( $plugin_basename ) ) {
$plugin_basename = ! empty( $_POST['plugin'] ) ? sanitize_text_field( $_POST['plugin'] ) : '';
$already_installed = true;
}
$plugin_basename = sanitize_text_field( wp_unslash( $plugin_basename ) );
$type = ! empty( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : '';
if ( 'plugin' !== $type ) {
$type = 'extension';
}
$result = array(
/* translators: "extension" or "plugin" as defined by $type */
'message' => sprintf( __( 'Could not activate the %s.', 'easy-digital-downloads' ), esc_html( $type ) ),
'is_activated' => false,
);
if ( empty( $plugin_basename ) || empty( $type ) ) {
wp_send_json_error( $result );
}
$result['basename'] = $plugin_basename;
// Set the GET variable for multi-plugin activation.
$_GET['activate-multi'] = true;
// Activate the plugin silently.
$activated = activate_plugin( $plugin_basename );
if ( is_wp_error( $activated ) ) {
wp_send_json_error( $result );
}
do_action( 'edd_after_ajax_activate_extension', sanitize_text_field( $plugin_basename ) );
// At this point we have successfully activated.
if ( $already_installed ) {
$message = 'plugin' === $type ? esc_html__( 'Plugin activated.', 'easy-digital-downloads' ) : esc_html__( 'Extension activated.', 'easy-digital-downloads' );
} else {
$message = 'plugin' === $type ? esc_html__( 'Plugin installed & activated.', 'easy-digital-downloads' ) : esc_html__( 'Extension installed & activated.', 'easy-digital-downloads' );
}
$success = array(
'is_activated' => true,
'message' => $message,
);
if ( edd_is_pro() && class_exists( '\\EDD\\Pro\\Admin\\Extensions\\Buttons' ) ) {
$buttons = new \EDD\Pro\Admin\Extensions\Buttons();
$success['status'] = __( 'Activated', 'easy-digital-downloads' );
$success['button'] = $buttons->get_activate_deactivate_button(
array(
'type' => $type,
'id' => filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT ),
'product' => filter_input( INPUT_POST, 'product', FILTER_SANITIZE_NUMBER_INT ),
'plugin' => $plugin_basename,
'action' => 'deactivate',
)
);
}
wp_send_json_success( $success );
}
/**
* Deactivates a plugin.
*
* @since 3.1.1
* @return void
*/
public function deactivate() {
$result = array(
'message' => __( 'There was an error while performing your request.', 'easy-digital-downloads' ),
'is_deactivated' => false,
);
// Check for permissions.
if ( ! check_ajax_referer( 'edd_extensionmanager', 'nonce', false ) || ! current_user_can( 'deactivate_plugins' ) ) {
$result['message'] = __( 'Plugin deactivation is not available for you on this site.', 'easy-digital-downloads' );
wp_send_json_error( $result );
}
$plugin = ! empty( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : false;
if ( empty( $plugin ) ) {
wp_send_json_error( $result );
}
// At this point, we are allowed to deactivate the extension.
$type = ! empty( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ) : '';
if ( 'plugin' !== $type ) {
$type = 'extension';
}
deactivate_plugins( $plugin );
$this->update_wp_activation_data( $plugin );
$success = array(
'message' => 'plugin' === $type ? esc_html__( 'Plugin deactivated.', 'easy-digital-downloads' ) : esc_html__( 'Extension deactivated.', 'easy-digital-downloads' ),
'is_deactivated' => true,
);
if ( edd_is_pro() && class_exists( '\\EDD\\Pro\\Admin\\Extensions\\Buttons' ) ) {
$buttons = new \EDD\Pro\Admin\Extensions\Buttons();
$success['button'] = $buttons->get_activate_deactivate_button(
array(
'type' => $type,
'id' => filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT ),
'product' => filter_input( INPUT_POST, 'product', FILTER_SANITIZE_NUMBER_INT ),
'plugin' => $plugin,
'action' => 'activate',
)
);
}
wp_send_json_success( $success );
}
/**
* Determine if the plugin/extension installations are allowed.
*
* @since 2.11.4
*
* @param string $type Should be `plugin` or `extension`.
*
* @return bool
*/
public function can_install( $type, $required_pass_id = false ) {
if ( ! current_user_can( 'install_plugins' ) || ( is_multisite() && ! is_super_admin() ) ) {
return false;
}
// Determine whether file modifications are allowed.
if ( ! wp_is_file_mod_allowed( 'edd_can_install' ) ) {
return false;
}
// All plugin checks are done.
if ( 'plugin' === $type ) {
return true;
}
return $this->pass_can_download( $required_pass_id );
}
/**
* Checks if a user's pass can download an extension.
*
* @since 2.11.4
* @return bool Returns true if the current site has an active pass and it is greater than or equal to the extension's minimum pass.
*/
public function pass_can_download( $required_pass_id = false ) {
$highest_pass_id = $this->pass_manager->highest_pass_id;
if ( ! $required_pass_id ) {
$required_pass_id = $this->required_pass_id;
}
return ! empty( $highest_pass_id ) && ! empty( $required_pass_id ) && $this->pass_manager->pass_compare( $highest_pass_id, $required_pass_id, '>=' );
}
/**
* Get all installed plugins.
*
* @since 2.11.4
* @return array
*/
public function get_plugins() {
if ( $this->all_plugins ) {
return $this->all_plugins;
}
$this->all_plugins = get_plugins();
return $this->all_plugins;
}
/**
* Check if a plugin is installed.
*
* @since 2.11.4
* @param string $plugin The path to the main plugin file, eg 'my-plugin/my-plugin.php'.
* @return boolean
*/
public function is_plugin_installed( $plugin ) {
return array_key_exists( $plugin, $this->get_plugins() );
}
/**
* Whether a given plugin is active or not.
*
* @since 2.11.4
* @param string|ProductData $basename_or_data The path to the main plugin file, eg 'my-plugin/my-plugin.php', or the product data object.
* @return boolean
*/
public function is_plugin_active( $basename_or_data ) {
$basename = ! empty( $basename_or_data->basename ) ? $basename_or_data->basename : $basename_or_data;
return ! empty( $basename ) && is_plugin_active( $basename );
}
/**
* Gets the plugin version.
*
* @since 3.1.1
* @param string $basename
* @return false|string
*/
protected function get_plugin_version( $basename ) {
if ( empty( $basename ) ) {
return false;
}
$plugins = $this->get_plugins();
return array_key_exists( $basename, $plugins )
? $plugins[ $basename ]['Version']
: false;
}
/**
* When a plugin is deactivated, update the related WP options.
*
* @since 3.1.1
* @param string $plugin
* @return void
*/
private function update_wp_activation_data( $plugin ) {
$deactivated = array(
$plugin => time(),
);
if ( ! is_network_admin() ) {
update_option( 'recently_activated', $deactivated + (array) get_option( 'recently_activated' ) );
} else {
update_site_option( 'recently_activated', $deactivated + (array) get_site_option( 'recently_activated' ) );
}
}
/**
* When the extension manager activates a plugin, possibly modify behavior.
*
* @since 3.1.0.4
*
* @param string $plugin_basename The plugin basename being activated via the extension manager.
*/
public function post_extension_activation( $plugin_basename ) {
if ( empty( $plugin_basename ) ) {
return;
}
switch ( $plugin_basename ) {
case 'wp-mail-smtp/wp_mail_smtp.php':
update_option( 'wp_mail_smtp_activation_prevent_redirect', true );
break;
case 'all-in-one-seo-pack/all_in_one_seo_pack.php':
update_option( 'aioseo_activation_redirect', true );
break;
case 'google-analytics-for-wordpress/googleanalytics.php':
delete_transient( '_monsterinsights_activation_redirect' );
break;
}
}
}

View File

@ -0,0 +1,286 @@
<?php
namespace EDD\Admin\Extensions;
use EDD\Admin\Pass_Manager;
class ExtensionsAPI {
/**
* Gets the product data from the EDD Products API.
*
* @since 2.11.4
* @param array $body The body for the API request.
* @param int $item_id The product ID, if querying a single product.
* @return false|array|ProductData
*/
public function get_product_data( $body = array(), $item_id = false ) {
if ( empty( $body ) ) {
if ( empty( $item_id ) ) {
return false;
}
$body = $this->get_api_body( $item_id );
}
$key = $this->array_key_first( $body );
// The option name is created from the first key/value pair of the API "body".
$option_name = sanitize_key( "edd_extension_{$key}_{$body[ $key ]}_data" );
$option = get_site_option( $option_name );
$is_stale = $this->option_has_expired( $option );
// The ProductData class.
$product_data = new ProductData();
// If the data is "fresh" and what we want exists, return it.
if ( $option && ! $is_stale ) {
if ( $item_id && ! empty( $option[ $item_id ] ) ) {
return $product_data->fromArray( $option[ $item_id ] );
} elseif ( ! empty( $option['timeout'] ) ) {
unset( $option['timeout'] );
return $option;
}
}
// Get all of the product data.
$all_product_data = $this->get_all_product_data();
// If no product data was retrieved, let the option sit for an hour.
if ( empty( $all_product_data ) ) {
$data = array(
'timeout' => strtotime( '+1 hour', time() ),
);
if ( $option && $is_stale ) {
$data = array_merge( $option, $data );
}
update_site_option( $option_name, $data );
if ( $item_id && ! empty( $option[ $item_id ] ) ) {
return $product_data->fromArray( $option[ $item_id ] );
}
unset( $option['timeout'] );
return $option;
}
$value = array(
'timeout' => strtotime( '+1 week', time() ),
);
if ( $item_id && ! empty( $all_product_data->$item_id ) ) {
$item = $all_product_data->$item_id;
$value[ $item_id ] = $this->get_item_data( $item );
} elseif ( in_array( $key, array( 'category', 'tag' ), true ) ) {
$term_id = $body[ $key ];
if ( 1592 === $body[ $key ] ) {
$value = $value + $this->get_pass_extensions_data( $all_product_data );
} else {
foreach ( $all_product_data as $item_id => $item ) {
if ( 'category' === $key && ( empty( $item->categories ) || ! in_array( $term_id, $item->categories, true ) ) ) {
continue;
} elseif ( 'tag' === $key && ( empty( $item->tags ) || ! in_array( $term_id, $item->tags, true ) ) ) {
continue;
}
$value[ $item_id ] = $this->get_item_data( $item );
}
}
}
if ( is_multisite() && get_option( $option_name ) ) {
delete_option( $option_name );
}
update_site_option( $option_name, $value );
unset( $value['timeout'] );
return $item_id && ! empty( $value[ $item_id ] ) ? $product_data->fromArray( $value[ $item_id ] ) : $value;
}
/**
* Gets the extensions data for displaying on the extensions page.
* The extensions are grouped by pass.
*
* @since 3.1.1
* @param object $all_product_data
* @return array
*/
private function get_pass_extensions_data( $all_product_data ) {
$personal_pass = array();
$extended_pass = array();
$pro_pass = array();
$all_access_pass = array();
$pass_manager = new Pass_Manager();
foreach ( $all_product_data as $item_id => $item ) {
if ( ! empty( $item->categories ) ) {
if ( ! in_array( 1592, $item->categories, true ) ) {
continue;
}
if ( in_array( $pass_manager->categories[ $pass_manager::PERSONAL_PASS_ID ], $item->categories, true ) ) {
$personal_pass[ $item_id ] = $this->get_item_data( $item );
} elseif ( in_array( $pass_manager->categories[ $pass_manager::EXTENDED_PASS_ID ], $item->categories, true ) ) {
$extended_pass[ $item_id ] = $this->get_item_data( $item );
} elseif ( in_array( $pass_manager->categories[ $pass_manager::PROFESSIONAL_PASS_ID ], $item->categories, true ) ) {
$pro_pass[ $item_id ] = $this->get_item_data( $item );
} else {
$all_access_pass[ $item_id ] = $this->get_item_data( $item );
}
}
}
return $personal_pass + $extended_pass + $pro_pass + $all_access_pass;
}
/**
* Gets all of the product data, either from an option or an API request.
* If the option exists and has data, it will be an object.
*
* @since 2.11.4
* @return object|false
*/
private function get_all_product_data() {
// Possibly all product data is in an option. If it is, return it.
$all_product_data = get_site_option( 'edd_all_extension_data' );
if ( $all_product_data && ! $this->option_has_expired( $all_product_data ) ) {
return ! empty( $all_product_data['products'] ) ? $all_product_data['products'] : false;
}
// Otherwise, query the API.
$url = add_query_arg(
array(
'edd_action' => 'extension_data',
),
$this->get_products_url()
);
$request = wp_remote_get(
esc_url_raw( $url ),
array(
'timeout' => 15,
'sslverify' => true,
)
);
// If there was an API error, set option and return false.
if ( is_wp_error( $request ) || ( 200 !== wp_remote_retrieve_response_code( $request ) ) ) {
update_site_option(
'edd_all_extension_data',
array(
'timeout' => strtotime( '+1 hour', time() ),
)
);
return false;
}
// Fresh data has been retrieved, so update the option with a four hour timeout.
$all_product_data = json_decode( wp_remote_retrieve_body( $request ) );
$data = array(
'timeout' => strtotime( '+4 hours', time() ),
'products' => $all_product_data,
);
if ( is_multisite() && get_option( 'edd_all_extension_data' ) ) {
delete_option( 'edd_all_extension_data' );
}
update_site_option( 'edd_all_extension_data', $data );
return $all_product_data;
}
/**
* Gets the product data as needed for the extension manager.
*
* @since 2.11.4
* @param object $item
* @return array
*/
private function get_item_data( $item ) {
return array(
'title' => ! empty( $item->title ) ? $item->title : '',
'slug' => ! empty( $item->slug ) ? $item->slug : '',
'image' => ! empty( $item->image ) ? $item->image : '',
'description' => ! empty( $item->excerpt ) ? $item->excerpt : '',
'basename' => ! empty( $item->custom_meta->basename ) ? $item->custom_meta->basename : '',
'tab' => ! empty( $item->custom_meta->settings_tab ) ? $item->custom_meta->settings_tab : '',
'section' => ! empty( $item->custom_meta->settings_section ) ? $item->custom_meta->settings_section : '',
'icon' => $this->get_icon( $item ),
'categories' => ! empty( $item->categories ) ? $item->categories : array(),
'terms' => ! empty( $item->terms ) ? $item->terms : array(),
'version' => ! empty( $item->version ) ? $item->version : false,
);
}
/**
* Gets the product icon.
*
* @since 3.1.1
* @param object $item
* @return string
*/
private function get_icon( $item ) {
$icon = ! empty( $item->custom_meta->icon ) ? $item->custom_meta->icon : '';
if ( $icon ) {
return $icon;
}
$icon_size = '2x';
return ! empty( $item->icons->{$icon_size} ) ? $item->icons->{$icon_size} : $icon;
}
/**
* Gets the base url for the products remote request.
*
* @since 2.11.4
* @return string
*/
private function get_products_url() {
if ( defined( 'EDD_PRODUCTS_URL' ) ) {
return EDD_PRODUCTS_URL;
}
return 'https://easydigitaldownloads.com/';
}
/**
* Gets the default array for the body of the API request.
* A class may override this by setting an array to query a tag or category.
* Note that the first array key/value pair are used to create the option name.
*
* @since 2.11.4
* @param int $item_id The product ID.
* @return array
*/
private function get_api_body( $item_id ) {
return array(
'product' => $item_id,
);
}
/**
* Gets the first key of an array.
* (Shims array_key_first for PHP < 7.3)
*
* @since 2.11.4
* @param array $array
* @return string|null
*/
private function array_key_first( array $array ) {
if ( function_exists( 'array_key_first' ) ) {
return array_key_first( $array );
}
foreach ( $array as $key => $unused ) {
return $key;
}
return null;
}
/**
* Checks whether a given option has "expired".
*
* @since 2.11.4
* @param array|false $option
* @return bool
*/
private function option_has_expired( $option ) {
return empty( $option['timeout'] ) || time() > $option['timeout'];
}
}

View File

@ -0,0 +1,83 @@
<?php
/**
* Manages legacy extensions which have been merged into EDD.
*
* @since 3.1.1
*/
namespace EDD\Admin\Extensions;
class Legacy implements \EDD\EventManagement\SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'admin_init' => 'manage_legacy_extensions',
);
}
/**
* Deactivates the legacy extension.
*
* @since 3.1.1
* @return void
*/
public function manage_legacy_extensions() {
foreach ( $this->get_extensions() as $extension ) {
add_action( "plugin_action_links_{$extension['basename']}", array( $this, 'update_plugin_links' ), 10, 2 );
if ( ! is_plugin_active( $extension['basename'] ) ) {
continue;
}
if ( $this->should_deactivate( $extension['basename'] ) ) {
deactivate_plugins( $extension['basename'] );
}
if ( ! empty( $extension['option'] ) ) {
delete_option( $extension['option'] );
}
}
}
/**
* Removes the activation link from the plugins table.
*
* @since 3.1.1
* @param array $links
* @param string $plugin_file
* @return array
*/
public function update_plugin_links( $links, $plugin_file ) {
$links['activate'] = __( 'Inactive &mdash; part of EDD', 'easy-digital-downloads' );
return $links;
}
/**
* Gets the array of extensions which have been merged into EDD.
*
* @sice 3.1.1
* @return array
*/
protected function get_extensions() {
return array(
'edd-manual-purchases' => array(
'basename' => 'edd-manual-purchases/edd-manual-purchases.php',
'option' => 'edd_manual_purchases_license_active',
),
);
}
/**
* Whether the plugin should be deactivated.
*
* @since 3.1.1
* @param string $basename
* @return bool
*/
protected function should_deactivate( $basename ) {
return true;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace EDD\Admin\Extensions;
use EDD\EventManagement\SubscriberInterface;
class Menu implements SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
$events = array();
if ( current_user_can( 'manage_shop_settings' ) ) {
$events['admin_menu'] = array( 'menu', 99999 );
}
return $events;
}
/**
* Create the Extensions submenu page under the "Downloads" menu
*
* @since 3.1.1
* @global $edd_add_ons_page
*/
public function menu() {
global $edd_add_ons_page;
$extensions_class = edd_get_namespace( 'Admin\\Extensions\\ExtensionPage' );
$extensions = new $extensions_class();
$edd_add_ons_page = add_submenu_page(
'edit.php?post_type=download',
__( 'EDD Extensions', 'easy-digital-downloads' ),
__( 'Extensions', 'easy-digital-downloads' ),
'manage_shop_settings',
'edd-addons',
array( $extensions, 'init' )
);
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace EDD\Admin\Extensions;
class ProductData {
/**
* The product name.
*
* @since 2.11.4
* @var string
*/
public $title;
/**
* The product slug.
*
* @since 2.11.4
* @var string
*/
public $slug = '';
/**
* The URL for the product featured image.
*
* @since 2.11.4
* @var string
*/
public $image;
/**
* The product description.
*
* @since 2.11.4
* @var string
*/
public $description;
/**
* The extension basename.
*
* @since 2.11.4
* @var string
*/
public $basename;
/**
* The settings tab where the extension settings will show.
*
* @since 2.11.4
* @var string
*/
public $tab;
/**
* The settings section for the extension.
*
* @since 2.11.4
* @var string
*/
public $section;
/**
* The product features.
*
* @since 2.11.4
* @var array
*/
public $features = array();
/**
* Take array and return object.
*
* @since 2.11.4
* @param array $array
* @return ProductData
* @throws \InvalidArgumentException
*/
public function fromArray( $array ) {
$expected_keys = array( 'title', 'slug', 'description', 'basename' );
$array_to_check = array_intersect_key( $array, array_flip( $expected_keys ) );
if ( empty( $array_to_check ) ) {
throw new \InvalidArgumentException(
'Invalid ProductData object, must have the exact following keys: ' . implode( ', ', $expected_keys )
);
}
$product_data = new self();
foreach ( $array as $key => $value ) {
$product_data->$key = $value;
}
return $product_data;
}
/**
* Merge an array of data into an object.
*
* @since 2.11.4
* @param array $configuration The custom configuration data.
* @return ProductData
*/
public function mergeConfig( array $configuration ) {
foreach ( $configuration as $key => $value ) {
$this->{$key} = $value;
}
return $this;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Buttons for extension cards.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\Admin\Extensions\Traits;
trait Buttons {
/**
* Outputs the button to activate/install a plugin/extension.
* If a link is passed in the args, create a button style link instead (@uses $this->link()).
*
* @since 2.11.4
* @param array $args The array of parameters for the button.
* @return void
*/
public function button( $args ) {
if ( ! empty( $args['href'] ) ) {
$this->link( $args );
return;
}
$defaults = array(
'button_class' => 'button-primary',
'plugin' => '',
'action' => '',
'button_text' => '',
'type' => 'plugin',
'id' => '',
'product' => '',
'pass' => $this->required_pass_id,
);
$args = wp_parse_args( $args, $defaults );
if ( empty( $args['button_text'] ) ) {
return;
}
?>
<button
class="button <?php echo esc_attr( $args['button_class'] ); ?> edd-extension-manager__action"
<?php
foreach ( $args as $key => $attribute ) {
if ( empty( $attribute ) || in_array( $key, array( 'button_class', 'button_text' ), true ) ) {
continue;
}
printf(
' data-%s="%s"',
esc_attr( $key ),
esc_attr( $attribute )
);
}
?>
>
<?php echo esc_html( $args['button_text'] ); ?>
</button>
<?php
}
/**
* Outputs the link, if it should be a link.
*
* @param array $args
* @return void
*/
public function link( $args ) {
$defaults = array(
'button_class' => 'button-primary',
'button_text' => '',
);
$args = wp_parse_args( $args, $defaults );
if ( empty( $args['button_text'] ) || empty( $args['href'] ) ) {
return;
}
?>
<a
class="button <?php echo esc_attr( $args['button_class'] ); ?>"
href="<?php echo esc_url( $args['href'] ); ?>"
<?php echo ! empty( $args['new_tab'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>
>
<?php echo esc_html( $args['button_text'] ); ?>
</a>
<?php
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace EDD\Admin\Installers;
/**
* Skin for on-the-fly addon installations.
*
* @since 1.0.0
* @since 1.5.6.1 Extend PluginSilentUpgraderSkin and clean up the class.
*/
class Install_Skin extends PluginSilentUpgraderSkin {
/**
* Instead of outputting HTML for errors, json_encode the errors and send them
* back to the Ajax script for processing.
*
* @since 1.0.0
*
* @param array $errors Array of errors with the install process.
*/
public function error( $errors ) {
if ( ! empty( $errors ) ) {
wp_send_json_error( $errors );
}
}
}

View File

@ -0,0 +1,608 @@
<?php
namespace EDD\Admin\Installers;
use WP_Error;
use WP_Upgrader;
use WP_Filesystem_Base;
/** \WP_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
/** \Plugin_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
/**
* In WP 5.3 a PHP 5.6 splat operator (...$args) was added to \WP_Upgrader_Skin::feedback().
* We need to remove all calls to *Skin::feedback() method, as we can't override it in own Skins
* without breaking support for PHP 5.3-5.5.
*
* @internal Please do not use this class outside of core EDD development. May be removed at any time.
*
* @since 1.5.6.1
*/
class PluginSilentUpgrader extends \Plugin_Upgrader {
/**
* Run an upgrade/installation.
*
* Attempt to download the package (if it is not a local file), unpack it, and
* install it in the destination folder.
*
* @since 1.5.6.1
*
* @param array $options {
* Array or string of arguments for upgrading/installing a package.
*
* @type string $package The full path or URI of the package to install.
* Default empty.
* @type string $destination The full path to the destination folder.
* Default empty.
* @type bool $clear_destination Whether to delete any files already in the
* destination folder. Default false.
* @type bool $clear_working Whether to delete the files form the working
* directory after copying to the destination.
* Default false.
* @type bool $abort_if_destination_exists Whether to abort the installation if the destination
* folder already exists. When true, `$clear_destination`
* should be false. Default true.
* @type bool $is_multi Whether this run is one of multiple upgrade/installation
* actions being performed in bulk. When true, the skin
* WP_Upgrader::header() and WP_Upgrader::footer()
* aren't called. Default false.
* @type array $hook_extra Extra arguments to pass to the filter hooks called by
* WP_Upgrader::run().
* }
* @return array|false|WP_error The result from self::install_package() on success, otherwise a WP_Error,
* or false if unable to connect to the filesystem.
*/
public function run( $options ) {
$defaults = array(
'package' => '', // Please always pass this.
'destination' => '', // And this
'clear_destination' => false,
'abort_if_destination_exists' => true, // Abort if the Destination directory exists, Pass clear_destination as false please
'clear_working' => true,
'is_multi' => false,
'hook_extra' => array(), // Pass any extra $hook_extra args here, this will be passed to any hooked filters.
);
$options = wp_parse_args( $options, $defaults );
/**
* Filter the package options before running an update.
*
* See also {@see 'upgrader_process_complete'}.
*
* @since 4.3.0
*
* @param array $options {
* Options used by the upgrader.
*
* @type string $package Package for update.
* @type string $destination Update location.
* @type bool $clear_destination Clear the destination resource.
* @type bool $clear_working Clear the working resource.
* @type bool $abort_if_destination_exists Abort if the Destination directory exists.
* @type bool $is_multi Whether the upgrader is running multiple times.
* @type array $hook_extra {
* Extra hook arguments.
*
* @type string $action Type of action. Default 'update'.
* @type string $type Type of update process. Accepts 'plugin', 'theme', or 'core'.
* @type bool $bulk Whether the update process is a bulk update. Default true.
* @type string $plugin Path to the plugin file relative to the plugins directory.
* @type string $theme The stylesheet or template name of the theme.
* @type string $language_update_type The language pack update type. Accepts 'plugin', 'theme',
* or 'core'.
* @type object $language_update The language pack update offer.
* }
* }
*/
$options = apply_filters( 'upgrader_package_options', $options );
if ( ! $options['is_multi'] ) { // call $this->header separately if running multiple times
$this->skin->header();
}
// Connect to the Filesystem first.
$res = $this->fs_connect( array( WP_CONTENT_DIR, $options['destination'] ) );
// Mainly for non-connected filesystem.
if ( ! $res ) {
if ( ! $options['is_multi'] ) {
$this->skin->footer();
}
return false;
}
$this->skin->before();
if ( is_wp_error( $res ) ) {
$this->skin->error( $res );
$this->skin->after();
if ( ! $options['is_multi'] ) {
$this->skin->footer();
}
return $res;
}
/*
* Download the package (Note, This just returns the filename
* of the file if the package is a local file)
*/
$download = $this->download_package( $options['package'], true );
// Allow for signature soft-fail.
// WARNING: This may be removed in the future.
if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
// Don't output the 'no signature could be found' failure message for now.
if ( (string) $download->get_error_code() !== 'signature_verification_no_signature' || WP_DEBUG ) {
// Outout the failure error as a normal feedback, and not as an error:
//$this->skin->feedback( $download->get_error_message() );
// Report this failure back to WordPress.org for debugging purposes.
wp_version_check(
array(
'signature_failure_code' => $download->get_error_code(),
'signature_failure_data' => $download->get_error_data(),
)
);
}
// Pretend this error didn't happen.
$download = $download->get_error_data( 'softfail-filename' );
}
if ( is_wp_error( $download ) ) {
$this->skin->error( $download );
$this->skin->after();
if ( ! $options['is_multi'] ) {
$this->skin->footer();
}
return $download;
}
$delete_package = ( (string) $download !== (string) $options['package'] ); // Do not delete a "local" file.
// Unzips the file into a temporary directory.
$working_dir = $this->unpack_package( $download, $delete_package );
if ( is_wp_error( $working_dir ) ) {
$this->skin->error( $working_dir );
$this->skin->after();
if ( ! $options['is_multi'] ) {
$this->skin->footer();
}
return $working_dir;
}
// With the given options, this installs it to the destination directory.
$result = $this->install_package(
array(
'source' => $working_dir,
'destination' => $options['destination'],
'clear_destination' => $options['clear_destination'],
'abort_if_destination_exists' => $options['abort_if_destination_exists'],
'clear_working' => $options['clear_working'],
'hook_extra' => $options['hook_extra'],
)
);
$this->skin->set_result( $result );
if ( is_wp_error( $result ) ) {
$this->skin->error( $result );
//$this->skin->feedback( 'process_failed' );
} else {
// Installation succeeded.
//$this->skin->feedback( 'process_success' );
}
$this->skin->after();
if ( ! $options['is_multi'] ) {
/**
* Fire when the upgrader process is complete.
*
* See also {@see 'upgrader_package_options'}.
*
* @since 3.6.0
* @since 3.7.0 Added to WP_Upgrader::run().
* @since 4.6.0 `$translations` was added as a possible argument to `$hook_extra`.
*
* @param WP_Upgrader $this WP_Upgrader instance. In other contexts, $this, might be a
* Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
* @param array $hook_extra {
* Array of bulk item update data.
*
* @type string $action Type of action. Default 'update'.
* @type string $type Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
* @type bool $bulk Whether the update process is a bulk update. Default true.
* @type array $plugins Array of the basename paths of the plugins' main files.
* @type array $themes The theme slugs.
* @type array $translations {
* Array of translations update data.
*
* @type string $language The locale the translation is for.
* @type string $type Type of translation. Accepts 'plugin', 'theme', or 'core'.
* @type string $slug Text domain the translation is for. The slug of a theme/plugin or
* 'default' for core translations.
* @type string $version The version of a theme, plugin, or core.
* }
* }
*/
do_action( 'upgrader_process_complete', $this, $options['hook_extra'] );
$this->skin->footer();
}
return $result;
}
/**
* Toggle maintenance mode for the site.
*
* Create/delete the maintenance file to enable/disable maintenance mode.
*
* @since 2.8.0
*
* @global WP_Filesystem_Base $wp_filesystem Subclass
*
* @param bool $enable True to enable maintenance mode, false to disable.
*/
public function maintenance_mode( $enable = false ) {
global $wp_filesystem;
$file = $wp_filesystem->abspath() . '.maintenance';
if ( $enable ) {
//$this->skin->feedback( 'maintenance_start' );
// Create maintenance file to signal that we are upgrading
$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
$wp_filesystem->delete( $file );
$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
//$this->skin->feedback( 'maintenance_end' );
$wp_filesystem->delete( $file );
}
}
/**
* Download a package.
*
* @since 2.8.0
* @since 5.5.0 Added the `$hook_extra` parameter.
*
* @param string $package The URI of the package. If this is the full path to an
* existing local file, it will be returned untouched.
* @param bool $check_signatures Whether to validate file signatures. Default false.
* @param array $hook_extra Extra arguments to pass to the filter hooks. Default empty array.
* @return string|WP_Error The full path to the downloaded package file, or a WP_Error object.
*/
public function download_package( $package, $check_signatures = false, $hook_extra = array() ) {
/**
* Filters whether to return the package.
*
* @since 3.7.0
* @since 5.5.0 Added the `$hook_extra` parameter.
*
* @param bool $reply Whether to bail without returning the package.
* Default false.
* @param string $package The package file name.
* @param WP_Upgrader $this The WP_Upgrader instance.
* @param array $hook_extra Extra arguments passed to hooked filters.
*/
$reply = apply_filters( 'upgrader_pre_download', false, $package, $this, $hook_extra );
if ( false !== $reply ) {
return $reply;
}
if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { // Local file or remote?
return $package; // Must be a local file.
}
if ( empty( $package ) ) {
return new WP_Error( 'no_package', $this->strings['no_package'] );
}
//$this->skin->feedback( 'downloading_package', $package );
$download_file = download_url( $package, 300, $check_signatures );
if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
}
return $download_file;
}
/**
* Unpack a compressed package file.
*
* @since 2.8.0
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @param string $package Full path to the package file.
* @param bool $delete_package Optional. Whether to delete the package file after attempting
* to unpack it. Default true.
* @return string|WP_Error The path to the unpacked contents, or a WP_Error on failure.
*/
public function unpack_package( $package, $delete_package = true ) {
global $wp_filesystem;
//$this->skin->feedback( 'unpack_package' );
$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
//Clean up contents of upgrade directory beforehand.
$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
if ( ! empty( $upgrade_files ) ) {
foreach ( $upgrade_files as $file ) {
$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
}
}
// We need a working directory - Strip off any .tmp or .zip suffixes
$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );
// Clean up working directory
if ( $wp_filesystem->is_dir( $working_dir ) ) {
$wp_filesystem->delete( $working_dir, true );
}
// Unzip package to working directory
$result = unzip_file( $package, $working_dir );
// Once extracted, delete the package if required.
if ( $delete_package ) {
unlink( $package );
}
if ( is_wp_error( $result ) ) {
$wp_filesystem->delete( $working_dir, true );
if ( $result->get_error_code() === 'incompatible_archive' ) {
return new WP_Error( 'incompatible_archive', $this->strings['incompatible_archive'], $result->get_error_data() );
}
return $result;
}
return $working_dir;
}
/**
* Install a package.
*
* Copies the contents of a package form a source directory, and installs them in
* a destination directory. Optionally removes the source. It can also optionally
* clear out the destination folder if it already exists.
*
* @since 2.8.0
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @global array $wp_theme_directories
*
* @param array|string $args {
* Optional. Array or string of arguments for installing a package. Default empty array.
*
* @type string $source Required path to the package source. Default empty.
* @type string $destination Required path to a folder to install the package in.
* Default empty.
* @type bool $clear_destination Whether to delete any files already in the destination
* folder. Default false.
* @type bool $clear_working Whether to delete the files form the working directory
* after copying to the destination. Default false.
* @type bool $abort_if_destination_exists Whether to abort the installation if
* the destination folder already exists. Default true.
* @type array $hook_extra Extra arguments to pass to the filter hooks called by
* WP_Upgrader::install_package(). Default empty array.
* }
*
* @return array|WP_Error The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.
*/
public function install_package( $args = array() ) {
global $wp_filesystem, $wp_theme_directories;
$defaults = array(
'source' => '', // Please always pass this
'destination' => '', // and this
'clear_destination' => false,
'clear_working' => false,
'abort_if_destination_exists' => true,
'hook_extra' => array(),
);
$args = wp_parse_args( $args, $defaults );
// These were previously extract()'d.
$source = $args['source'];
$destination = $args['destination'];
$clear_destination = $args['clear_destination'];
// @todo does this need to be replaced?
// wpforms_set_time_limit( 300 );
if ( empty( $source ) || empty( $destination ) ) {
return new WP_Error( 'bad_request', $this->strings['bad_request'] );
}
//$this->skin->feedback( 'installing_package' );
/**
* Filter the install response before the installation has started.
*
* Returning a truthy value, or one that could be evaluated as a WP_Error
* will effectively short-circuit the installation, returning that value
* instead.
*
* @since 2.8.0
*
* @param bool|WP_Error $response Response.
* @param array $hook_extra Extra arguments passed to hooked filters.
*/
$res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] );
if ( is_wp_error( $res ) ) {
return $res;
}
// Retain the Original source and destinations.
$remote_source = $args['source'];
$local_destination = $destination;
$source_files = array_keys( $wp_filesystem->dirlist( $remote_source ) );
$remote_destination = $wp_filesystem->find_folder( $local_destination );
$count_source_files = count( $source_files );
// Locate which directory to copy to the new folder, This is based on the actual folder holding the files.
if ( $count_source_files === 1 && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) { // Only one folder? Then we want its contents.
$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
} elseif ( $count_source_files === 0 ) {
return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] ); // There are no files?
} else { // It's only a single file, the upgrader will use the folder name of this file as the destination folder. Folder name is based on zip filename.
$source = trailingslashit( $args['source'] );
}
/**
* Filter the source file location for the upgrade package.
*
* @since 2.8.0
* @since 4.4.0 The $hook_extra parameter became available.
*
* @param string $source File source location.
* @param string $remote_source Remote file source location.
* @param WP_Upgrader $this WP_Upgrader instance.
* @param array $hook_extra Extra arguments passed to hooked filters.
*/
$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );
if ( is_wp_error( $source ) ) {
return $source;
}
// Has the source location changed? If so, we need a new source_files list.
if ( $source !== $remote_source ) {
$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
}
/*
* Protection against deleting files in any important base directories.
* Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the
* destination directory (WP_PLUGIN_DIR / wp-content/themes) intending
* to copy the directory into the directory, whilst they pass the source
* as the actual files to copy.
*/
$protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' );
if ( is_array( $wp_theme_directories ) ) {
$protected_directories = array_merge( $protected_directories, $wp_theme_directories );
}
if ( in_array( $destination, $protected_directories ) ) {
$remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) );
$destination = trailingslashit( $destination ) . trailingslashit( basename( $source ) );
}
if ( $clear_destination ) {
// We're going to clear the destination if there's something there.
$removed = $this->clear_destination( $remote_destination );
/**
* Filter whether the upgrader cleared the destination.
*
* @since 2.8.0
*
* @param mixed $removed Whether the destination was cleared. true on success, WP_Error on failure
* @param string $local_destination The local package destination.
* @param string $remote_destination The remote package destination.
* @param array $hook_extra Extra arguments passed to hooked filters.
*/
$removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] );
if ( is_wp_error( $removed ) ) {
return $removed;
}
} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
// If we're not clearing the destination folder and something exists there already, Bail.
// But first check to see if there are actually any files in the folder.
$_files = $wp_filesystem->dirlist( $remote_destination );
if ( ! empty( $_files ) ) {
$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
}
}
// Create destination if needed.
if ( ! $wp_filesystem->exists( $remote_destination ) ) {
if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
}
}
// Copy new version of item into place.
$result = copy_dir( $source, $remote_destination );
if ( is_wp_error( $result ) ) {
if ( $args['clear_working'] ) {
$wp_filesystem->delete( $remote_source, true );
}
return $result;
}
// Clear the Working folder?
if ( $args['clear_working'] ) {
$wp_filesystem->delete( $remote_source, true );
}
$destination_name = basename( str_replace( $local_destination, '', $destination ) );
if ( $destination_name === '.' ) {
$destination_name = '';
}
$this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' );
/**
* Filter the installation response after the installation has finished.
*
* @since 2.8.0
*
* @param bool $response Installation response.
* @param array $hook_extra Extra arguments passed to hooked filters.
* @param array $result Installation result data.
*/
$res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result );
if ( is_wp_error( $res ) ) {
$this->result = $res;
return $res;
}
// Bombard the calling function will all the info which we've just used.
return $this->result;
}
/**
* Install a plugin package.
*
* @since 1.6.3
*
* @param string $package The full local path or URI of the package.
* @param array $args Optional. Other arguments for installing a plugin package. Default empty array.
*
* @return bool|\WP_Error True if the installation was successful, false or a WP_Error otherwise.
*/
public function install( $package, $args = array() ) {
$result = parent::install( $package, $args );
if ( true === $result ) {
do_action( 'edd_plugin_installed', $package );
}
return $result;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace EDD\Admin\Installers;
/** \WP_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
/**
* Class PluginSilentUpgraderSkin.
*
* @internal Please do not use this class outside of core WPForms development. May be removed at any time.
*
* @since 2.11.4
*/
class PluginSilentUpgraderSkin extends \WP_Upgrader_Skin {
/**
* Empty out the header of its HTML content and only check to see if it has
* been performed or not.
*
* @since 2.11.4
*/
public function header() {
}
/**
* Empty out the footer of its HTML contents.
*
* @since 2.11.4
*/
public function footer() {
}
/**
* Instead of outputting HTML for errors, just return them.
* Ajax request will just ignore it.
*
* @since 2.11.4
*
* @param array $errors Array of errors with the install process.
*
* @return array
*/
public function error( $errors ) {
return $errors;
}
/**
* Empty out JavaScript output that calls function to decrement the update counts.
*
* @since 2.11.4
*
* @param string $type Type of update count to decrement.
*/
public function decrement_update_count( $type ) {
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Helper class to add a custom class to a downlaod menu item.
*
* @since 3.1.1
*/
namespace EDD\Admin\Menu;
class LinkClass {
/**
* Adds a custom CSS class to a download menu item based on matching a target.
*
* @since 3.1.1
* @param string $target
* @param string $class
*/
public function __construct( $target, $class ) {
global $submenu;
if ( empty( $submenu['edit.php?post_type=download'] ) ) {
return;
}
$target_position = $this->get_target_position( $submenu, $target );
if ( is_null( $target_position ) ) {
return;
}
// Prepare an HTML class.
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
if ( isset( $submenu['edit.php?post_type=download'][ $target_position ][4] ) ) {
$submenu['edit.php?post_type=download'][ $target_position ][4] .= " {$class}";
} else {
$submenu['edit.php?post_type=download'][ $target_position ][] = $class;
}
}
/**
* Gets the target position/key in the submenu.
*
* @since 3.1.1
* @param array $submenu
* @param string $target
* @return null|int
*/
private function get_target_position( $submenu, $target ) {
return key(
array_filter(
$submenu['edit.php?post_type=download'],
static function( $item ) use ( $target ) {
if ( $target === $item[2] ) {
return true;
};
return false !== strpos( $item[2], $target );
}
)
);
}
}

View File

@ -0,0 +1,206 @@
<?php
/**
* Onboarding Wizard ajax functions.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding;
class Ajax implements \EDD\EventManagement\SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'wp_ajax_edd_onboarding_telemetry_settings' => 'ajax_save_telemetry_settings',
'wp_ajax_edd_onboarding_create_product' => 'create_product',
'wp_ajax_edd_onboarding_started' => 'ajax_onboarding_started',
'wp_ajax_edd_onboarding_completed' => 'ajax_onboarding_completed',
'wp_ajax_edd_onboarding_skipped' => 'ajax_onboarding_skipped',
'wp_ajax_edds_stripe_connect_account_info' => array( 'disconnect_url', 5 ),
);
}
/**
* Ajax callback for saving telemetry option.
*
* @since 3.1.1
*/
public function ajax_save_telemetry_settings() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit();
}
if ( ! current_user_can( 'manage_shop_settings' ) ) {
exit;
}
if ( isset( $_REQUEST['telemetry_toggle'] ) ) {
edd_update_option( 'allow_tracking', filter_var( $_REQUEST['telemetry_toggle'], FILTER_VALIDATE_BOOLEAN ) );
}
update_option( 'edd_tracking_notice', true );
exit;
}
/**
* Ajax callback for creating a product.
*
* @since 3.1.1
*/
public function create_product() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit();
}
if ( ! current_user_can( 'edit_products' ) ) {
return;
}
$response = array( 'success' => false );
// Prepare product post details.
$product = array(
'post_title' => wp_strip_all_tags( $_REQUEST['product_title'] ),
'post_status' => 'draft',
'post_type' => 'download',
);
// Insert the product into the database.
$post_id = wp_insert_post( $product );
if ( $post_id ) {
$post = get_post( $post_id );
// Save meta fields.
edd_download_meta_box_fields_save( $post_id, $post );
// Set featured image.
if ( ! empty( $_REQUEST['product_image_id'] ) ) {
set_post_thumbnail( $post_id, absint( $_REQUEST['product_image_id'] ) );
}
$response['success'] = true;
$response['redirect_url'] = get_edit_post_link( $post_id );
}
wp_send_json( $response );
exit;
}
/**
* Ajax callback when user started the Onboarding flow.
*
* @since 3.1.1
*/
public function ajax_onboarding_started() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit;
}
if ( get_option( 'edd_onboarding_completed' ) ) {
exit;
}
if ( ! current_user_can( 'manage_options' ) ) {
exit;
}
update_option( 'edd_onboarding_started', current_time( 'Y-m-d H:i:s' ), false );
exit;
}
/**
* Ajax callback for completing the Onboarding.
*
* @since 3.1.1
*/
public function ajax_onboarding_completed() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit;
}
if ( get_option( 'edd_onboarding_completed' ) ) {
exit;
}
if ( ! current_user_can( 'manage_options' ) ) {
exit;
}
update_option( 'edd_onboarding_completed', current_time( 'Y-m-d H:i:s' ), false );
update_option( 'edd_tracking_notice', true );
$this->clean_onboarding_options();
exit;
}
/**
* Ajax callback for skipping the Onboarding.
*
* @since 3.1.1
*/
public function ajax_onboarding_skipped() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit();
}
if ( get_option( 'edd_onboarding_completed' ) ) {
exit;
}
if ( ! current_user_can( 'manage_options' ) ) {
exit;
}
update_option( 'edd_onboarding_completed', current_time( 'Y-m-d H:i:s' ), false );
$this->clean_onboarding_options();
exit;
}
/**
* Filters the Stripe disconnect URL.
* This has to be hooked into the ajax action before the main ajax work is run.
*
* @since 3.1.1
* @return void
*/
public function disconnect_url() {
add_filter(
'edds_stripe_connect_disconnect_url',
function( $url ) {
if ( empty( $_REQUEST['onboardingWizard'] ) ) {
return $url;
}
$stripe_connect_disconnect_url = edd_get_admin_url(
array(
'page' => 'edd-onboarding-wizard',
'current_step' => 'payment_methods',
'edds-stripe-disconnect' => true,
)
);
return wp_nonce_url( $stripe_connect_disconnect_url, 'edds-stripe-connect-disconnect' );
},
15
);
}
/**
* Clean onboarding options.
*
* @since 3.1.1
*/
private function clean_onboarding_options() {
delete_option( 'edd_onboarding_latest_step' );
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Onboarding Wizard Helpers.
*
* @package EDD
* @subpackage Emails
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1
*/
namespace EDD\Admin\Onboarding;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
trait Helpers {
/**
* Extract requested fields from the settings array.
*
* @param array $sections Array of fields in sections that we want to extract.
*
* @since 3.1.1
* @return bool
*/
public function extract_settings_fields( $sections = array() ) {
global $wp_settings_fields;
$extracted_fields = array();
// Fields extraction.
foreach ( $sections as $section_name => $section ) {
if ( ! empty( $wp_settings_fields[ $section_name ][ $section_name ] ) ) {
foreach ( $section as $field ) {
$field_name = "edd_settings[{$field}]";
if ( array_key_exists( $field_name, $wp_settings_fields[ $section_name ][ $section_name ] ) ) {
$extracted_fields[ $field_name ] = $wp_settings_fields[ $section_name ][ $section_name ][ $field_name ];
}
}
}
}
return $extracted_fields;
}
/**
* Get fields HTML.
*
* @param array $screen_settings Fields.
*
* @since 3.1.1
*/
public function settings_html( $screen_settings ) {
foreach ( $screen_settings as $field ) :
$class = '';
if ( ! empty( $field['args']['class'] ) ) {
$class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
}
echo "<tr{$class}>";
if ( ! empty( $field['args']['label_for'] ) ) {
echo '<th scope="row"><label for="' . esc_attr( $field['args']['label_for'] ) . '">' . $field['title'] . '</label></th>';
} else {
echo '<th scope="row">' . $field['title'] . '</th>';
}
echo '<td>';
if ( ! empty( $field['args']['std'] ) ) {
$field['args']['allow_blank'] = false;
}
call_user_func( $field['callback'], $field['args'] );
echo '</td>';
echo '</tr>';
endforeach;
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* Show a dismissal notice when skipping the onboarding wizard.
*/
namespace EDD\Admin\Onboarding;
class Notice extends \EDD\Admin\Promos\Notices\Notice {
/**
* Action hook for displaying the notice.
*/
const DISPLAY_HOOK = 'download_page_edd-onboarding-wizard';
/**
* Type of promotional notice.
*/
const TYPE = 'overlay';
/**
* Sets the notice to not be dismissible.
*/
const DISMISSIBLE = false;
/**
* Displays the notice content.
*
* @todo Chris set up words
* @since 3.1.1
* @return void
*/
protected function _display() {
?>
<h2><?php esc_html_e( 'Wait! We still haven\'t finished the setup.', 'easy-digital-downloads' ); ?></h2>
<p><?php esc_html_e( 'Our Quick Setup Wizard can help you get you ready to sell your first product in minutes. If you choose to exit now, you will need to manually configure key aspects of your store. You can always restart this wizard by going to Downloads > Tools, and clicking on the \'Restart Setup Wizard\' button.', 'easy-digital-downloads' ); ?></p>
<div class="edd-onboarding__actions">
<button class="button button-primary edd-promo-notice-dismiss"><?php esc_html_e( 'Let\'s finish now!', 'easy-digital-downloads' ); ?></button>
<button class="button button-secondary edd-promo-notice-dismiss edd-onboarding__dismiss"><?php esc_html_e( 'No thanks, I\'ll do it all myself.', 'easy-digital-downloads' ); ?></button>
</div>
<?php
}
/**
* Registers a custom notice ID so it's not created from the class name.
*
* @since 3.1.1
* @return string
*/
public function get_id() {
return 'onboarding-dismiss';
}
/**
* Duration (in seconds) that the notice is dismissed for.
* Setting to 1 so that it's always available to the wizard.
*
* @since 3.1.1
* @return int
*/
public static function dismiss_duration() {
return 1;
}
protected function _should_display() {
return ! get_option( 'edd_onboarding_completed' );
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Onboarding Wizard Business Info Step.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
class BusinessInfo extends Step {
/**
* Get step view.
*
* @since 3.1.1
*/
public function step_html() {
$sections = array(
'edd_settings_general_main' => array(
'business_settings',
'entity_name',
'entity_type',
'business_address',
'business_address_2',
'business_city',
'business_postal_code',
'base_country',
'base_state',
),
'edd_settings_general_currency' => array(
'currency_settings',
'currency',
'currency_position',
'thousands_separator',
'decimal_separator',
),
);
?>
<form method="post" action="options.php" class="edd-settings-form">
<?php settings_fields( 'edd_settings' ); ?>
<table class="form-table" role="presentation">
<tbody>
<?php echo $this->settings_html( $this->extract_settings_fields( $sections ) ); ?>
</tbody>
</table>
<input type="hidden" name="edd_tab_override" value="general" />
<input type="hidden" name="edd_section_override" value="main" />
</form>
<?php
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* Onboarding Wizard Configure Emails Step.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
class ConfigureEmails extends Step {
/**
* Get step view.
*
* @since 3.1.1
*/
public function step_html() {
$sections = array(
'edd_settings_emails_main' => array(
'email_logo',
'from_name',
'from_email',
),
);
$sections_purchase_receipt = array(
'edd_settings_emails_purchase_receipts' => array(
'purchase_subject',
),
);
?>
<form method="post" action="options.php" class="edd-settings-form">
<?php settings_fields( 'edd_settings' ); ?>
<table class="form-table" role="presentation">
<tbody>
<?php echo $this->settings_html( $this->extract_settings_fields( $sections ) ); ?>
<?php echo $this->settings_html( $this->extract_settings_fields( $sections_purchase_receipt ) ); ?>
<tr>
<th scope="row">
<label for="edd_settings[purchase_receipt]"><?php echo esc_html_e( 'Purchase Receipt Email', 'easy-digital-downloads' ); ?></label>
</th>
</tr>
<tr>
<td colspan="2">
<?php edd_email_tags_inserter_thickbox_content(); ?>
<div id="edd-onboarding__insert-marker-button" style="display: none;">
<a href="#TB_inline?width=640&inlineId=edd-insert-email-tag" class="edd-email-tags-inserter thickbox button edd-thickbox" style="padding-left: 0.4em;">
<span class="wp-media-buttons-icon dashicons dashicons-editor-code"></span>
<?php esc_html_e( 'Insert Marker', 'easy-digital-downloads' ); ?>
</a>
</div>
<textarea name="edd_settings[purchase_receipt]" id="edd_settings_purchase_receipt" rows="12" style="width: 100%;"><?php echo wp_kses_post( wpautop( edd_get_option( 'purchase_receipt', edd_get_email_body_content() ) ) ); ?></textarea>
</td>
</tr>
</tbody>
</table>
<input type="hidden" name="edd_tab_override" value="emails" />
<input type="hidden" name="edd_section_override" value="purchase_receipts" />
</form>
<?php
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Onboarding Wizard Payment Methods Step.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
class PaymentMethods extends Step {
/**
* Get step view.
*
* @since 3.1.1
*/
public function step_html() {
?>
<div class="edd-onboarding__stripe-content-holder">
<div class="edd-onboarding__stripe-content-logo">
<img src="<?php echo esc_url( EDD_PLUGIN_URL . 'assets/images/onboarding/stripe-logo.svg' ); ?>" alt="">
<span><?php echo esc_html_e( 'The worlds most powerful and easy to use payment gateway.', 'easy-digital-downloads' ); ?></span>
</div>
<strong class="edd-onboarding__stripe-features-title"><?php echo esc_html_e( 'Stripe Features we can add:', 'easy-digital-downloads' ); ?></strong>
<ol class="edd-onboarding__stripe-features-listing">
<li><?php echo esc_html_e( 'Secure checkout', 'easy-digital-downloads' ); ?></li>
<li><?php echo esc_html_e( 'Accept all major credit cards', 'easy-digital-downloads' ); ?></li>
<li><?php echo esc_html_e( 'Supports subscriptions', 'easy-digital-downloads' ); ?></li>
<li><?php echo esc_html_e( 'Fraud prevention tools', 'easy-digital-downloads' ); ?></li>
<li><?php echo esc_html_e( 'Apple Pay & Google Pay', 'easy-digital-downloads' ); ?></li>
<li><?php echo esc_html_e( 'And more…', 'easy-digital-downloads' ); ?></li>
</ol>
<div class="edd-onboarding__button-stripe">
<?php echo edds_stripe_connect_setting_field(); ?>
</div>
<div class="edd-onboarding__stripe-additional-text">
<span><?php echo esc_html_e( 'Start accepting payments with Stripe by connecting your account. Stripe Connect helps ensure easier setup and improved security.', 'easy-digital-downloads' ); ?></span>
</div>
</div>
<?php
}
}

View File

@ -0,0 +1,143 @@
<?php
/**
* Onboarding Wizard Products Step.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
class Products extends Step {
/**
* Get step view.
*
* @since 3.1.1
*/
public function step_html() {
$currency_position = edd_get_option( 'currency_position', 'before' );
?>
<form method="post" class="edd-onboarding__create-product-form">
<input type="hidden" name="_edd_product_type" value="0">
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><h3><?php echo esc_html_e( 'Product details', 'easy-digital-downloads' ); ?></h3></th>
<td><span alt="f223" class="edd-help-tip dashicons dashicons-editor-help" title="<?php echo esc_attr( __( 'We\'ll get started with some basic information. Don\'t worry, you can add more details later. When you\'re finished here, the product will be saved as a draft so you can finish up later.', 'easy-digital-downloads' ) ); ?>"></span></td>
</tr>
<tr>
<th scope="row"><label for="product_title"><?php echo esc_html_e( 'Product Name', 'easy-digital-downloads' ); ?></label></th>
<td>
<input type="text" class="regular-text" id="product_title" name="product_title" required>
</td>
</tr>
<tr>
<th scope="row"><label for="product_image_url"><?php echo esc_html_e( 'Product Image', 'easy-digital-downloads' ); ?></label></th>
<td>
<div class="edd-onboarding__product-image-wrapper">
<input type="hidden" id="product_image_id" name="product_image_id">
<div class="edd-upload-button-wrapper">
<input type="text" class="regular-text" id="product_image_url" class="" name="product_image_url" data-attachment-id-field="#product_image_id" />
<button data-input="#product_image_url" data-uploader-title="<?php echo esc_attr( __( 'Set image', 'easy-digital-downloads' ) ); ?>" data-uploader-button-text="<?php echo esc_attr( __( 'Set image', 'easy-digital-downloads' ) ); ?>" class="button edd_settings_upload_button button-secondary"><?php esc_html_e( 'Set Image', 'easy-digital-downloads' ); ?></button>
</div>
</div>
</td>
</tr>
<tr>
<th scope="row"><label for="edd_variable_pricing"><?php echo apply_filters( 'edd_price_options_heading', __( 'Pricing Options', 'easy-digital-downloads' ) ); ?></label></th>
<td>
<input style="display:none;" type="checkbox" class="edd-form-group__input" name="_variable_pricing" id="edd_variable_pricing" value="1"/>
<div class="edd-onboarding__pricing-option-pill">
<button class="left-option active" data-variable-pricing="false"><?php echo esc_html_e( 'Single price', 'easy-digital-downloads' ); ?></button>
<button class="right-option" data-variable-pricing="true"><?php echo esc_html_e( 'Variable price', 'easy-digital-downloads' ); ?></button>
</div>
</td>
</tr>
<tr class="edd-onboarding__product-single-price">
<th scope="row"><label for="edd_price"><?php echo esc_html_e( 'Product Price', 'easy-digital-downloads' ); ?></label></th>
<td>
<div class="edd-form-group__control">
<?php
$price_args = array(
'name' => 'edd_price',
'id' => 'edd_price',
'value' => '0.00',
'class' => 'edd-form-group__input edd-price-field',
);
if ( 'before' === $currency_position ) {
?>
<span class="edd-amount-control__currency is-before"><?php echo esc_html( edd_currency_filter( '' ) ); ?></span>
<?php
echo EDD()->html->text( $price_args );
} else {
echo EDD()->html->text( $price_args );
?>
<span class="edd-amount-control__currency is-after"><?php echo esc_html( edd_currency_filter( '' ) ); ?></span>
<?php
}
do_action( 'edd_price_field', null );
?>
</div>
</td>
</tr>
<tr class="edd-onboarding__product-variable-price no-table-row-padding">
<td colspan="2">
<div id="edd_variable_price_fields" class="edd_pricing_fields edd-onboarding__product-variable-price-fields">
<input type="hidden" id="edd_variable_prices" class="edd_variable_prices_name_field" value=""/>
<div id="edd_price_fields" class="edd_meta_table_wrap">
<div class="widefat edd_repeatable_table">
<div class="edd-price-option-fields edd-repeatables-wrap">
<div class="edd_variable_prices_wrapper edd_repeatable_row" data-key="1">
<?php do_action( 'edd_render_price_row', 1, array(), null, 1 ); ?>
</div>
</div>
<div class="edd-add-repeatable-row">
<button class="button button-secondary edd_add_repeatable"><?php echo esc_html_e( 'Add New Price', 'easy-digital-downloads' ); ?></button>
</div>
</div>
</div>
</div><!--end #edd_variable_price_fields-->
</td>
</tr>
<tr>
<th scope="row">
<div class="edd-form-group">
<div class="edd-form-group__control">
<label class="edd-toggle edd-onboarding__upload-files-toggle" for="_edd_upload_files">
<?php echo EDD()->html->checkbox( array( 'name' => '_edd_upload_files', 'current' => null, 'class' => 'edd-form-group__input', ) ); ?>
<span><?php echo esc_html_e( 'Add your first file', 'easy-digital-downloads' ); ?></span>
</label>
</div>
</div>
</th>
<td>
<span alt="f223" class="edd-help-tip dashicons dashicons-editor-help" title="<?php echo esc_attr( __( 'Ready to add your first downloadable file to your product? Great! These files will be protected and only available to people who purchase your product. Not ready yet? No problem, you can always add and update files later.', 'easy-digital-downloads' ) ); ?>"></span>
</td>
</tr>
<tr class="edd-onboarding__product-files-row" style="display: none;">
<td colspan="2">
<?php edd_render_files_field( null ); ?>
</td>
</tr>
</tbody>
</table>
</form>
<div class="edd-onboarding__product-created" style="display: none;">
<h1>🎉 <?php esc_html_e( 'Congratulations!', 'easy-digital-downloads' ); ?></h1>
<p><?php esc_html_e( 'You\'ve set up your store and your first product has been created.', 'easy-digital-downloads' ); ?></p>
<a href="<?php echo esc_url( edd_get_admin_url() ); ?>" class="button button-primary button-hero edd-onboarding__edit-my-product"><?php esc_html_e( 'Edit My Product', 'easy-digital-downloads' ); ?></a>
<a href="<?php echo esc_url( edd_get_admin_url( array( 'page' => 'edd-addons' ) ) ); ?>" class="button button-secondary button-hero"><?php esc_html_e( 'Explore Extensions', 'easy-digital-downloads' ); ?></a>
</div>
<?php
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Onboarding Wizard Step absctract class.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
abstract class Step {
use \EDD\Admin\Onboarding\Helpers;
/**
* Get step view.
*
* @since 3.1.1
*/
abstract public function step_html();
}

View File

@ -0,0 +1,238 @@
<?php
/**
* Onboarding Wizard Tools Step.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding\Steps;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
class Tools extends Step {
/**
* Get step view.
*
* @since 3.1.1
*/
public function step_html() {
$can_install_plugins = current_user_can( 'install_plugins' );
$available_plugins = $this->get_plugins();
?>
<div class="edd-onboarding__install-plugins">
<div class="edd-onboarding__plugins-list">
<?php
foreach ( $available_plugins as $plugin ) :
$checked = '';
$disabled = '';
$readonly = '';
$id = str_replace( ' ', '-', strtolower( $plugin['plugin_name'] ) );
if ( isset( $plugin['prechecked'] ) && $plugin['prechecked'] ) {
$checked = ' checked';
}
if ( isset( $plugin['disabled'] ) && $plugin['disabled'] ) {
$disabled = ' disabled';
}
if ( isset( $plugin['readonly'] ) && $plugin['readonly'] ) {
$readonly = ' onClick="return false;"';
}
?>
<div class="edd-onboarding__plugins-plugin">
<h3><?php echo esc_html( $plugin['name'] ); ?></h3>
<div class="edd-onboarding__plugins-details">
<label for="<?php echo esc_attr( $id ); ?>"><?php echo esc_html( $plugin['description'] ); ?>
<div class="edd-onboarding__plugins-control">
<?php if ( ! $can_install_plugins && ! empty( $plugin['plugin_url'] ) ) : ?>
<a href="<?php echo esc_url( $plugin['plugin_url'] ); ?>" class="edd-onboarding__plugins-external-link" target="_blank"><span class="dashicons dashicons-external"></span></a>
<?php else : ?>
<div class="checkbox-control checkbox-control--checkbox">
<input id="<?php echo esc_attr( $id ); ?>" class="edd-onboarding__plugin-install" data-plugin-name="<?php echo esc_attr( $plugin['plugin_name'] ); ?>" data-action="<?php echo esc_attr( $plugin['action'] ); ?>" data-plugin-file="<?php echo esc_attr( $plugin['plugin_file'] ); ?>" value="<?php echo esc_attr( $plugin['plugin_zip'] ); ?>" type="checkbox"<?php echo $checked.$disabled.$readonly;?>/>
<div class="checkbox-control__indicator"></div>
</div>
<?php endif; ?>
</div>
</label>
<?php if ( ! empty( $plugin['active'] ) ) : ?>
<p>
<em>
<?php
/* translators: the plugin name. */
printf( esc_html__( '%s is already active.', 'easy-digital-downloads' ), esc_html( $plugin['plugin_name'] ) );
?>
</em>
</p>
<?php elseif ( ! empty( $plugin['has_feature'] ) ) : ?>
<p>
<em><?php esc_html_e( 'You already have a solution installed for this feature.', 'easy-digital-downloads' ); ?></em>
</p>
<?php endif; ?>
</div>
</div>
<?php
endforeach;
?>
</div>
<div class="edd-onboarding__get-suggestions-section">
<h3>
<?php esc_html_e( 'Join the EDD Community', 'easy-digital-downloads' ); ?>
</h3>
<label for="edd-onboarding__telemery-toggle" class="edd-onboarding__get-suggestions-section_label">
<?php esc_html_e( 'Help us provide a better experience and faster fixes by sharing some anonymous data about how you use Easy Digital Downloads.', 'easy-digital-downloads' ); ?>
</label>
<div class="edd-toggle">
<input type="checkbox" id="edd-onboarding__telemery-toggle" class="edd-onboarding__get-suggestions-section_input" name="telemetry" value="1" checked>
</div>
</div>
<div class="edd-onboarding__selected-plugins">
<p><?php esc_html_e( 'Based on your selection above, the following plugins will be installed:', 'easy-digital-downloads' ); ?> <span class="edd-onboarding__selected-plugins-text"></span></p>
</div>
</div>
<div class="edd-onboarding__install-failed" style="display: none;">
<h3><?php esc_html_e( 'Some features were not able to be installed!', 'easy-digital-downloads' ); ?></h3>
<p>
<?php
wp_kses(
/* translators: list of plugins that were not able to be installed or activated */
printf( __( 'Don\'t worry, everything will still work without them! You can install %s later by going to Plugins > Add New.', 'easy-digital-downloads' ), '<span class="edd-onboarding__failed-plugins-text"></span>' ),
array( 'span' )
);
?>
</p>
<button class="button button-primary button-hero edd-onboarding__button-skip-step"><?php esc_html_e( 'Continue', 'easy-digital-downloads' ); ?></button>
</div>
<div class="edd-onboarding__install-success-wrapper" style="display: none;">
<div class="edd-onboarding__install-success">
<span class="emoji">🥳</span>
<span><?php esc_html_e( 'Plugins were successfully installed!', 'easy-digital-downloads' ); ?></span>
</div>
</div>
<?php
}
/**
* Gets the plugins to install/activate.
*
* @since 3.1.1
* @return array
*/
private function get_plugins() {
$extension_manager = new \EDD\Admin\Extensions\Extension_Manager();
$available_plugins = array(
array(
'name' => __( 'Essential eCommerce Features', 'easy-digital-downloads' ),
'description' => __( 'Get all the essential eCommerce features to sell digital products with WordPress.', 'easy-digital-downloads' ),
'prechecked' => true,
'readonly' => true,
'disabled' => true,
'plugin_name' => __( 'Easy Digital Downloads', 'easy-digital-downloads' ),
'plugin_file' => '',
'plugin_zip' => '',
'plugin_url' => '',
'action' => '',
),
array(
'name' => __( 'Optimize Checkout', 'easy-digital-downloads' ),
'description' => __( 'Improve the checkout experience by auto-creating user accounts for new customers.', 'easy-digital-downloads' ),
'prechecked' => true,
'plugin_name' => 'Auto Register',
'plugin_file' => 'edd-auto-register/edd-auto-register.php',
'plugin_zip' => 'https://downloads.wordpress.org/plugin/edd-auto-register.zip',
'plugin_url' => 'https://wordpress.org/plugins/edd-auto-register',
'action' => 'install',
),
array(
'name' => __( 'Reliable Email Delivery', 'easy-digital-downloads' ),
'description' => __( 'Email deliverability is one of the most important services for an eCommerce store. Dont leave your customers in the dark.', 'easy-digital-downloads' ),
'prechecked' => true,
'plugin_name' => 'WP Mail SMTP',
'plugin_file' => 'wp-mail-smtp/wp_mail_smtp.php',
'plugin_zip' => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
'plugin_url' => 'https://wordpress.org/plugins/wp-mail-smtp/',
'action' => 'install',
'conflicts' => array(
'wp-mail-smtp-pro/wp_mail_smtp.php',
),
),
array(
'name' => __( 'Analytics Tools', 'easy-digital-downloads' ),
'description' => __( 'Get the #1 analytics plugin to see useful information about your visitors right inside your WordPress dashboard.', 'easy-digital-downloads' ),
'prechecked' => true,
'plugin_name' => 'MonsterInsights',
'plugin_file' => 'google-analytics-for-wordpress/googleanalytics.php',
'plugin_zip' => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
'plugin_url' => 'https://wordpress.org/plugins/google-analytics-for-wordpress/',
'action' => 'install',
'conflicts' => array(
'google-analytics-premium/googleanalytics-premium.php',
'google-analytics-dashboard-for-wp/gadwp.php',
'exactmetrics-premium/exactmetrics-premium.php',
'wp-analytify/wp-analytify.php',
'ga-google-analytics/ga-google-analytics.php',
),
),
array(
'name' => __( 'SEO', 'easy-digital-downloads' ),
'description' => __( 'Get the tools used by millions of smart business owners to analyze and optimize their stores traffic with SEO.', 'easy-digital-downloads' ),
'prechecked' => true,
'plugin_name' => 'All In One SEO Pack',
'plugin_file' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
'plugin_zip' => 'https://downloads.wordpress.org/plugin/all-in-one-seo-pack.zip',
'plugin_url' => 'https://wordpress.org/plugins/all-in-one-seo-pack/',
'action' => 'install',
'conflicts' => array(
'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
'wordpress-seo/wp-seo.php',
'wordpress-seo-premium/wp-seo-premium.php',
),
),
);
// Check the state of the plugins in the current environment.
foreach ( $available_plugins as $key => $plugin ) {
// If the plugin has a conflict with another plugin, remove it from the list.
if ( ! empty( $plugin['conflicts'] ) ) {
foreach ( $plugin['conflicts'] as $conflicting_slug ) {
if ( is_plugin_active( $conflicting_slug ) ) {
$available_plugins[ $key ]['disabled'] = true;
$available_plugins[ $key ]['prechecked'] = true;
$available_plugins[ $key ]['readonly'] = true;
$available_plugins[ $key ]['has_feature'] = true;
break;
}
}
}
if ( isset( $plugin['disabled'] ) && $plugin['disabled'] ) {
continue;
}
// If plugin is already installed, set the action to activate.
if ( $extension_manager->is_plugin_installed( $plugin['plugin_file'] ) ) {
$available_plugins[ $key ]['action'] = 'activate';
}
// If this plugin is activated, disable the checkbox on the front.
if ( is_plugin_active( $plugin['plugin_file'] ) ) {
$available_plugins[ $key ]['prechecked'] = true;
$available_plugins[ $key ]['disabled'] = true;
$available_plugins[ $key ]['action'] = '';
$available_plugins[ $key ]['active'] = true;
}
}
return $available_plugins;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Adds a tool section to allow a user to revisit the onboarding wizard.
*/
namespace EDD\Admin\Onboarding;
class Tools implements \EDD\EventManagement\SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
if ( ! current_user_can( 'manage_options' ) || ! get_option( 'edd_onboarding_completed' ) ) {
return array();
}
return array(
'edd_tools_tab_general' => 'restart_onboarding',
);
}
/**
* Adds a "tool" to allow users to restart the onboarding wizard.
*
* @since 3.1.1
* @return void
*/
public function restart_onboarding() {
?>
<div class="postbox">
<h3><?php esc_html_e( 'Restart the Setup Wizard', 'easy-digital-downloads' ); ?></h3>
<div class="inside edd-onboarding">
<p><?php esc_html_e( 'If you would like to revisit our setup wizard, you can at any time.', 'easy-digital-downloads' ); ?></p>
<a class="button button-secondary" href="<?php echo esc_url( edd_get_admin_url( array( 'page' => 'edd-onboarding-wizard' ) ) ); ?>"><?php esc_html_e( 'Restart the Setup Wizard', 'easy-digital-downloads' ); ?></a>
</div>
</div>
<?php
}
}

View File

@ -0,0 +1,635 @@
<?php
/**
* Onboarding Wizard Class.
*
* @package EDD
* @subpackage Onboarding
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Admin\Onboarding;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* EDD_Onboarding Class.
*
* Takes care of everything related to Onboarding Wizard.
*
* @since 3.1.1
*/
class Wizard implements \EDD\EventManagement\SubscriberInterface {
/**
* Current Onboarding step.
*
* @since 3.1.1
*
* @var string
*/
private $current_step = 'business_info';
/**
* Current Onboarding step index.
*
* @since 3.1.1
*
* @var int
*/
private $current_step_index = 1;
/**
* Onboarding steps.
*
* @since 3.1.1
*
* @var array
*/
private $onboarding_steps = array();
/**
* True if user started onboarding process.
*
* @since 3.1.1
*
* @var bool
*/
private $onboarding_started = false;
/**
* Whether onboarding has been completed once.
*
* @since 3.1.1
* @var bool
*/
private $onboarding_completed = false;
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'admin_init' => 'redirect',
'admin_menu' => array( 'add_menu_item', 5 ),
'wp_ajax_edd_onboarding_load_step' => 'ajax_onboarding_load_step',
'load-download_page_edd-onboarding-wizard' => 'load_onboarding_wizard',
'admin_enqueue_scripts' => 'enqueue_onboarding_scripts',
);
}
/**
* Maybe redirect to the onboarding wizard.
*
* @since 3.1.1
* @return void
*/
public function redirect() {
if ( wp_doing_ajax() ) {
return;
}
if ( ! get_transient( 'edd_onboarding_redirect' ) ) {
return;
}
delete_transient( 'edd_onboarding_redirect' );
if ( get_option( 'edd_onboarding_prevent_redirect' ) ) {
return;
}
if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
edd_redirect(
edd_get_admin_url(
array(
'page' => 'edd-onboarding-wizard',
)
)
);
}
/**
* Add Onboarding Wizard submenu page.
*
* @since 3.1.1
*/
public function add_menu_item() {
add_submenu_page( 'edit.php?post_type=download', __( 'Setup', 'easy-digital-downloads' ), __( 'Setup', 'easy-digital-downloads' ), 'manage_shop_settings', 'edd-onboarding-wizard', array( $this, 'onboarding_wizard_sub_page' ) );
if ( $this->has_onboarding_been_completed() ) {
remove_submenu_page( 'edit.php?post_type=download', 'edd-onboarding-wizard' );
}
add_action( 'admin_head', array( $this, 'adjust_menu_item_class' ) );
}
/**
* Adds the custom pro menu item class.
*
* @since 3.1.1
* @return void
*/
public function adjust_menu_item_class() {
new \EDD\Admin\Menu\LinkClass( 'edd-onboarding-wizard', 'edd-onboarding__menu-item' );
}
/**
* Determine if we are on Onboarding Wizard screen
* and load all of the neccesarry hooks and actions.
*
* @since 3.1.1
*/
public function load_onboarding_wizard() {
if ( ! $this->is_wizard() ) {
return;
}
// Hide EDD header.
remove_action( 'admin_notices', 'edd_admin_header', 1 );
// Set variables.
$this->onboarding_started = $this->has_onboarding_started();
$this->set_onboarding_steps();
$this->set_current_onboarding_step();
// We don't want any notices on our screen.
remove_all_actions( 'admin_notices' );
remove_all_actions( 'all_admin_notices' );
// Override Stripe callback urls.
add_filter( 'edds_stripe_connect_url', array( $this, 'update_stripe_connect_url' ), 15 );
add_filter( 'edd_pointers', function( $pointers ) {
return array();
} );
}
/**
* Load scripts and styles.
*
* @since 3.1.1
*/
public function enqueue_onboarding_scripts() {
if ( ! $this->is_wizard() ) {
return;
}
wp_enqueue_style( 'edd-admin-onboarding' );
wp_enqueue_script( 'edd-admin-onboarding' );
wp_enqueue_style( 'edd-extension-manager' );
wp_enqueue_script( 'edd-extension-manager' );
wp_enqueue_media();
wp_enqueue_editor();
if ( array_key_exists( 'payment_methods', $this->onboarding_steps ) ) {
edd_stripe_connect_admin_script( 'download_page_edd-settings' );
}
edd_email_tags_inserter_enqueue_scripts();
}
/**
* Override Stripe connect url.
*
* @since 3.1.1
*/
public function update_stripe_connect_url() {
$return_url = edd_get_admin_url(
array(
'redirect_screen' => 'onboarding-wizard',
)
);
return add_query_arg(
array(
'live_mode' => (int) ! edd_is_test_mode(),
'state' => str_pad( wp_rand( wp_rand(), PHP_INT_MAX ), 100, wp_rand(), STR_PAD_BOTH ),
'customer_site_url' => urlencode( esc_url_raw( $return_url ) ),
),
'https://easydigitaldownloads.com/?edd_gateway_connect_init=stripe_connect'
);
}
/**
* Set onboarding steps.
*
* @since 3.1.1
*/
public function set_onboarding_steps() {
$this->onboarding_steps = array(
'business_info' => array(
'step_title' => __( 'Business', 'easy-digital-downloads' ),
'step_headline' => __( 'Tell us a little bit about your business.', 'easy-digital-downloads' ),
'step_intro' => __( 'Where is your business located? This helps Easy Digital Downloads configure the checkout and receipt templates.', 'easy-digital-downloads' ),
'step_handler' => 'BusinessInfo',
),
'payment_methods' => array(
'step_title' => __( 'Payment Methods', 'easy-digital-downloads' ),
'step_headline' => __( 'Start accepting payments today!', 'easy-digital-downloads' ),
'step_intro' => '',
'step_handler' => 'PaymentMethods',
),
'configure_emails' => array(
'step_title' => __( 'Emails', 'easy-digital-downloads' ),
'step_headline' => __( 'Configure your Receipts', 'easy-digital-downloads' ),
'step_intro' => __( 'Customize the purchase receipt that your customers will receive.', 'easy-digital-downloads' ),
'step_handler' => 'ConfigureEmails',
),
'tools' => array(
'step_title' => __( 'Tools', 'easy-digital-downloads' ),
'step_headline' => __( 'Conversion and Optimization tools', 'easy-digital-downloads' ),
'step_intro' => __( 'We have selected our recommended tools and features to help boost conversions and optimize your digital store.', 'easy-digital-downloads' ),
'step_handler' => 'Tools',
),
'products' => array(
'step_title' => __( 'Products', 'easy-digital-downloads' ),
'step_headline' => __( 'What are you going to sell?', 'easy-digital-downloads' ),
'step_intro' => __( 'Let\'s get started creating your first awesome product.', 'easy-digital-downloads' ),
'step_handler' => 'Products',
),
);
// If Stripe classes are not available, remove payment methods step.
if ( ! defined( 'EDD_STRIPE_VERSION' ) ) {
unset( $this->onboarding_steps['payment_methods'] );
}
// Determine products step intro.
$products = new \WP_Query(
array(
'post_type' => 'download',
'posts_per_page' => 1,
'no_found_rows' => true,
'fields' => 'ids',
)
);
if ( ! empty( $products->posts ) ) {
$this->onboarding_steps['products']['step_intro'] = __( 'Let\'s get started with your next great product.', 'easy-digital-downloads' );
}
// Set step index in the array and load ajax handlers.
$index = 1;
foreach ( $this->onboarding_steps as $key => $value ) {
$this->onboarding_steps[ $key ]['step_index'] = $index;
$index++;
}
}
/**
* Set current onboarding step.
*
* @since 3.1.1
*/
public function set_current_onboarding_step() {
// If Onboarding hasn't started yet, we force the first default step.
if ( ! $this->onboarding_started ) {
return;
}
// User is requesting a specific step.
$this->current_step = $this->get_current_step();
// If requested step does not exist, abort.
if ( ! isset( $this->onboarding_steps[ $this->current_step ] ) ) {
wp_die( __( 'Unknown Onboarding Step.', 'easy-digital-downloads' ), __( 'Onboarding Wizard', 'easy-digital-downloads' ), 404 );
}
$this->current_step_index = $this->onboarding_steps[ $this->current_step ]['step_index'];
if ( $this->has_onboarding_been_completed() ) {
return;
}
update_option( 'edd_onboarding_latest_step', $this->current_step, false );
}
/**
* Get previous step.
*
* @since 3.1.1
*/
public function get_previous_step() {
$internal_step = $this->current_step_index - 2;
$step_keys = array_keys( $this->onboarding_steps );
if ( isset( $step_keys[ $internal_step ] ) ) {
return $step_keys[ $internal_step ];
}
return false;
}
/**
* Get current step.
*
* @since 3.1.1
*/
public function get_current_step() {
if ( isset( $_GET['current_step'] ) ) {
return sanitize_key( $_GET['current_step'] );
}
return sanitize_key( get_option( 'edd_onboarding_latest_step', $this->current_step ) );
}
/**
* Get current step details.
*
* @since 3.1.1
*/
public function get_current_step_details() {
return $this->onboarding_steps[ $this->get_current_step() ];
}
/**
* Get next step.
*
* @since 3.1.1
*/
public function get_next_step() {
$internal_step = $this->current_step_index;
$step_keys = array_keys( $this->onboarding_steps );
if ( isset( $step_keys[ $internal_step ] ) ) {
return $step_keys[ $internal_step ];
}
return false;
}
/**
* Get pagination.
*
* @since 3.1.1
*/
public function get_step_pagination() {
return array(
'previous' => $this->get_previous_step(),
'current' => $this->get_current_step(),
'next' => $this->get_next_step(),
);
}
/**
* Onboarding Wizard subpage screen.
*
* @since 3.1.1
*/
public function onboarding_wizard_sub_page() {
$onboarding_initial_style = ( ! $this->onboarding_started ) ? ' style="display:none;"' : '';
?>
<?php wp_nonce_field( 'edd_onboarding_wizard' ); ?>
<div class="edd-onboarding wrap">
<div class="edd-onboarding__logo">
<img src="<?php echo esc_url( EDD_PLUGIN_URL . 'assets/images/logo-edd-dark.svg' ); ?>" alt="">
</div>
<div class="edd-onboarding__wrapper">
<div class="edd-onboarding__loading" style="display: none;">
<div class="edd-onboarding__loading-content-wrapper">
<div class="edd-onboarding__loading-status"></div>
</div>
</div>
<?php $this->get_welcome_screen(); ?>
<div class="edd-onboarding__after-welcome-screen"<?php echo $onboarding_initial_style; ?>>
<div class="edd-onboarding__current-step">
<?php $this->load_step_view(); ?>
</div>
</div>
</div>
</div>
<?php
}
/**
* Welcome screen.
*
* @since 3.1.1
*/
public function get_welcome_screen() {
if ( $this->onboarding_started ) {
return;
}
$testimonials = array(
array(
'name' => 'Joe Casabona',
'company' => 'How I Built It',
'content' => 'The problem with many e-commerce platforms to sell online courses is they aren\'t made with only digital goods in mind. <span class="big">EDD doesn\'t have that problem, and as a result their platform is perfectly made for selling my online courses.</span>',
'avatar' => 'joe.jpg',
'stars' => 5,
),
array(
'name' => 'Nicolas Martin',
'company' => 'Flea Market Insiders',
'content' => 'Before EDD\'s Recurring Payments was made available, we were only able to sell one-time subscriptions to our customers. Since implementing recurring payments, we\'ve been able to offer quarterly and yearly subscriptions and subsequently <span class="big">increase our subscriptions revenue by 200%.</span>',
'avatar' => 'nicolas.jpg',
'stars' => 5,
),
array(
'name' => 'Bob Dunn',
'company' => 'BobWP',
'content' => 'If anyone asks me what they should use for downloadable products on their WordPress site, <span class="big">it\'s a no-brainer as far as EDD goes.</span>',
'avatar' => 'bob.jpg',
'stars' => 5,
),
);
?>
<div class="edd-onboarding__welcome-screen">
<div class="edd-onboarding__welcome-screen-inner">
<h1>👋 <?php esc_html_e( 'Welcome, and thanks for choosing us!', 'easy-digital-downloads' ); ?></h1>
<p><?php esc_html_e( 'Easy Digital Downloads setup is fast and easy. We\'ll walk you through the quick initial process. And don\'t worry. You can go back and change anything you do at anytime. Nothing\'s permanent (unless you want it to be). So feel free to explore!', 'easy-digital-downloads' ); ?></p>
<button class="button button-hero edd-onboarding__welcome-screen-get-started"><?php esc_html_e( 'Get Started', 'easy-digital-downloads' ); ?></button>
<h2><?php esc_html_e( 'Creators ❤️ Easy Digital Downloads', 'easy-digital-downloads' ); ?></h2>
<div class="edd-onboarding__testimonials-wrapper">
<?php foreach ( $testimonials as $testimonial ) : ?>
<div class="edd-onboarding__testimonial">
<div class="edd-onboarding__testimonial-profile">
<img class="edd-onboarding__testimonial-avatar" src="<?php echo esc_url( EDD_PLUGIN_URL . "assets/images/onboarding/{$testimonial['avatar']}" ); ?>" />
</div>
<div class="edd-onboarding__testimonial-content">
<p><?php echo wp_kses_post( $testimonial['content'] ); ?></p>
<div class="edd-onboarding__testimonial-info">
<span class="testimonial-name"><?php echo esc_html( $testimonial['name'] ); ?></span>
<span class="testimonial-company"><?php echo esc_html( $testimonial['company'] ); ?></span>
<span class="testimonial-stars">
<?php for ( $star = 1; $star <= $testimonial['stars']; $star++ ) : ?>
<span class="dashicons dashicons-star-filled"></span>
<?php endfor; ?>
</span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php
}
/**
* Load requested step HTML.
*
* @since 3.1.1
*/
private function load_step_view() {
$current_step_details = $this->get_current_step_details();
$pagination = $this->get_step_pagination();
$step_class_name = 'EDD\\Admin\\Onboarding\\Steps\\' . $current_step_details['step_handler'];
$step_class = new $step_class_name();
?>
<input type="hidden" class="edd-onboarding_current-previous-step" value="<?php echo esc_attr( $this->get_previous_step() ); ?>">
<input type="hidden" class="edd-onboarding_current-step" value="<?php echo esc_attr( $this->get_current_step() ); ?>">
<input type="hidden" class="edd-onboarding_current-next-step" value="<?php echo esc_attr( $this->get_next_step() ); ?>">
<!-- STEPS NAVIGATION -->
<div class="edd-onboarding__steps">
<ul>
<?php
foreach ( $this->onboarding_steps as $step_key => $step ) :
$step_url = edd_get_admin_url(
array(
'post_type' => 'download',
'page' => 'edd-onboarding-wizard',
'current_step' => sanitize_key( $step_key ),
)
);
$classes = array();
// Determine if this step is active.
if ( $step['step_index'] === $this->current_step_index ) {
$classes[] = 'active-step';
}
// Determine if this step is completed.
if ( $this->current_step_index > $step['step_index'] ) {
$classes[] = 'completed-step';
}
?>
<li class="<?php echo implode( ' ', array_map( 'esc_attr', $classes ) ); ?>">
<a href="<?php echo esc_url( $step_url ); ?>">
<span class="edd-onboarding__steps__number"><?php echo esc_html( $step['step_index'] ); ?></span>
<small class="edd-onboarding__steps__name"><?php echo esc_html( $step['step_title'] ); ?> </small>
</a>
</li>
<?php
endforeach;
?>
</ul>
</div>
<div class="edd-onboarding__single-step">
<!-- STEP VIEW -->
<div class="edd-onboarding__single-step-inner">
<h1 class="edd-onboarding__single-step-title"><?php echo esc_html( $current_step_details['step_headline'] ); ?></h1>
<h2 class="edd-onboarding__single-step-subtitle"><?php echo esc_html( $current_step_details['step_intro'] ); ?></h2>
<?php
$step_class->step_html();
?>
</div>
<div class="edd-onboarding__single-step-footer">
<div>
<?php if ( $pagination['previous'] ) : ?>
<button class="edd-onboarding__button-back">← <?php echo esc_html_e( 'Go Back', 'easy-digital-downloads' ); ?></button>
<?php endif; ?>
</div>
<div>
<button class="button button-secondary button-hero edd-onboarding__button-skip-step"><?php echo esc_html_e( 'Skip this step', 'easy-digital-downloads' ); ?></button>
<button class="button button-primary button-hero edd-onboarding__button-save-step"><?php echo esc_html_e( 'Save & Continue', 'easy-digital-downloads' ); ?></button>
</div>
</div>
</div>
<div class="edd-onboarding__close-and-exit">
<button class="<?php echo esc_attr( implode( ' ', $this->get_close_exit_button_classes() ) ); ?>"><?php echo esc_html_e( 'Close and Exit Without Saving', 'easy-digital-downloads' ); ?></button>
</div>
<input type="hidden" id="edd-onboarding__exit" value="<?php echo esc_url( admin_url( 'edit.php?post_type=download' ) ); ?>" />
<?php
}
/**
* Ajax callback for loading single step view.
*
* @since 3.1.1
*/
public function ajax_onboarding_load_step() {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'edd_onboarding_wizard' ) ) {
exit();
}
// When AJAX loads, we need to initilalize everything.
$this->load_onboarding_wizard();
// Now load the step.
$this->load_step_view();
exit;
}
/**
* Whether the current request is the onboarding wizard.
*
* @since 3.1.1
* @return bool
*/
private function is_wizard() {
// Abort if we are not requesting Onboarding Wizard.
if ( ! empty( $_REQUEST['page'] ) && 'edd-onboarding-wizard' !== wp_unslash( $_REQUEST['page'] ) ) {
return false;
}
// Stripe calls are marked with onboardingWizard request parameter.
if ( empty( $_REQUEST['page'] ) && empty( $_REQUEST['onboardingWizard'] ) ) {
return false;
}
return true;
}
/**
* Whether the onboarding wizard has started.
* Returns true if the onboarding has been marked as completed, too.
*
* @since 3.1.1
* @return bool
*/
private function has_onboarding_started() {
if ( $this->onboarding_started ) {
return true;
}
return get_option( 'edd_onboarding_started', false ) || $this->has_onboarding_been_completed();
}
/**
* Whether the onboarding wizard has been completed.
*
* @since 3.1.1
* @return bool
*/
private function has_onboarding_been_completed() {
if ( $this->onboarding_completed ) {
return true;
}
return get_option( 'edd_onboarding_completed', false );
}
/**
* Gets the classes for the "Close and Exit without Saving" button.
* If onboarding has already completed, we don't need to show the confirmation again.
*
* @since 3.1.1
* @return array
*/
private function get_close_exit_button_classes() {
$classes = array( 'button', 'button-link' );
if ( ! $this->has_onboarding_been_completed() ) {
$classes[] = 'edd-promo-notice__trigger';
} else {
$classes[] = 'edd-onboarding__dismiss';
}
return $classes;
}
}

View File

@ -0,0 +1,83 @@
<?php
/**
* Actions functionality for EDD passes.
*
* @package EDD
* @subpackage EDD/PassHandler
*/
namespace EDD\Admin\PassHandler;
use EDD\EventManagement\SubscriberInterface;
class Actions implements SubscriberInterface {
/**
* The pass handler.
*
* @var \EDD\Admin|PassHandler\Handler;
*/
protected $handler;
public function __construct( Handler $handler ) {
$this->handler = $handler;
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_refresh_pass_status' => 'refresh',
);
}
/**
* When the "refresh" button is clicked, performs a remote license request to check the pass status.
* Sets a ten minute transient to avoid a double check (this is running twice for some reason) and excessive clicking.
*
* @since 3.1.1
* @return void
*/
public function refresh() {
if ( ! current_user_can( 'manage_options' ) ) {
edd_redirect( $this->handler->get_extensions_url() );
}
if ( get_transient( 'edd_pass_refreshed' ) ) {
edd_redirect( $this->handler->get_extensions_url() );
}
$pass_data = $this->handler->get_pro_license();
if ( empty( $pass_data->key ) ) {
edd_redirect(
edd_get_admin_url(
array(
'page' => 'edd-settings',
'edd-message' => 'missing-pass-key',
)
)
);
}
$api_params = array(
'edd_action' => 'check_license',
'license' => $pass_data->key,
'item_id' => $pass_data->pass_id,
'item_name' => $pass_data->item_name,
);
$license_data = $this->handler->remote_request( $api_params );
if ( empty( $license_data->success ) ) {
edd_redirect( $this->handler->get_extensions_url() );
}
$pass_manager = new \EDD\Admin\Pass_Manager();
$pass_manager->maybe_set_pass_flag( $pass_data->key, $license_data );
$this->handler->update_pro_license( $license_data );
set_transient( 'edd_pass_refreshed', true, 10 * MINUTE_IN_SECONDS );
edd_redirect( $this->handler->get_extensions_url() );
}
}

View File

@ -0,0 +1,197 @@
<?php
/**
* Ajax functionality for EDD passes.
*
* @package EDD
* @subpackage EDD/PassHandler
*/
namespace EDD\Admin\PassHandler;
use EDD\EventManagement\SubscriberInterface;
use EDD\Admin\Pass_Manager;
class Ajax implements SubscriberInterface {
/**
* The EDD Pass Manager class.
*
* @var \EDD\Admin\Pass_Manager
*/
protected $pass_manager;
/**
* The pass handler.
*
* @var \EDD\Admin|PassHandler\Handler;
*/
protected $handler;
public function __construct( Handler $handler ) {
$this->handler = $handler;
$this->pass_manager = new Pass_Manager();
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
return array(
'wp_ajax_edd_verify_pass' => 'verify',
'wp_ajax_edd_deactivate_pass' => 'deactivate',
'wp_ajax_edd_delete_pass' => 'delete',
);
}
/**
* Attempt to verify a pass license.
*
* @since 3.1.1
* @return void
*/
public function verify() {
if ( ! $this->can_manage_pass() ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this pass.', 'easy-digital-downloads' ) ),
)
);
}
$license_key = ! empty( $_POST['license'] ) ? sanitize_text_field( $_POST['license'] ) : false;
if ( ! $license_key ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'Please enter a license key.', 'easy-digital-downloads' ) ),
)
);
}
wp_send_json_success( $this->get_verification_response( $license_key ) );
}
/**
* Gets the array of response parameters for a successful license key activation.
*
* @since 3.1.1
* @param string $license_key
* @return array
*/
private function get_verification_response( $license_key ) {
$oth = hash( 'sha512', wp_rand() );
$endpoint = admin_url( 'admin-ajax.php' );
$redirect = edd_get_admin_url( array( 'page' => 'edd-settings' ) );
update_option( 'edd_connect_token', $oth );
$url = add_query_arg(
array(
'key' => $license_key,
'oth' => $oth,
'endpoint' => $endpoint,
'version' => EDD_VERSION,
'siteurl' => admin_url(),
'homeurl' => home_url(),
'redirect' => rawurldecode( base64_encode( $redirect ) ), // phpcs:ignore
),
'https://upgrade.easydigitaldownloads.com'
);
return array(
'message' => false,
'actions' => '',
'url' => $url,
'back_url' => add_query_arg(
array(
'action' => 'edd_connect',
'oth' => $oth,
),
$endpoint
),
);
}
/**
* Attempt to deactivate a pass license.
*
* @since 3.1.1
* @return void
*/
public function deactivate() {
if ( ! $this->can_manage_pass() ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this pass.', 'easy-digital-downloads' ) ),
)
);
}
$pass_data = $this->handler->get_pro_license();
$api_params = array(
'edd_action' => 'deactivate_license',
'license' => $pass_data->key,
'item_id' => urlencode( $pass_data->pass_id ),
);
$license_data = $this->handler->remote_request( $api_params );
$this->handler->update_pro_license( $license_data );
$this->pass_manager->maybe_remove_pass_flag( $pass_data->key );
wp_send_json_success(
array(
'message' => wpautop( __( 'Your pass was successfully deactivated.', 'easy-digital-downloads' ) ),
'actions' => $this->handler->get_pass_actions( 'inactive', $pass_data->key ),
)
);
}
/**
* Deletes a pass key and the related option.
*
* @since 3.1.1
* @return void
*/
public function delete() {
if ( ! $this->can_manage_pass( 'edd_passhandler-delete' ) ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this pass.', 'easy-digital-downloads' ) ),
)
);
}
$license = $this->handler->get_pro_license();
$license->delete();
wp_send_json_success(
array(
'message' => wpautop( __( 'Pass key deleted.', 'easy-digital-downloads' ) ),
)
);
}
/**
* Whether the current user can manage the pass.
* Checks the user capabilities, tokenizer, and nonce.
*
* @since 3.1.1
* @param string $nonce The name of the specific nonce to validate.
* @return bool
*/
protected function can_manage_pass( $nonce = 'edd_passhandler' ) {
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
$token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
$timestamp = isset( $_POST['timestamp'] ) ? sanitize_text_field( $_POST['timestamp'] ) : '';
if ( empty( $timestamp ) || empty( $token ) ) {
return false;
}
return \EDD\Utils\Tokenizer::is_token_valid( $token, $timestamp ) && wp_verify_nonce( $_POST['nonce'], $nonce );
}
}

View File

@ -0,0 +1,176 @@
<?php
/**
* Base Pass Handler class.
*
* @package EDD
* @subpackage EDD/PassHandler
*/
namespace EDD\Admin\PassHandler;
class Handler {
/**
* Gets the pass data.
*
* @since 3.1.1
* @return array
*/
public function get_pro_license() {
return new \EDD\Licensing\License( 'pro' );
}
/**
* Updates the pass data.
*
* @since 3.1.1
* @param object $license_data
* @return bool
*/
public function update_pro_license( $license_data ) {
// When updating pass data, always delete the extension data.
delete_site_option( 'edd_extension_category_1592_data' );
$license = $this->get_pro_license();
return $license->save( $license_data );
}
/**
* Gets the button for the pass field.
*
* @since 3.1.1
* @param string $status The pass status.
* @param string $key The license key.
* @param bool $echo Whether to echo the button.
* @return string
*/
public function get_pass_actions( $status, $key = '', $echo = false ) {
$button = $this->get_button_args( $status, $key );
$timestamp = time();
if ( ! $echo ) {
ob_start();
}
?>
<div class="edd-pass-handler__actions">
<button
class="button button-<?php echo esc_attr( $button['class'] ); ?> edd-pass-handler__action"
data-action="<?php echo esc_attr( $button['action'] ); ?>"
data-timestamp="<?php echo esc_attr( $timestamp ); ?>"
data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_passhandler' ) ); ?>"
>
<?php echo esc_html( $button['label'] ); ?>
</button>
<?php if ( ! empty( $key ) && in_array( $button['action'], array( 'activate', 'verify' ), true ) ) : ?>
<button
class="button button-secondary edd-pass-handler__delete"
data-action="delete"
data-timestamp="<?php echo esc_attr( $timestamp ); ?>"
data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_passhandler-delete' ) ); ?>"
>
<?php esc_html_e( 'Delete', 'easy-digital-downloads' ); ?>
</button>
<?php
endif;
if ( 'deactivate' === $button['action'] ) {
$this->do_extensions_link();
}
?>
</div>
<?php
if ( ! $echo ) {
return ob_get_clean();
}
}
/**
* Get the button parameters based on the status.
*
* @since 3.1.1
* @param string $state
* @param string $key
* @return array
*/
private function get_button_args( $state = 'inactive', $key = '' ) {
if ( ! empty( $key ) && in_array( $state, array( 'valid', 'active' ), true ) ) {
return array(
'action' => 'deactivate',
'label' => __( 'Deactivate', 'easy-digital-downloads' ),
'class' => 'secondary',
);
}
if ( edd_is_pro() ) {
return array(
'action' => 'activate',
'label' => __( 'Activate License', 'easy-digital-downloads' ),
'class' => 'primary',
);
}
return array(
'action' => 'verify',
'label' => __( 'Verify License Key', 'easy-digital-downloads' ),
'class' => 'primary',
);
}
/**
* Prints the link to the extensions screen.
*
* @since 3.1.1
* @return string
*/
private function do_extensions_link() {
printf(
'<a class="button button-primary edd-pass-handler__extensions-link" href="%s">%s</a>',
esc_url( $this->get_extensions_url() ),
esc_html__( 'View Extensions', 'easy-digital-downloads' )
);
}
/**
* Gets the extensions screen URL.
*
* @return string
*/
public function get_extensions_url() {
return edd_get_admin_url(
array(
'page' => 'edd-addons',
)
);
}
/**
* Makes the remote request to activate/deactivate a license key.
*
* @since 3.1.1
* @param array $api_params
* @return stdClass|void
*/
public function remote_request( $api_params ) {
$api_params = wp_parse_args(
$api_params,
array(
'url' => home_url(),
)
);
$api = new \EDD\Licensing\API();
$response = $api->make_request( $api_params );
// Make sure there are no errors
if ( ! $response ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'We could not reach the EDD server.', 'easy-digital-downloads' ) ),
)
);
}
return $response;
}
}

View File

@ -0,0 +1,194 @@
<?php
/**
* Settings display/functions for EDD passes.
* @package EDD
* @subpackage Admin/PassHandler
*/
namespace EDD\Admin\PassHandler;
use EDD\EventManagement\SubscriberInterface;
class Settings implements SubscriberInterface {
/**
* The pass handler.
*
* @var \EDD\Admin|PassHandler\Handler;
*/
protected $handler;
public function __construct( Handler $handler ) {
$this->handler = $handler;
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_tab_top_general_main' => 'do_pass_field',
'admin_enqueue_scripts' => 'register_assets',
);
}
/**
* Outputs the EDD pass license field on the main EDD settings screen.
*
* @since 3.1.1
* @return void
*/
public function do_pass_field() {
$pro_license = $this->handler->get_pro_license();
$license_key = $pro_license->key;
if ( empty( $pro_license->key ) ) {
$pass_manager = new \EDD\Admin\Pass_Manager();
if ( ! empty( $pass_manager->highest_license_key ) ) {
$license_key = $pass_manager->highest_license_key;
}
}
$this->enqueue();
?>
<h3><?php echo esc_html( $this->get_heading_text() ); ?></h3>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row">
<label for="edd_pass_key"><?php esc_html_e( 'License Key', 'easy-digital-downloads' ); ?></label>
</th>
<td>
<?php $this->show_free_message( $pro_license ); ?>
<div class="edd-pass-handler__control">
<input
id="edd_pass_key"
type="password"
class="regular-text"
value="<?php echo esc_attr( $license_key ); ?>"
placeholder="<?php esc_html_e( 'Paste license key', 'easy-digital-downloads' ); ?>"
<?php echo ( ! empty( $pro_license->key ) && 'valid' === $pro_license->license ? 'readonly' : '' ); ?>
>
<?php $this->handler->get_pass_actions( $pro_license->license, $pro_license->key, true ); ?>
</div>
<?php
if ( edd_is_pro() ) {
$messages = new \EDD\Licensing\Messages(
array(
'status' => $pro_license->license,
'license_key' => $pro_license->key,
'expires' => $pro_license->expires,
'name' => $pro_license->item_name,
'subscription' => $pro_license->subscription,
)
);
$message = $messages->get_message();
if ( $message ) {
echo wp_kses_post( wpautop( $message ) );
}
} elseif ( empty( $pro_license->key ) && ! empty( $license_key ) ) {
?>
<p><?php esc_html_e( 'We see that you have an active pass--just hit Verify to get started with (Pro).', 'easy-digital-downloads' ); ?></p>
<?php
}
?>
</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Registers the pass handler script and style.
*
* @since 3.1.1
* @return void
*/
public function register_assets() {
if ( wp_script_is( 'edd-pass-handler', 'registered' ) ) {
return;
}
if ( ! edd_is_admin_page( 'settings' ) ) {
return;
}
wp_register_style( 'edd-pass-handler', EDD_PLUGIN_URL . 'assets/css/edd-admin-pass-handler.min.css', array(), EDD_VERSION );
wp_register_script( 'edd-pass-handler', EDD_PLUGIN_URL . 'assets/js/edd-admin-pass-handler.js', array( 'jquery' ), EDD_VERSION, true );
wp_localize_script(
'edd-pass-handler',
'EDDPassManager',
array(
'verifying' => __( 'Verifying', 'easy-digital-downloads' ),
'activating' => __( 'Activating', 'easy-digital-downloads' ),
'deactivating' => __( 'Deactivating', 'easy-digital-downloads' ),
'verify_loader' => __( 'Just a moment while we connect your site and upgrade you to (Pro).', 'easy-digital-downloads' ),
)
);
}
/**
* Enqueues the pass handler script/style.
*
* @since 3.1.1
* @return void
*/
public function enqueue() {
wp_enqueue_style( 'edd-pass-handler' );
wp_enqueue_script( 'edd-pass-handler' );
}
/**
* Gets the heading text for the pass key field.
*
* @since 3.1.1
* @return string
*/
private function get_heading_text() {
return edd_is_pro() ?
__( 'Easy Digital Downloads (Pro) Key', 'easy-digital-downloads' ) :
__( 'Go Pro With Easy Digital Downloads', 'easy-digital-downloads' );
}
/**
* Show the free message to users without active passes.
*
* @since 3.1.1
* @param array $pro_license
* @return void
*/
private function show_free_message( $pro_license ) {
if ( edd_is_pro() ) {
return;
}
?>
<div class="edd-pass-handler__description">
<p>
<?php esc_html_e( 'You\'re using Easy Digital Downloads &mdash; no license needed. Enjoy!', 'easy-digital-downloads' ); ?>
<img src="<?php echo esc_url( EDD_PLUGIN_URL . 'assets/images/icons/icon-smiley.svg' ); ?>" alt="" class="emoji">
</p>
<p>
<?php
$url = edd_link_helper(
'https://easydigitaldownloads.com/lite-upgrade/',
array(
'utm_medium' => 'settings-general',
'utm_content' => 'upgrade-to-pro',
)
);
echo wp_kses_post(
sprintf(
/* translators: 1. opening link tag; do not translate; 2. closing link tag; do not translate. */
__( 'To unlock more features, consider %1$supgrading to Pro%2$s.', 'easy-digital-downloads' ),
'<strong><a href="' . $url . '">',
'</a></strong>'
)
);
?>
</p>
<p><?php esc_html_e( 'As a valued EDD user you receive 50% off, automatically applied at checkout!', 'easy-digital-downloads' ); ?></p>
<p><?php esc_html_e( 'Already purchased? Simply enter your license key to enable EDD (Pro).', 'easy-digital-downloads' ); ?></p>
</div>
<?php
}
}

View File

@ -0,0 +1,422 @@
<?php
/**
* Pass Manager
*
* Tool for determining what kind of pass, if any, is activated on the site.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.10.6
*/
namespace EDD\Admin;
class Pass_Manager {
const PERSONAL_PASS_ID = 1245715;
const EXTENDED_PASS_ID = 1245716;
const PROFESSIONAL_PASS_ID = 1245717;
const ALL_ACCESS_PASS_ID = 1150319;
const ALL_ACCESS_PASS_LIFETIME_ID = 1464807;
/**
* ID of the highest tier pass that's activated.
*
* @var int|null
*/
public $highest_pass_id = null;
/**
* License key of the highest tier pass that's activated.
*
* @var string|null
*/
public $highest_license_key = null;
/**
* Pass data from the database. This will be an array with
* the key being the license key, and the value being another
* array with the `pass_id` and `time_checked`.
*
* @var array|null
*/
public $pass_data;
/**
* Whether or not we've stored any pass data yet.
* If no pass data has been stored, then that means the user
* might have a pass activated, we just haven't figured it out
* yet and we're still waiting for the first cron to run.
*
* @see \EDD_License::weekly_license_check()
* @see \EDD_License::activate_license()
*
* @var bool
*/
public $has_pass_data = false;
/**
* Number of license keys entered on this site.
*
* @var int
*/
public $number_license_keys;
/**
* Hierarchy of passes. This helps us determine if one pass
* is "higher" than another.
*
* @see Pass_Manager::pass_compare()
*
* @var int[]
*/
private static $pass_hierarchy = array(
self::PERSONAL_PASS_ID => 10,
self::EXTENDED_PASS_ID => 20,
self::PROFESSIONAL_PASS_ID => 30,
self::ALL_ACCESS_PASS_ID => 40,
self::ALL_ACCESS_PASS_LIFETIME_ID => 50,
);
/**
* The base category assigned to each pass.
*
* @var int[]
*/
public $categories = array(
self::PERSONAL_PASS_ID => 2166,
self::EXTENDED_PASS_ID => 2165,
self::PROFESSIONAL_PASS_ID => 2164,
);
/**
* The pro license.
*
* @since 3.1.1
* @var EDD\Licensing\License
*/
private $pro_license;
/**
* Pass_Manager constructor.
*/
public function __construct() {
$this->pro_license = $this->get_pro_license();
if ( 'valid' === $this->pro_license ) {
$this->highest_license_key = $this->pro_license->key;
$this->highest_pass_id = $this->pro_license->item_id;
$this->has_pass_data = true;
} else {
// Set up the highest pass data.
$pass_data = get_option( 'edd_pass_licenses' );
if ( false !== $pass_data ) {
$this->pass_data = json_decode( $pass_data, true );
$this->has_pass_data = true;
}
$this->set_highest_pass_data();
}
$this->number_license_keys = count( \EDD\Extensions\get_licensed_extension_slugs() );
}
/**
* Gets the highest pass and defines its data.
*
* @since 2.11.4
* @return void
*/
private function set_highest_pass_data() {
if ( ! $this->has_pass_data || ! is_array( $this->pass_data ) ) {
return;
}
$highest_license_key = null;
$highest_pass_id = null;
foreach ( $this->pass_data as $license_key => $pass_data ) {
/*
* If this pass was last verified more than 2 months ago, we're not using it.
* This ensures we never deal with a "stale" record for a pass that's no longer
* actually activated, but still exists in our DB array for some reason.
*
* Our cron job should always be updating with active data once per week.
*/
if ( empty( $pass_data['time_checked'] ) || strtotime( '-2 months' ) > $pass_data['time_checked'] ) {
continue;
}
// We need a pass ID.
if ( empty( $pass_data['pass_id'] ) ) {
continue;
}
// If we don't yet have a "highest pass", then this one is it automatically.
if ( empty( $highest_pass_id ) ) {
$highest_license_key = $license_key;
$highest_pass_id = intval( $pass_data['pass_id'] );
continue;
}
// Otherwise, this pass only takes over the highest pass if it's actually higher.
if ( self::pass_compare( (int) $pass_data['pass_id'], $highest_pass_id, '>' ) ) {
$highest_license_key = $license_key;
$highest_pass_id = intval( $pass_data['pass_id'] );
}
}
$this->highest_license_key = $highest_license_key;
$this->highest_pass_id = $highest_pass_id;
}
/**
* Whether or not a pass is activated.
*
* @since 2.10.6
*
* @return bool
*/
public function has_pass() {
return ! empty( $this->highest_pass_id );
}
/**
* If this is a "free install". That means there are no à la carte or pass licenses activated.
*
* @since 2.11.4
*
* @return bool
*/
public function isFree() {
return 0 === $this->number_license_keys && empty( $this->highest_pass_id );
}
/**
* If this is a "pro install". This means they have the pro version of EDD installed and a valid pass key.
* To check only whether there is an active pass, use `has_pass` instead.
*
* @since 3.1
*
* @return bool
*/
public static function isPro() {
if ( ! edd_is_pro() ) {
return false;
}
$license = ( new self() )->pro_license;
return $license->key && 'valid' === $license->license;
}
/**
* Gets the pro license object.
*
* @since 3.1.1
* @return EDD\Licensing\License
*/
private function get_pro_license() {
return new \EDD\Licensing\License( 'pro' );
}
/**
* If this site has an individual product license active (à la carte), but no pass active.
*
* @since 2.11.4
*
* @return bool
*/
public function hasIndividualLicense() {
return ! $this->isFree() && ! $this->has_pass();
}
/**
* If this site has a Personal Pass active.
*
* @since 2.11.4
*
* @return bool
*/
public function hasPersonalPass() {
try {
return self::pass_compare( $this->highest_pass_id, self::PERSONAL_PASS_ID, '=' );
} catch ( \Exception $e ) {
return false;
}
}
/**
* If this site has an Extended Pass active.
*
* @since 2.11.4
*
* @return bool
*/
public function hasExtendedPass() {
try {
return self::pass_compare( $this->highest_pass_id, self::EXTENDED_PASS_ID, '=' );
} catch ( \Exception $e ) {
return false;
}
}
/**
* If this site has a Professional Pass active.
*
* @since 2.11.4
*
* @return bool
*/
public function hasProfessionalPass() {
try {
return self::pass_compare( $this->highest_pass_id, self::PROFESSIONAL_PASS_ID, '=' );
} catch ( \Exception $e ) {
return false;
}
}
/**
* If this site has an All Access Pass active.
* Note: This uses >= to account for both All Access and lifetime All Access.
*
* @since 2.11.4
*
* @return bool
*/
public function hasAllAccessPass() {
try {
return self::pass_compare( $this->highest_pass_id, self::ALL_ACCESS_PASS_ID, '>=' );
} catch ( \Exception $e ) {
return false;
}
}
/**
* Compares two passes with each other according to the supplied operator.
*
* @since 2.10.6
*
* @param int $pass_1 ID of the first pass.
* @param int $pass_2 ID of the second pass
* @param string $comparison Comparison operator.
*
* @return bool
* @throws \InvalidArgumentException
*/
public static function pass_compare( $pass_1, $pass_2, $comparison = '>' ) {
if ( ! array_key_exists( $pass_1, self::$pass_hierarchy ) ) {
throw new \InvalidArgumentException( 'Invalid pass 1: ' . $pass_1 );
}
if ( ! array_key_exists( $pass_2, self::$pass_hierarchy ) ) {
throw new \InvalidArgumentException( 'Invalid pass 2: ' . $pass_2 );
}
return version_compare( self::$pass_hierarchy[ $pass_1 ], self::$pass_hierarchy[ $pass_2 ], $comparison );
}
/**
* Whether the current pass can access a product by its categories.
*
* @param array $categories The array of a product's categories.
* @return false|int Returns false if the pass cannot access; returns the pass ID if it can.
*/
public function can_access_categories( array $categories ) {
if ( ! $this->has_pass() ) {
return false;
}
if ( $this->hasAllAccessPass() ) {
return $this->highest_pass_id;
}
$categories_to_check = array_intersect( $this->categories, $categories );
if ( empty( $categories_to_check ) ) {
return false;
}
foreach ( $categories_to_check as $category_id ) {
if ( in_array( (int) $category_id, $this->categories, true ) ) {
$pass_id = array_search( (int) $category_id, $this->categories, true );
if ( self::pass_compare( $this->highest_pass_id, $pass_id, '>=' ) ) {
return $pass_id;
}
}
}
return false;
}
/**
* Gets the pass name from an ID.
*
* @since 3.1.1
* @param int $pass_id
* @return string
*/
public function get_pass_name( $pass_id = null ) {
if ( 'valid' === $this->pro_license->license && ! empty( $this->pro_license->item_name ) ) {
return $this->pro_license->item_name;
}
if ( empty( $pass_id ) ) {
$pass_id = $this->highest_pass_id;
}
$names = array(
self::PERSONAL_PASS_ID => __( 'Personal Pass', 'easy-digital-downloads' ),
self::EXTENDED_PASS_ID => __( 'Extended Pass', 'easy-digital-downloads' ),
self::PROFESSIONAL_PASS_ID => __( 'Professional Pass', 'easy-digital-downloads' ),
self::ALL_ACCESS_PASS_ID => __( 'All Access Pass', 'easy-digital-downloads' ),
self::ALL_ACCESS_PASS_LIFETIME_ID => __( 'Lifetime All Access Pass', 'easy-digital-downloads' ),
);
return ! empty( $pass_id ) && array_key_exists( $pass_id, $names ) ? $names[ $pass_id ] : '';
}
/**
* If the supplied license key is for a pass, updates the `edd_pass_licenses` option with
* the pass ID and the date it was checked.
*
* Note: It's intentional that the `edd_pass_licenses` option is always updated, even if
* the provided license data is not for a pass. This is so we have a clearer idea
* of when the checks started coming through. If the option doesn't exist in the DB
* at all, then we haven't checked any licenses.
*
* @since 2.10.6
* @since 3.1.1 Moved from the license handler class to the Pass Manager class.
*
* @param string $license
* @param object $api_data
*/
public function maybe_set_pass_flag( $license, $api_data ) {
$passes = get_option( 'edd_pass_licenses' );
$passes = ! empty( $passes ) ? json_decode( $passes, true ) : array();
if ( ! empty( $api_data->pass_id ) && ! empty( $api_data->license ) && 'valid' === $api_data->license ) {
$passes[ $license ] = array(
'pass_id' => intval( $api_data->pass_id ),
'time_checked' => time(),
);
} elseif ( array_key_exists( $license, $passes ) ) {
unset( $passes[ $license ] );
}
update_option( 'edd_pass_licenses', json_encode( $passes ) );
}
/**
* Removes the pass flag for the supplied license. This happens when a license
* is deactivated.
*
* @since 2.10.6
* @since 3.1.1 Moved from the license handler class to the Pass Manager class.
*
* @param string $license
*/
public function maybe_remove_pass_flag( $license ) {
$passes = get_option( 'edd_pass_licenses' );
$passes = ! empty( $passes ) ? json_decode( $passes, true ) : array();
if ( array_key_exists( $license, $passes ) ) {
unset( $passes[ $license ] );
}
update_option( 'edd_pass_licenses', json_encode( $passes ) );
}
}

View File

@ -0,0 +1,270 @@
<?php
/**
* License Upgrade Notice
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.10.6
*/
namespace EDD\Admin\Promos\Notices;
use EDD\Admin\Pass_Manager;
class License_Upgrade_Notice extends Notice {
const DISPLAY_HOOK = 'in_admin_header';
/**
* @var Pass_Manager
*/
private $pass_manager;
/**
* License_Upgrade_Notice constructor.
*/
public function __construct() {
$this->pass_manager = new Pass_Manager();
}
/**
* This notice lasts 90 days.
*
* @return int
*/
public static function dismiss_duration() {
return 3 * MONTH_IN_SECONDS;
}
/**
* Determines if the current page is an EDD admin page.
*
* @return bool
*/
private function is_edd_admin_page() {
if ( defined( 'EDD_DOING_TESTS' ) && EDD_DOING_TESTS ) {
return true;
}
$screen = get_current_screen();
if ( ! $screen instanceof \WP_Screen || in_array( $screen->id, array( 'dashboard', 'download_page_edd-onboarding-wizard' ), true ) || $screen->is_block_editor() || ! edd_is_admin_page( '', '', false ) ) {
return false;
}
return true;
}
/**
* @inheritDoc
*
* @return bool
*/
protected function _should_display() {
if ( $this->meets_never_display_conditions() ) {
return false;
}
// Someone with no license keys entered always sees a notice.
if ( $this->pass_manager->isFree() ) {
return true;
}
// If we have no pass data yet, don't show the notice because we don't yet know what it should say.
if ( ! $this->pass_manager->has_pass_data ) {
return false;
}
// If someone has an extended pass or higher, and has an active AffiliateWP license, don't show.
try {
if (
$this->pass_manager->has_pass() &&
Pass_Manager::pass_compare( $this->pass_manager->highest_pass_id, Pass_Manager::EXTENDED_PASS_ID, '>=' ) &&
$this->has_affiliate_wp_license() &&
$this->has_mi_license()
) {
return false;
}
} catch ( \Exception $e ) {
return true;
}
return true;
}
/**
* Defines general conditions which mean the license upgrade notice should not display at all.
*
* @since 3.1.1
* @return bool
*/
protected function meets_never_display_conditions() {
if ( ! $this->is_edd_admin_page() ) {
return true;
}
if ( ! get_option( 'edd_onboarding_completed', false ) ) {
return true;
}
return false;
}
/**
* Determines whether or not AffiliateWP is installed and has a license key.
*
* @since 2.10.6
*
* @return bool
*/
private function has_affiliate_wp_license() {
if ( ! function_exists( 'affiliate_wp' ) ) {
return false;
}
return (bool) affiliate_wp()->settings->get( 'license_key' );
}
/**
* Determines whether or not MonsterInsights is installed and has a license key.
*
* @since 2.11.6
*
* @return bool
*/
private function has_mi_license() {
if ( ! class_exists( 'MonsterInsights' ) ) {
return false;
}
$mi_license = \MonsterInsights::$instance->license->get_license_key();
return ! empty( $mi_license );
}
/**
* @inheritDoc
*/
protected function _display() {
try {
if ( $this->pass_manager->isFree() ) {
$utm_parameters = $this->query_args( 'core' );
$link_url = $this->build_url(
'https://easydigitaldownloads.com/lite-upgrade/',
$utm_parameters
);
$help_url = edd_get_admin_url(
array(
'page' => 'edd-settings',
)
);
printf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'You are using the free version of Easy Digital Downloads. %1$sPurchase a pass%2$s to get email marketing tools and recurring payments. Already have a Pass? %3$sActivate it now%4$s', 'easy-digital-downloads' ),
'<a href="' . $link_url . '" target="_blank">',
'</a>',
'<a href="' . esc_url( $help_url ) . '">',
'</a>'
);
} elseif ( ! $this->pass_manager->highest_pass_id ) {
$utm_parameters = $this->query_args( 'extension-license' );
$link_url = $this->build_url(
'https://easydigitaldownloads.com/your-account/',
$utm_parameters
);
// Individual product license active, but no pass.
printf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'For access to additional Easy Digital Downloads extensions to grow your store, consider %1$spurchasing a pass%2$s.', 'easy-digital-downloads' ),
'<a href="' . $link_url . '" target="_blank">',
'</a>'
);
} elseif ( Pass_Manager::pass_compare( $this->pass_manager->highest_pass_id, Pass_Manager::PERSONAL_PASS_ID, '=' ) ) {
$utm_parameters = $this->query_args( 'personal-pass' );
$link_url = $this->build_url(
'https://easydigitaldownloads.com/your-account/',
$utm_parameters
);
// Personal pass active.
printf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'You are using Easy Digital Downloads with a Personal Pass. Consider %1$supgrading%2$s to get recurring payments and more.', 'easy-digital-downloads' ),
'<a href="' . $link_url . '" target="_blank">',
'</a>'
);
} elseif ( Pass_Manager::pass_compare( $this->pass_manager->highest_pass_id, Pass_Manager::EXTENDED_PASS_ID, '>=' ) ) {
if ( ! $this->has_affiliate_wp_license() ) {
$link_url = edd_link_helper(
'https://affiliatewp.com',
array(
'utm_medium' => 'top-promo',
'utm_content' => 'affiliate-wp',
)
);
printf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'Grow your business and make more money with affiliate marketing. %1$sGet AffiliateWP%2$s', 'easy-digital-downloads' ),
'<a href="' . $link_url . '" target="_blank">',
'</a>'
);
} elseif( ! $this->has_mi_license() ) {
printf(
/* Translators: %1$s opening anchor tag; %2$s closing anchor tag */
__( 'Gain access to powerful insights to grow your traffic and revenue. %1$sGet MonsterInsights%2$s', 'easy-digital-downloads' ),
'<a href="' . esc_url( 'https://monsterinsights.com?utm_campaign=xsell&utm_source=eddplugin&utm_content=top-promo' ) . '" target="_blank">',
'</a>'
);
}
}
} catch ( \Exception $e ) {
// If we're in here, that means we have an invalid pass ID... what should we do? :thinking:.
}
}
/**
* Builds the UTM parameters for the URLs.
*
* @since 2.10.6
*
* @param string $upgrade_from License type upgraded from.
* @param string $source Current page.
*
* @return string[]
*/
private function query_args( $upgrade_from, $source = '' ) {
return array(
'utm_medium' => 'top-promo',
'utm_content' => 'upgrade-from-' . urlencode( $upgrade_from ),
);
}
/**
* Build a link with UTM parameters
*
* @since 3.1
*
* @param string $url The Base URL.
* @param array $utm_parameters The UTM tags for the URL.
*
* @return string
*/
private function build_url( $url, $utm_parameters ) {
return esc_url(
edd_link_helper(
$url,
$utm_parameters,
false
)
);
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Show an upgrade notice on the extensions page.
*/
namespace EDD\Admin\Promos\Notices;
class Lite extends Notice {
/**
* Action hook for displaying the notice.
*/
const DISPLAY_HOOK = 'download_page_edd-addons';
/**
* The priority for the display hook.
*/
const DISPLAY_PRIORITY = 5;
/**
* Type of promotional notice.
*/
const TYPE = 'overlay';
/**
* Displays the notice content.
*
* @return void
*/
protected function _display() {
$upgrade_link = edd_link_helper(
'https://easydigitaldownloads.com/lite-upgrade',
array(
'utm_medium' => 'extensions-page-overlay',
'utm_content' => 'upgrade-to-pro',
)
);
?>
<h2><?php esc_html_e( 'Thanks for your interest in Easy Digital Downloads (Pro)!', 'easy-digital-downloads' ); ?></h2>
<p><?php esc_html_e( 'After purchasing a license, just enter your license key on the EDD Settings page. This will let your site automatically upgrade to Easy Digital Downloads (Pro)!', 'easy-digital-downloads' ); ?></p>
<p><?php esc_html_e( '(Don\'t worry, all your products, orders, and settings will be preserved.)', 'easy-digital-downloads' ); ?></p>
<?php $this->do_features(); ?>
<a href="<?php echo esc_attr( $upgrade_link ); ?>" class="button button-primary"><?php esc_html_e( 'Upgrade to Pro', 'easy-digital-downloads' ); ?></a>
<br />
<?php
$this->do_learn_more_link();
}
/**
* Duration (in seconds) that the notice is dismissed for.
* `0` means it's dismissed permanently.
*
* @return int
*/
public static function dismiss_duration() {
return 1;
}
/**
* Outputs the features list.
*
* @since 3.1.1
* @return void
*/
protected function do_features() {
?>
<ul class="edd-promo-notice__features">
<?php
$list_items = array(
'gateways' => __( 'Pro Payment Gateways', 'easy-digital-downloads' ),
'email-marketing' => __( 'Email Marketing Integrations', 'easy-digital-downloads' ),
'subscriptions' => __( 'Sell Subscriptions', 'easy-digital-downloads' ),
'lead-magnets' => __( 'Build Lead Magnets', 'easy-digital-downloads' ),
'bundle' => __( 'Advanced Bundle Features', 'easy-digital-downloads' ),
'automate' => __( 'Automate Your Business', 'easy-digital-downloads' ),
);
foreach ( $list_items as $icon => $label ) {
printf(
'<li><img src="%s" alt=""/>%s</li>',
esc_url( EDD_PLUGIN_URL . "assets/images/icons/icon-{$icon}.svg" ),
esc_html( $label )
);
}
?>
</ul>
<?php
}
/**
* Outputs the "learn more about all extensions" link.
*
* @since 3.1.1
* @return void
*/
protected function do_learn_more_link() {
$support_link = edd_link_helper(
'https://easydigitaldownloads.com/support',
array(
'utm_medium' => 'extensions-page-overlay',
'utm_content' => 'have-questions',
)
);
?>
<a href="<?php echo esc_url( $support_link ); ?>" class="edd-admin-notice-overlay__link"><?php esc_html_e( 'Have a question? Let us know!', 'easy-digital-downloads' ); ?></a>
<?php
}
/**
* @inheritDoc
* @since 3.1.1
* @return bool
*/
protected function _should_display() {
return ! edd_is_pro();
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* Notice
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.10.6
*/
namespace EDD\Admin\Promos\Notices;
use EDD\Admin\Promos\PromoHandler;
abstract class Notice {
/**
* Action hook for displaying the notice.
*/
const DISPLAY_HOOK = 'admin_notices';
/**
* The priority for the display hook.
*/
const DISPLAY_PRIORITY = 10;
/**
* Type of promotional notice.
*/
const TYPE = 'top-of-page';
/**
* Whether or not the notice can be dismissed.
*/
const DISMISSIBLE = true;
/**
* The capability required to view/dismiss the notice.
*/
const CAPABILITY = 'manage_options';
/**
* Displays the notice content.
*
* @return void
*/
abstract protected function _display();
/**
* Generates a unique ID for this notice.
* It's the class name (without the namespace) and with underscores converted to hyphens.
*
* @since 2.10.6
*
* @return string
*/
public function get_id() {
return strtolower( str_replace( '_', '-', basename( str_replace( '\\', '/', get_class( $this ) ) ) ) );
}
/**
* Determines whether or not the notice should be displayed.
* Typically individual notices should not override this method, as it combines
* a dismissal check and custom display logic (`_should_display()`). Custom logic
* should go in `_should_display()`.
*
* @since 2.10.6
*
* @return bool
*/
public function should_display() {
return current_user_can( static::CAPABILITY ) && ! PromoHandler::is_dismissed( $this->get_id() ) && $this->_should_display();
}
/**
* Duration (in seconds) that the notice is dismissed for.
* `0` means it's dismissed permanently.
*
* @return int
*/
public static function dismiss_duration() {
return 0;
}
/**
* Individual notices can override this method to control display logic.
*
* @since 2.10.6
*
* @return bool
*/
protected function _should_display() {
return true;
}
/**
* Displays the notice.
* Individual notices typically should not override this method, as it contains
* all the notice wrapper logic. Instead, notices should override `_display()`
*
* @since 2.10.6
* @return void
*/
public function display() {
?>
<div
id="edd-admin-notice-<?php echo esc_attr( $this->get_id() ); ?>"
class="<?php echo esc_attr( implode( ' ', $this->get_css_classes() ) ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd-dismiss-notice-' . $this->get_id() ) ); ?>"
data-id="<?php echo esc_attr( $this->get_id() ); ?>"
data-lifespan="<?php echo esc_attr( static::dismiss_duration() ); ?>"
>
<?php $this->_display(); ?>
<?php if ( static::DISMISSIBLE ) : ?>
<button class="button-link edd-promo-notice-dismiss">
&times;
<span class="screen-reader-text"><?php esc_html_e( 'Dismiss notice', 'easy-digital-downloads' ); ?></span>
</button>
<?php endif; ?>
</div>
<?php
}
/**
* Gets the array of CSS classes for the notice.
*
* @since 3.1.1
* @return array
*/
protected function get_css_classes() {
$type = sanitize_html_class( static::TYPE );
return array(
"edd-admin-notice-{$type}",
'edd-promo-notice',
);
}
}

View File

@ -0,0 +1,193 @@
<?php
/**
* Promo Handler
*
* Handles logic for displaying and dismissing promotional notices.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 2.10.6
*/
namespace EDD\Admin\Promos;
use EDD\EventManagement\SubscriberInterface;
use EDD\Admin\Promos\Notices\Notice;
use Sandhills\Utils\Persistent_Dismissible;
class PromoHandler implements SubscriberInterface {
/**
* Registered notices.
*
* @var string[]
*/
protected $notices = array(
'\\EDD\\Admin\\Promos\\Notices\\License_Upgrade_Notice',
'\\EDD\\Admin\\Promos\\Notices\\Five_Star_Review_Dashboard',
'\\EDD\\Admin\\Promos\\Notices\\Five_Star_Review_Settings',
'\\EDD\\Admin\\Promos\\Notices\\Lite',
'\\EDD\\Admin\\Onboarding\\Notice',
);
/**
* Notices constructor.
*/
public function __construct() {
$this->load_notices();
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'wp_ajax_edd_dismiss_promo_notice' => 'dismiss_notice',
);
}
/**
* Loads and displays all registered promotional notices.
*
* @since 2.10.6
*/
private function load_notices() {
foreach ( $this->get_notices() as $notice_class_name ) {
if ( ! class_exists( $notice_class_name ) ) {
$file_name = strtolower( str_replace( '_', '-', basename( str_replace( '\\', '/', $notice_class_name ) ) ) );
$file_path = EDD_PLUGIN_DIR . 'includes/admin/promos/notices/class-' . $file_name . '.php';
if ( file_exists( $file_path ) ) {
require_once $file_path;
}
}
if ( ! class_exists( $notice_class_name ) ) {
continue;
}
add_action( $notice_class_name::DISPLAY_HOOK, function () use ( $notice_class_name ) {
/** @var Notice $notice */
$notice = new $notice_class_name();
if ( $notice->should_display() ) {
$notice->display();
}
}, $notice_class_name::DISPLAY_PRIORITY );
}
}
/**
* Gets the notices.
* Implemented as a method so that extending classes can access.
*
* @since 3.1.1
* @return array
*/
protected function get_notices() {
return $this->notices;
}
/**
* Determines whether or not a notice has been dismissed.
*
* @since 2.10.6
*
* @param string $id ID of the notice to check.
*
* @return bool
*/
public static function is_dismissed( $id ) {
$is_dismissed = (bool) Persistent_Dismissible::get( array(
'id' => 'edd-' . $id
) );
return true === $is_dismissed;
}
/**
* Dismisses a notice.
*
* @since 2.10.6
*
* @param string $id ID of the notice to dismiss.
* @param int $dismissal_length Number of seconds to dismiss the notice for, or `0` for forever.
*/
public static function dismiss( $id, $dismissal_length = 0 ) {
Persistent_Dismissible::set( array(
'id' => 'edd-' . $id,
'life' => $dismissal_length
) );
}
/**
* AJAX callback for dismissing a notice.
*
* @since 2.10.6
*/
public function dismiss_notice() {
$notice_id = ! empty( $_POST['notice_id'] ) ? sanitize_text_field( $_POST['notice_id'] ) : false;
if ( empty( $notice_id ) ) {
wp_send_json_error( __( 'Missing notice ID.', 'easy-digital-downloads' ), 400 );
}
if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'edd-dismiss-notice-' . sanitize_key( $_POST['notice_id'] ) ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), 403 );
}
$notice_class_name = $this->get_notice_class_name( $notice_id );
// No matching notice class was found.
if ( ! $notice_class_name ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), 403 );
}
// Check whether the current user can dismiss the notice.
if ( ! defined( $notice_class_name . '::CAPABILITY' ) || ! current_user_can( $notice_class_name::CAPABILITY ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'easy-digital-downloads' ), 403 );
}
$dismissal_length = ! empty( $_POST['lifespan'] ) ? absint( $_POST['lifespan'] ) : 0;
self::dismiss( sanitize_key( $_POST['notice_id'] ), $dismissal_length );
wp_send_json_success();
}
/**
* Gets the notice class name for a given notice ID.
*
* @since 2.11.4
* @param string $notice_id The notice ID to match.
* @return bool|string The class name or false if no matching class was found.
*/
private function get_notice_class_name( $notice_id ) {
$notice_class_name = false;
// Look through the registered notice classes for the one being dismissed.
foreach ( $this->notices as $notice_class_to_check ) {
if ( ! class_exists( $notice_class_to_check ) ) {
$file_name = strtolower( str_replace( '_', '-', basename( str_replace( '\\', '/', $notice_class_to_check ) ) ) );
$file_path = EDD_PLUGIN_DIR . 'includes/admin/promos/notices/class-' . $file_name . '.php';
if ( file_exists( $file_path ) ) {
require_once $file_path;
}
}
if ( ! class_exists( $notice_class_to_check ) ) {
continue;
}
$notice = new $notice_class_to_check();
if ( $notice->get_id() === $notice_id ) {
$notice_class_name = $notice_class_to_check;
break;
}
}
return $notice_class_name;
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Email Marketing
*
* Manages automatic installation/activation for email marketing extensions.
*
* @package EDD
* @subpackage EmailMarketing
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.11.4
*/
namespace EDD\Admin\Settings;
use \EDD\Admin\Extensions\Extension;
use EDD\EventManagement\SubscriberInterface;
class EmailMarketing extends Extension implements SubscriberInterface {
/**
* The EDD settings tab where this extension should show.
*
* @since 2.11.4
* @var string
*/
protected $settings_tab = 'marketing';
/**
* The settings section for this item.
*
* @since 2.11.5
* @var string
*/
protected $settings_section = 'email_marketing';
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_sections_marketing' => 'add_section',
'edd_settings_tab_top_marketing_email_marketing' => 'field',
);
}
/**
* Adds an email marketing section to the Marketing tab.
*
* @since 2.11.4
* @param array $sections
* @return array
*/
public function add_section( $sections ) {
if ( ! $this->is_edd_settings_screen() ) {
return $sections;
}
$product_data = $this->get_product_data();
if ( ! $product_data || ! is_array( $product_data ) ) {
return $sections;
}
$sections[ $this->settings_section ] = __( 'Email Marketing', 'easy-digital-downloads' );
return $sections;
}
/**
* Gets the customized configuration for the extension card.
*
* @since 2.11.4
* @param \EDD\Admin\Extensions\ProductData $product_data The product data object.
* @return array
*/
protected function get_configuration( \EDD\Admin\Extensions\ProductData $product_data ) {
$configuration = array();
if ( ! empty( $product_data->title ) ) {
/* translators: the product name */
$configuration['heading'] = sprintf( __( 'Get %s Today!', 'easy-digital-downloads' ), $product_data->title );
}
return $configuration;
}
/**
* Adds the email marketing extensions as cards.
*
* @since 2.11.4
* @return void
*/
public function field() {
$this->hide_submit_button();
if ( $this->is_activated() ) {
printf( '<p>%s</p>', esc_html__( 'Looks like you have an email marketing extension installed, but we support more providers!', 'easy-digital-downloads' ) );
}
?>
<div class="edd-extension-manager__card-group">
<?php
foreach ( $this->get_product_data() as $item_id => $extension ) {
$this->do_single_extension_card( $item_id );
}
?>
</div>
<?php
}
/**
* Overrides the body array sent to the Products API.
*
* @since 2.11.4
* @return array
*/
protected function get_api_body() {
return array(
'tag' => 1578,
);
}
/**
* Whether any email marketing extension is active.
*
* @since 2.11.4
*
* @return bool True if any email marketing extension is active.
*/
protected function is_activated() {
foreach ( $this->get_product_data() as $extension ) {
// The data is stored in the database as an array--at this point it has not been converted to an object.
if ( ! empty( $extension['basename'] ) && $this->manager->is_plugin_active( $extension['basename'] ) ) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* Invoices
*
* Manages automatic installation/activation for Invoices.
*
* @package EDD
* @subpackage Invoices
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.11.4
*/
namespace EDD\Admin\Settings;
use \EDD\Admin\Extensions\Extension;
use EDD\EventManagement\SubscriberInterface;
class Invoices extends Extension implements SubscriberInterface {
/**
* The product ID on EDD.
*
* @var integer
*/
protected $item_id = 375153;
/**
* The EDD settings tab where this extension should show.
*
* @since 2.11.4
* @var string
*/
protected $settings_tab = 'gateways';
/**
* The settings section for this item.
*
* @since 2.11.5
* @var string
*/
protected $settings_section = 'invoices';
/**
* The pass level required to access this extension.
*/
const PASS_LEVEL = \EDD\Admin\Pass_Manager::EXTENDED_PASS_ID;
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_sections_gateways' => 'add_section',
'edd_settings_tab_top_gateways_invoices' => 'settings_field',
'edd_settings_tab_bottom_gateways_invoices' => 'hide_submit_button',
);
}
/**
* Gets the custom configuration for Invoices.
*
* @since 2.11.4
* @param \EDD\Admin\Extensions\ProductData $product_data The product data object.
* @return array
*/
protected function get_configuration( \EDD\Admin\Extensions\ProductData $product_data ) {
return array(
'style' => 'detailed-2col',
'heading' => 'Attractive Invoices For Your Customers',
'description' => $this->get_custom_description(),
'features' => array(
'Generate Attractive Invoices',
'Build Customer Confidence',
'PDF Download Support',
'Include in Purchase Emails',
'Customizable Templates',
),
);
}
/**
* Gets a custom description for the Invoices extension card.
*
* @since 2.11.4
* @return string
*/
private function get_custom_description() {
$description = array(
'Impress customers and build customer loyalty with attractive invoices. Making it easy to locate, save, and print purchase history builds trust with customers.',
'Provide a professional experience with customizable templates and one-click PDF downloads. ',
);
return $this->format_description( $description );
}
/**
* Adds the Invoices Payments section to the settings.
*
* @param array $sections
* @return array
*/
public function add_section( $sections ) {
if ( ! $this->can_show_product_section() ) {
return $sections;
}
$sections[ $this->settings_section ] = __( 'Invoices', 'easy-digital-downloads' );
return $sections;
}
/**
* Whether EDD Invoices active or not.
*
* @since 2.11.4
*
* @return bool True if Invoices is active.
*/
protected function is_activated() {
if ( $this->manager->is_plugin_active( $this->get_product_data() ) ) {
return true;
}
return class_exists( 'EDDInvoices' );
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* Recurring Payments
*
* Manages automatic activation for Recurring Payments.
*
* @package EDD
* @subpackage Recurring
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.11.4
*/
namespace EDD\Admin\Settings;
use \EDD\Admin\Extensions\Extension;
use EDD\EventManagement\SubscriberInterface;
class Recurring extends Extension implements SubscriberInterface {
/**
* The product ID on EDD.
*
* @var integer
*/
protected $item_id = 28530;
/**
* The EDD settings tab where this extension should show.
*
* @since 2.11.4
* @var string
*/
protected $settings_tab = 'gateways';
/**
* The pass level required to access this extension.
*/
const PASS_LEVEL = \EDD\Admin\Pass_Manager::EXTENDED_PASS_ID;
/**
* The settings section for this item.
*
* @since 2.11.5
* @var string
*/
protected $settings_section = 'recurring';
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_sections_gateways' => 'add_section',
'edd_settings_tab_top_gateways_recurring' => 'settings_field',
'edd_settings_tab_bottom_gateways_recurring' => 'hide_submit_button',
);
}
/**
* Gets the custom configuration for Recurring.
*
* @since 2.11.4
* @param \EDD\Admin\Extensions\ProductData $product_data The product data object.
* @return array
*/
protected function get_configuration( \EDD\Admin\Extensions\ProductData $product_data ) {
return array(
'style' => 'detailed-2col',
'heading' => 'Increase Revenue By Selling Subscriptions!',
'description' => $this->get_custom_description(),
'features' => array(
'Flexible Recurring Payments',
'Custom Reminder Emails',
'Free Trial Support',
'Signup Fees',
'Recurring Revenue Reports',
),
);
}
/**
* Gets a custom description for the Recurring extension card.
*
* @since 2.11.4
* @return string
*/
private function get_custom_description() {
$description = array(
'Grow stable income by selling subscriptions and make renewals hassle free for your customers.',
'When your customers are automatically billed, you reduce the risk of missed payments and retain more customers.',
);
return $this->format_description( $description );
}
/**
* Adds the Recurring Payments section to the settings.
*
* @param array $sections
* @return array
*/
public function add_section( $sections ) {
if ( ! $this->can_show_product_section() ) {
return $sections;
}
$sections[ $this->settings_section ] = __( 'Recurring Payments', 'easy-digital-downloads' );
return $sections;
}
/**
* Whether EDD Recurring active or not.
*
* @since 2.11.4
*
* @return bool True if Recurring is active.
*/
protected function is_activated() {
if ( $this->manager->is_plugin_active( $this->get_product_data() ) ) {
return true;
}
return class_exists( 'EDD_Recurring' );
}
}

View File

@ -0,0 +1,157 @@
<?php
/**
* Reviews
*
* Manages automatic activation for Reviews.
*
* @package EDD
* @subpackage Reviews
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.11.4
*/
namespace EDD\Admin\Settings;
use \EDD\Admin\Extensions\Extension;
use EDD\EventManagement\SubscriberInterface;
class Reviews extends Extension implements SubscriberInterface {
/**
* The product ID on EDD.
*
* @var integer
*/
protected $item_id = 37976;
/**
* The EDD settings tab where this extension should show.
*
* @since 2.11.4
* @var string
*/
protected $settings_tab = 'marketing';
/**
* The settings section for this item.
*
* @since 2.11.5
* @var string
*/
protected $settings_section = 'reviews';
/**
* The pass level required to access this extension.
*/
const PASS_LEVEL = \EDD\Admin\Pass_Manager::EXTENDED_PASS_ID;
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_sections_marketing' => 'add_section',
'edd_settings_tab_top_marketing_reviews' => 'settings_field',
'edd_settings_tab_bottom_marketing_reviews' => 'hide_submit_button',
'add_meta_boxes' => 'maybe_do_metabox',
);
}
/**
* Gets the custom configuration for Reviews.
*
* @since 2.11.4
* @param \EDD\Admin\Extensions\ProductData $product_data The product data object.
* @return array
*/
protected function get_configuration( \EDD\Admin\Extensions\ProductData $product_data ) {
$configuration = array(
'heading' => 'Build Trust With Real Customer Reviews',
);
$settings_configuration = array(
'style' => 'detailed-2col',
'description' => $this->get_custom_description(),
'features' => array(
'Request Reviews',
'Incentivize Reviewers',
'Full Schema.org Support',
'Embed Reviews Via Blocks',
'Limit Reviews to Customers',
'Vendor Reviews (with Frontend Submissions)',
),
);
return $this->is_edd_settings_screen() ? array_merge( $configuration, $settings_configuration ) : $configuration;
}
/**
* Gets a custom description for the Reviews extension card.
*
* @since 2.11.4
* @return string
*/
private function get_custom_description() {
$description = array(
'Increase sales on your site with social proof. 70% of online shoppers don\'t purchase before reading reviews.',
'Easily collect, manage, and beautifully display reviews all from your WordPress dashboard.',
);
return $this->format_description( $description );
}
/**
* Adds the Reviews section to the settings.
*
* @param array $sections
* @return array
*/
public function add_section( $sections ) {
if ( ! $this->can_show_product_section() ) {
return $sections;
}
$sections[ $this->settings_section ] = __( 'Reviews', 'easy-digital-downloads' );
return $sections;
}
/**
* If Reviews is not active, registers a metabox on individual download edit screen.
*
* @since 2.11.4
* @return void
*/
public function maybe_do_metabox() {
if ( ! $this->is_download_edit_screen() ) {
return;
}
if ( $this->is_activated() ) {
return;
}
add_meta_box(
'edd-reviews-status',
__( 'Product Reviews', 'easy-digital-downloads' ),
array( $this, 'settings_field' ),
'download',
'side',
'low'
);
}
/**
* Whether EDD Reviews active or not.
*
* @since 2.11.4
*
* @return bool True if Reviews is active.
*/
protected function is_activated() {
if ( $this->manager->is_plugin_active( $this->get_product_data() ) ) {
return true;
}
return function_exists( 'edd_reviews' );
}
}

View File

@ -0,0 +1,242 @@
<?php
/**
* WP Mail SMTP
*
* Manages automatic installation/activation for WP Mail SMTP.
*
* @package EDD
* @subpackage WP_SMTP
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.11.4
*/
namespace EDD\Admin\Settings;
use EDD\EventManagement\SubscriberInterface;
class WP_SMTP implements SubscriberInterface {
/**
* Array of configuration data for WP Mail SMTP.
*
* @var array
*/
private $config = array(
'lite_plugin' => 'wp-mail-smtp/wp_mail_smtp.php',
'lite_wporg_url' => 'https://wordpress.org/plugins/wp-mail-smtp/',
'lite_download_url' => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
'pro_plugin' => 'wp-mail-smtp-pro/wp_mail_smtp.php',
'smtp_settings' => 'admin.php?page=wp-mail-smtp',
'smtp_wizard' => 'admin.php?page=wp-mail-smtp-setup-wizard',
);
/**
* The Extension Manager
*
* @var \EDD\Admin\Extensions\Extension_Manager
*/
private $manager;
public function __construct() {
$this->manager = new \EDD\Admin\Extensions\Extension_Manager();
}
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'edd_settings_emails' => 'register_setting',
'edd_wpsmtp' => 'settings_field',
);
}
/**
* Register the setting to show the WP SMTP installer if it isn't active.
*
* @param array $settings
* @return array
*/
public function register_setting( $settings ) {
if ( $this->is_smtp_configured() ) {
return $settings;
}
$settings['main']['wpsmtp'] = array(
'id' => 'wpsmtp',
'name' => __( 'Improve Email Deliverability', 'easy-digital-downloads' ),
'desc' => '',
'type' => 'hook',
);
return $settings;
}
/**
* Output the settings field (installation helper).
*
* @param array $args
* @return void
*/
public function settings_field( $args ) {
$this->manager->enqueue();
?>
<div class="edd-extension-manager__body">
<p class="edd-extension-manager__description">
<?php esc_html_e( 'WP Mail SMTP allows you to easily set up WordPress to use a trusted provider to reliably send emails, including sales notifications.', 'easy-digital-downloads' ); ?>
</p>
<div class="edd-extension-manager__group edd-extension-manager__actions">
<div class="edd-extension-manager__step">
<?php $this->manager->button( $this->get_button_parameters() ); ?>
</div>
<?php
if ( $this->is_smtp_activated() ) {
return;
}
?>
<div class="edd-extension-manager__step" style="display:none;">
<?php $this->manager->link( $this->get_link_parameters() ); ?>
</div>
</div>
</div>
<?php
}
/**
* Gets the button parameters.
*
* @return array
*/
private function get_button_parameters() {
$button = array();
// If neither the lite nor pro plugin is installed, the button will prompt to install and activate the lite plugin.
if ( ! $this->manager->is_plugin_installed( $this->config['lite_plugin'] ) && ! $this->manager->is_plugin_installed( $this->config['pro_plugin'] ) ) {
$button['plugin'] = $this->config['lite_download_url'];
$button['action'] = 'install';
$button['button_text'] = __( 'Install & Activate WP Mail SMTP', 'easy-digital-downloads' );
} elseif ( ! $this->is_smtp_activated() ) {
// If one of the SMTP plugins is installed, but not activated, the button will prompt to activate it.
$button['plugin'] = $this->config['lite_plugin'];
$button['action'] = 'activate';
$button['button_text'] = __( 'Activate WP Mail SMTP', 'easy-digital-downloads' );
} elseif ( ! $this->is_smtp_configured() ) {
// If the plugin is active, but not configured, the button will send them to the setup wizard.
$button = $this->get_link_parameters();
}
return $button;
}
/**
* Gets the array of parameters for the link to configure WP Mail SMTP.
*
* @since 2.11.4
* @return array
*/
private function get_link_parameters() {
return $this->is_smtp_configured() ?
array(
'button_text' => __( 'Configure WP Mail SMTP', 'easy-digital-downloads' ),
'href' => admin_url( $this->config['smtp_settings'] ),
) :
array(
'button_text' => __( 'Run the WP Mail SMTP Setup Wizard', 'easy-digital-downloads' ),
'href' => admin_url( $this->config['smtp_wizard'] ),
);
}
/**
* Whether WP Mail SMTP plugin configured or not.
*
* @since 2.11.4
*
* @return bool True if some mailer is selected and configured properly.
*/
protected function is_smtp_configured() {
if ( ! $this->is_smtp_activated() || ! class_exists( '\\WPMailSMTP\\Options' ) ) {
return false;
}
$phpmailer = $this->get_phpmailer();
$mailer = \WPMailSMTP\Options::init()->get( 'mail', 'mailer' );
$is_mailer_complete = ! empty( $mailer ) && wp_mail_smtp()->get_providers()->get_mailer( $mailer, $phpmailer )->is_mailer_complete();
return 'mail' !== $mailer && $is_mailer_complete;
}
/**
* Whether WP Mail SMTP plugin active or not.
*
* @since 2.11.4
*
* @return bool True if SMTP plugin is active.
*/
protected function is_smtp_activated() {
return function_exists( 'wp_mail_smtp' ) && ( is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] ) );
}
/**
* Get $phpmailer instance.
*
* @since 2.11.4
*
* @return \PHPMailer|\PHPMailer\PHPMailer\PHPMailer Instance of PHPMailer.
*/
protected function get_phpmailer() {
if ( version_compare( get_bloginfo( 'version' ), '5.5-alpha', '<' ) ) {
$phpmailer = $this->get_phpmailer_v5();
} else {
$phpmailer = $this->get_phpmailer_v6();
}
return $phpmailer;
}
/**
* Get $phpmailer v5 instance.
*
* @since 2.11.4
*
* @return \PHPMailer Instance of PHPMailer.
*/
private function get_phpmailer_v5() {
global $phpmailer;
if ( ! ( $phpmailer instanceof \PHPMailer ) ) {
require_once ABSPATH . WPINC . '/class-phpmailer.php';
require_once ABSPATH . WPINC . '/class-smtp.php';
$phpmailer = new \PHPMailer( true ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
return $phpmailer;
}
/**
* Get $phpmailer v6 instance.
*
* @since 2.11.4
*
* @return \PHPMailer\PHPMailer\PHPMailer Instance of PHPMailer.
*/
private function get_phpmailer_v6() {
global $phpmailer;
if ( ! ( $phpmailer instanceof \PHPMailer\PHPMailer\PHPMailer ) ) {
require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
$phpmailer = new \PHPMailer\PHPMailer\PHPMailer( true ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
return $phpmailer;
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* Core class for adding event subscribers.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD;
class Core extends EventManagement\Subscribers {
/**
* Gets the service providers for EDD.
*
* @return array
*/
protected function get_service_providers() {
return array(
new Admin\PassHandler\Ajax( $this->pass_handler ),
new Admin\Extensions\Extension_Manager(),
);
}
/**
* Gets the admin service providers.
*
* @since 3.1.1
* @return array
*/
protected function get_admin_providers() {
if ( ! is_admin() ) {
return array();
}
return array(
new Admin\PassHandler\Settings( $this->pass_handler ),
new Admin\PassHandler\Actions( $this->pass_handler ),
new Admin\Extensions\Menu(),
new Admin\Settings\EmailMarketing(),
new Admin\Settings\Invoices(),
new Admin\Settings\Recurring(),
new Admin\Settings\Reviews(),
new Admin\Settings\WP_SMTP(),
new Admin\Downloads\Meta(),
new Admin\Onboarding\Tools(),
new Admin\Onboarding\Wizard(),
new Admin\Onboarding\Ajax(),
new Licensing\Ajax(),
);
}
/**
* Gets providers that may be extended/replaced in lite/pro.
*
* @return array
*/
protected function get_replaceable_providers() {
return array(
new Admin\Extensions\Legacy(),
new Admin\Promos\PromoHandler(),
);
}
}

View File

@ -0,0 +1,242 @@
<?php
/**
* Currency
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 3.0
*/
namespace EDD\Currency;
class Currency {
/**
* @var string Currency code.
*/
public $code;
/**
* @var string Symbol/text to display before amounts.
*/
public $prefix;
/**
* @var string Symbol/text to display after amounts.
*/
public $suffix;
/**
* @var string Symbol to use in the prefix/suffix.
*/
public $symbol;
/**
* @var string Decimal separator.
*/
public $decimal_separator = '.';
/**
* @var string Thousands separator.
*/
public $thousands_separator = ',';
/**
* @var int Number of decimals.
*/
public $number_decimals = 2;
/**
* @var string Currency position.
*/
public $position = 'before';
/**
* Currency constructor.
*
* @param string $currency_code
*/
public function __construct( $currency_code ) {
$this->code = strtoupper( $currency_code );
$this->setup();
}
/**
* Returns a new currency object.
*
* @param string $currency_code
*
* @since 3.0
*
* @return Currency
*/
public static function from_code( $currency_code ) {
return new self( $currency_code );
}
/**
* Sets up properties.
*
* @since 3.0
*/
private function setup() {
$this->symbol = $this->get_symbol();
$this->decimal_separator = edd_get_option( 'decimal_separator', '.' );
$this->thousands_separator = edd_get_option( 'thousands_separator', ',' );
$this->position = edd_get_option( 'currency_position', 'before' );
/**
* Filters the decimal separator.
*
* @param string $decimal_separator
* @param string $code
*
* @since 3.0
*/
$this->decimal_separator = apply_filters( 'edd_currency_decimal_separator', $this->decimal_separator, $this->code );
/**
* Filters the thousands separator.
*
* @param string $thousands_separator
* @param string $code
*
* @since 3.0
*/
$this->thousands_separator = apply_filters( 'edd_currency_thousands_separator', $this->thousands_separator, $this->code );
$separator = $this->_has_space_around_symbol() ? ' ' : '';
if ( 'before' === $this->position ) {
$this->prefix = $this->symbol . $separator;
} else {
$this->suffix = $separator . $this->symbol;
}
/**
* Filters the currency prefix.
*
* @param string $prefix
* @param string $code
*
* @since 3.0
*/
$this->prefix = apply_filters( 'edd_currency_prefix', $this->prefix, $this->code );
/**
* Filters the currency suffix.
*
* @param string $prefix
* @param string $code
*
* @since 3.0
*/
$this->suffix = apply_filters( 'edd_currency_suffix', $this->suffix, $this->code );
$this->number_decimals = $this->_is_zero_decimal() ? 0 : 2;
}
/**
* Whether or not this currency has a space between the symbol and the amount.
*
* @since 3.0
*
* @return bool
*/
private function _has_space_around_symbol() {
return ! in_array( $this->code, array(
'GBP',
'BRL',
'EUR',
'USD',
'AUD',
'CAD',
'HKD',
'MXN',
'SGD',
'JPY'
) );
}
/**
* Returns the symbol for this currency.
* Depending on settings, this will be used as either the prefix or the suffix.
*
* @since 3.0
* @return string
*/
public function get_symbol() {
switch ( $this->code ) :
case "GBP" :
$symbol = '&pound;';
break;
case "BRL" :
$symbol = 'R&#36;';
break;
case "EUR" :
$symbol = '&euro;';
break;
case "USD" :
case "AUD" :
case "NZD" :
case "CAD" :
case "HKD" :
case "MXN" :
case "SGD" :
$symbol = '&#36;';
break;
case "JPY" :
$symbol = '&yen;';
break;
case "AOA" :
$symbol = 'Kz';
break;
default :
$symbol = $this->code;
break;
endswitch;
/**
* Filters the currency symbol.
*
* @since unknown
*
* @param string $symbol Currency symbol.
* @param string $code Currency code.
*/
return apply_filters( 'edd_currency_symbol', $symbol, $this->code );
}
/**
* Determines whether or not the currency is zero-decimal.
*
* @since 3.0
*
* @return bool
*/
private function _is_zero_decimal() {
$currencies = array(
'bif',
'clp',
'djf',
'gnf',
'huf',
'jpy',
'kmf',
'krw',
'mga',
'pyg',
'rwf',
'ugx',
'vnd',
'vuv',
'xaf',
'xof',
'xpf',
);
return in_array( strtolower( $this->code ), $currencies, true );
}
}

View File

@ -0,0 +1,232 @@
<?php
/**
* Money Formatter
*
* Formats an amount of money in various ways, according to the provided currency.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license GPL2+
* @since 3.0
*/
namespace EDD\Currency;
class Money_Formatter {
/**
* @var int|float Current working amount.
*/
public $amount;
/**
* @var float Typed amount.
*/
public $typed_amount;
/**
* @var int|float Original, unmodified amount passed in via constructor.
*/
private $original_amount;
/**
* @var Currency
*/
private $currency;
/**
* Money_Formatter constructor.
*
* @param $amount
* @param Currency $currency
*/
public function __construct( $amount, Currency $currency ) {
$this->original_amount = $amount;
$this->amount = $amount;
$this->typed_amount = $amount;
$this->currency = $currency;
}
/**
* Un-formats an amount.
* This ensures the amount is put into a state where we can perform mathematical
* operations on it --- that means using `.` as the decimal separator and no
* thousands separator.
*
* @return float|int
*/
private function unformat() {
$amount = $this->original_amount;
$sep_found = strpos( $amount, $this->currency->decimal_separator );
if ( ',' === $this->currency->decimal_separator && false !== $sep_found ) {
$whole = substr( $amount, 0, $sep_found );
$part = substr( $amount, $sep_found + 1, ( strlen( $amount ) - 1 ) );
$amount = $whole . '.' . $part;
}
// Strip "," and " " from the amount (if set as the thousands separator).
foreach ( array( ',', ' ' ) as $thousands_separator ) {
if ( $thousands_separator === $this->currency->thousands_separator && false !== strpos( $amount, $this->currency->thousands_separator ) ) {
$amount = str_replace( $thousands_separator, '', $amount );
}
}
return $amount;
}
/**
* Returns the number of decimals ot use for the formatted amount.
*
* Based on the currency code used when instantiating the class, determines how many
* decimal points the value should have once formatted.
*
* @param bool $decimals If we should include decimals or not in the formatted amount.
* @param float $amount The amount to format.
*
* @return int The number of decimals places to use when formatting the amount.
*/
private function get_decimals( $decimals, $amount ) {
/**
* Filter number of decimals to use for formatted amount
*
* @since unknown
*
* @param int $number Default 2. Number of decimals.
* @param int|string $amount Amount being formatted.
* @param string $currency_code Currency code being formatted.
*/
return apply_filters( 'edd_format_amount_decimals', $decimals ? $this->currency->number_decimals : 0, $amount, $this->currency->code );
}
/**
* Formats the amount for display.
* Does not apply the currency code.
*
* @since 3.0
*
* @param bool $decimals If we should include decimal places or not when formatting.
*
* @return Money_Formatter
*/
public function format_for_display( $decimals = true ) {
$amount = $this->unformat();
if ( empty( $amount ) ) {
$amount = 0;
}
$decimals = $this->get_decimals( $decimals, $amount );
// Format amount using decimals and separators (also rounds up or down).
$formatted = number_format( (float) $amount, $decimals, $this->currency->decimal_separator, $this->currency->thousands_separator );
/**
* Filter the formatted amount before returning
*
* @since unknown
*
* @param mixed $formatted Formatted amount.
* @param mixed $amount Original amount.
* @param int $decimals Default 2. Number of decimals.
* @param string $decimal_separator Default '.'. Decimal separator.
* @param string $thousands_separator Default ','. Thousands separator.
* @param string $currency_code Currency used for formatting.
*/
$this->amount = apply_filters( 'edd_format_amount', $formatted, $amount, $decimals, $this->currency->decimal_separator, $this->currency->thousands_separator, $this->currency->code );
return $this;
}
/**
* Formats the amount for typed data returns.
* Does not apply the currency code and returns a foat instead of a string.
*
* @since 3.0
*
* @param bool $decimals If we should include decimal places or not when formatting.
*
* @return Money_Formatter
*/
public function format_for_typed( $decimals = true ) {
$amount = $this->unformat();
if ( empty( $amount ) ) {
$amount = 0;
}
$decimals = $this->get_decimals( $decimals, $amount );
/**
* Since we want to return a float value here, intentionally only supply a decimal separator.
*
* The separators here are hard coded intentionally as we're looking to get truncated, raw format of float
* which requires '.' for decimal separators and no thousands separator.
*
* This is also intentionally not filtered for the time being.
*/
$formatted = floatval( number_format( (float) $amount, $decimals, '.', '' ) );
// Set the amount to $this->amount.
$this->typed_amount = $formatted;
return $this;
}
/**
* Applies the currency prefix/suffix to the amount.
*
* @since 3.0
* @return string
*/
public function apply_symbol() {
$amount = $this->amount;
$is_negative = is_numeric( $this->amount ) && $this->amount < 0;
// Remove "-" from start.
if ( $is_negative ) {
$amount = substr( $amount, 1 );
}
$formatted = '';
if ( ! empty( $this->currency->prefix ) ) {
$formatted .= $this->currency->prefix;
}
$formatted .= $amount;
if ( ! empty( $this->currency->suffix ) ) {
$formatted .= $this->currency->suffix;
}
if ( ! empty( $this->currency->prefix ) ) {
/**
* Filters the output with a prefix.
*
* @param string $formatted
* @param string $currency_code
* @param string $amount
*/
$formatted = apply_filters( 'edd_' . strtolower( $this->currency->code ) . '_currency_filter_before', $formatted, $this->currency->code, $amount );
}
if ( ! empty( $this->currency->suffix ) ) {
/**
* Filters the output with a suffix.
*
* @param string $formatted
* @param string $currency_code
* @param string $amount
*/
$formatted = apply_filters( 'edd_' . strtolower( $this->currency->code ) . '_currency_filter_after', $formatted, $this->currency->code, $amount );
}
// Add the "-" sign back to the start of the string.
if ( $is_negative ) {
$formatted = '-' . $formatted;
}
return $formatted;
}
}

View File

@ -0,0 +1,248 @@
<?php
/**
* Notifications Database
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license GPL2+
* @since 2.11.4
*/
namespace EDD\Database;
use EDD\Models\Notification;
use EDD\Utils\EnvironmentChecker;
use EDD\Utils\NotificationImporter;
class NotificationsDB {
/**
* Constructor
*/
public function __construct() {
add_action( 'edd_daily_scheduled_events', array( $this, 'schedule_daily_notification_checks' ) );
}
/**
* Add a cron event to check for new notifications.
*
* @since 2.11.4
*/
public static function schedule_daily_notification_checks() {
$importer = new NotificationImporter();
$importer->run();
}
/**
* Let MySQL handle most of the defaults.
* We just set the dates here to ensure they get saved in UTC.
*
* @since 2.11.4
*
* @return array
*/
public function get_column_defaults() {
return array(
'date_created' => gmdate( 'Y-m-d H:i:s' ),
'date_updated' => gmdate( 'Y-m-d H:i:s' ),
);
}
/**
* Adds or updates a local notification.
*
* @param array $data
* @return false|int Returns false if the notification could not be added/updated; the ID of the notification if it could.
*/
public function maybe_add_local_notification( $data = array() ) {
// A remote_id is required and it cannot be numeric for local notifications.
if ( empty( $data['remote_id'] ) || is_numeric( $data['remote_id'] ) ) {
return false;
}
// The source is always always local.
$data['source'] = 'local';
$existing = $this->get_item_by( 'remote_id', $data['remote_id'] );
if ( $existing ) {
return $this->update(
$existing->id,
$data
);
}
return $this->insert( $data );
}
/**
* JSON-encodes any relevant columns.
*
* @since 2.11.4
*
* @param array $data
*
* @return array
*/
protected function maybeJsonEncode( $data ) {
$jsonColumns = array( 'buttons', 'conditions' );
foreach ( $jsonColumns as $column ) {
if ( ! empty( $data[ $column ] ) && is_array( $data[ $column ] ) ) {
$data[ $column ] = json_encode( $data[ $column ] );
}
}
return $data;
}
/**
* Inserts a new notification.
*
* @since 2.11.4
*
* @param array $data
* @param string $type
*
* @return int
*/
public function insert( $data, $type = 'notification' ) {
$data = $this->maybeJsonEncode( $data );
$notifications = new \EDD\Database\Queries\Notification();
$result = $notifications->add_item( $data );
wp_cache_delete( 'edd_active_notification_count', 'edd_notifications' );
return $result;
}
/**
* Updates an existing notification.
*
* @since 2.11.4
*
* @param int $row_id
* @param array $data
* @param string $where
*
* @return bool
*/
public function update( $row_id, $data = array(), $where = '' ) {
$notifications = new \EDD\Database\Queries\Notification();
return $notifications->update_item( $row_id, $this->maybeJsonEncode( $data ) );
}
/**
* Gets a notification by ID.
*
* @param int $id
* @return false|Notification
*/
public function get( $id ) {
$notifications = new \EDD\Database\Queries\Notification();
return $notifications->get_item( $id );
}
/**
* Gets an item by the column name and value.
*
* @param string $column_name
* @param string $column_value
* @return false|Notification
*/
public function get_item_by( $column_name = '', $column_value = '' ) {
$notifications = new \EDD\Database\Queries\Notification();
return $notifications->get_item_by( $column_name, $column_value );
}
/**
* Returns all notifications that have not been dismissed and should be
* displayed on this site.
*
* @since 2.11.4
*
* @param bool $conditionsOnly If set to true, then only the `conditions` column is retrieved
* for each notification.
*
* @return Notification[]
*/
public function getActiveNotifications( $conditionsOnly = false ) {
global $wpdb;
$environmentChecker = new EnvironmentChecker();
$notifications = $wpdb->get_results( $this->getActiveQuery( $conditionsOnly ) );
$models = array();
if ( is_array( $notifications ) ) {
foreach ( $notifications as $notification ) {
$model = new Notification( (array) $notification );
try {
// Only add to the array if all conditions are met or if the notification has no conditions.
if (
! $model->conditions ||
( is_array( $model->conditions ) && $environmentChecker->meetsConditions( $model->conditions ) )
) {
$models[] = $model;
}
} catch ( \Exception $e ) {
}
}
}
unset( $notifications );
return $models;
}
/**
* Builds the query for selecting or counting active notifications.
*
* @since 2.11.4
*
* @param bool $conditionsOnly
*
* @return string
*/
private function getActiveQuery( $conditionsOnly = false ) {
global $wpdb;
$select = $conditionsOnly ? 'conditions' : '*';
return $wpdb->prepare(
"SELECT {$select} FROM {$wpdb->edd_notifications}
WHERE dismissed = 0
AND (start <= %s OR start IS NULL)
AND (end >= %s OR end IS NULL)
ORDER BY start DESC, id DESC",
gmdate( 'Y-m-d H:i:s' ),
gmdate( 'Y-m-d H:i:s' )
);
}
/**
* Counts the number of active notifications.
* Note: We can't actually do a real `COUNT(*)` on the database, because we want
* to double-check the conditions are met before displaying. That's why we use
* `getActiveNotifications()` which runs the conditions through the EnvironmentChecker.
*
* @since 2.11.4
*
* @return int
*/
public function countActiveNotifications() {
$numberActive = wp_cache_get( 'edd_active_notification_count', 'edd_notifications' );
if ( false === $numberActive ) {
$numberActive = count( $this->getActiveNotifications( true ) );
wp_cache_set( 'edd_active_notification_count', $numberActive, 'edd_notifications' );
}
return $numberActive;
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Notification Query Class.
*
* @package EDD
* @subpackage Database\Queries
* @copyright Copyright (c) 2023, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Database\Queries;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
use EDD\Database\Query;
/**
* Class used for querying items.
*
* @since 3.1.1
*
* @see \EDD\Database\Queries\Notification::__construct() for accepted arguments.
*/
class Notification extends Query {
/** Table Properties ******************************************************/
/**
* Name of the database table to query.
*
* @since 3.1.1
* @access public
* @var string
*/
protected $table_name = 'notifications';
/**
* String used to alias the database table in MySQL statement.
*
* @since 3.1.1
* @access public
* @var string
*/
protected $table_alias = 'n';
/**
* Name of class used to setup the database schema
*
* @since 3.1.1
* @access public
* @var string
*/
protected $table_schema = '\\EDD\\Database\\Schemas\\Notifications';
/** Item ******************************************************************/
/**
* Name for a single item
*
* @since 3.1.1
* @access public
* @var string
*/
protected $item_name = 'notification';
/**
* Plural version for a group of items.
*
* @since 3.1.1
* @access public
* @var string
*/
protected $item_name_plural = 'notifications';
/**
* Callback function for turning IDs into objects
*
* @since 3.1.1
* @access public
* @var mixed
*/
protected $item_shape = 'EDD\\Notifications\\Notification';
/** Cache *****************************************************************/
/**
* Group to cache queries and queried items in.
*
* @since 3.1.1
* @access public
* @var string
*/
protected $cache_group = 'notifications';
/** Methods ***************************************************************/
/**
* Sets up the query, based on the query vars passed.
*
* @since 3.1.1
* @access public
*
* @param string|array $query {
* Optional. Array or query string of query parameters. Default empty.
*
* @type int $id An notification ID to only return that notification. Default empty.
* @type array $id__in Array of notification IDs to include. Default empty.
* @type array $id__not_in Array of notification IDs to exclude. Default empty.
* }
*/
public function __construct( $query = array() ) {
parent::__construct( $query );
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Notification Database Object Class.
*
* @package EDD
* @subpackage Database\Rows
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Database\Rows;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
use EDD\Database\Row;
/**
* Notification database row class.
*
* This class exists solely to encapsulate database schema changes, to help
* separate the needs of the application layer from the requirements of the
* database layer.
*
* For example, if a database column is renamed or a return value needs to be
* formatted differently, this class will make sure old values are still
* supported and new values do not conflict.
*
* @since 3.1.1
*/
class Notification extends Row {
}

View File

@ -0,0 +1,151 @@
<?php
/**
* Notifications Schema Class.
*
* @package EDD
* @subpackage Database\Schemas
* @copyright Copyright (c) 2023, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Database\Schemas;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
use EDD\Database\Schema;
/**
* Notifications Schema Class.
*
* @since 3.1.1
*/
class Notifications extends Schema {
/**
* Array of database column objects
*
* @since 3.1.1
* @access public
* @var array
*/
public $columns = array(
// id
array(
'name' => 'id',
'type' => 'bigint',
'length' => '20',
'unsigned' => true,
'extra' => 'auto_increment',
'primary' => true,
'sortable' => true,
),
// remote_id
array(
'name' => 'remote_id',
'type' => 'varchar',
'length' => '20',
'default' => null,
),
// source
array(
'name' => 'source',
'type' => 'varchar',
'default' => 'api',
'allow_null' => false,
),
// title
array(
'name' => 'title',
'type' => 'text',
'default' => '',
'allow_null' => false,
),
// content
array(
'name' => 'content',
'type' => 'longtext',
'default' => '',
'allow_null' => false,
),
// buttons
array(
'name' => 'buttons',
'type' => 'longtext',
'default' => null,
'allow_null' => true,
),
// type
array(
'name' => 'type',
'type' => 'varchar',
'length' => '64',
'allow_null' => true,
),
// conditions
array(
'name' => 'conditions',
'type' => 'longtext',
'default' => null,
'allow_null' => true,
),
// start
array(
'name' => 'start',
'type' => 'datetime',
'default' => null,
'date_query' => true,
'sortable' => true,
'allow_null' => true,
),
// end
array(
'name' => 'end',
'type' => 'datetime',
'default' => null,
'date_query' => true,
'sortable' => true,
'allow_null' => true,
),
// dismissed
array(
'name' => 'dismissed',
'type' => 'tinyint',
'length' => '1',
'unsigned' => true,
'allow_null' => true,
'default' => 0,
),
// date_created
array(
'name' => 'date_created',
'type' => 'datetime',
'default' => '', // Defaults to current time in query class
'date_query' => true,
'sortable' => true,
'created' => true,
),
// date_updated
array(
'name' => 'date_updated',
'type' => 'datetime',
'default' => '', // Defaults to current time in query class
'date_query' => true,
'sortable' => true,
'modified' => true,
),
);
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Notifications Table.
*
* @package EDD
* @subpackage Database\Tables
* @copyright Copyright (c) 2023, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.1.1
*/
namespace EDD\Database\Tables;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
use EDD\Database\Table;
/**
* Setup the notifications database table.
*
* @since 3.1.1
*/
final class Notifications extends Table {
/**
* Table name.
*
* @access protected
* @since 3.1.1
* @var string
*/
protected $name = 'notifications';
/**
* Database version.
*
* @access protected
* @since 3.1.1
* @var int
*/
protected $version = 202302131;
/**
* Array of upgrade versions and methods
*
* @since 3.1.1
*
* @var array
*/
protected $upgrades = array(
'202301251' => 202301251,
'202302131' => 202302131,
);
/**
* Setup the database schema.
*
* @access protected
* @since 3.1.1
* @return void
*/
protected function set_schema() {
$this->schema = "id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
remote_id varchar(20) DEFAULT NULL,
source varchar(20) NOT NULL DEFAULT 'api',
title text NOT NULL,
content longtext NOT NULL,
buttons longtext DEFAULT NULL,
type varchar(64) NOT NULL DEFAULT 'success',
conditions longtext DEFAULT NULL,
start datetime DEFAULT NULL,
end datetime DEFAULT NULL,
dismissed tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
date_created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
date_updated datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY dismissed_start_end (dismissed, start, end),
KEY remote_id (remote_id)";
}
/**
* Deletes the original database version option.
*
* @since 3.1.1
* @return bool
*/
protected function __202301251() {
return delete_option( "{$this->table_name}_db_version" );
}
/**
* Upgrade to version 202302131
* - Add the `source` text column and modify the remote_id column.
*
* @since 3.1.1
*
* @return boolean
*/
protected function __202302131() {
$updates = array(
'add-source' => false,
'remote-id' => false,
);
$columns = $this->get_db()->get_results( "SHOW FIELDS FROM {$this->table_name} WHERE Field = 'remote_id';" );
if ( false === $this->column_exists( 'source' ) ) {
$source = $this->get_db()->query(
"ALTER TABLE {$this->table_name} ADD COLUMN `source` varchar(20) NOT NULL DEFAULT 'api' AFTER `remote_id`;"
);
if ( $this->is_success( $source ) ) {
$updates['add-source'] = $this->get_db()->query( "UPDATE {$this->table_name} SET `source` = 'api'" );
}
} else {
$updates['add-source'] = true;
}
$remote_id_column = $this->get_db()->get_row( "SHOW FIELDS FROM {$this->table_name} WHERE Field = 'remote_id'" );
if ( 'varchar(20)' !== $remote_id_column->Type ) {
$updates['remote-id'] = $this->get_db()->query(
"ALTER TABLE {$this->table_name} MODIFY COLUMN `remote_id` varchar(20) DEFAULT NULL;"
);
} else {
$updates['remote-id'] = true;
}
foreach ( $updates as $query_key => $result ) {
if ( ! $this->is_success( $result ) ) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,224 @@
<?php
/**
* Search functionality for downloads.
*/
namespace EDD\Downloads;
class Search {
/**
* Retrieve a downloads drop down
*
* @since 3.1.0.5 Copied from `edd_ajax_download_search`
*
* @return void
*/
public function ajax_search() {
// We store the last search in a transient for 30 seconds. This _might_
// result in a race condition if 2 users are looking at the exact same time,
// but we'll worry about that later if that situation ever happens.
$args = get_transient( 'edd_download_search' );
// Parse args.
$search = wp_parse_args(
(array) $args,
array(
'text' => '',
'results' => array(),
)
);
// Get the search string.
$new_search = isset( $_GET['s'] )
? sanitize_text_field( $_GET['s'] )
: '';
// Limit to only alphanumeric characters, including unicode and spaces.
$new_search = preg_replace( '/[^\pL^\pN\pZ]/', ' ', $new_search );
// Bail early if the search text has not changed.
if ( $search['text'] === $new_search ) {
echo wp_json_encode( $search['results'] );
edd_die();
}
// Set the local static search variable.
$search['text'] = $new_search;
// Are we excluding the current ID?
$excludes = isset( $_GET['current_id'] )
? array_unique( array_map( 'absint', (array) $_GET['current_id'] ) )
: array();
// Are we excluding bundles?
$no_bundles = isset( $_GET['no_bundles'] )
? filter_var( $_GET['no_bundles'], FILTER_VALIDATE_BOOLEAN )
: false;
// Are we including variations?
$variations = isset( $_GET['variations'] )
? filter_var( $_GET['variations'], FILTER_VALIDATE_BOOLEAN )
: false;
$variations_only = isset( $_GET['variations_only'] )
? filter_var( $_GET['variations_only'], FILTER_VALIDATE_BOOLEAN )
: false;
// Are we including all statuses, or only public ones?
$status = ! current_user_can( 'edit_products' )
? apply_filters( 'edd_product_dropdown_status_nopriv', array( 'publish' ) )
: apply_filters( 'edd_product_dropdown_status', array( 'publish', 'draft', 'private', 'future' ) );
// Default query arguments.
$args = array(
'orderby' => 'title',
'order' => 'ASC',
'post_type' => 'download',
'posts_per_page' => 50,
'post_status' => implode( ',', $status ), // String.
'post__not_in' => $excludes, // Array.
'edd_search' => $new_search, // String.
'suppress_filters' => false,
);
// Maybe exclude bundles.
if ( true === $no_bundles ) {
$args['meta_query'] = array(
'relation' => 'OR',
array(
'key' => '_edd_product_type',
'value' => 'bundle',
'compare' => '!=',
),
array(
'key' => '_edd_product_type',
'value' => 'bundle',
'compare' => 'NOT EXISTS',
),
);
}
add_filter( 'posts_where', array( $this, 'filter_where' ), 10, 2 );
// Get downloads.
$items = get_posts( $args );
remove_filter( 'posts_where', array( $this, 'filter_where' ), 10, 2 );
// Pluck title & ID.
if ( ! empty( $items ) ) {
$items = wp_list_pluck( $items, 'post_title', 'ID' );
// Loop through all items...
foreach ( $items as $post_id => $title ) {
$product_title = $title;
// Look for variable pricing.
$prices = edd_get_variable_prices( $post_id );
if ( ! empty( $prices ) && ( false === $variations || ! $variations_only ) ) {
$title .= ' (' . __( 'All Price Options', 'easy-digital-downloads' ) . ')';
}
if ( empty( $prices ) || ! $variations_only ) {
// Add item to results array.
$search['results'][] = array(
'id' => $post_id,
'name' => $title,
);
}
// Maybe include variable pricing.
if ( ! empty( $variations ) && ! empty( $prices ) ) {
foreach ( $prices as $key => $value ) {
$name = ! empty( $value['name'] ) ? $value['name'] : '';
if ( ! empty( $name ) ) {
$search['results'][] = array(
'id' => $post_id . '_' . $key,
'name' => esc_html( $product_title . ': ' . $name ),
);
}
}
}
}
} else {
// Empty the results array.
$search['results'] = array();
}
// Update the transient.
set_transient( 'edd_download_search', $search, 30 );
// Output the results.
echo wp_json_encode( $search['results'] );
// Done!
edd_die();
}
/**
* Filters the WHERE SQL query for the edd_download_search.
* This searches the download titles only, not the excerpt/content.
*
* @since 3.1.0.2
* @since 3.1.0.5 Moved to EDD\Downloads\Ajax.
* @param string $where
* @param WP_Query $wp_query
* @return string
*/
public function filter_where( $where, $wp_query ) {
$search = $wp_query->get( 'edd_search' );
if ( ! $search ) {
return $where;
}
$terms = $this->parse_search_terms( $search );
if ( empty( $terms ) ) {
return $where;
}
global $wpdb;
$query = '';
foreach ( $terms as $term ) {
$operator = empty( $query ) ? '' : ' AND ';
$term = $wpdb->esc_like( $term );
$query .= "{$operator}{$wpdb->posts}.post_title LIKE '%{$term}%'";
}
if ( $query ) {
$where .= " AND ({$query})";
}
return $where;
}
/**
* Parses the search terms to allow for a "fuzzy" search.
*
* @since 3.1.0.5
* @param string $search
* @return array
*/
protected function parse_search_terms( $search ) {
$terms = explode( ' ', $search );
$strtolower = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';
$checked = array();
foreach ( $terms as $term ) {
// Keep before/after spaces when term is for exact match.
if ( preg_match( '/^".+"$/', $term ) ) {
$term = trim( $term, "\"'" );
} else {
$term = trim( $term, "\"' " );
}
// Avoid single A-Z and single dashes.
if ( ! $term || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
continue;
}
$checked[] = $term;
}
return $checked;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace EDD\EventManagement;
/**
* The WordPress event manager manages events using the WordPress plugin API.
*/
class EventManager extends PluginAPIManager {
/**
* Add an event subscriber.
*
* The event manager registers all the hooks that the given subscriber
* wants to register with the WordPress Plugin API.
*
* @param SubscriberInterface $subscriber
*/
public function add_subscriber( SubscriberInterface $subscriber ) {
foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) {
$this->add_subscriber_callback( $subscriber, $hook_name, $parameters );
}
}
/**
* Remove an event subscriber.
*
* The event manager removes all the hooks that the given subscriber
* wants to register with the WordPress Plugin API.
*
* @param SubscriberInterface $subscriber
*/
public function remove_subscriber( SubscriberInterface $subscriber ) {
foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) {
$this->remove_subscriber_callback( $subscriber, $hook_name, $parameters );
}
}
/**
* Adds the given subscriber's callback to a specific hook
* of the WordPress plugin API.
*
* @param SubscriberInterface $subscriber
* @param string $hook_name
* @param mixed $parameters
*/
private function add_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) {
if ( is_string( $parameters ) ) {
$this->add_callback( $hook_name, array( $subscriber, $parameters ) );
} elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) {
$this->add_callback( $hook_name, array( $subscriber, $parameters[0] ), isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 );
}
}
/**
* Removes the given subscriber's callback to a specific hook
* of the WordPress plugin API.
*
* @param SubscriberInterface $subscriber
* @param string $hook_name
* @param mixed $parameters
*/
private function remove_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) {
if ( is_string( $parameters ) ) {
$this->remove_callback( $hook_name, array( $subscriber, $parameters ) );
} elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) {
$this->remove_callback( $hook_name, array( $subscriber, $parameters[0] ), isset( $parameters[1] ) ? $parameters[1] : 10 );
}
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* EDD's custom Plugin API Manager.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\EventManagement;
class PluginAPIManager {
/**
* Adds a callback to a specific hook of the WordPress plugin API.
*
* @uses add_filter()
*
* @param string $hook_name
* @param callable $callback
* @param int $priority
* @param int $accepted_args
*/
public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
add_filter( $hook_name, $callback, $priority, $accepted_args );
}
/**
* Executes all the callbacks registered with the given hook.
*
* @uses do_action()
*
* @param string $hook_name
*/
public function execute() {
return call_user_func_array( 'do_action', func_get_args() );
}
/**
* Filters the given value by applying all the changes from the callbacks
* registered with the given hook. Returns the filtered value.
*
* @uses apply_filters()
*
* @param string $hook_name
* @param mixed $value
*
* @return mixed
*/
public function filter() {
return call_user_func_array( 'apply_filters', func_get_args() );
}
/**
* Get the name of the hook that WordPress plugin API is executing. Returns
* false if it isn't executing a hook.
*
* @uses current_filter()
*
* @return string|bool
*/
public function get_current_hook() {
return current_filter();
}
/**
* Checks the WordPress plugin API to see if the given hook has
* the given callback. The priority of the callback will be returned
* or false. If no callback is given will return true or false if
* there's any callbacks registered to the hook.
*
* @uses has_filter()
*
* @param string $hook_name
* @param mixed $callback
*
* @return bool|int
*/
public function has_callback( $hook_name, $callback = false ) {
return has_filter( $hook_name, $callback );
}
/**
* Removes the given callback from the given hook. The WordPress plugin API only
* removes the hook if the callback and priority match a registered hook.
*
* @uses remove_filter()
*
* @param string $hook_name
* @param callable $callback
* @param int $priority
*
* @return bool
*/
public function remove_callback( $hook_name, $callback, $priority = 10 ) {
return remove_filter( $hook_name, $callback, $priority );
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* Subscriber interface for registering event listeners.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\EventManagement;
interface SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* The array key is the event name. The value can be:
*
* * The method name
* * An array with the method name and priority
* * An array with the method name, priority and number of accepted arguments
*
* For instance:
*
* * array('event_name' => 'method_name')
* * array('event_name' => array('method_name', $priority))
* * array('event_name' => array('method_name', $priority, $accepted_args))
*
* @return array
*/
public static function get_subscribed_events();
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Class to handle registering and adding service providers for EDD.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\EventManagement;
abstract class Subscribers {
/**
* The pass handler.
*
* @since 3.1.1
* @var EDD\Admin\PassHandler\Handler
*/
protected $pass_handler;
public function __construct() {
$this->pass_handler = new \EDD\Admin\PassHandler\Handler();
$this->add_service_providers();
}
/**
* Add registered service providers.
*
* @since 3.1.1
* @return void
*/
private function add_service_providers() {
$events = new EventManager();
if ( ! $events instanceof EventManager ) {
return;
}
$service_providers = array_merge(
$this->get_service_providers(),
$this->get_admin_providers(),
$this->get_replaceable_providers()
);
// Attach subscribers.
foreach ( $service_providers as $service_provider ) {
try {
$events->add_subscriber( $service_provider );
} catch ( Exception $e ) {
// Do not subscribe.
}
}
}
/**
* Gets providers that may be extended/replaced in lite/pro.
*
* @return array
*/
protected function get_replaceable_providers() {
return array();
}
/**
* Gets the service providers for EDD.
*
* @return array
*/
abstract protected function get_service_providers();
/**
* Gets the admin service providers for EDD.
*
* @return array
*/
abstract protected function get_admin_providers();
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Licensing API
*
* Tool for making requests to the Software Licensing API.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2022, Easy Digital Downloads, LLC
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Licensing;
class API {
/**
* The Software Licensing API URL.
*
* @since 3.1.1
* @var string
*/
private $api_url = 'https://easydigitaldownloads.com/edd-sl-api';
/**
* Gets the API URL.
*
* @since 3.1.1
* @return string
*/
public function get_url() {
return $this->api_url;
}
/**
* Makes a request to the Software Licensing API.
*
* @since 3.1.1
* @param array $api_params The parameters for the API request.
* @return false|stdClass
*/
public function make_request( $api_params = array() ) {
if ( empty( $api_params ) || ! is_array( $api_params ) ) {
return false;
}
$request = wp_remote_get(
$this->api_url,
array(
'timeout' => 15,
'sslverify' => true,
'body' => $this->get_body( $api_params ),
)
);
// If there was an API error, return false.
if ( is_wp_error( $request ) || ( 200 !== wp_remote_retrieve_response_code( $request ) ) ) {
return false;
}
return json_decode( wp_remote_retrieve_body( $request ) );
}
/**
* Updates the API parameters with the defaults.
*
* @param array $api_params The parameters for the specific request.
* @return array
*/
private function get_body( array $api_params ) {
return wp_parse_args(
$api_params,
array(
'url' => home_url(),
)
);
}
}

View File

@ -0,0 +1,212 @@
<?php
/**
* Handle ajax initiated licensing actions.
*/
namespace EDD\Licensing;
use EDD\EventManagement\SubscriberInterface;
class Ajax implements SubscriberInterface {
use Traits\Controls;
/**
* The license object.
*
* @var \EDD\Licensing\License
*/
private $license;
/**
* The license key.
*
* @var string
*/
private $license_key;
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @since 3.1.1
* @return array
*/
public static function get_subscribed_events() {
return array(
'wp_ajax_edd_activate_extension_license' => 'activate',
'wp_ajax_edd_deactivate_extension_license' => 'deactivate',
'wp_ajax_edd_delete_extension_license' => 'delete',
);
}
/**
* Attempt to activate an extension license.
*
* @since 3.1.1
* @return void
*/
public function activate() {
if ( ! $this->can_manage() ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this extension.', 'easy-digital-downloads' ) ),
)
);
}
if ( ! empty( $_POST['license'] ) ) {
$this->license_key = sanitize_text_field( $_POST['license'] );
}
if ( ! $this->license_key ) {
wp_send_json_error(
array(
'message' => __( 'No key provided.', 'easy-digital-downloads' ),
)
);
}
$this->name = filter_input( INPUT_POST, 'item_name', FILTER_SANITIZE_SPECIAL_CHARS );
$api_params = array(
'edd_action' => 'activate_license',
'license' => $this->license_key,
'item_name' => $this->name,
'item_id' => filter_input( INPUT_POST, 'item_id', FILTER_SANITIZE_NUMBER_INT ),
);
$api = new API();
$license_data = $api->make_request( $api_params );
if ( empty( $license_data->success ) ) {
if ( ! empty( $license_data ) ) {
$messages = new \EDD\Licensing\Messages(
array(
'status' => $license_data->error,
'license_key' => $this->license_key,
'expires' => ! empty( $license_data->expires ) ? $license_data->expires : false,
'name' => $this->name,
'subscription' => ! empty( $license_data->subscription ) ? $license_data->subscription : null,
)
);
$message = $messages->get_message();
} else {
$message = __( 'Your license key could not be activated.', 'easy-digital-downloads' );
}
wp_send_json_error(
array(
'message' => wpautop( $message ),
)
);
}
set_site_transient( 'update_plugins', null );
$pass_manager = new \EDD\Admin\Pass_Manager();
$pass_manager->maybe_set_pass_flag( $this->license_key, $license_data );
// Clear the option for licensed extensions to force regeneration.
if ( ! empty( $license_data->license ) && 'valid' === $license_data->license ) {
delete_option( 'edd_licensed_extensions' );
}
edd_update_option( filter_input( INPUT_POST, 'key', FILTER_SANITIZE_SPECIAL_CHARS ), $this->license_key );
$license = new License( $this->name );
$license->save( $license_data );
// Get the license again.
$this->license = new License( $this->name );
$this->set_up_license_data();
wp_send_json_success(
array(
'message' => $this->do_message( false ),
'actions' => $this->get_actions( 'valid' ),
)
);
}
/**
* Attempt to deactivate an extension license.
*
* @since 3.1.1
* @return void
*/
public function deactivate() {
if ( ! $this->can_manage() ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this extension.', 'easy-digital-downloads' ) ),
)
);
}
$this->name = filter_input( INPUT_POST, 'item_name', FILTER_SANITIZE_SPECIAL_CHARS );
$item_id = filter_input( INPUT_POST, 'item_id', FILTER_SANITIZE_NUMBER_INT );
$this->license = new License( $this->name );
$this->license_key = $this->license->key;
$api_params = array(
'edd_action' => 'deactivate_license',
'license' => $this->license_key,
'item_id' => urlencode( $item_id ),
);
$api = new API();
$license_data = $api->make_request( $api_params );
$this->license->save( $license_data );
$pass_manager = new \EDD\Admin\Pass_Manager();
$pass_manager->maybe_remove_pass_flag( $this->license_key );
wp_send_json_success(
array(
'message' => wpautop( __( 'Your license key has been deactivated.', 'easy-digital-downloads' ) ),
'actions' => $this->get_actions( $license_data->license ),
)
);
}
/**
* Deletes an extension key and the related option.
*
* @since 3.1.1
* @return void
*/
public function delete() {
if ( ! $this->can_manage( 'edd_licensehandler-delete' ) ) {
wp_send_json_error(
array(
'message' => wpautop( __( 'You do not have permission to manage this extension.', 'easy-digital-downloads' ) ),
)
);
}
$this->name = filter_input( INPUT_POST, 'item_name', FILTER_SANITIZE_SPECIAL_CHARS );
$this->license = new License( $this->name );
$this->license->delete();
edd_delete_option( filter_input( INPUT_POST, 'key', FILTER_SANITIZE_SPECIAL_CHARS ) );
wp_send_json_success(
array(
'message' => wpautop( __( 'License key deleted.', 'easy-digital-downloads' ) ),
)
);
}
/**
* Whether the current user can manage the extension.
* Checks the user capabilities, tokenizer, and nonce.
*
* @since 3.1.1
* @param string $nonce The name of the specific nonce to validate.
* @return bool
*/
protected function can_manage( $nonce = 'edd_licensehandler' ) {
if ( ! current_user_can( 'manage_shop_settings' ) ) {
return false;
}
$token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
$timestamp = isset( $_POST['timestamp'] ) ? sanitize_text_field( $_POST['timestamp'] ) : '';
if ( empty( $timestamp ) || empty( $token ) ) {
return false;
}
return \EDD\Utils\Tokenizer::is_token_valid( $token, $timestamp ) && wp_verify_nonce( $_POST['nonce'], $nonce );
}
}

View File

@ -0,0 +1,281 @@
<?php
namespace EDD\Licensing;
class License {
/**
* The license status.
*
* @var string
*/
public $license = '';
/**
* The product ID.
*
* @var int
*/
public $item_id;
/**
* The product name.
*
* @var string
*/
public $item_name;
/**
* The license activation limit.
*
* @var int
*/
public $license_limit;
/**
* The number of sites on which this license is active.
*
* @var int
*/
public $site_count = 0;
/**
* The license expiration date.
*
* @var string
*/
public $expires;
/**
* The number of activations left.
*
* @var int
*/
public $activations_left;
/**
* The order ID for the license.
*
* @var int
*/
public $payment_id;
/**
* The product price ID.
*
* @var false|int
*/
public $price_id = false;
/**
* The customer's pass ID.
*
* @var null|int
*/
public $pass_id;
/**
* The error code for a license.
*
* @var string
*/
public $error;
/**
* The license key.
*
* @var string
*/
public $key = '';
/**
* Whether the API request was successful.
*
* @var bool
*/
public $success = false;
/**
* The subscription status.
*
* @since 3.1.1
* @var string
*/
public $subscription;
/**
* The product shortname.
*
* @var string
*/
private $product_shortname;
/**
* The option name for the license data.
*
* @var string
*/
private $option_name;
/**
* The option name for the license data.
*
* @var string
*/
private $custom_key_option;
/**
* Whether the option is for a single site or site meta (multisite).
*
* @var bool
*/
private $single_site = true;
/**
* The class constructor.
*
* @param string $product_name The product/item name.
* @param null|string $custom_key_option For backwards compatibility with extensions who saved key data as a custom option name.
*/
public function __construct( $product_name, $custom_key_option = null ) {
$this->product_shortname = 'edd_' . preg_replace( '/[^a-zA-Z0-9_\s]/', '', str_replace( ' ', '_', strtolower( $product_name ) ) );
$this->option_name = "{$this->product_shortname}_license_active";
if ( 'pro' === $product_name ) {
$this->option_name = "{$this->product_shortname}_license";
$this->single_site = false;
} elseif ( $custom_key_option && $custom_key_option !== $this->option_name ) {
$this->custom_key_option = $custom_key_option;
}
$this->get();
}
/**
* Saves the license data option.
*
* @since 3.1.1
* @param object $license_data
* @return bool
*/
public function save( $license_data ) {
if ( $this->single_site ) {
return update_option(
$this->option_name,
$license_data,
false
);
}
return update_site_option(
$this->option_name,
$license_data
);
}
/**
* Deletes a license key and related license data.
*
* @since 3.1.1
* @return void
*/
public function delete() {
if ( ! $this->single_site ) {
delete_site_option( $this->option_name );
delete_site_option( "{$this->product_shortname}_license_key" );
return;
}
delete_option( $this->option_name );
edd_delete_option( "{$this->product_shortname}_license_key" );
if ( $this->custom_key_option ) {
edd_delete_option( $this->custom_key_option );
}
}
/**
* Selectively update just one piece of the license data.
*
* @since 3.1.1
* @param array $data
* @return bool
*/
public function update( array $data ) {
$option = $this->single_site ? get_option( $this->option_name, false ) : get_site_option( $this->option_name, false );
$update = false;
foreach ( $data as $key => $value ) {
if ( $value !== $option->$key && in_array( $key, $this->get_editable_keys(), true ) ) {
$option->$key = $value;
$update = true;
}
}
return $update ? $this->save( $option ) : false;
}
/**
* Gets the license key for the license.
*
* @return string
*/
public function get_license_key() {
$option_name = "{$this->product_shortname}_license_key";
$option = trim(
$this->single_site ?
edd_get_option( $option_name, '' ) :
get_site_option( $option_name, '' )
);
if ( ! empty( $option ) || 'edd_pro' === $this->product_shortname ) {
return $option;
}
/**
* Allows for backwards compatibility with old license options,
* i.e. if the plugins had license key fields previously, the license
* handler will automatically pick these up and use those in lieu of the
* user having to reactivate their license.
*/
return trim( $this->custom_key_option ? edd_get_option( $this->custom_key_option, '' ) : $option );
}
/**
* Gets the license object mapped to the class defaults.
*
* @return EDD\Licensing\License
*/
public function get() {
$this->key = $this->get_license_key();
if ( empty( $this->key ) ) {
return $this;
}
$option = $this->single_site ? get_option( $this->option_name, false ) : get_site_option( $this->option_name, false );
if ( ! $option ) {
return $this;
}
foreach ( (array) $option as $key => $value ) {
if ( property_exists( $this, $key ) ) {
$this->$key = $value;
}
}
if ( ! $this->success && is_null( $this->error ) && 'valid' !== $this->license ) {
$this->error = $this->license;
}
return $this;
}
/**
* Only allow certain keys to be modified.
*
* @since 3.1.1
* @return array
*/
private function get_editable_keys() {
return array( 'license', 'error', 'success' );
}
}

View File

@ -0,0 +1,274 @@
<?php
/**
* Messages for license key activation results.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\Licensing;
class Messages {
/**
* The array of license data.
*
* @var array
*/
private $license_data = array();
/**
* The license expiration as a timestamp, or false if no expiration.
*
* @var bool|int
*/
private $expiration = false;
/**
* The current timestamp.
*
* @var int
*/
private $now;
public function __construct( $license_data = array() ) {
$this->license_data = wp_parse_args(
$license_data,
array(
'status' => '',
'expires' => '',
'name' => '',
'license_key' => '',
'subscription' => false,
)
);
$this->now = current_time( 'timestamp' );
if ( ! empty( $this->license_data['expires'] ) && 'lifetime' !== $this->license_data['expires'] ) {
if ( ! is_numeric( $this->license_data['expires'] ) ) {
$this->expiration = strtotime( $this->license_data['expires'], $this->now );
} else {
$this->expiration = $this->license_data['expires'];
}
}
}
/**
* Gets the appropriate licensing message from an array of license data.
*
* @since 3.1.1
* @return string
*/
public function get_message() {
$name = $this->license_data['name'] ?: __( 'license key', 'easy-digital-downloads' );
switch ( $this->license_data['status'] ) {
case 'expired':
$args = array(
'utm_medium' => 'license-notice',
'utm_content' => 'expired',
);
if ( ! empty( $this->license_data['license_key'] ) ) {
$args['license_key'] = $this->license_data['license_key'];
}
$url = edd_link_helper(
'https://easydigitaldownloads.com/checkout/',
$args
);
if ( $this->expiration ) {
$message = sprintf(
/* translators: 1. license expiration date; 2. opening link tag; 3. closing link tag. */
__( 'Your license key expired on %1$s. Please %2$srenew your license key%3$s.', 'easy-digital-downloads' ),
edd_date_i18n( $this->expiration ),
'<a href="' . $url . '" target="_blank">',
'</a>'
);
} else {
$message = sprintf(
/* translators: 1. opening link tag; 2. closing link tag. */
__( 'Your license key has expired. Please %1$srenew your license key%2$s.', 'easy-digital-downloads' ),
'<a href="' . $url . '" target="_blank">',
'</a>'
);
}
break;
case 'revoked':
case 'disabled':
$url = edd_link_helper(
'https://easydigitaldownloads.com/support/',
array(
'utm_medium' => 'license-notice',
'utm_content' => 'revoked',
)
);
$message = sprintf(
/* translators: 1. opening link tag; 2. closing link tag. */
__( 'Your license key has been disabled. Please %1$scontact support%2$s for more information.', 'easy-digital-downloads' ),
'<a href="' . $url . '" target="_blank">',
'</a>'
);
break;
case 'missing':
$url = edd_link_helper(
'https://easydigitaldownloads.com/your-account/',
array(
'utm_medium' => 'license-notice',
'utm_content' => 'missing',
)
);
$message = sprintf(
/* translators: 1. opening link tag; 2. closing link tag. */
__( 'Invalid license. Please %1$svisit your account page%2$s and verify it.', 'easy-digital-downloads' ),
'<a href="' . $url . '" target="_blank">',
'</a>'
);
break;
case 'site_inactive':
$url = edd_link_helper(
'https://easydigitaldownloads.com/your-account/',
array(
'utm_medium' => 'license-notice',
'utm_content' => 'inactive',
)
);
$message = sprintf(
/* translators: 1. the extension name; 2. opening link tag; 3. closing link tag. */
__( 'Your %1$s is not active for this URL. Please %2$svisit your account page%3$s to manage your license keys.', 'easy-digital-downloads' ),
esc_html( $name ),
'<a href="' . $url . '" target="_blank">',
'</a>'
);
break;
case 'invalid':
case 'invalid_item_id':
case 'item_name_mismatch':
case 'key_mismatch':
$message = sprintf(
/* translators: the extension name. */
__( 'This appears to be an invalid license key for %s.', 'easy-digital-downloads' ),
$name
);
break;
case 'no_activations_left':
$url = edd_link_helper(
'https://easydigitaldownloads.com/your-account/',
array(
'utm_medium' => 'license-notice',
'utm_content' => 'at-limit',
)
);
$message = sprintf(
/* translators: 1. opening link tag; 2 closing link tag. */
__( 'Your license key has reached its activation limit. %1$sView possible upgrades%2$s now.', 'easy-digital-downloads' ),
'<a href="' . $url . '">',
'</a>'
);
break;
case 'license_not_activable':
$message = __( 'The key you entered belongs to a bundle, please use the product specific license key.', 'easy-digital-downloads' );
break;
case 'deactivated':
$message = __( 'Your license key has been deactivated.', 'easy-digital-downloads' );
break;
case 'valid':
$message = $this->get_valid_message();
if ( $this->license_data['subscription'] && 'lifetime' !== $this->license_data['subscription'] ) {
$message .= $this->get_subscription_message();
}
break;
default:
if ( ! empty( $this->license_data['license_key'] ) ) {
$error = ! empty( $this->license->error ) ? $this->license->error : __( 'unknown_error', 'easy-digital-downloads' );
$message = sprintf(
/* translators: 1. the error code; 2. opening link tag; 3. closing link tag. */
__( 'There was an error with this license key: %1$s. Please %2$scontact our support team%3$s.', 'easy-digital-downloads' ),
'<code>' . $error . '</code>',
'<a href="https://easydigitaldownloads.com/support">',
'</a>'
);
} else {
$message = sprintf(
/* translators: the extension name. */
__( 'Unlicensed: currently not receiving updates.', 'easy-digital-downloads' )
);
}
break;
}
return $message;
}
/**
* Gets the message text for a valid license.
*
* @since 3.1.1
* @return string
*/
private function get_valid_message() {
if ( ! empty( $this->license_data['expires'] ) && 'lifetime' === $this->license_data['expires'] ) {
return __( 'License key never expires.', 'easy-digital-downloads' );
}
if ( ( $this->expiration > $this->now ) && ( $this->expiration - $this->now < ( DAY_IN_SECONDS * 30 ) ) ) {
return sprintf(
/* translators: the license expiration date. */
__( 'Your license key expires soon! It expires on %s.', 'easy-digital-downloads' ),
edd_date_i18n( $this->expiration )
);
}
return sprintf(
/* translators: the license expiration date. */
__( 'Your license key expires on %s.', 'easy-digital-downloads' ),
edd_date_i18n( $this->expiration )
);
}
/**
* Gets the message for a license's subscription.
*
* @since 3.1.1
* @return string
*/
private function get_subscription_message() {
if ( 'active' === $this->license_data['subscription'] ) {
return ' ' . __( 'Your license subscription is active and will automatically renew.', 'easy-digital-downloads' );
}
return ' ' . sprintf(
/* translators: the license subscription status. */
__( 'Your license subscription is %s and will not automatically renew.', 'easy-digital-downloads' ),
$this->get_subscription_status_label( $this->license_data['subscription'] )
);
}
/**
* Gets the subscription status label as a translatable string.
*
* @since 3.1.1
* @param string $status
* @return string
*/
private function get_subscription_status_label( $status ) {
$statii = array(
'pending' => __( 'pending', 'easy-digital-downloads' ),
'active' => __( 'active', 'easy-digital-downloads' ),
'cancelled' => __( 'cancelled', 'easy-digital-downloads' ),
'expired' => __( 'expired', 'easy-digital-downloads' ),
'trialling' => __( 'trialling', 'easy-digital-downloads' ),
'failing' => __( 'failing', 'easy-digital-downloads' ),
'completed' => __( 'completed', 'easy-digital-downloads' ),
);
return array_key_exists( $status, $statii ) ? $statii[ $status ] : $status;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Handles the settings fields for extension licenses.
*/
namespace EDD\Licensing;
class Settings {
use Traits\Controls;
/**
* The array of options for the settings field.
*
* @var array
*/
private $args;
/**
* The license object.
*
* @var \EDD\Licensing\License
*/
private $license;
/**
* The license key.
*
* @var string
*/
private $license_key;
public function __construct( $args ) {
$this->args = $args;
$this->license = new License( $this->args['name'], $this->args['options']['is_valid_license_option'] );
$this->license_key = $this->license->key;
$this->name = $this->args['name'];
$this->set_up_license_data();
$this->do_settings_field();
add_action( 'admin_print_footer_scripts', array( $this, 'do_script' ) );
}
/**
* Adds the licensing JS to the screen.
*
* @since 3.1.1
* @return void
*/
public function do_script() {
if ( wp_script_is( 'edd-licensing' ) ) {
return;
}
wp_enqueue_script( 'edd-licensing', EDD_PLUGIN_URL . 'assets/js/edd-admin-licensing.js', array( 'jquery' ), EDD_VERSION, true );
wp_localize_script(
'edd-licensing',
'EDDLicenseHandler',
array(
'activating' => __( 'Activating', 'easy-digital-downloads' ),
'deactivating' => __( 'Deactivating', 'easy-digital-downloads' ),
)
);
wp_print_scripts( 'edd-licensing' );
?>
<style>p.submit{display:none;}</style>
<?php
}
/**
* Renders the license key settings field.
*
* @since 3.1.1
* @return void
*/
private function do_settings_field() {
if ( ! $this->included_in_pass ) {
?>
<div class="edd-license__control">
<input
type="password"
autocomplete="off"
class="regular-text"
id="edd_settings[<?php echo esc_attr( $this->args['id'] ); ?>]"
name="edd_settings[<?php echo esc_attr( $this->args['id'] ); ?>]"
value="<?php echo sanitize_key( $this->license_key ); ?>"
<?php echo $this->included_in_pass ? ' readonly' : ''; ?>
<?php if ( ! empty( $this->args['options']['item_id'] ) ) : ?>
data-item="<?php echo esc_attr( $this->args['options']['item_id'] ); ?>"
<?php endif; ?>
data-name="<?php echo esc_attr( $this->args['name'] ); ?>"
data-key="<?php echo esc_attr( $this->args['id'] ); ?>"
/>
<?php
$this->get_actions( $this->license->license, true );
?>
</div>
<?php
}
$this->do_message();
do_action( 'edd/admin/settings/licenses/settings_field', $this->license, $this->included_in_pass );
}
}

View File

@ -0,0 +1,241 @@
<?php
namespace EDD\Licensing\Traits;
use \EDD\Admin\Pass_Manager;
trait Controls {
/**
* The pass manager.
*
* @var \EDD\Admin\Pass_Manager
*/
private $pass_manager;
/**
* The CSS class for the message.
*
* @var string
*/
private $class;
/**
* The license status.
*
* @var string
*/
private $license_status;
/**
* The message to display below the license key input.
*
* @var string
*/
private $message;
/**
* Whether the current product is covered by a pass.
*
* @var bool
*/
private $included_in_pass = false;
/**
* The item name.
*
* @var string
*/
private $name;
/**
* Gets the button for the pass field.
*
* @since 3.1.1
* @param string $status The pass status.
* @param bool $echo Whether to echo the button.
* @return string
*/
public function get_actions( $status, $echo = false ) {
$button = $this->get_button_args( $status );
$timestamp = time();
if ( ! $echo ) {
ob_start();
}
?>
<div class="edd-licensing__actions">
<button
class="button button-<?php echo esc_attr( $button['class'] ); ?> edd-license__action"
data-action="<?php echo esc_attr( $button['action'] ); ?>"
data-timestamp="<?php echo esc_attr( $timestamp ); ?>"
data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_licensehandler' ) ); ?>"
>
<?php echo esc_html( $button['label'] ); ?>
</button>
<?php if ( ! empty( $this->license_key ) && 'activate' === $button['action'] ) : ?>
<button
class="button button-secondary edd-license__delete"
data-action="delete"
data-timestamp="<?php echo esc_attr( $timestamp ); ?>"
data-token="<?php echo esc_attr( \EDD\Utils\Tokenizer::tokenize( $timestamp ) ); ?>"
data-nonce="<?php echo esc_attr( wp_create_nonce( 'edd_licensehandler-delete' ) ); ?>"
>
<?php esc_html_e( 'Delete', 'easy-digital-downloads' ); ?>
</button>
<?php endif; ?>
</div>
<?php
if ( ! $echo ) {
return ob_get_clean();
}
}
/**
* Get the button parameters based on the status.
*
* @since 3.1.1
* @param string $state
* @return array
*/
private function get_button_args( $state = 'inactive' ) {
if ( in_array( $state, array( 'valid', 'active' ), true ) ) {
return array(
'action' => 'deactivate',
'label' => __( 'Deactivate', 'easy-digital-downloads' ),
'class' => 'secondary',
);
}
return array(
'action' => 'activate',
'label' => __( 'Activate', 'easy-digital-downloads' ),
'class' => 'secondary',
);
}
/**
* Outputs the license key message.
*
* @since 3.1.1
* @return void
*/
private function do_message( $echo = true ) {
if ( empty( $this->message ) ) {
return '';
}
$classes = array(
'edd-license-data',
"edd-license-{$this->class}",
$this->license_status,
);
if ( ! $echo ) {
ob_start();
}
?>
<div class="<?php echo esc_attr( implode( ' ', array_filter( $classes ) ) ); ?>">
<p><?php echo wp_kses_post( $this->message ); ?></p>
</div>
<?php
if ( ! $echo ) {
return ob_get_clean();
}
}
/**
* Sets up the license data.
*
* @since 3.1.1
* @return void
*/
private function set_up_license_data() {
$class = 'empty';
$license_status = null;
$status = $this->license->license;
$messages = new \EDD\Licensing\Messages(
array(
'status' => $status,
'license_key' => $this->license_key,
'expires' => ! empty( $this->license->expires ) ? $this->license->expires : '',
'name' => $this->name,
)
);
$message = $messages->get_message();
if ( ! empty( $this->license ) ) {
$now = current_time( 'timestamp' );
$expiration = ! empty( $this->license->expires )
? strtotime( $this->license->expires, $now )
: false;
// activate_license 'invalid' on anything other than valid, so if there was an error capture it
if ( false === $this->license->success ) {
$class = ! empty( $this->license->error ) ? $this->license->error : 'error';
$license_status = "license-{$class}-notice";
} else {
$class = 'valid';
if ( 'lifetime' === $this->license->expires ) {
$license_status = 'license-lifetime-notice';
} elseif ( ( $expiration > $now ) && ( $expiration - $now < ( DAY_IN_SECONDS * 30 ) ) ) {
$license_status = 'license-expires-soon-notice';
} else {
$license_status = 'license-expiration-date-notice';
}
}
}
$pass_manager = $this->get_pass_manager();
if ( 'valid' !== $class && $pass_manager->has_pass_data && $this->is_included_in_pass() ) {
$this->included_in_pass = true;
$class = 'included-in-pass';
/* translators: the all acess pass name. */
$message = sprintf( __( 'Your %s gives you access to this extension.', 'easy-digital-downloads' ), '<strong>' . $pass_manager->get_pass_name() . '</strong>' );
}
$this->class = $class;
$this->message = $message;
$this->license_status = $license_status;
}
/**
* Whether a given product is included in the customer's active pass.
*
* @since 3.1.1
* @return bool
*/
private function is_included_in_pass() {
$pass_manager = $this->get_pass_manager();
// All Access and lifetime passes can access everything.
if ( $pass_manager->hasAllAccessPass() ) {
return true;
}
// If we don't know the item ID we can't assume anything.
if ( empty( $this->args['options']['item_id'] ) ) {
return false;
}
$api = new \EDD\Admin\Extensions\ExtensionsAPI();
$api_item_id = $this->args['options']['item_id'];
$product_data = $api->get_product_data( array(), $api_item_id );
if ( ! $product_data || empty( $product_data->categories ) ) {
return false;
}
return (bool) $pass_manager->can_access_categories( $product_data->categories );
}
/**
* Gets the pass manager.
*
* @return EDD\Admin\Pass_Manager
*/
private function get_pass_manager() {
if ( $this->pass_manager ) {
return $this->pass_manager;
}
$this->pass_manager = new Pass_Manager();
return $this->pass_manager;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Admin menu functionality.
*
* @package EDD
*/
namespace EDD\Lite\Admin;
use \EDD\EventManagement\SubscriberInterface;
class Menu implements SubscriberInterface {
/**
* Returns an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events() {
$events = array();
if ( current_user_can( 'manage_shop_settings' ) ) {
$events['admin_menu'] = array( 'pro_link', 9000 );
}
return $events;
}
public function pro_link() {
$pass_manager = new \EDD\Admin\Pass_Manager();
$onboarding_completed = get_option( 'edd_onboarding_completed', false );
if ( $onboarding_completed && ! $pass_manager->has_pass() ) {
add_submenu_page(
'edit.php?post_type=download',
esc_html__( 'Upgrade to Pro', 'easy-digital-downloads' ),
esc_html__( 'Upgrade to Pro', 'easy-digital-downloads' ),
'manage_shop_settings',
edd_link_helper(
'https://easydigitaldownloads.com/lite-upgrade',
array(
'utm_medium' => 'admin-menu',
'utm_content' => 'upgrade-to-pro',
)
)
);
add_action( 'admin_head', array( $this, 'adjust_pro_menu_item_class' ) );
}
}
/**
* Adds the custom pro menu item class.
*
* @since 3.1.1
* @return void
*/
public function adjust_pro_menu_item_class() {
new \EDD\Admin\Menu\LinkClass( 'https://easydigitaldownloads.com/lite-upgrade', 'edd-sidebar__upgrade-pro' );
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace EDD\Lite\Admin\PassHandler;
use \EDD\EventManagement\SubscriberInterface;
/**
* Easy Digital Downloads Connect.
*
* EDD Connect is our service that makes it easy for non-techy users to
* upgrade to EDD (Pro) without having to manually install EDD (Pro) plugin.
*
* @since 3.1.1
*/
use \EDD\Admin\Pass_Manager;
class Connect implements SubscriberInterface {
/**
* The EDD Pass Manager class.
*
* @var \EDD\Admin\Pass_Manager
*/
protected $pass_manager;
/**
* The pass handler.
*
* @var \EDD\Admin\PassHandler\Handler;
*/
protected $handler;
public function __construct( \EDD\Admin\PassHandler\Handler $handler ) {
$this->handler = $handler;
$this->pass_manager = new Pass_Manager();
}
public static function get_subscribed_events() {
return array(
'wp_ajax_nopriv_easydigitaldownloads_connect_process' => 'process',
);
}
/**
* Process EDD Connect.
*
* @since 3.1.1
*/
public function process() {
$error = esc_html__( 'There was an error while installing an upgrade. Please download the plugin from easydigitaldownloads.com and install it manually.', 'easy-digital-downloads' );
// Verify params present (oth & download link).
$post_oth = ! empty( $_REQUEST['oth'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['oth'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$post_url = ! empty( $_REQUEST['file'] ) ? esc_url_raw( wp_unslash( $_REQUEST['file'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( empty( $post_oth ) || empty( $post_url ) ) {
wp_send_json_error( $error );
}
// Verify oth.
$oth = get_option( 'edd_connect_token' );
if ( empty( $oth ) || ! hash_equals( $oth, $post_oth ) ) {
wp_send_json_error( $error );
}
// Delete so cannot replay.
delete_option( 'edd_connect_token' );
// Check license key.
$license_key = ! empty( $_REQUEST['key'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( empty( $license_key ) ) {
wp_send_json_error( __( 'No key provided.', 'easy-digital-downloads' ) );
}
if ( ! empty( $_REQUEST['license'] ) ) {
update_site_option( 'edd_pro_license_key', $license_key );
$license_data = (object) $_REQUEST['license'];
$this->handler->update_pro_license( $license_data );
$this->pass_manager->maybe_set_pass_flag( $license_key, $license_data );
}
if ( ! get_option( 'edd_pro_activation_date', false ) ) {
update_option( 'edd_pro_activation_date', time() );
}
// If pro is already active, return a success message.
if ( edd_is_pro() ) {
wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'easy-digital-downloads' ) );
}
// Set the current screen to avoid undefined notices.
set_current_screen( 'download_page_edd-settings' );
// Verify pro not installed.
$active = activate_plugin( 'easy-digital-downloads-pro/easy-digital-downloads.php', '', false, true );
if ( ! is_wp_error( $active ) ) {
wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'easy-digital-downloads' ) );
}
// Prepare variables.
$url = esc_url_raw(
edd_get_admin_url(
array( 'page' => 'edd-settings' )
)
);
$creds = request_filesystem_credentials( $url, '', false, false, null );
// Check for file system permissions.
if ( false === $creds || ! WP_Filesystem( $creds ) ) {
wp_send_json_error(
esc_html__( 'There was an error while installing an upgrade. Please check file system permissions and try again. Also, you can download the plugin from easydigitaldownloads.com and install it manually.', 'easy-digital-downloads' )
);
}
/*
* We do not need any extra credentials if we have gotten this far, so let's install the plugin.
*/
// Do not allow WordPress to search/download translations, as this will break JS output.
remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
// Create the plugin upgrader with our custom skin.
$installer = new \EDD\Admin\Installers\PluginSilentUpgrader( new \EDD\Admin\Installers\Install_Skin() );
// Error check.
if ( ! method_exists( $installer, 'install' ) ) {
wp_send_json_error( $error );
}
$installer->install( $post_url ); // phpcs:ignore
// Flush the cache and return the newly installed plugin basename.
wp_cache_flush();
$plugin_basename = $installer->plugin_info();
if ( $plugin_basename ) {
// Activate the plugin silently.
$activated = activate_plugin( $plugin_basename, '', false, true );
if ( ! is_wp_error( $activated ) ) {
wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'easy-digital-downloads' ) );
}
$error = esc_html__( 'Easy Digital Downloads (Pro) was installed, but needs to be activated on the Plugins page inside your WordPress admin.', 'easy-digital-downloads' );
}
wp_send_json_error( $error );
}
}

View File

@ -0,0 +1,228 @@
<?php
/**
* Class for adding a pointer notice for users who have an active pass, but not a pro license.
*
* @since 3.1.1
*/
namespace EDD\Lite\Admin\PassHandler;
use \EDD\EventManagement\SubscriberInterface;
class Pointer implements SubscriberInterface {
public static function get_subscribed_events() {
return array(
'admin_menu' => 'add_menu_item_class',
'user_register' => 'dismiss_pointers_for_new_users',
'admin_enqueue_scripts' => 'pointers',
);
}
/**
* Add class to the Onboarding Wizard subpage menu item.
*
* @since 3.1.1
*/
public function add_menu_item_class() {
new \EDD\Admin\Menu\LinkClass( 'edd-settings', 'edd-settings__menu-item' );
}
/**
* Maybe show an admin pointer showing a message about the new menu locations.
*
* @since 3.1.1
* @return void
*/
public function pointers() {
$pointers = $this->get_pointers();
if ( empty( $pointers ) ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script( 'edd-pointers', EDD_PLUGIN_URL . 'assets/lite/js/pointers.js', array( 'wp-pointer' ), EDD_VERSION, true );
wp_localize_script( 'edd-pointers', 'eddPointers', $pointers );
}
/**
* Gets the array of pointer notices.
*
* @since 3.1.1
* @return array
*/
private function get_pointers() {
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
if ( ! $this->has_pass_no_license() ) {
return false;
}
// Exclude some pages from showing our pointers so we don't interfeer with user behavior.
$excluded_pages = array(
'update-core.php',
'plugin-install.php',
);
global $pagenow;
if ( in_array( $pagenow, $excluded_pages, true ) ) {
return false;
}
$valid_pointers = array();
$dismissed = $this->get_user_dismissals( get_current_user_id() );
$pointers = array();
if ( ! edd_is_admin_page( 'download' ) ) {
// Add pointers that need to be registered when we are not on an EDD Admin Page.
$pointers[] = array(
'pointer_id' => 'edd_activate_pass_non_edd_setting_page',
'target' => '#menu-posts-download',
'options' => array(
'content' => $this->get_default_pass_upgrade_content(),
'position' => array(
'edge' => 'left',
'align' => 'middle',
),
),
);
} else {
// Add pointers that need to be registered on EDD Admin Pages.
$pointers[] = array(
'pointer_id' => 'edd_activate_pass_edd_setting_page',
'target' => '.edd-settings__menu-item:not(.current)',
'options' => array(
'content' => $this->get_default_pass_upgrade_content(),
'position' => array(
'edge' => 'left',
'align' => 'middle',
),
),
);
$pointers[] = array(
'pointer_id' => 'edd_activate_pass_button',
'target' => '.edd-pass-handler__action',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p>%s</p>',
__( 'Install the Pro Version!', 'easy-digital-downloads' ),
__( 'We see you already have an active pass. Click here to verify your license key and we\'ll connect you to install Easy Digital Downloads (Pro).', 'easy-digital-downloads' )
),
'position' => array(
'edge' => 'bottom',
'align' => 'left',
),
),
);
}
/**
* Allows adding pointers for registration within the EDD Ecosystem.
*
* @since 3.1.1
* @param array $pointers The registerd pointers for EDD to load.
*/
$pointers = apply_filters( 'edd_pointers', $pointers );
foreach ( $pointers as $pointer ) {
if (
empty( $pointer ) ||
empty( $pointer['pointer_id'] ) ||
empty( $pointer['target'] ) ||
empty( $pointer['options'] ) ||
in_array( $pointer['pointer_id'], $dismissed, true )
) {
continue;
}
$valid_pointers['pointers'][] = $pointer;
}
return $valid_pointers;
}
/**
* Gets the dismissed_wp_pointers user meta.
*
* @since 3.1.1
* @param int $user_id THe current user ID.
* @return array
*/
public function get_user_dismissals( $user_id ) {
return explode( ',', (string) get_user_meta( $user_id, 'dismissed_wp_pointers', true ) );
}
/**
* Dismisses the pointer notices for new users.
*
* @since 3.1.1
* @param int $user_id The new user ID.
* @return void
*/
public function dismiss_pointers_for_new_users( $user_id ) {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( $this->has_pass_no_license() ) {
return;
}
$dismissals = $this->get_user_dismissals( $user_id );
if ( ! in_array( 'edd_activate_pass', $dismissals, true ) ) {
$dismissals[] = 'edd_activate_pass';
}
if ( ! in_array( 'edd_activate_pass_button', $dismissals, true ) ) {
$dismissals[] = 'edd_activate_pass_button';
}
if ( ! in_array( 'edd_activate_pass_non_edd_setting_page', $dismissals, true ) ) {
$dismissals[] = 'edd_activate_pass_non_edd_setting_page';
}
update_user_meta( $user_id, 'dismissed_wp_pointers', implode( ',', array_filter( $dismissals ) ) );
}
/**
* Checks whether the site has an active pass, but hasn't entered the pro license key yet.
*
* @since 3.1.1
* @return bool
*/
private function has_pass_no_license() {
$pro_license = new \EDD\Licensing\License( 'pro' );
if ( ! empty( $pro_license->key ) ) {
return false;
}
$pass_manager = new \EDD\Admin\Pass_Manager();
return ! empty( $pass_manager->highest_license_key );
}
/**
* Gets the default notice content for users with passes.
*
* @since 3.1.1.2
* @return string
*/
private function get_default_pass_upgrade_content() {
$settings_url = edd_get_admin_url(
array(
'page' => 'edd-settings',
)
);
return sprintf(
'<h3>%s</h3><p>%s</p>',
__( 'You\'re eligible to install EDD (Pro)!', 'easy-digital-downloads' ),
sprintf(
/* translators: 1. opening anchor tag; 2. closing anchor tag */
__( 'Good news! With your pass subscription, you can install the Pro version of Easy Digital Downloads. %1$sVisit the settings page%2$s to verify your license and access Pro only features.', 'easy-digital-downloads' ),
'<a href="' . esc_url( $settings_url ) . '">',
'</a>'
)
);
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Core class for adding event subscribers.
*
* @since 3.1.1
* @package EDD
*/
namespace EDD\Lite;
class Core extends \EDD\Core {
/**
* Gets the service providers for EDD.
*
* @return array
*/
protected function get_service_providers() {
return array_merge( parent::get_service_providers(), $this->get_lite_providers() );
}
/**
* Gets the admin service providers.
*
* @since 3.1.1
* @return array
*/
protected function get_admin_providers() {
return array_merge( parent::get_admin_providers(), $this->get_lite_admin_providers() );
}
/**
* Gets the lite service providers.
*
* @since 3.1.1
* @return array
*/
private function get_lite_providers() {
return array(
new Admin\PassHandler\Connect( $this->pass_handler ),
);
}
/**
* Gets the lite admin providers.
*
* @since 3.1.1
* @return array
*/
private function get_lite_admin_providers() {
if ( ! is_admin() ) {
return array();
}
return array(
new Admin\Menu(),
new Admin\PassHandler\Pointer(),
);
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* Notification.php
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license GPL2+
* @since 2.11.4
*/
namespace EDD\Models;
class Notification {
/**
* @var int Unique internal ID.
*/
public $id;
/**
* @var null|int ID from the remote feed. If `null` then this notification was added internally
* and not via the remote import.
*/
public $remote_id = null;
/**
* @var string Title of the notification.
*/
public $title;
/**
* @var string Notification content.
*/
public $content;
/**
* @var array|null Button information, if set.
*/
public $buttons;
/**
* @var string Notification type, including: `warning`, `error`, `info`, or `success`.
*/
public $type;
/**
* @var array|null Conditions that must be met to display this notification. If `null`
* then there are no conditions.
*/
public $conditions = null;
/**
* @var null|string Date to start displaying the notification.
*/
public $start = null;
/**
* @var null|string Date to stop displaying the notification.
*/
public $end = null;
/**
* @var bool Whether this notification has been dismissed by the user.
*/
public $dismissed = false;
/**
* @var string Date the notification was added to the database.
*/
public $date_created;
/**
* @var string Date the notification was last updated in the database.
*/
public $date_updated;
/**
* @var string[]
*/
protected $casts = array(
'id' => 'int',
'remote_id' => 'int',
'buttons' => 'array',
'conditions' => 'array',
'dismissed' => 'bool',
);
/**
* Constructor
*
* @param array $data Row from the database.
*/
public function __construct( $data = array() ) {
foreach ( $data as $property => $value ) {
if ( property_exists( $this, $property ) ) {
$this->{$property} = $this->castAttribute( $property, $value );
}
}
}
/**
* Casts a property to its designated type.
*
* @todo Move to trait or base class.
*
* @since 2.11.4
*
* @param string $propertyName
* @param mixed $value
*
* @return bool|float|int|mixed|string|null
*/
private function castAttribute( $propertyName, $value ) {
if ( ! array_key_exists( $propertyName, $this->casts ) ) {
return $value;
}
// Let null be null.
if ( is_null( $value ) ) {
return null;
}
switch ( $this->casts[ $propertyName ] ) {
case 'array' :
return json_decode( $value, true );
case 'bool' :
return (bool) $value;
case 'float' :
return (float) $value;
case 'int' :
return (int) $value;
case 'string' :
return (string) $value;
default :
return $value;
}
}
/**
* Returns the icon name to use for this notification type.
*
* @since 2.11.4
*
* @return string
*/
public function getIcon() {
switch ( $this->type ) {
case 'warning' :
return 'warning';
case 'error' :
return 'dismiss';
case 'info' :
return 'admin-generic';
case 'success' :
default :
return 'yes-alt';
}
}
/**
* Converts this model to an array.
*
* @todo Move to trait.
*
* @since 2.11.4
*
* @return array
*/
public function toArray() {
$data = array();
/*
* get_object_vars() returns non-public properties when used within the class
* so we're using a ReflectionClass to get the public properties only.
*/
$object = new \ReflectionClass( $this );
foreach ( $object->getProperties( \ReflectionProperty::IS_PUBLIC ) as $property ) {
if ( $property instanceof \ReflectionProperty && isset( $this->{$property->name} ) ) {
$data[ $property->name ] = $this->{$property->name};
}
}
$data['icon_name'] = $this->getIcon();
/* Translators: %s - a length of time (e.g. "1 second") */
$data['relative_date'] = sprintf( __( '%s ago', 'easy-digital-downloads' ), human_time_diff( strtotime( $this->date_created ) ) );
return $data;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Telemetry Data.
*
* Gets the data to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
class Data {
/**
* The unique anonymized site ID.
*
* @var string
*/
private $id;
/**
* Gets all of the site data.
*
* @return false|array
*/
public function get() {
$data = array(
'id' => $this->get_id(),
);
$classes = array(
'environment' => new Environment(),
'integrations' => new Integrations(),
'licenses' => new Licenses(),
'sales' => new Orders(),
'refunds' => new Orders( 'refund' ),
'settings' => new Settings(),
'stats' => new Stats(),
);
foreach ( $classes as $key => $class ) {
$data[ $key ] = $class->get();
}
return $data;
}
/**
* Gets the unique site ID.
* This is generated from the home URL and two random pieces of data
* to create a hashed site ID that anonymizes the site data.
*
* @since 3.1.1
* @return string
*/
private function get_id() {
$this->id = get_option( 'edd_telemetry_uuid' );
if ( $this->id ) {
return $this->id;
}
$home_url = get_home_url();
$uuid = wp_generate_uuid4();
$today = gmdate( 'now' );
$this->id = md5( $home_url . $uuid . $today );
update_option( 'edd_telemetry_uuid', $this->id, false );
return $this->id;
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Gets the environment to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
class Environment {
public function get() {
$data = array(
'php_version' => phpversion(),
'wp_version' => $this->get_wp_version(),
'edd_version' => EDD_VERSION,
'edd_pro' => (int) (bool) edd_is_pro(),
'locale' => get_locale(),
'active_theme' => $this->get_active_theme(),
'multisite' => (int) (bool) is_multisite(),
'is_ssl' => (int) (bool) is_ssl(),
'stripe_connect' => (int) (bool) edd_get_option( 'stripe_connect_account_id' ),
);
$server = $this->parse_server();
return array_merge( $data, $server );
}
/**
* Adds the server data to the array of data.
*
* @since 3.1.1
* @return array
*/
private function parse_server() {
$server = ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : 'unknown' );
$server = explode( '/', $server );
$data = array(
'server' => $server[0],
);
if ( isset( $server[1] ) ) {
$data['server_version'] = $server[1];
}
return $data;
}
/**
* Gets the WordPress version.
*
* @since 3.1.1
* @return string
*/
private function get_wp_version() {
$version = get_bloginfo( 'version' );
$version = explode( '-', $version );
return reset( $version );
}
/**
* Gets the active theme name.
*
* @since 3.1.1
* @return string
*/
private function get_active_theme() {
$active_theme = wp_get_theme();
return $active_theme->name;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Gets the store settings data to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
class Integrations {
/**
* Gets the integrations data.
*
* @todo currently returning two different kinds of data.
* @since 3.1.1
* @return array
*/
public function get() {
$data = array();
foreach ( $this->get_all_plugins() as $basename => $details ) {
if ( ! $this->should_log_integration( $basename, $details ) ) {
continue;
}
$data[] = array(
'name' => $details['Name'],
'type' => $this->is_core_integration( $basename, $details ) ? 'core' : 'external',
'version' => $details['Version'],
);
}
return $data;
}
/**
* Gets all plugins on the site.
*
* @since 3.1.1
* @return array
*/
private function get_all_plugins() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugins();
}
/**
* Whether the integration should be included in the data.
*
* @since 3.1.1
* @param string $basename
* @param array $details
* @return bool
*/
private function should_log_integration( $basename, $details ) {
if ( ! is_plugin_active( $basename ) ) {
return false;
}
return true;
}
/**
* Whether the integration is an EDD or third party integration.
*
* @since 3.1.1
* @param string $basename
* @param array $details
* @return bool
*/
private function is_core_integration( $basename, $details ) {
if ( 'Easy Digital Downloads' === $details['Author'] ) {
return true;
}
if ( in_array( untrailingslashit( $details['AuthorURI'] ), array( 'https://easydigitaldownloads.com', 'https://sandhillsdev.com' ), true ) ) {
return false !== strpos( $details['PluginURI'], 'easydigitaldownloads.com' );
}
return false;
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Gets the licensing data to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
use EDD\Licensing\License;
class Licenses {
/**
* Gets the gateway data.
*
* @since 3.1.1
* @return array
*/
public function get() {
$data = $this->get_extensions();
$pro_license = $this->get_pro_license();
if ( $pro_license ) {
$data[] = $pro_license;
}
return $data;
}
/**
* Gets the pro license status.
*
* @since 3.1.1
* @return array
*/
private function get_pro_license() {
$pro_license = new License( 'pro' );
return array(
'extension' => 'edd_pro',
'status' => $pro_license->license,
);
}
/**
* Gets licensed extensions' statuses.
*
* @since 3.1.1
* @return array
*/
private function get_extensions() {
$data = array();
$extensions = \EDD\Extensions\get_licensed_extension_slugs();
foreach ( $extensions as $slug ) {
$shortname = str_replace( 'edd_', '', $slug );
$license = new License( $shortname );
if ( ! empty( $license->license ) ) {
$data[] = array(
'extension' => $slug,
'status' => $license->license,
);
}
}
return $data;
}
}

View File

@ -0,0 +1,166 @@
<?php
/**
* Gets the order data to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
class Orders {
/**
* The order type to query.
*
* @var string
*/
protected $type;
public function __construct( $type = 'sale' ) {
$this->type = $type;
}
/**
* Gets the gateway data.
*
* @return array
*/
public function get() {
$data = array(
'all_gateways' => $this->get_totals(),
);
foreach ( $this->get_date_ranges() as $type => $start ) {
foreach ( $this->get_totals_by_gateway( $start ) as $gateway => $currency ) {
foreach ( $currency as $code => $amounts ) {
$data[ $gateway ][ $code ][ $type ] = $amounts;
}
}
}
return $data;
}
/**
* Gets the store order totals for all currencies and gateways.
*
* @since 3.1.1
* @return array
*/
private function get_totals() {
$data = array();
foreach ( $this->get_date_ranges() as $type => $start ) {
$results = $this->get_totals_by_date( $start );
foreach ( $results as $result ) {
$data['all_currencies'][ $type ] = array(
'count' => $result->sales,
'total' => $result->earnings,
);
}
}
return $data;
}
/**
* Gets the order count/total for a given date range.
*
* @since 3.1.1
* @param string $start The start date (optional).
* @return array
*/
private function get_totals_by_gateway( $start = '' ) {
$data = array();
foreach ( $this->get_results_by_date( $start ) as $total ) {
$gateway = $total->gateway ? $total->gateway : 'unknown';
$currency = $total->currency ? $total->currency : 'unknown';
$data[ $gateway ][ $currency ] = array(
'count' => $total->sales,
'total' => $total->earnings,
);
}
return $data;
}
/**
* Gets order totals by date.
*
* @since 3.1.1
* @param string $start
* @return array
*/
private function get_totals_by_date( $start = '' ) {
global $wpdb;
return $wpdb->get_results(
"SELECT COUNT(*) as sales, SUM(total) as earnings
FROM {$wpdb->edd_orders}
WHERE type = '{$this->type}'
{$this->get_status_query()}
{$this->get_date_query( $start )}
LIMIT 0, 99999;"
);
}
/**
* Gets orders grouped by gateway and currency.
*
* @since 3.1.1
* @param string $start
* @return array
*/
private function get_results_by_date( $start = '' ) {
global $wpdb;
return $wpdb->get_results(
"SELECT gateway, currency, COUNT(*) as sales, SUM(total) as earnings
FROM {$wpdb->edd_orders}
WHERE type = '{$this->type}'
{$this->get_status_query()}
{$this->get_date_query( $start )}
GROUP BY gateway, currency
LIMIT 0, 99999;"
);
}
/**
* Gets the status query string.
*
* @since 3.1.1
* @return string
*/
private function get_status_query() {
return "AND status IN ('" . implode( "', '", edd_get_gross_order_statuses() ) . "')";
}
/**
* Gets the date query string.
*
* @since 3.1.1
* @param string $start
* @return string
*/
private function get_date_query( $start = '' ) {
return $start ? sprintf(
"AND ( date_completed >= '%s' AND date_completed <= '%s' )",
gmdate( 'Y-m-d 00:00:00', strtotime( $start ) ),
gmdate( 'Y-m-d 00:00:00', strtotime( 'today' ) )
) : '';
}
/**
* Gets the date ranges for each query.
*
* @since 3.1.1
* @return array
*/
private function get_date_ranges() {
return array(
'lifetime' => '',
'week' => '-1 week',
'month' => '-30 days',
);
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* Gets the store settings data to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
class Settings {
/**
* Gets the array of settings data.
*
* @since 3.1.1
* @return array
*/
public function get() {
$data = array();
$settings_tabs = edd_get_settings_tabs();
$settings = edd_get_registered_settings();
foreach ( $settings_tabs as $tab_key => $tab_contents ) {
$tab_sections = edd_get_settings_tab_sections( $tab_key );
foreach ( $tab_sections as $section_key => $section_title ) {
$section_setting_types = edd_get_registered_settings_types( $tab_key, $section_key );
if ( ! empty( $settings[ $tab_key ] ) && ! empty( $settings[ $tab_key ][ $section_key ] ) ) {
$section_settings = $settings[ $tab_key ][ $section_key ];
foreach ( $section_settings as $setting_key => $setting ) {
$value = $this->get_setting_value( $tab_key, $section_key, $setting_key );
// If the value is null, it's a skipped setting.
if ( ! is_null( $value ) ) {
$setting_id = isset( $setting['id'] ) ? $setting['id'] : sanitize_title( $setting['name'] );
$data[ $setting_id ] = $value;
}
}
}
}
}
return $data;
}
/**
* Gets the id and value for an individual setting.
*
* @param string $tab_key
* @param string $section_key
* @param string $setting_key
* @return mixed
*/
private function get_setting_value( $tab_key, $section_key, $setting_key ) {
$setting = edd_get_registered_setting_details( $tab_key, $section_key, $setting_key );
if ( ! $this->can_include_setting( $setting ) ) {
return null;
}
$default = isset( $setting['std'] ) ? $setting['std'] : '';
$value = edd_get_option( $setting['id'], $default );
if ( in_array( $setting['type'], array( 'checkbox', 'checkbox_description' ), true ) ) {
return (int) (bool) $value;
}
if ( empty( $value ) && 'currency' === $setting['id'] ) {
return edd_get_currency();
}
if ( in_array( $setting['type'], $this->text_settings(), true ) ) {
return $this->anonymize_setting( $value );
}
if ( $this->should_populate_array( $setting ) ) {
return $this->update_setting_value_array( $value, $setting );
}
return $value;
}
/**
* Evaluates whether a setting can be included in the telemetry data.
*
* @since 3.1.1
* @param array $setting
* @return bool
*/
private function can_include_setting( $setting ) {
// If the setting is marked readonly then it's not really a setting.
if ( ! empty( $setting['args']['readonly'] ) ) {
return false;
}
// Certain types of settings should always be skipped.
if ( in_array( $setting['type'], $this->skipped_settings_types(), true ) ) {
return false;
}
// Settings known to be PII are excluded.
if ( in_array( $setting['id'], $this->sensitive_settings(), true ) ) {
return false;
}
// Text settings are always excluded unless specifically included.
if ( in_array( $setting['type'], $this->text_settings(), true ) && ! in_array( $setting['id'], $this->allowed_text_settings(), true ) ) {
return false;
}
return true;
}
/**
* These settings types are either not settings or nearly always full of sensitive data/PII.
*
* @since 3.1.1
* @return array
*/
private function skipped_settings_types() {
return array_merge(
edd_get_non_setting_types(),
array(
'rich_editor',
'upload',
'color',
'recapture',
)
);
}
/**
* These settings are known to be sensitive/PII and are not otherwise excluded.
*
* @since 3.1.1
* @return array
*/
private function sensitive_settings() {
return array(
'base_state',
'paypal_live_client_id',
'paypal_live_client_secret',
'paypal_sandbox_client_id',
'paypal_sandbox_client_secret',
);
}
/**
* We assume that any text field should be excluded unless it's in this array.
*
* @since 3.1.1
* @return array
*/
private function allowed_text_settings() {
return array();
}
/**
* Settings types which will be strings and which should be evaluated for PII.
*
* @since 3.1.1
* @return array
*/
private function text_settings() {
return array(
'text',
'textarea',
'email',
);
}
/**
* Whether an array of settings should be populated, due to the setting type.
*
* @since 3.1.1
* @param array $setting
* @return bool
*/
private function should_populate_array( $setting ) {
$settings = array( 'gateways', 'accepted_cards' );
return 'multicheck' === $setting['type'] || in_array( $setting['id'], $settings, true );
}
/**
* Attempts to anonymize a setting value.
* @todo check how we want to replace--values or empty strings?
*
* @since 3.1.1
* @param string $value
* @return string
*/
private function anonymize_setting( $value ) {
$admin_email = get_bloginfo( 'admin_email' );
$value = str_replace( $admin_email, 'email@website.dev', $value );
$site_name = get_bloginfo( 'name' );
$value = str_replace( $site_name, 'Site Name', $value );
$home_url = get_home_url();
$value = str_replace( $home_url, 'website.dev', $value );
return $value;
}
/**
* Updates the an array setting value to include all options.
*
* @since 3.1.1
* @param mixed $saved_value The actual saved value (can be empty).
* @param array $setting The setting definition.
* @return array
*/
private function update_setting_value_array( $saved_value, $setting ) {
$value = array();
foreach ( $setting['options'] as $key => $label ) {
if ( is_array( $saved_value ) && ! empty( $saved_value[ $key ] ) ) {
$value[] = $key;
}
}
return $value;
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Gets the store stats to send to our telemetry server.
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2023, Easy Digital Downloads
* @license GPL2+
* @since 3.1.1
*/
namespace EDD\Telemetry;
use EDD\Admin\Pass_Manager;
class Stats {
/**
* The number of products on the site.
*
* @since 3.1.1
* @var int
*/
private $product_count;
public function get() {
return array(
'activated' => $this->convert_timestamp( edd_get_activation_date() ),
'pro_activated' => $this->convert_timestamp( get_option( 'edd_pro_activation_date' ) ),
'first_order' => $this->get_first_order_date(),
'onboarding_started' => get_option( 'edd_onboarding_started' ),
'onboarding_completed' => get_option( 'edd_onboarding_completed' ),
'products' => $this->get_product_count(),
'pass_id' => $this->get_pass_id(),
);
}
/**
* Gets the date of the first completed order.
*
* @since 3.1.1
* @return string
*/
private function get_first_order_date() {
$orders = edd_get_orders(
array(
'mode' => 'live',
'status__in' => edd_get_complete_order_statuses(),
'number' => 1,
'fields' => 'date_completed',
'orderby' => 'id',
'order' => 'ASC',
),
);
return ! empty( $orders ) ? reset( $orders ) : '';
}
/**
* Converts a timestamp value to a date string for consistent dates.
*
* @since 3.1.1
* @param string $timestamp
* @return string
*/
private function convert_timestamp( $timestamp = '' ) {
return $timestamp ? gmdate( 'Y-m-d H:i:s', $timestamp ) : '';
}
/**
* Gets the site pass ID.
*
* @since 3.1.1
* @return int|string
*/
private function get_pass_id() {
$pass_manager = new Pass_Manager();
return $pass_manager->highest_pass_id;
}
/**
* Gets the number of published products on the website.
*
* @since 3.1.1
* @return int
*/
private function get_product_count() {
if ( $this->product_count ) {
return $this->product_count;
}
$query = new \WP_Query(
array(
'post_type' => 'download',
'status' => 'publish',
'nopaging' => true,
)
);
$this->product_count = $query->found_posts;
return $this->product_count;
}
/**
* Gets the average total earnings per product.
*
* @since 3.1.1
* @return float
*/
private function get_average_per_product() {
global $wpdb;
$results = $wpdb->get_results(
"SELECT SUM(total) as earnings
FROM {$wpdb->edd_orders}
WHERE type = 'sale'
AND status IN ('" . implode( "', '", edd_get_gross_order_statuses() ) . "')
LIMIT 0, 99999;"
);
if ( empty( $results ) ) {
return 0;
}
$results = reset( $results );
$products = $this->get_product_count();
return $results->earnings / $products;
}
}

View File

@ -0,0 +1,227 @@
<?php
/**
* EnvironmentChecker.php
*
* Checks to see if the environment matches the passed conditions.
* Supported conditions include:
*
* - EDD version number -- either specific versions or wildcards (e.g. "2.x").
* - Type of license (pass level, à la carte, free).
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license GPL2+
* @since 2.11.4
*/
namespace EDD\Utils;
use EDD\Admin\Pass_Manager;
class EnvironmentChecker {
/**
* @var Pass_Manager
*/
protected $passManager;
/**
* Types of license/pass conditions that we support.
* The key is the condition slug and the value is the corresponding
* method to call in the `Pass_Manager` class to check the condition.
*
* @since 2.11.4
*
* @var string[]
*/
protected $validLicenseConditions = array(
'free' => 'isFree',
'ala-carte' => 'hasIndividualLicense',
'pass-personal' => 'hasPersonalPass',
'pass-extended' => 'hasExtendedPass',
'pass-professional' => 'hasProfessionalPass',
'pass-all-access' => 'hasAllAccessPass',
'pass-any' => 'has_pass',
);
/**
* Constructor.
*/
public function __construct() {
$this->passManager = new Pass_Manager();
}
/**
* Checks to see if this environment meets the specified condition.
*
* @since 2.11.4
*
* @param string $condition Condition to check. Can either be a type of license/pass or a version number.
*
* @return bool
* @throws \InvalidArgumentException
*/
public function meetsCondition( $condition ) {
if ( array_key_exists( $condition, $this->validLicenseConditions ) ) {
return $this->hasLicenseType( $condition );
} elseif ( $this->isPaymentGateway( $condition ) ) {
return $this->paymentGatewayMatch( array_keys( edd_get_enabled_payment_gateways() ), $condition );
} elseif ( $this->isVersionNumber( $condition ) ) {
return $this->versionNumbersMatch( EDD_VERSION, $condition );
}
throw new \InvalidArgumentException( 'Invalid condition. Must either be a type of license or a version number.' );
}
/**
* Checks to see if this environment meets all the specified conditions. If any one condition
* is not met then this returns false.
*
* @since 2.11.4
*
* @param array $conditions
*
* @return bool
*/
public function meetsConditions( $conditions ) {
foreach ( $conditions as $condition ) {
if ( ! $this->meetsCondition( $condition ) ) {
return false;
}
}
return true;
}
/**
* Determines if the site has the specified pass condition.
*
* @see EnvironmentChecker::$validLicenseConditions
*
* @since 2.11.4
*
* @param string $passLevel License type that we're checking to see if the system has.
*
* @return bool
* @throws \InvalidArgumentException
*/
protected function hasLicenseType( $passLevel ) {
$method = isset( $this->validLicenseConditions[ $passLevel ] )
? $this->validLicenseConditions[ $passLevel ]
: false;
if ( ! $method || ! method_exists( $this->passManager, $method ) ) {
throw new \InvalidArgumentException( sprintf( 'Method %s not found in Pass_Manager.', $method ) );
}
return call_user_func( array( $this->passManager, $method ) );
}
/**
* Determines if the provided condition is a payment gateway.
*
* @since 2.11.4
*
* @param string $condition
*
* @return bool
*/
protected function isPaymentGateway( $condition ) {
return 'gateway-' === substr( $condition, 0, 8 );
}
/**
* Determines if the supplied gateway condition is applicable to this site.
* Will return `true` if the condition is the slug of a payment gateway (potentially with a `gateway-` prefix)
* that's enabled on this site.
*
* @since 2.11.4
*
* @param array $enabledGateways Gateways that are enabled on this site.
* @param string $condition Gateway we're checking to see if it's enabled.
*
* @return bool True if the gateway is enabled, false if not.
*/
public function paymentGatewayMatch( $enabledGateways, $condition ) {
$gatewayToCheck = str_replace( 'gateway-', '', $condition );
return in_array( $gatewayToCheck, $enabledGateways, true );
}
/**
* Determines if the provided condition is a version number.
*
* @since 2.11.4
*
* @param string $condition
*
* @return bool
*/
protected function isVersionNumber( $condition ) {
// First character should always be numeric.
if ( ! is_numeric( substr( $condition, 0, 1 ) ) ) {
return false;
}
// Must contain at least one `.` or `-`.
return false !== strpos( $condition, '.' ) || false !== strpos( $condition, '-' );
}
/**
* Determines if two version numbers match, or if the `$currentVersion` falls within the wildcard
* range specified by `$compareVersion`.
*
* @since 2.11.4
*
* @param string $currentVersion Version number currently in use. This must be a full, exact version number.
* @param string $compareVersion Version to compare with. This can either be an exact version number or a
* wildcard (e.g. `2.11.3` or `2.x`). Hyphens are also accepted in lieu of
* full stops (e.g. `2-11-3` or `2-x`).
*
* @return bool
* @throws \InvalidArgumentException
*/
public function versionNumbersMatch( $currentVersion, $compareVersion ) {
$currentVersionPieces = explode( '.', $currentVersion );
if ( false !== strpos( $compareVersion, '.' ) ) {
$compareVersionPieces = explode( '.', $compareVersion );
} else if ( false !== strpos( $compareVersion, '-' ) ) {
$compareVersionPieces = explode( '-', $compareVersion );
} else {
throw new \InvalidArgumentException( sprintf(
'Invalid version number: %s',
$compareVersion
) );
}
$numberCurrentVersionParts = count( $currentVersionPieces );
$numberCompareVersionParts = count( $compareVersionPieces );
/*
* Normalize the two parts so that they have the same lengths and
* wildcards (`x`) are removed.
*/
for ( $i = 0; $i < $numberCurrentVersionParts || $i < $numberCompareVersionParts; $i ++ ) {
if ( isset( $compareVersionPieces[ $i ] ) && 'x' === strtolower( $compareVersionPieces[ $i ] ) ) {
unset( $compareVersionPieces[ $i ] );
}
if ( ! isset( $currentVersionPieces[ $i ] ) ) {
unset( $compareVersionPieces[ $i ] );
} elseif ( ! isset( $compareVersionPieces[ $i ] ) ) {
unset( $currentVersionPieces[ $i ] );
}
}
// Now make sure all the numbers match.
foreach ( $compareVersionPieces as $index => $versionPiece ) {
if ( ! isset( $currentVersionPieces[ $index ] ) || $currentVersionPieces[ $index ] !== $versionPiece ) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,251 @@
<?php
/**
* NotificationImporter.php
*
* @package easy-digital-downloads
* @copyright Copyright (c) 2021, Easy Digital Downloads
* @license GPL2+
* @since 2.11.4
*/
namespace EDD\Utils;
class NotificationImporter {
/**
* @var EnvironmentChecker
*/
protected $environmentChecker;
/**
* Constructor
*/
public function __construct() {
$this->environmentChecker = new EnvironmentChecker();
}
/**
* Fetches notifications from the API and imports them locally.
*
* @since 2.11.4
*/
public function run() {
$request_timeout = get_option( 'edd_notification_req_timeout', false );
if ( false !== $request_timeout && current_time( 'timestamp' ) < $request_timeout ) {
edd_debug_log( 'Skipping notifications API request, timeout not reached' );
return;
}
edd_debug_log( 'Fetching notifications via ' . $this->getApiEndpoint() );
try {
$notifications = $this->fetchNotifications();
// If successful, make it so we don't request for another 23 hours.
update_option( 'edd_notification_req_timeout', current_time( 'timestamp' ) + ( HOUR_IN_SECONDS * 23 ), false );
} catch ( \Exception $e ) {
edd_debug_log( sprintf( 'Notification fetch exception: %s', $e->getMessage() ) );
// If for some reason our request failed, delay for 4 hours.
update_option( 'edd_notification_req_timeout', current_time( 'timestamp' ) + ( HOUR_IN_SECONDS * 4 ), false );
return;
}
foreach ( $notifications as $notification ) {
$notificationId = isset( $notification->id ) ? $notification->id : 'unknown';
edd_debug_log( sprintf( 'Processing notification ID %s', $notificationId ) );
try {
$this->validateNotification( $notification );
$existingId = $this->get_column_by( 'id', 'remote_id', $notification->id );
if ( $existingId ) {
edd_debug_log( '-- Updating existing notification.' );
$this->updateExistingNotification( $existingId, $notification );
} else {
edd_debug_log( '-- Inserting new notification.' );
$this->insertNewNotification( $notification );
}
} catch ( \Exception $e ) {
edd_debug_log( sprintf( '-- Notification processing failure for ID %s: %s', $notificationId, $e->getMessage() ) );
}
}
}
/**
* Returns the API endpoint to query.
*
* @since 2.11.4
*
* @return string
*/
protected function getApiEndpoint() {
if ( defined( 'EDD_NOTIFICATIONS_API_URL' ) ) {
return EDD_NOTIFICATIONS_API_URL;
}
return 'https://plugin.easydigitaldownloads.com/wp-content/notifications.json';
}
/**
* Retrieves notifications from the remote API endpoint.
*
* @since 2.11.4
*
* @return array
* @throws \Exception
*/
public function fetchNotifications() {
$response = wp_remote_get( $this->getApiEndpoint() );
if ( is_wp_error( $response ) ) {
throw new \Exception( $response->get_error_message() );
}
$notifications = wp_remote_retrieve_body( $response );
return ! empty( $notifications ) ? json_decode( $notifications ) : array();
}
/**
* Validates the notification from the remote API to make sure we actually
* want to save it.
*
* @since 2.11.4
*
* @param object $notification
*
* @throws \Exception
*/
public function validateNotification( $notification ) {
// Make sure we have all the required data.
$requiredProperties = array(
'id',
'title',
'content',
);
$missing = array_diff( $requiredProperties, array_keys( get_object_vars( $notification ) ) );
if ( $missing ) {
throw new \Exception( sprintf( 'Missing required properties: %s', json_encode( array_values( $missing ) ) ) );
}
// Don't save the notification if it has expired.
if ( ! empty( $notification->end ) && time() > strtotime( $notification->end ) ) {
throw new \Exception( 'Notification has expired.' );
}
// Ignore if notification was created before EDD was installed.
if ( ! empty( $notification->start ) && edd_get_activation_date() > strtotime( $notification->start ) ) {
throw new \Exception( 'Notification created prior to EDD activation.' );
}
if (
! empty( $notification->type ) &&
is_array( $notification->type ) &&
! $this->environmentChecker->meetsConditions( $notification->type )
) {
throw new \Exception( 'Condition(s) not met.' );
}
}
/**
* Retrieves the array of notification data to insert into the database.
* Use in both inserts and updates.
*
* @since 2.11.4
*
* @param object $notification
*
* @return array
*/
protected function getNotificationData( $notification ) {
return array(
'remote_id' => $notification->id,
'title' => $notification->title,
'content' => $notification->content,
'buttons' => $this->parseButtons( $notification ),
'type' => ! empty( $notification->notification_type ) ? $notification->notification_type : 'success',
'conditions' => ! empty( $notification->type ) ? $notification->type : null,
'start' => ! empty( $notification->start ) ? $notification->start : null,
'end' => ! empty( $notification->end ) ? $notification->end : null,
);
}
/**
* Parses and formats buttons from the remote notification object.
*
* @since 2.11.4
*
* @param object $notification
*
* @return array|null
*/
protected function parseButtons( $notification ) {
if ( empty( $notification->btns ) ) {
return null;
}
$buttons = array();
foreach ( (array) $notification->btns as $buttonType => $buttonInfo ) {
if ( empty( $buttonInfo->url ) || empty( $buttonInfo->text ) ) {
continue;
}
$buttons[] = array(
'type' => ( 'main' === $buttonType ) ? 'primary' : 'secondary',
'url' => $buttonInfo->url,
'text' => $buttonInfo->text,
);
}
return ! empty( $buttons ) ? $buttons : null;
}
/**
* Inserts a new notification into the database.
*
* @since 2.11.4
*
* @param object $notification
* @throws \Exception
*/
protected function insertNewNotification( $notification ) {
$result = EDD()->notifications->insert( $this->getNotificationData( $notification ) );
if ( ! $result ) {
throw new \Exception( 'Failed to insert into database.' );
}
}
/**
* Updates an existing notification.
*
* @since 2.11.4
*
* @param int $existingId
* @param object $notification
*/
protected function updateExistingNotification( $existingId, $notification ) {
EDD()->notifications->update( $existingId, wp_parse_args( $this->getNotificationData( $notification ), array(
'date_updated' => gmdate( 'Y-m-d H:i:s' ),
) ) );
}
/**
* Retrieve a specific column's value by the the specified column / value
*
* @since 3.1.1
* @return string
*/
private function get_column_by( $column, $column_where, $column_value ) {
global $wpdb;
$column_where = esc_sql( $column_where );
$column = esc_sql( $column );
return $wpdb->get_var( $wpdb->prepare( "SELECT $column FROM {$wpdb->edd_notifications} WHERE $column_where = %s LIMIT 1;", $column_value ) );
}
}