CMS Project Sync

This commit is contained in:
2026-04-15 15:59:53 -04:00
parent 015ea75186
commit a747e2a1d9
11220 changed files with 2590467 additions and 0 deletions
@@ -0,0 +1,52 @@
<?php
namespace WPForms;
use WPForms\Admin\Tools\Views\Import;
/**
* Class API.
*
* @since 1.8.6
*/
class API {
/**
* Registry.
* Contains name of the class and method to be called.
* For non-static methods, should contain the id to operate via wpforms->get( 'class' ).
*
* @todo Add non-static methods processing.
*
* @since 1.8.6
*
* @var array[]
*/
private $registry = [
'import_forms' => [
'class' => Import::class,
'method' => 'import_forms',
],
];
/**
* Magic method to call a method from registry.
*
* @since 1.8.6
*
* @param string $name Method name.
* @param array $args Arguments.
*
* @return mixed|null
*/
public function __call( string $name, array $args ) {
$callback = $this->registry[ $name ] ?? null;
if ( $callback === null ) {
return null;
}
return call_user_func( [ $callback['class'], $callback['method'] ], ...$args );
}
}
@@ -0,0 +1,62 @@
<?php
namespace WPForms\Access;
/**
* Access/Capability management.
*
* @since 1.5.8
*/
class Capabilities {
/**
* Init class.
*
* @since 1.5.8
*/
public function init() {
}
/**
* Init conditions.
*
* @since 1.5.8.2
*/
public function init_allowed() {
return false;
}
/**
* Check permissions for currently logged in user.
*
* @since 1.5.8
*
* @param array|string $caps Capability name(s).
* @param int $id Optional. ID of the specific object to check against if capability is a "meta" cap.
* "Meta" capabilities, e.g. 'edit_post', 'edit_user', etc., are capabilities used
* by map_meta_cap() to map to other "primitive" capabilities, e.g. 'edit_posts',
* edit_others_posts', etc. Accessed via func_get_args() and passed to WP_User::has_cap(),
* then map_meta_cap().
*
* @return bool
*/
public function current_user_can( $caps = [], $id = 0 ) {
return \current_user_can( \wpforms_get_capability_manage_options() );
}
/**
* Get a first valid capability from an array of capabilities.
*
* @since 1.5.8
*
* @param array $caps Array of capabilities to check.
*
* @return string
*/
public function get_menu_cap( $caps ) {
return \wpforms_get_capability_manage_options();
}
}
@@ -0,0 +1,600 @@
<?php
namespace WPForms\Admin\Addons;
/**
* Addons data handler.
*
* @since 1.6.6
*/
class Addons {
/**
* Basic license.
*
* @since 1.8.2
*/
const BASIC = 'basic';
/**
* Plus license.
*
* @since 1.8.2
*/
const PLUS = 'plus';
/**
* Pro license.
*
* @since 1.8.2
*/
const PRO = 'pro';
/**
* Elite license.
*
* @since 1.8.2
*/
const ELITE = 'elite';
/**
* Agency license.
*
* @since 1.8.2
*/
const AGENCY = 'agency';
/**
* Ultimate license.
*
* @since 1.8.2
*/
const ULTIMATE = 'ultimate';
/**
* Addons cache object.
*
* @since 1.6.6
*
* @var AddonsCache
*/
private $cache;
/**
* All Addons data.
*
* @since 1.6.6
*
* @var array
*/
private $addons;
/**
* WPForms addons text domains.
*
* @since 1.9.2
*
* @var array
*/
private $addons_text_domains = [];
/**
* WPForms addons titles.
*
* @since 1.9.2
*
* @var array
*/
private $addons_titles = [];
/**
* Determine if the class is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load() {
global $pagenow;
$has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] );
$allowed_pages = in_array( $pagenow ?? '', [ 'plugins.php', 'update-core.php', 'plugin-install.php' ], true );
$allowed_ajax = $pagenow === 'admin-ajax.php' && isset( $_POST['action'] ) && $_POST['action'] === 'update-plugin'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$allowed_requests = $allowed_pages || $allowed_ajax || wpforms_is_admin_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' );
return $has_permissions && $allowed_requests;
}
/**
* Initialize class.
*
* @since 1.6.6
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
$this->cache = wpforms()->obj( 'addons_cache' );
global $pagenow;
// Force update addons cache if we are on the update-core.php page.
// This is necessary to update addons data while checking for all available updates.
if ( $pagenow === 'update-core.php' ) {
$this->cache->update( true );
}
$this->addons = $this->cache->get();
$this->populate_addons_data();
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.6
*/
protected function hooks() {
global $pagenow;
/**
* Fire before admin addons init.
*
* @since 1.6.7
*/
do_action( 'wpforms_admin_addons_init' );
// Filter Gettext only on Plugin list and Updates pages.
if ( $pagenow === 'update-core.php' || $pagenow === 'plugins.php' ) {
add_action( 'gettext', [ $this, 'filter_gettext' ], 10, 3 );
}
}
/**
* Get all addons data as array.
*
* @since 1.6.6
*
* @param bool $force_cache_update Determine if we need to update cache. Default is `false`.
*
* @return array
*/
public function get_all( bool $force_cache_update = false ) {
if ( ! $this->allow_load() ) {
return [];
}
if ( $force_cache_update ) {
$this->cache->update( true );
$this->addons = $this->cache->get();
}
// WPForms 1.8.7 core includes Custom Captcha.
// The Custom Captcha addon will only work on WPForms 1.8.6 and earlier versions.
unset( $this->addons['wpforms-captcha'] );
return $this->get_sorted_addons();
}
/**
* Get sorted addons data.
* Recommended addons will be displayed first,
* then new addons, then featured addons,
* and then all other addons.
*
* @since 1.8.9
*
* @return array
*/
private function get_sorted_addons(): array {
if ( empty( $this->addons ) ) {
return [];
}
$recommended = array_filter(
$this->addons,
static function ( $addon ) {
return ! empty( $addon['recommended'] );
}
);
$new = array_filter(
$this->addons,
static function ( $addon ) {
return ! empty( $addon['new'] );
}
);
$featured = array_filter(
$this->addons,
static function ( $addon ) {
return ! empty( $addon['featured'] );
}
);
return array_merge( $recommended, $new, $featured, $this->addons );
}
/**
* Get filtered addons data.
*
* Usage:
* ->get_filtered( $this->addons, [ 'category' => 'payments' ] ) - addons for the payments panel.
* ->get_filtered( $this->addons, [ 'license' => 'elite' ] ) - addons available for 'elite' license.
*
* @since 1.6.6
*
* @param array $addons Raw addons data.
* @param array $args Arguments array.
*
* @return array Addons data filtered according to given arguments.
*/
private function get_filtered( array $addons, array $args ): array {
$args = wp_parse_args(
$args,
[
'category' => '',
'license' => '',
]
);
$args = array_map( 'strtolower', $args );
$filtered_addons = [];
foreach ( $addons as $addon ) {
foreach ( $args as $arg_key => $arg_value ) {
$addon_value = wpforms_array_get_by_path( $addon, $arg_key, '' );
if (
is_array( $addon_value ) &&
// We cannot use preg_quote here, as $arg_value could contain regex like 'crm|email-marketing|integration'.
preg_grep( '/^' . $arg_value . '$/', $addon_value )
) {
$filtered_addons[] = $addon;
}
}
}
return $filtered_addons;
}
/**
* Get available addons data by category.
*
* @since 1.6.6
*
* @param string $category Addon category.
*
* @return array.
*/
public function get_by_category( string $category ) {
return $this->get_by_path( 'category', $category );
}
/**
* Get available addons data by path.
*
* @since 1.9.8.6
*
* @param string $path Path in addons multidimensional array.
* May be 'category' or 'form_builder.category' or 'settings_integrations.category', etc.
* @param string $value Addons multidimensional array value we are looking for in the path.
*
* @return array
*/
public function get_by_path( string $path, $value ): array {
return $this->get_filtered( $this->get_available(), [ $path => $value ] );
}
/**
* Get available addons data by license.
*
* @since 1.6.6
*
* @param string $license Addon license.
*
* @return array.
* @noinspection PhpUnused
*/
public function get_by_license( string $license ) {
return $this->get_filtered( $this->get_available(), [ 'license' => $license ] );
}
/**
* Get available addons data by slugs.
*
* @since 1.6.8
*
* @param array|mixed $slugs Addon slugs.
*
* @return array
*/
public function get_by_slugs( $slugs ) {
if ( empty( $slugs ) || ! is_array( $slugs ) ) {
return [];
}
$result_addons = [];
foreach ( $slugs as $slug ) {
$addon = $this->get_addon( $slug );
if ( ! empty( $addon ) ) {
$result_addons[] = $addon;
}
}
return $result_addons;
}
/**
* Get available addon data by slug.
*
* @since 1.6.6
*
* @param string|bool $slug Addon slug can be both "wpforms-drip" and "drip".
*
* @return array Single addon data. Empty array if addon is not found.
*/
public function get_addon( $slug ) {
$slug = (string) $slug;
$slug = 'wpforms-' . str_replace( 'wpforms-', '', sanitize_key( $slug ) );
$addon = $this->get_available()[ $slug ] ?? [];
// In case if addon is "not available" let's try to get and prepare addon data from all addons.
if ( empty( $addon ) ) {
$addon = ! empty( $this->addons[ $slug ] ) ? $this->prepare_addon_data( $this->addons[ $slug ] ) : [];
}
return $addon;
}
/**
* Check if addon is active.
*
* @since 1.8.9
*
* @param string $slug Addon slug.
*
* @return bool
*/
public function is_active( string $slug ): bool {
$addon = $this->get_addon( $slug );
return isset( $addon['status'] ) && $addon['status'] === 'active';
}
/**
* Get license level of the addon.
*
* @since 1.6.6
*
* @param array|string $addon Addon data array OR addon slug.
*
* @return string License level: pro | elite.
*/
private function get_license_level( $addon ) {
if ( empty( $addon ) ) {
return '';
}
$levels = [ self::BASIC, self::PLUS, self::PRO, self::ELITE, self::AGENCY, self::ULTIMATE ];
$license = '';
$addon_license = $this->get_addon_license( $addon );
foreach ( $levels as $level ) {
if ( in_array( $level, $addon_license, true ) ) {
$license = $level;
break;
}
}
if ( empty( $license ) ) {
return '';
}
return in_array( $license, [ self::BASIC, self::PLUS, self::PRO ], true ) ? self::PRO : self::ELITE;
}
/**
* Get addon license.
*
* @since 1.8.2
*
* @param array|string $addon Addon data array OR addon slug.
*
* @return array
*/
private function get_addon_license( $addon ) {
$addon = is_string( $addon ) ? $this->get_addon( $addon ) : $addon;
return $this->default_data( $addon, 'license', [] );
}
/**
* Determine if a user's license level has access.
*
* @since 1.6.6
*
* @param array|string $addon Addon data array OR addon slug.
*
* @return bool
*/
protected function has_access( $addon ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
return false;
}
/**
* Return array of addons available to display. All data is prepared and normalized.
* "Available to display" means that addon needs to be displayed as an education item (addon is not installed or not activated).
*
* @since 1.6.6
*
* @return array
*/
public function get_available() {
static $available_addons = [];
if ( $available_addons ) {
return $available_addons;
}
if ( empty( $this->addons ) || ! is_array( $this->addons ) ) {
return [];
}
$available_addons = array_map( [ $this, 'prepare_addon_data' ], $this->addons );
$available_addons = array_filter(
$available_addons,
static function ( $addon ) {
return isset( $addon['status'], $addon['plugin_allow'] ) && ( $addon['status'] !== 'active' || ! $addon['plugin_allow'] );
}
);
return $available_addons;
}
/**
* Prepare addon data.
*
* @since 1.6.6
*
* @param array|mixed $addon Addon data.
*
* @return array Extended addon data.
*/
protected function prepare_addon_data( $addon ) {
if ( empty( $addon ) ) {
return [];
}
$addon['title'] = $this->default_data( $addon, 'title', '' );
$addon['slug'] = $this->default_data( $addon, 'slug', '' );
// We need the cleared name of the addon, without the 'addon' suffix, for further use.
$addon['name'] = preg_replace( '/ addon$/i', '', $addon['title'] );
$addon['modal_name'] = sprintf( /* translators: %s - addon name. */
esc_html__( '%s addon', 'wpforms-lite' ),
$addon['name']
);
$addon['clear_slug'] = str_replace( 'wpforms-', '', $addon['slug'] );
$addon['utm_content'] = ucwords( str_replace( '-', ' ', $addon['clear_slug'] ) );
$addon['license'] = $this->default_data( $addon, 'license', [] );
$addon['license_level'] = $this->get_license_level( $addon );
$addon['icon'] = $this->default_data( $addon, 'icon', '' );
$addon['path'] = sprintf( '%1$s/%1$s.php', $addon['slug'] );
$addon['video'] = $this->default_data( $addon, 'video', '' );
$addon['plugin_allow'] = $this->has_access( $addon );
$addon['status'] = 'missing';
$addon['action'] = 'upgrade';
$addon['page_url'] = $this->default_data( $addon, 'url', '' );
$addon['doc_url'] = $this->default_data( $addon, 'doc', '' );
$addon['url'] = '';
static $nonce = '';
$nonce = empty( $nonce ) ? wp_create_nonce( 'wpforms-admin' ) : $nonce;
$addon['nonce'] = $nonce;
return $addon;
}
/**
* Get default data.
*
* @since 1.8.2
*
* @param array|mixed $addon Addon data.
* @param string $key Key.
* @param mixed $default_data Default data.
*
* @return array|string|mixed
*/
private function default_data( $addon, string $key, $default_data ) {
if ( is_string( $default_data ) ) {
return ! empty( $addon[ $key ] ) ? $addon[ $key ] : $default_data;
}
if ( is_array( $default_data ) ) {
return ! empty( $addon[ $key ] ) ? (array) $addon[ $key ] : $default_data;
}
return $addon[ $key ] ?? '';
}
/**
* Populate addons data.
*
* @since 1.9.2
*
* @return void
*/
private function populate_addons_data() {
foreach ( $this->addons as $addon ) {
$this->addons_text_domains[] = $addon['slug'];
$this->addons_titles[] = 'WPForms ' . str_replace( ' Addon', '', $addon['title'] );
}
}
/**
* Filter Gettext.
*
* This filter allows us to prevent empty translations from being returned
* on the `plugins` page for addon name and description.
*
* @since 1.9.2
*
* @param string|mixed $translation Translated text.
* @param string|mixed $text Text to translate.
* @param string|mixed $domain Text domain.
*
* @return string Translated text.
*/
public function filter_gettext( $translation, $text, $domain ): string {
$translation = (string) $translation;
$text = (string) $text;
$domain = (string) $domain;
if ( ! in_array( $domain, $this->addons_text_domains, true ) ) {
return $translation;
}
// Prevent empty translations from being returned and don't translate addon names.
if ( ! trim( $translation ) || in_array( $text, $this->addons_titles, true ) ) {
$translation = $text;
}
return $translation;
}
}
@@ -0,0 +1,137 @@
<?php
namespace WPForms\Admin\Addons;
use WPForms\Helpers\CacheBase;
/**
* Addons cache handler.
*
* @since 1.6.6
*/
class AddonsCache extends CacheBase {
/**
* Remote source URL.
*
* @since 1.8.9
*
* @var string
*/
const REMOTE_SOURCE = 'https://wpformsapi.com/feeds/v1/addons/';
/**
* Determine if the class is allowed to load.
*
* @since 1.6.8
*
* @return bool
*/
protected function allow_load() {
if ( wp_doing_cron() || wpforms_doing_wp_cli() ) {
return true;
}
$has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] );
$allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' );
return $has_permissions && $allowed_requests;
}
/**
* Provide settings.
*
* @since 1.6.6
*
* @return array Settings array.
*/
protected function setup() {
return [
// Remote source URL.
'remote_source' => $this->get_remote_source(),
// Addons cache file name.
'cache_file' => 'addons.json',
/**
* Time-to-live of the addons cache file in seconds.
*
* This applies to `uploads/wpforms/cache/addons.json` file.
*
* @since 1.6.8
*
* @param integer $cache_ttl Cache time-to-live, in seconds.
* Default value: WEEK_IN_SECONDS.
*/
'cache_ttl' => (int) apply_filters( 'wpforms_admin_addons_cache_ttl', WEEK_IN_SECONDS ),
// Scheduled update action.
'update_action' => 'wpforms_admin_addons_cache_update',
];
}
/**
* Get remote source URL.
*
* @since 1.8.9
*
* @return string
*/
protected function get_remote_source(): string {
return defined( 'WPFORMS_ADDONS_REMOTE_SOURCE' ) ? WPFORMS_ADDONS_REMOTE_SOURCE : self::REMOTE_SOURCE;
}
/**
* Prepare addons data to store in a local cache -
* generate addons icon image file name for further use.
*
* @since 1.6.6
*
* @param array $data Raw addons data.
*
* @return array Prepared data for caching (with icons).
*/
protected function prepare_cache_data( $data ): array {
if ( empty( $data ) || ! is_array( $data ) ) {
return [];
}
$addons_cache = [];
foreach ( $data as $addon ) {
// Addon icon.
$addon['icon'] = str_replace( 'wpforms-', 'addon-icon-', $addon['slug'] ) . '.png';
// Special case when plugin addon renamed, for instance:
// Sendinblue to Brevo, or ConvertKit to Kit,
// but we keep the old slug for compatibility.
foreach (
[
'wpforms-sendinblue' => [
'old' => 'sendinblue',
'new' => 'brevo',
],
'wpforms-convertkit' => [
'old' => 'convertkit',
'new' => 'kit',
],
] as $slug => $renamed
) {
if ( $addon['slug'] === $slug ) {
$addon['icon'] = str_replace( $renamed['old'], $renamed['new'], $addon['icon'] );
}
}
// Use slug as a key for further usage.
$addons_cache[ $addon['slug'] ] = $addon;
}
return $addons_cache;
}
}
@@ -0,0 +1,712 @@
<?php
namespace WPForms\Admin;
use WP_Admin_Bar;
/**
* WPForms admin bar menu.
*
* @since 1.6.0
*/
class AdminBarMenu {
/**
* Initialize class.
*
* @since 1.6.0
*/
public function init() {
if ( ! $this->has_access() ) {
return;
}
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.6.0
*/
public function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_css' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_css' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_js' ] );
add_action( 'admin_bar_menu', [ $this, 'register' ], 999 );
add_action( 'wpforms_wp_footer_end', [ $this, 'menu_forms_data_html' ] );
}
/**
* Determine whether the current user has access to see the admin bar menu.
*
* @since 1.6.0
*
* @return bool
*/
public function has_access(): bool {
$access = false;
if (
is_admin_bar_showing() &&
wpforms_current_user_can() &&
! wpforms_setting( 'hide-admin-bar', false )
) {
$access = true;
}
/**
* Filters whether the current user has access to see the admin bar menu.
*
* @since 1.6.0
*
* @param bool $access Whether the current user has access to see the admin bar menu.
*/
return (bool) apply_filters( 'wpforms_admin_adminbarmenu_has_access', $access ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Determine whether new notifications are available.
*
* @since 1.6.0
*
* @return bool
*/
public function has_notifications() {
return wpforms()->obj( 'notifications' )->get_count();
}
/**
* Enqueue CSS styles.
*
* @since 1.6.0
*/
public function enqueue_css() {
$min = wpforms_get_min_suffix();
wp_enqueue_style(
'wpforms-admin-bar',
WPFORMS_PLUGIN_URL . "assets/css/admin-bar{$min}.css",
[],
WPFORMS_VERSION
);
// Apply WordPress pre/post 5.7 accent color, only when admin bar is displayed on the frontend or we're
// inside the Form Builder - it does not load some WP core admin styles, including themes.
if ( wpforms_is_admin_page( 'builder' ) || ! is_admin() ) {
wp_add_inline_style(
'wpforms-admin-bar',
sprintf(
'#wpadminbar .wpforms-menu-notification-counter, #wpadminbar .wpforms-menu-notification-indicator {
background-color: %s !important;
color: #ffffff !important;
}',
version_compare( get_bloginfo( 'version' ), '5.7', '<' ) ? '#ca4a1f' : '#d63638'
)
);
}
}
/**
* Enqueue JavaScript files.
*
* @since 1.6.5
*/
public function enqueue_js() {
wp_add_inline_script(
'admin-bar',
"( function() {
function wpforms_admin_bar_menu_init() {
var template = document.getElementById( 'tmpl-wpforms-admin-menubar-data' ),
notifications = document.getElementById( 'wp-admin-bar-wpforms-notifications' );
if ( ! template ) {
return;
}
if ( ! notifications ) {
var menu = document.getElementById( 'wp-admin-bar-wpforms-menu-default' );
if ( ! menu ) {
return;
}
menu.insertAdjacentHTML( 'afterBegin', template.innerHTML );
} else {
notifications.insertAdjacentHTML( 'afterend', template.innerHTML );
}
};
document.addEventListener( 'DOMContentLoaded', wpforms_admin_bar_menu_init );
}() );",
'before'
);
}
/**
* Register and render admin bar menu items.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function register( WP_Admin_Bar $wp_admin_bar ) {
$items = (array) apply_filters(
'wpforms_admin_adminbarmenu_register',
[
'main_menu',
'notification_menu',
'all_forms_menu',
'add_new_menu',
'all_payments_menu',
'settings_menu',
'tools_menu',
'community_menu',
'support_menu',
],
$wp_admin_bar
);
foreach ( $items as $item ) {
$this->{ $item }( $wp_admin_bar );
do_action( "wpforms_admin_adminbarmenu_register_{$item}_after", $wp_admin_bar );
}
$this->register_settings_submenu( $wp_admin_bar );
$this->register_tools_submenu( $wp_admin_bar );
}
/**
* Register Settings submenu.
*
* @since 1.9.2
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
private function register_settings_submenu( WP_Admin_Bar $wp_admin_bar ) {
/**
* Filters the Settings submenu items.
*
* @since 1.9.2
*
* @param array $items Array of submenu items.
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
$items = (array) apply_filters(
'wpforms_admin_bar_menu_register_settings_submenu',
[
'wpforms-general-settings' => [
'title' => __( 'General', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=general',
],
'wpforms-email-settings' => [
'title' => __( 'Email', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=email',
],
'wpforms-captcha-settings' => [
'title' => __( 'CAPTCHA', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=captcha',
],
'wpforms-validation-settings' => [
'title' => __( 'Validation', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=validation',
],
'wpforms-payments-settings' => [
'title' => __( 'Payments', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=payments',
],
'wpforms-integrations-settings' => [
'title' => __( 'Integrations', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=integrations',
],
'wpforms-geolocation-settings' => [
'title' => __( 'Geolocation', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=geolocation',
],
'wpforms-access-settings' => [
'title' => __( 'Access Control', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=access',
],
'wpforms-misc-settings' => [
'title' => __( 'Misc', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-settings&view=misc',
],
],
$wp_admin_bar
);
foreach ( $items as $item_id => $args ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-settings',
'id' => sanitize_key( $item_id ),
'title' => esc_html( $args['title'] ),
'href' => admin_url( $args['path'] ),
]
);
/**
* Fires after the Settings submenu item is registered.
*
* @since 1.9.2
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
do_action( "wpforms_admin_bar_menu_register_settings_submenu_{$item_id}_after", $wp_admin_bar );
}
}
/**
* Register Tools submenu.
*
* @since 1.9.3
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
private function register_tools_submenu( WP_Admin_Bar $wp_admin_bar ) {
/**
* Filters the Tools submenu items.
*
* @since 1.9.3
*
* @param array $items Array of submenu items.
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*
* @return array
*/
$items = (array) apply_filters(
'wpforms_admin_bar_menu_register_tools_submenu',
[
'wpforms-tools-import' => [
'title' => esc_html__( 'Import', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=import',
],
'wpforms-tools-export' => [
'title' => esc_html__( 'Export', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=export',
],
'wpforms-tools-entry-automation' => [
'title' => esc_html__( 'Entry Automation', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=entry-automation',
],
'wpforms-tools-system' => [
'title' => esc_html__( 'System Info', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=system',
],
'wpforms-tools-action-scheduler' => [
'title' => esc_html__( 'Scheduled Actions', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms',
],
'wpforms-tools-logs' => [
'title' => esc_html__( 'Logs', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=logs',
],
'wpforms-tools-wpcode' => [
'title' => esc_html__( 'Code Snippets', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=wpcode',
],
],
$wp_admin_bar
);
foreach ( $items as $item_id => $args ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-tools',
'id' => sanitize_key( $item_id ),
'title' => esc_html( $args['title'] ),
'href' => admin_url( $args['path'] ),
]
);
/**
* Fires after the Tools submenu item is registered.
*
* @since 1.9.2
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
do_action( "wpforms_admin_bar_menu_register_tools_submenu_{$item_id}_after", $wp_admin_bar );
}
$this->register_action_scheduler_submenu( $wp_admin_bar );
}
/**
* Register Action Scheduler submenu.
*
* @since 1.9.3
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
private function register_action_scheduler_submenu( WP_Admin_Bar $wp_admin_bar ) {
/**
* Filters the Action Scheduler submenu items.
*
* @since 1.9.3
*
* @param array $items Array of submenu items.
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*
* @return array
*/
$items = apply_filters(
'wpforms_admin_bar_menu_register_action_scheduler_submenu',
[
'wpforms-tools-action-scheduler-all' => [
'title' => esc_html__( 'View All', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&orderby=hook&order=desc',
],
'wpforms-tools-action-scheduler-complete' => [
'title' => esc_html__( 'Completed Actions', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=complete&orderby=hook&order=desc',
],
'wpforms-tools-action-scheduler-failed' => [
'title' => esc_html__( 'Failed Actions', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=failed&orderby=hook&order=desc',
],
'wpforms-tools-action-scheduler-pending' => [
'title' => esc_html__( 'Pending Actions', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=pending&orderby=hook&order=desc',
],
'wpforms-tools-action-scheduler-past-due' => [
'title' => esc_html__( 'Past Due Actions', 'wpforms-lite' ),
'path' => 'admin.php?page=wpforms-tools&view=action-scheduler&s=wpforms&status=past-due&orderby=hook&order=desc',
],
],
$wp_admin_bar
);
foreach ( $items as $item_id => $args ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-tools-action-scheduler',
'id' => sanitize_key( $item_id ),
'title' => esc_html( $args['title'] ),
'href' => admin_url( $args['path'] ),
]
);
/**
* Fires after the Action Scheduler submenu item is registered.
*
* @since 1.9.3
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
do_action( "wpforms_admin_bar_menu_register_action_scheduler_submenu_{$item_id}_after", $wp_admin_bar );
}
}
/**
* Render primary top-level admin bar menu item.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function main_menu( WP_Admin_Bar $wp_admin_bar ) {
$indicator = '';
$notifications = $this->has_notifications();
if ( $notifications ) {
$count = $notifications < 10 ? $notifications : '!';
$indicator = ' <div class="wp-core-ui wp-ui-notification wpforms-menu-notification-counter">' . $count . '</div>';
}
$wp_admin_bar->add_menu(
[
'id' => 'wpforms-menu',
'title' => 'WPForms' . $indicator,
'href' => admin_url( 'admin.php?page=wpforms-overview' ),
]
);
}
/**
* Render Notifications admin bar menu item.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function notification_menu( WP_Admin_Bar $wp_admin_bar ) {
if ( ! $this->has_notifications() ) {
return;
}
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-notifications',
'title' => esc_html__( 'Notifications', 'wpforms-lite' ) . ' <div class="wp-core-ui wp-ui-notification wpforms-menu-notification-indicator"></div>',
'href' => admin_url( 'admin.php?page=wpforms-overview' ),
]
);
}
/**
* Render All Forms admin bar menu item.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function all_forms_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-forms',
'title' => esc_html__( 'All Forms', 'wpforms-lite' ),
'href' => admin_url( 'admin.php?page=wpforms-overview' ),
]
);
}
/**
* Render All Payments admin bar menu item.
*
* @since 1.8.4
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function all_payments_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-payments',
'title' => esc_html__( 'Payments', 'wpforms-lite' ),
'href' => add_query_arg(
[
'page' => 'wpforms-payments',
],
admin_url( 'admin.php' )
),
]
);
}
/**
* Render Add New admin bar menu item.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function add_new_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-add-new',
'title' => esc_html__( 'Add New Form', 'wpforms-lite' ),
'href' => admin_url( 'admin.php?page=wpforms-builder' ),
]
);
}
/**
* Render Settings admin bar menu item.
*
* @since 1.9.2
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function settings_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-settings',
'title' => esc_html__( 'Settings', 'wpforms-lite' ),
'href' => admin_url( 'admin.php?page=wpforms-settings' ),
]
);
}
/**
* Add Tools menu to the admin bar.
*
* @since 1.9.3
*
* @param WP_Admin_Bar $wp_admin_bar The admin bar object.
*/
public function tools_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-tools',
'title' => esc_html__( 'Tools', 'wpforms-lite' ),
'href' => admin_url( 'admin.php?page=wpforms-tools' ),
]
);
}
/**
* Render Community admin bar menu item.
*
* @since 1.6.0
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function community_menu( WP_Admin_Bar $wp_admin_bar ) {
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-community',
'title' => esc_html__( 'Community', 'wpforms-lite' ),
'href' => 'https://www.facebook.com/groups/wpformsvip/',
'meta' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
]
);
}
/**
* Render Support admin bar menu item.
*
* @since 1.6.0
* @since 1.7.4 Update the `Support` item title to `Help Docs`.
*
* @param WP_Admin_Bar $wp_admin_bar WordPress Admin Bar object.
*/
public function support_menu( WP_Admin_Bar $wp_admin_bar ) {
$href = add_query_arg(
[
'utm_campaign' => wpforms()->is_pro() ? 'plugin' : 'liteplugin',
'utm_medium' => 'admin-bar',
'utm_source' => 'WordPress',
'utm_content' => 'Documentation',
],
'https://wpforms.com/docs/'
);
$wp_admin_bar->add_menu(
[
'parent' => 'wpforms-menu',
'id' => 'wpforms-help-docs',
'title' => esc_html__( 'Help Docs', 'wpforms-lite' ),
'href' => $href,
'meta' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
]
);
}
/**
* Get form data for JS to modify the admin bar menu.
*
* @since 1.6.5
* @since 1.8.4 Added the View Payments link.
*
* @param array $forms Forms array.
*
* @return array
*/
protected function get_forms_data( $forms ) {
$data = [
'has_notifications' => $this->has_notifications(),
'edit_text' => esc_html__( 'Edit Form', 'wpforms-lite' ),
'entry_text' => esc_html__( 'View Entries', 'wpforms-lite' ),
'payment_text' => esc_html__( 'View Payments', 'wpforms-lite' ),
'survey_text' => esc_html__( 'Survey Results', 'wpforms-lite' ),
'forms' => [],
];
$admin_url = admin_url( 'admin.php' );
foreach ( $forms as $form ) {
$form_id = absint( $form['id'] );
if ( empty( $form_id ) ) {
continue;
}
/* translators: %d - form ID. */
$form_title = sprintf( esc_html__( 'Form ID: %d', 'wpforms-lite' ), $form_id );
if ( ! empty( $form['settings']['form_title'] ) ) {
$form_title = wp_html_excerpt(
sanitize_text_field( $form['settings']['form_title'] ),
99,
'&hellip;'
);
}
$has_payments = wpforms()->obj( 'payment' )->get_by( 'form_id', $form_id );
$data['forms'][] = apply_filters(
'wpforms_admin_adminbarmenu_get_form_data',
[
'form_id' => $form_id,
'title' => $form_title,
'edit_url' => add_query_arg(
[
'page' => 'wpforms-builder',
'view' => 'fields',
'form_id' => $form_id,
],
$admin_url
),
'payments_url' => $has_payments ? add_query_arg(
[
'page' => 'wpforms-payments',
'form_id' => $form_id,
],
$admin_url
) : '',
]
);
}
return $data;
}
/**
* Add form(s) data to the page.
*
* @since 1.6.5
*
* @param array $forms Forms array.
*/
public function menu_forms_data_html( $forms ) {
if ( empty( $forms ) ) {
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin-bar-menu',
[
'forms_data' => $this->get_forms_data( $forms ),
],
true
);
}
}
@@ -0,0 +1,155 @@
<?php
namespace WPForms\Admin\Base\Tables\DataObjects;
/**
* Column data object base class.
*
* @since 1.8.6
*/
abstract class ColumnBase {
/**
* Column ID.
*
* @since 1.8.6
*
* @var string|int
*/
protected $id;
/**
* Column label.
*
* @since 1.8.6
*
* @var string
*/
protected $label;
/**
* Label HTML markup.
*
* @since 1.8.6
*
* @var string
*/
protected $label_html;
/**
* Is column draggable.
*
* @since 1.8.6
*
* @var bool
*/
protected $is_draggable;
/**
* Column type.
*
* @since 1.8.6
*
* @var string
*/
protected $type;
/**
* Is column readonly.
*
* @since 1.8.6
*
* @var bool
*/
protected $readonly;
/**
* Column constructor.
*
* @since 1.8.6
*
* @param int|string $id Column ID.
* @param array $settings Column settings.
*/
public function __construct( $id, array $settings ) {
$this->id = $id;
$this->label = $settings['label'] ?? '';
$this->label_html = empty( $settings['label_html'] ) ? $this->label : $settings['label_html'];
$this->is_draggable = $settings['draggable'] ?? true;
$this->type = empty( $settings['type'] ) ? $id : $settings['type'];
$this->readonly = $settings['readonly'] ?? false;
}
/**
* Get column ID.
*
* @since 1.8.6
*
* @return string|int
*/
public function get_id() {
return $this->id;
}
/**
* Get column label.
*
* @since 1.8.6
*
* @return string
*/
public function get_label(): string {
return $this->label;
}
/**
* Get column label HTML.
*
* @since 1.8.6
*
* @return string
*/
public function get_label_html(): string {
return $this->label_html;
}
/**
* Get the column type.
*
* @since 1.8.6
*
* @return string
*/
public function get_type(): string {
return $this->type;
}
/**
* Is column draggable.
*
* @since 1.8.6
*
* @return bool
*/
public function is_draggable(): bool {
return $this->is_draggable;
}
/**
* Is column readonly.
*
* @since 1.8.6
*
* @return bool
*/
public function is_readonly(): bool {
return $this->readonly;
}
}
@@ -0,0 +1,85 @@
<?php
namespace WPForms\Admin\Base\Tables\Facades;
/**
* Column facade class.
*
* Hides the complexity of columns' collection behind a simple interface.
*
* @since 1.8.6
*/
abstract class ColumnsBase {
/**
* Get columns.
*
* Returns all possible columns.
*
* @since 1.8.6
*
* @return array Array of columns as objects.
*/
protected static function get_all(): array {
return [];
}
/**
* Get columns' keys for the columns which user selected to be displayed.
*
* It returns an array of keys in the order they should be displayed.
* It returns draggable and non-draggable columns.
*
* @since 1.8.6
*
* @return array
*/
public static function get_selected_columns_keys(): array {
return [];
}
/**
* Check if the form has selected columns.
*
* @since 1.8.6
*
* @return bool
*/
public static function has_selected_columns(): bool {
return ! empty( static::get_selected_columns_keys() );
}
/**
* Get columns' keys for the columns which the user has not selected to be displayed.
*
* It returns draggable and non-draggable columns.
*
* @since 1.8.6
*
* @return array
*/
public static function get_not_selected_columns_keys(): array {
$selected = static::get_selected_columns_keys();
$all = array_keys( static::get_all() );
return array_diff( $all, $selected );
}
/**
* Validate column key.
*
* @since 1.8.6
*
* @param string|int $key Column key.
*
* @return bool
*/
public static function validate_column_key( $key ): bool {
return isset( static::get_all()[ $key ] );
}
}
@@ -0,0 +1,140 @@
<?php
namespace WPForms\Admin\Blocks;
/**
* Class for rendering links in the admin area.
*
* @since 1.9.7
*/
class Links {
/**
* Render links.
*
* @since 1.9.7
*
* @param array $utm_params UTM parameters to append to links.
*/
public static function render( array $utm_params ): void {
$links = self::get_links( $utm_params );
echo '<div class="wpforms-links">';
foreach ( $links as $slug => $link ) {
self::render_link( $slug, $link );
}
echo '</div>';
}
/**
* Render a single link.
*
* @since 1.9.7
*
* @param string $slug The slug for the link.
* @param array $link The link data.
*/
private static function render_link( string $slug, array $link ): void {
$url = $link['url'] ?? '#';
$text = $link['text'] ?? '';
$class = $link['class'] ?? '';
$target = $link['target'] ?? '_self';
$icon = $link['icon'] ?? '';
printf(
'<a href="%1$s" target="%2$s" rel="noopener noreferrer" class="wpforms-link wpforms-link-%3$s %4$s">%6$s%5$s</a>',
esc_url( $url ),
esc_attr( $target ),
esc_attr( $slug ),
esc_attr( $class ),
esc_html( $text ),
wp_kses(
$icon,
[
'svg' => [
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
'fill' => true,
],
'path' => [
'd' => true,
'fill' => true,
],
]
)
);
}
/**
* Get links.
*
* @since 1.9.7
*
* @param array $utm_params UTM parameters to append to links.
*
* @return array
*/
private static function get_links( array $utm_params ): array {
return [
'docs' => [
'url' => self::get_utm_link(
'https://wpforms.com/docs/',
$utm_params['docs'] ?? []
),
'text' => esc_html__( 'Docs', 'wpforms-lite' ),
'target' => '_blank',
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="13" height="17" viewBox="0 0 13 17" fill="none"><path d="M2.33765 14.5854H10.3376C10.5876 14.5854 10.8376 14.3667 10.8376 14.0854V5.08545H8.33765C7.77515 5.08545 7.33765 4.64795 7.33765 4.08545V1.58545H2.33765C2.0564 1.58545 1.83765 1.83545 1.83765 2.08545V14.0854C1.83765 14.3667 2.0564 14.5854 2.33765 14.5854ZM2.33765 0.0854492H7.4939C8.02515 0.0854492 8.52515 0.304199 8.90015 0.679199L11.7439 3.52295C12.1189 3.89795 12.3376 4.39795 12.3376 4.9292V14.0854C12.3376 15.2104 11.4314 16.0854 10.3376 16.0854H2.33765C1.21265 16.0854 0.337646 15.2104 0.337646 14.0854V2.08545C0.337646 0.991699 1.21265 0.0854492 2.33765 0.0854492ZM4.08765 8.08545H8.58765C8.9939 8.08545 9.33765 8.4292 9.33765 8.83545C9.33765 9.27295 8.9939 9.58545 8.58765 9.58545H4.08765C3.65015 9.58545 3.33765 9.27295 3.33765 8.83545C3.33765 8.4292 3.65015 8.08545 4.08765 8.08545ZM4.08765 11.0854H8.58765C8.9939 11.0854 9.33765 11.4292 9.33765 11.8354C9.33765 12.2729 8.9939 12.5854 8.58765 12.5854H4.08765C3.65015 12.5854 3.33765 12.2729 3.33765 11.8354C3.33765 11.4292 3.65015 11.0854 4.08765 11.0854Z" fill="#646970"/></svg>',
],
'videos' => [
'url' => 'https://www.youtube.com/@wpforms/videos',
'text' => esc_html__( 'Videos', 'wpforms-lite' ),
'target' => '_blank',
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><path d="M14.8376 8.06982C14.8376 5.75732 13.5876 3.63232 11.5876 2.44482C9.5564 1.28857 7.08765 1.28857 5.08765 2.44482C3.0564 3.63232 1.83765 5.75732 1.83765 8.06982C1.83765 10.4136 3.0564 12.5386 5.08765 13.7261C7.08765 14.8823 9.5564 14.8823 11.5876 13.7261C13.5876 12.5386 14.8376 10.4136 14.8376 8.06982ZM0.337646 8.06982C0.337646 5.22607 1.83765 2.60107 4.33765 1.16357C6.8064 -0.273926 9.83765 -0.273926 12.3376 1.16357C14.8064 2.60107 16.3376 5.22607 16.3376 8.06982C16.3376 10.9448 14.8064 13.5698 12.3376 15.0073C9.83765 16.4448 6.8064 16.4448 4.33765 15.0073C1.83765 13.5698 0.337646 10.9448 0.337646 8.06982ZM6.21265 4.69482C6.4314 4.53857 6.7439 4.53857 6.96265 4.69482L11.4626 7.44482C11.6814 7.56982 11.8376 7.81982 11.8376 8.10107C11.8376 8.35107 11.6814 8.60107 11.4626 8.72607L6.96265 11.4761C6.7439 11.6323 6.4314 11.6323 6.21265 11.5073C5.96265 11.3511 5.83765 11.1011 5.83765 10.8511V5.35107C5.83765 5.06982 5.96265 4.81982 6.21265 4.69482Z" fill="#646970"/></svg>',
],
'support' => [
'url' => wpforms()->is_pro() ?
self::get_utm_link(
'https://wpforms.com/account/support/',
$utm_params['support'] ?? []
) : 'https://wordpress.org/support/plugin/wpforms-lite/',
'text' => wpforms()->is_pro() ? esc_html__( 'Support', 'wpforms-lite' ) : esc_html__( 'Support Forum', 'wpforms-lite' ),
'target' => '_blank',
'icon' => wpforms()->is_pro() ?
'<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><path d="M14.8376 8.06982C14.8376 5.75732 13.5876 3.63232 11.5876 2.44482C9.5564 1.28857 7.08765 1.28857 5.08765 2.44482C3.0564 3.63232 1.83765 5.75732 1.83765 8.06982C1.83765 10.4136 3.0564 12.5386 5.08765 13.7261C7.08765 14.8823 9.5564 14.8823 11.5876 13.7261C13.5876 12.5386 14.8376 10.4136 14.8376 8.06982ZM0.337646 8.06982C0.337646 5.22607 1.83765 2.60107 4.33765 1.16357C6.8064 -0.273926 9.83765 -0.273926 12.3376 1.16357C14.8064 2.60107 16.3376 5.22607 16.3376 8.06982C16.3376 10.9448 14.8064 13.5698 12.3376 15.0073C9.83765 16.4448 6.8064 16.4448 4.33765 15.0073C1.83765 13.5698 0.337646 10.9448 0.337646 8.06982ZM5.6189 5.25732C5.8689 4.53857 6.52515 4.06982 7.27515 4.06982H9.08765C10.1814 4.06982 11.0876 4.97607 11.0876 6.06982C11.0876 6.75732 10.6814 7.41357 10.0876 7.75732L9.08765 8.35107C9.0564 8.75732 8.7439 9.06982 8.33765 9.06982C7.90015 9.06982 7.58765 8.75732 7.58765 8.31982V7.91357C7.58765 7.63232 7.71265 7.38232 7.96265 7.25732L9.33765 6.47607C9.4939 6.38232 9.58765 6.22607 9.58765 6.06982C9.58765 5.78857 9.3689 5.60107 9.08765 5.60107H7.27515C7.1814 5.60107 7.08765 5.66357 7.0564 5.75732L7.02515 5.78857C6.90015 6.19482 6.46265 6.38232 6.08765 6.25732C5.6814 6.10107 5.4939 5.66357 5.6189 5.28857V5.25732ZM7.33765 11.0698C7.33765 10.5386 7.77515 10.0698 8.33765 10.0698C8.8689 10.0698 9.33765 10.5386 9.33765 11.0698C9.33765 11.6323 8.8689 12.0698 8.33765 12.0698C7.77515 12.0698 7.33765 11.6323 7.33765 11.0698Z" fill="#646970"/></svg>' :
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16" fill="none"><path d="M3.09554 9.83838C3.06625 9.89697 3.03695 9.95557 2.97836 9.98486C2.91976 10.1313 2.83187 10.2485 2.77328 10.395C3.27133 10.2778 3.74008 10.0728 4.20883 9.86768C4.35531 9.80908 4.4725 9.75049 4.58968 9.69189C4.88265 9.54541 5.17562 9.51611 5.49789 9.57471C5.84945 9.6333 6.20101 9.6626 6.61117 9.6626C9.42367 9.6626 11.2987 7.7876 11.2987 5.9126C11.2987 4.06689 9.42367 2.1626 6.61117 2.1626C3.76937 2.1626 1.92367 4.06689 1.92367 5.9126C1.92367 6.73291 2.24593 7.52393 2.86117 8.19775C3.30062 8.63721 3.38851 9.28174 3.09554 9.83838ZM6.61117 11.0688C6.11312 11.0688 5.67367 11.0396 5.20492 10.9517C5.08773 11.0103 4.94125 11.0981 4.79476 11.1567C3.74008 11.6255 2.48031 12.0063 1.22054 12.0063C0.927575 12.0063 0.663904 11.8599 0.546716 11.5962C0.458825 11.3325 0.488122 11.0396 0.6932 10.8345C1.10336 10.3657 1.45492 9.83838 1.77718 9.31104C1.80648 9.25244 1.83578 9.19385 1.83578 9.16455C1.01547 8.28564 0.517419 7.14307 0.517419 5.9126C0.517419 3.0708 3.24203 0.756348 6.61117 0.756348C9.95101 0.756348 12.7049 3.0708 12.7049 5.9126C12.7049 8.78369 9.95101 11.0688 6.61117 11.0688ZM13.1737 14.8188C10.7713 14.8188 8.69125 13.647 7.69515 11.9478C8.1932 11.8599 8.69125 11.7427 9.16 11.5962C9.92172 12.6509 11.3573 13.4126 13.1444 13.4126C13.5252 13.4126 13.9061 13.3833 14.2577 13.3247C14.5799 13.2661 14.8729 13.2954 15.1659 13.4419C15.283 13.5005 15.4002 13.5591 15.5467 13.6177C16.0155 13.8228 16.4842 13.9985 16.9823 14.145C16.9237 13.9985 16.8358 13.8813 16.7772 13.7349C16.7479 13.6763 16.6893 13.6177 16.66 13.5591C16.3963 13.0317 16.4842 12.3872 16.8944 11.9478C17.5096 11.2739 17.8612 10.4829 17.8612 9.6626C17.8612 7.93408 16.1912 6.14697 13.6424 5.94189V5.9126C13.6424 5.44385 13.5545 4.9751 13.4373 4.53564C16.6893 4.65283 19.2674 6.90869 19.2674 9.6626C19.2674 10.8931 18.7401 12.0356 17.9198 12.9146C17.9198 12.9731 17.9491 13.0024 17.9784 13.061C18.3006 13.5884 18.6522 14.1157 19.0623 14.5845C19.2674 14.7896 19.2967 15.0825 19.2088 15.3462C19.0916 15.6099 18.828 15.7563 18.5643 15.7563C17.3045 15.7563 16.0155 15.3755 14.9608 14.9067C14.8143 14.8481 14.6678 14.7603 14.5506 14.7017C14.0819 14.7896 13.6424 14.8188 13.1737 14.8188Z" fill="#50575E"/></svg>',
],
'whats-new' => [
'text' => esc_html__( 'Whats New', 'wpforms-lite' ),
'class' => 'wpforms-splash-modal-open',
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="20" viewBox="0 0 17 20" fill="none"><path d="M13.6014 1.63137L14.7985 6.09878C15.4146 6.22486 16.0232 6.80589 16.2497 7.65107C16.4762 8.49626 16.2477 9.33393 15.7771 9.75119L16.9661 14.1884C17.0712 14.5808 16.9268 15.0077 16.605 15.2557C16.2833 15.5037 15.8364 15.5264 15.4919 15.3275L13.8079 14.3552C11.9708 13.2946 9.79038 13.0053 7.73779 13.5553L7.4963 13.62L8.53158 17.4837C8.67717 18.027 8.33762 18.571 7.82447 18.7085L5.89262 19.2261C5.34929 19.3717 4.81346 19.0623 4.66788 18.519L3.6326 14.6553C2.54594 14.9465 1.47428 14.3277 1.18311 13.2411L0.406655 10.3433C0.123572 9.28681 0.734202 8.18497 1.82087 7.8938L5.92605 6.79382C7.97865 6.24383 9.7223 4.9031 10.783 3.06598L11.7633 1.41214C11.9622 1.06769 12.3605 0.863896 12.7632 0.917765C13.1659 0.971634 13.5044 1.26915 13.6014 1.63137ZM12.2924 4.47327C10.9482 6.58047 8.85851 8.07862 6.44369 8.72567L6.20221 8.79037L6.97867 11.6882L7.22015 11.6234C9.63496 10.9764 12.2019 11.2592 14.4195 12.412L12.2924 4.47327Z" fill="#646970"/></svg>',
],
];
}
/**
* Get UTM link.
*
* @since 1.9.7
*
* @param string $url The URL to which UTM parameters will be added.
* @param array $utm_params UTM parameters to append to the URL.
*
* @return string
*/
private static function get_utm_link( string $url, array $utm_params ): string {
return wpforms_utm_link(
$url,
$utm_params['medium'] ?? '',
$utm_params['content'] ?? '',
$utm_params['term'] ?? ''
);
}
}
@@ -0,0 +1,382 @@
<?php
namespace WPForms\Admin\Builder;
use WPForms\Requirements\Requirements;
/**
* Addons class.
*
* @since 1.9.2
*/
class Addons {
/**
* List of addon options.
*
* @since 1.9.2
*/
private const FIELD_OPTIONS = [
'calculations' => [
'calculation_code',
'calculation_code_js',
'calculation_code_php',
'calculation_is_enabled',
],
'form-locker' => [
'unique_answer',
],
'geolocation' => [
'display_map',
'enable_address_autocomplete',
'map_position',
],
'surveys-polls' => [
'survey',
],
'quiz' => [
'quiz_enabled',
'choices' => [
'quiz_personality',
'quiz_weight',
],
],
];
/**
* Initialize.
*
* @since 1.9.2
*
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function init() {
$this->hooks();
}
/**
* Add hooks.
*
* @since 1.9.2
*/
private function hooks(): void {
add_filter( 'wpforms_save_form_args', [ $this, 'save_disabled_addons_options' ], 10, 3 );
}
/**
* Field's options added by an addon can be deleted when the addon is deactivated or have incompatible status.
* The options are fully controlled by the addon when addon is active and compatible.
*
* @since 1.9.2
*
* @param array|mixed $post_data Post data.
*
* @return array
*/
public function save_disabled_addons_options( $post_data ): array {
$post_data = (array) $post_data;
$form_obj = wpforms()->obj( 'form' );
$form_data = json_decode( stripslashes( $post_data['post_content'] ?? '' ), true );
$form_id = $form_data['id'] ?? '';
if ( ! $form_obj || ! $form_id ) {
return $post_data;
}
$previous_form_data = $form_obj->get( $form_id, [ 'content_only' => true ] );
$not_validated_addons = Requirements::get_instance()->get_not_validated_addons();
if ( empty( $previous_form_data ) || empty( $not_validated_addons ) ) {
return $post_data;
}
foreach ( $not_validated_addons as $path ) {
$slug = str_replace( 'wpforms-', '', basename( $path, '.php' ) );
$this->preserve_addon( $slug, $form_data, $previous_form_data );
}
$this->preserve_providers( $form_data, $previous_form_data );
$this->preserve_payments( $form_data, $previous_form_data );
$post_data['post_content'] = wpforms_encode( $form_data );
return $post_data;
}
/**
* Preserve addon fields, settings, panels, notifications, etc.
*
* @since 1.9.3
*
* @param string $slug Addon slug.
* @param array $form_data Form data.
* @param array $previous_form_data Previous form data.
*
* @return void
*/
private function preserve_addon( string $slug, array &$form_data, array $previous_form_data ): void {
if ( ! empty( $form_data['fields'] ) && ! empty( $previous_form_data['fields'] ) ) {
$this->preserve_addon_fields_settings( $slug, $form_data['fields'], $previous_form_data['fields'] );
}
$this->preserve_addon_panel( $slug, $form_data, $previous_form_data );
if ( ! empty( $form_data['settings'] ) && ! empty( $previous_form_data['settings'] ) ) {
$this->preserve_addon_settings( $slug, $form_data['settings'], $previous_form_data['settings'] );
}
if ( ! empty( $form_data['settings']['notifications'] ) && ! empty( $previous_form_data['settings']['notifications'] ) ) {
$this->preserve_addon_notifications(
$slug,
$form_data['settings']['notifications'],
$previous_form_data['settings']['notifications']
);
}
}
/**
* Preserve addon fields.
*
* @since 1.9.5
*
* @param string $slug Addon slug.
* @param array $new_fields Form fields settings.
* @param array $previous_fields Previous form fields settings.
*
* @return void
*/
private function preserve_addon_fields_settings( string $slug, array &$new_fields, array $previous_fields ): void {
foreach ( $previous_fields as $field_id => $previous_field ) {
$new_field = $new_fields[ $field_id ] ?? [];
if ( empty( $new_field ) ) {
continue;
}
$this->preserve_addon_field_settings( $slug, $new_field, $previous_field );
$new_fields[ $field_id ] = $new_field;
}
}
/**
* Preserve addon field.
*
* @since 1.9.5
*
* @param string $slug Addon slug.
* @param array $new_field Previous form fields settings.
* @param array $previous_field Form fields settings.
*
* @return void
*/
private function preserve_addon_field_settings( string $slug, array &$new_field, array $previous_field ): void {
$prefix = $this->prepare_prefix( $slug );
$changed_settings = array_diff_key( $previous_field, $new_field );
$preserve_fields = self::FIELD_OPTIONS[ $slug ] ?? [];
foreach ( $changed_settings as $setting_name => $setting_value ) {
if (
strpos( $setting_name, $prefix ) === 0 ||
in_array( $setting_name, $preserve_fields, true )
) {
$new_field[ $setting_name ] = $setting_value;
}
}
if (
! empty( $preserve_fields['choices'] ) &&
is_array( $preserve_fields['choices'] ) &&
! empty( $new_field['choices'] ) &&
is_array( $new_field['choices'] )
) {
$this->preserve_addon_field_choices_settings( $preserve_fields['choices'], $new_field, $previous_field );
}
}
/**
* Preserve addon field choices settings.
*
* @since 1.9.9
*
* @param array $choice_settings Choice settings.
* @param array $new_field Previous form fields settings.
* @param array $previous_field Form fields settings.
*
* @return void
*/
private function preserve_addon_field_choices_settings( array $choice_settings, array &$new_field, array $previous_field ): void {
if ( ! isset( $previous_field['choices'] ) || ! is_array( $previous_field['choices'] ) ) {
return;
}
$previous_choices = $previous_field['choices'];
foreach ( $new_field['choices'] as $choice_id => $choice ) {
foreach ( $choice_settings as $setting_name ) {
if ( isset( $previous_choices[ $choice_id ][ $setting_name ] ) ) {
$new_field['choices'][ $choice_id ][ $setting_name ] = $previous_choices[ $choice_id ][ $setting_name ];
}
}
}
}
/**
* Preserve addon panel.
*
* @since 1.9.3
*
* @param string $slug Addon slug.
* @param array $form_data Form data.
* @param array $previous_form_data Previous form data.
*/
private function preserve_addon_panel( string $slug, array &$form_data, array $previous_form_data ): void {
$panel = $this->prepare_prefix( $slug );
// The addon settings stored its own panel, e.g., $form_data[lead_forms], $form_data[webhooks], etc.
if ( ! empty( $previous_form_data[ $panel ] ) ) {
$form_data[ $panel ] = $previous_form_data[ $panel ];
}
}
/**
* Preserve addon settings stored inside the settings panel with a specific prefix.
* e.g. $form_data[settings][{$prefix}_enabled], $form_data[settings][{$prefix}_email], etc.
*
* @since 1.9.4
*
* @param string $slug Addon option prefix.
* @param array $new_settings Form settings.
* @param array $previous_settings Previous form settings.
*/
private function preserve_addon_settings( string $slug, array &$new_settings, array $previous_settings ): void {
$prefix = $this->prepare_prefix( $slug );
static $legacy_options = [
'offline_forms' => [ 'offline_form' ],
'user_registration' => [ 'user_login_hide', 'user_reset_hide' ],
'surveys_polls' => [ 'survey_enable', 'poll_enable' ],
];
// BC: User Registration addon has `registration_` prefix instead of `user_registration`.
if ( $prefix === 'user_registration' ) {
$prefix = 'registration';
}
foreach ( $previous_settings as $setting_name => $value ) {
if ( strpos( $setting_name, $prefix ) === 0 ) {
$new_settings[ $setting_name ] = $value;
continue;
}
// BC: The options don't have a prefix and hard-coded in the `$legacy_options` variable.
if ( isset( $legacy_options[ $prefix ] ) && in_array( $setting_name, $legacy_options[ $prefix ], true ) ) {
$new_settings[ $setting_name ] = $value;
}
}
}
/**
* Preserve addon notifications.
*
* @since 1.9.4
*
* @param string $slug Addon slug.
* @param array $new_notifications List of form notifications.
* @param array $previous_notifications Previously saved list of form notifications.
*
* @return void
*/
private function preserve_addon_notifications( string $slug, array &$new_notifications, array $previous_notifications ): void {
$prefix = $this->prepare_prefix( $slug );
foreach ( $previous_notifications as $notification_id => $notification_settings ) {
if ( empty( $new_notifications[ $notification_id ] ) ) {
continue;
}
$changed_notification_settings = array_diff_key( $notification_settings, $new_notifications[ $notification_id ] );
foreach ( $changed_notification_settings as $setting_name => $value ) {
if ( strpos( $setting_name, $prefix ) === 0 ) {
$new_notifications[ $notification_id ][ $setting_name ] = $value;
}
}
}
}
/**
* Preserve Providers that are not active.
*
* @since 1.9.4
*
* @param array $form_data Form data.
* @param array $previous_form_data Previous form data.
*/
private function preserve_providers( array &$form_data, array $previous_form_data ): void {
if ( empty( $previous_form_data['providers'] ) ) {
return;
}
$active_providers = wpforms_get_providers_available();
foreach ( $previous_form_data['providers'] as $slug => $provider ) {
if ( ! empty( $active_providers[ $slug ] ) ) {
continue;
}
$form_data['providers'][ $slug ] = $provider;
}
}
/**
* Preserve Payments providers that are not active.
*
* @since 1.9.4
*
* @param array $form_data Form data.
* @param array $previous_form_data Previous form data.
*/
private function preserve_payments( array &$form_data, array $previous_form_data ): void {
if ( empty( $previous_form_data['payments'] ) ) {
return;
}
foreach ( $previous_form_data['payments'] as $slug => $value ) {
if ( ! empty( $form_data['payments'][ $slug ] ) ) {
continue;
}
$form_data['payments'][ $slug ] = $value;
}
}
/**
* Convert slug to a addon prefix.
*
* @since 1.9.4
*
* @param string $slug Addon slug.
*
* @return string
*/
private function prepare_prefix( string $slug ): string {
return str_replace( '-', '_', $slug );
}
}
@@ -0,0 +1,146 @@
<?php
namespace WPForms\Admin\Builder\Ajax;
/**
* Form Builder Panel Loader AJAX actions.
*
* @since 1.8.6
*/
class PanelLoader {
/**
* Determine if the class is allowed to load.
*
* @since 1.8.6
*
* @return bool
*/
private function allow_load(): bool {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';
// Load only in the case of AJAX calls form the Form Builder.
return wpforms_is_admin_ajax() && strpos( $action, 'wpforms_builder_' ) === 0;
}
/**
* Initialize class.
*
* @since 1.8.6
*/
public function init(): void {
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.6
*/
private function hooks(): void {
add_action( 'wp_ajax_wpforms_builder_load_panel', [ $this, 'load_panel_content' ] );
}
/**
* Save tags.
*
* @since 1.8.6
*/
public function load_panel_content(): void {
check_ajax_referer( 'wpforms-builder', 'nonce' );
$form_id = absint( filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_NUMBER_INT ) );
if ( ! wpforms_current_user_can( 'edit_forms', $form_id ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) );
}
$data = $this->get_prepared_data( 'load_panel' );
$panel = $data['panel'] ?? '';
$panel_class = '\WPForms_Builder_Panel_' . ucfirst( $panel );
$panel_obj = $this->get_panel_obj( $panel_class, $panel );
ob_start();
$panel_obj->panel_output( [], $panel );
$panel_content = ob_get_clean();
wp_send_json_success( $panel_content );
}
/**
* Get panel object.
*
* @since 1.9.4
*
* @param string $panel_class Panel class name.
* @param string $panel Panel name.
*
* @return object
*/
private function get_panel_obj( string $panel_class, string $panel ) {
if ( ! class_exists( $panel_class ) ) {
// Load panel base class.
require_once WPFORMS_PLUGIN_DIR . 'includes/admin/builder/panels/class-base.php';
$file = WPFORMS_PLUGIN_DIR . "includes/admin/builder/panels/class-{$panel}.php";
$file_pro = WPFORMS_PLUGIN_DIR . "pro/includes/admin/builder/panels/class-{$panel}.php";
if ( file_exists( $file_pro ) && wpforms()->is_pro() ) {
require_once $file_pro;
} elseif ( file_exists( $file ) ) {
require_once $file;
}
}
$panel_obj = $panel_class::instance();
if ( ! method_exists( $panel_obj, 'panel_content' ) ) {
wp_send_json_error( esc_html__( 'Invalid panel.', 'wpforms-lite' ) );
}
return $panel_obj;
}
/**
* Get prepared data before perform ajax action.
*
* @since 1.8.6
*
* @param string $action Action: `save` OR `delete`.
*
* @return array
* @noinspection PhpSameParameterValueInspection
*/
private function get_prepared_data( string $action ): array {
// Run a security check.
if ( ! check_ajax_referer( 'wpforms-builder', 'nonce', false ) ) {
wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) );
}
// Check for permissions.
if ( ! wpforms_current_user_can( 'edit_forms' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
$data = [];
if ( $action === 'load_panel' ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$data['panel'] = ! empty( $_POST['panel'] ) ? sanitize_key( $_POST['panel'] ) : '';
}
return $data;
}
}
@@ -0,0 +1,51 @@
<?php
namespace WPForms\Admin\Builder\Ajax;
/**
* Save the form data.
*
* @since 1.9.4
*/
class SaveForm {
/**
* The form fields processing while saving the form.
*
* @since 1.9.4
*
* @param array $fields Form fields data.
* @param array $form_data Form data.
*
* @return array
*/
public function process_fields( array $fields, array $form_data ): array {
$form_obj = wpforms()->obj( 'form' );
if ( ! $form_obj || empty( $fields ) || empty( $form_data['id'] ) ) {
return $fields;
}
$saved_form_data = $form_obj->get( $form_data['id'], [ 'content_only' => true ] );
foreach ( $fields as $field_id => $field_data ) {
if ( empty( $field_data['type'] ) ) {
continue;
}
/**
* Filter field settings before saving the form.
*
* @since 1.9.4
*
* @param array $field_data Field data.
* @param array $form_data Forms data.
* @param array $saved_form_data Saved form data.
*/
$fields[ $field_id ] = apply_filters( "wpforms_admin_builder_ajax_save_form_field_{$field_data['type']}", $field_data, $form_data, $saved_form_data );
}
return $fields;
}
}
@@ -0,0 +1,423 @@
<?php
namespace WPForms\Admin\Builder;
use WPForms\Forms\Akismet;
use WPForms_Builder_Panel_Settings;
/**
* AntiSpam class.
*
* @since 1.7.8
*/
class AntiSpam {
/**
* Form data and settings.
*
* @since 1.7.8
*
* @var array
*/
private $form_data;
/**
* Init class.
*
* @since 1.7.8
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.7.8
*/
protected function hooks() {
add_action( 'wpforms_form_settings_panel_content', [ $this, 'panel_content' ], 10, 2 );
}
/**
* Add a content for `Spam Protection and Security` panel.
*
* @since 1.7.8
*
* @param WPForms_Builder_Panel_Settings $instance Settings panel instance.
*/
public function panel_content( $instance ) {
$this->form_data = $this->update_settings_form_data( $instance->form_data );
echo '<div class="wpforms-panel-content-section wpforms-panel-content-section-anti_spam">';
echo '<div class="wpforms-panel-content-section-title">';
esc_html_e( 'Spam Protection and Security', 'wpforms-lite' );
echo '</div>';
$antispam = wpforms_panel_field(
'toggle',
'settings',
'antispam_v3',
$this->form_data,
__( 'Enable modern anti-spam protection', 'wpforms-lite' ),
[
'value' => (int) ! empty( $this->form_data['settings']['antispam_v3'] ),
'tooltip' => __( 'Turn on invisible modern spam protection.', 'wpforms-lite' ),
],
false
);
wpforms_panel_fields_group(
$antispam,
[
'description' => __( 'Behind-the-scenes spam filtering that\'s invisible to your visitors.', 'wpforms-lite' ),
'title' => __( 'Protection', 'wpforms-lite' ),
]
);
if ( ! empty( $this->form_data['settings']['antispam'] ) && empty( $this->form_data['settings']['antispam_v3'] ) ) {
wpforms_panel_field(
'toggle',
'settings',
'antispam',
$this->form_data,
__( 'Enable anti-spam protection', 'wpforms-lite' ),
[
'tooltip' => __( 'Turn on invisible spam protection.', 'wpforms-lite' ),
]
);
}
if ( ! empty( $this->form_data['settings']['honeypot'] ) && empty( $this->form_data['settings']['antispam_v3'] ) ) {
wpforms_panel_field(
'toggle',
'settings',
'honeypot',
$this->form_data,
__( 'Enable anti-spam honeypot', 'wpforms-lite' )
);
}
$this->akismet_settings();
$this->store_spam_entries_settings();
$this->time_limit_settings();
$this->captcha_settings();
// Hidden setting to store blocked entries by filtering as a spam.
// This setting is needed to keep backward compatibility with old forms.
wpforms_panel_field(
'checkbox',
'anti_spam',
'filtering_store_spam',
$this->form_data,
'',
[
'parent' => 'settings',
'class' => 'wpforms-hidden',
]
);
/**
* Fires once in the end of content panel before Also Available section.
*
* @since 1.7.8
*
* @param array $form_data Form data and settings.
*/
do_action( 'wpforms_admin_builder_anti_spam_panel_content', $this->form_data );
wpforms_panel_fields_group(
$this->get_also_available_block(),
[
'unfoldable' => true,
'default' => 'opened',
'group' => 'also_available',
'title' => __( 'Also Available', 'wpforms-lite' ),
'borders' => [ 'top' ],
]
);
echo '</div>';
}
/**
* Update the form data on the builder settings panel.
*
* @since 1.9.2
*
* @param array $form_data Form data.
*
* @return array
*/
private function update_settings_form_data( array $form_data ): array {
if ( ! $form_data ) {
return $form_data;
}
// Update `Filtering` store spam entries behaviour.
// Enable for new forms and old forms without any `Filtering` setting enabled.
if (
empty( $form_data['settings']['anti_spam']['filtering_store_spam'] ) &&
empty( $form_data['settings']['anti_spam']['country_filter']['enable'] ) &&
empty( $form_data['settings']['anti_spam']['keyword_filter']['enable'] )
) {
$form_data['settings']['anti_spam']['filtering_store_spam'] = true;
}
return $form_data;
}
/**
* Output the *CAPTCHA settings.
*
* @since 1.7.8
*/
private function captcha_settings() {
$captcha_settings = wpforms_get_captcha_settings();
if ( empty( $captcha_settings['provider'] ) || $captcha_settings['provider'] === 'none' ) {
return;
}
if (
$captcha_settings['provider'] !== 'hcaptcha' && (
empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] )
)
) {
return;
}
if ( $captcha_settings['provider'] === 'hcaptcha' && empty( $captcha_settings['site_key'] ) ) {
return;
}
$captcha_types = [
'hcaptcha' => __( 'Enable hCaptcha', 'wpforms-lite' ),
'turnstile' => __( 'Enable Cloudflare Turnstile', 'wpforms-lite' ),
'recaptcha' => [
'v2' => __( 'Enable Google Checkbox v2 reCAPTCHA', 'wpforms-lite' ),
'invisible' => __( 'Enable Google Invisible v2 reCAPTCHA', 'wpforms-lite' ),
'v3' => __( 'Enable Google v3 reCAPTCHA', 'wpforms-lite' ),
],
];
$is_recaptcha = $captcha_settings['provider'] === 'recaptcha';
$captcha_types = $is_recaptcha ? $captcha_types['recaptcha'] : $captcha_types;
$captcha_key = $is_recaptcha ? $captcha_settings['recaptcha_type'] : $captcha_settings['provider'];
$label = ! empty( $captcha_types[ $captcha_key ] ) ? $captcha_types[ $captcha_key ] : '';
$recaptcha = wpforms_panel_field(
'toggle',
'settings',
'recaptcha',
$this->form_data,
$label,
[
'data' => [
'provider' => $captcha_settings['provider'],
],
'tooltip' => __( 'Enable third-party CAPTCHAs to prevent form submissions from bots.', 'wpforms-lite' ),
],
false
);
wpforms_panel_fields_group(
$recaptcha,
[
'description' => __( 'Automated tests that help to prevent bots from submitting your forms.', 'wpforms-lite' ),
'title' => __( 'CAPTCHA', 'wpforms-lite' ),
'borders' => [ 'top' ],
]
);
}
/**
* Output the Spam Entries Store settings.
*
* @since 1.8.3
*/
public function store_spam_entries_settings() {
if ( ! wpforms()->is_pro() ) {
return;
}
$disable_entries = $this->form_data['settings']['disable_entries'] ?? 0;
wpforms_panel_field(
'toggle',
'settings',
'store_spam_entries',
$this->form_data,
__( 'Store spam entries in the database', 'wpforms-lite' ),
[
'value' => $this->form_data['settings']['store_spam_entries'] ?? 0,
'class' => $disable_entries ? 'wpforms-hidden' : '',
]
);
}
/**
* Output the Time Limit settings.
*
* @since 1.8.3
*/
private function time_limit_settings() {
wpforms_panel_field(
'toggle',
'anti_spam',
'enable',
$this->form_data,
__( 'Enable minimum time to submit', 'wpforms-lite' ),
[
'parent' => 'settings',
'subsection' => 'time_limit',
'tooltip' => __( 'Set a minimum amount of time a user must spend on a form before submitting.', 'wpforms-lite' ),
'input_class' => 'wpforms-panel-field-toggle-next-field',
]
);
wpforms_panel_field(
'text',
'anti_spam',
'duration',
$this->form_data,
__( 'Minimum time to submit', 'wpforms-lite' ),
[
'parent' => 'settings',
'subsection' => 'time_limit',
'type' => 'number',
'min' => 1,
'default' => 2,
'after' => sprintf( '<span class="wpforms-panel-field-after">%s</span>', __( 'seconds', 'wpforms-lite' ) ),
]
);
}
/**
* Output the Akismet settings.
*
* @since 1.7.8
*/
private function akismet_settings() {
if ( ! Akismet::is_installed() ) {
return;
}
$args = [];
if ( ! Akismet::is_configured() ) {
$args['data']['akismet-status'] = 'akismet_no_api_key';
}
if ( ! Akismet::is_activated() ) {
$args['data']['akismet-status'] = 'akismet_not_activated';
}
// If Akismet isn't available, disable the Akismet toggle.
if ( isset( $args['data'] ) ) {
$args['input_class'] = 'wpforms-akismet-disabled';
$args['value'] = '0';
}
wpforms_panel_field(
'toggle',
'settings',
'akismet',
$this->form_data,
__( 'Enable Akismet anti-spam protection', 'wpforms-lite' ),
$args
);
}
/**
* Get the Also Available block.
*
* @since 1.7.8
*
* @return string
*/
private function get_also_available_block() {
$get_started_button_text = __( 'Get Started &rarr;', 'wpforms-lite' );
$upgrade_to_pro_text = __( 'Upgrade to Pro', 'wpforms-lite' );
$captcha_settings = wpforms_get_captcha_settings();
$upgrade_url = 'https://wpforms.com/lite-upgrade/';
$utm_medium = 'Builder Settings';
$blocks = [
'country_filter' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/country-filter.svg',
'title' => __( 'Country Filter', 'wpforms-lite' ),
'description' => __( 'Stop spam at its source. Allow or deny entries from specific countries.', 'wpforms-lite' ),
'link' => wpforms_utm_link( $upgrade_url, $utm_medium, 'Country Filter Feature' ),
'link_text' => $upgrade_to_pro_text,
'class' => 'wpforms-panel-content-also-available-item-upgrade-to-pro',
'show' => ! wpforms()->is_pro(),
],
'keyword_filter' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/keyword-filter.svg',
'title' => __( 'Keyword Filter', 'wpforms-lite' ),
'description' => __( 'Block form entries that contain specific words or phrases that you define.', 'wpforms-lite' ),
'link' => wpforms_utm_link( $upgrade_url, $utm_medium, 'Keyword Filter Feature' ),
'link_text' => $upgrade_to_pro_text,
'class' => 'wpforms-panel-content-also-available-item-upgrade-to-pro',
'show' => ! wpforms()->is_pro(),
],
'custom_captcha' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/custom-captcha.svg',
'title' => __( 'Custom Captcha', 'wpforms-lite' ),
'description' => __( 'Ask custom questions or require your visitor to answer a random math puzzle.', 'wpforms-lite' ),
'link' => wpforms()->is_pro() ? '#' : wpforms_utm_link( $upgrade_url, $utm_medium, 'Custom Captcha Addon' ),
'link_text' => wpforms()->is_pro() ? __( 'Add to Form', 'wpforms-lite' ) : $upgrade_to_pro_text,
'class' => wpforms()->is_pro() ? 'wpforms-panel-content-also-available-item-add-captcha' : 'wpforms-panel-content-also-available-item-upgrade-to-pro',
'show' => true,
],
'reCAPTCHA' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/recaptcha.svg',
'title' => 'reCAPTCHA',
'description' => __( 'Add Google\'s free anti-spam service and choose between visible or invisible CAPTCHAs.','wpforms-lite' ),
'link' => wpforms_utm_link( 'https://wpforms.com/docs/how-to-set-up-and-use-recaptcha-in-wpforms/', $utm_medium, 'reCAPTCHA Feature' ),
'link_text' => $get_started_button_text,
'show' => $captcha_settings['provider'] !== 'recaptcha' || empty( wpforms_setting( 'captcha-provider' ) ),
],
'hCaptcha' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/hcaptcha.svg',
'title' => 'hCaptcha',
'description' => __( 'Turn on free, privacy-oriented spam prevention that displays a visual CAPTCHA.','wpforms-lite' ),
'link' => wpforms_utm_link( 'https://wpforms.com/docs/how-to-set-up-and-use-hcaptcha-in-wpforms/', $utm_medium, 'hCaptcha Feature' ),
'link_text' => $get_started_button_text,
'show' => $captcha_settings['provider'] !== 'hcaptcha',
],
'turnstile' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/cloudflare.svg',
'title' => 'Cloudflare Turnstile',
'description' => __( 'Enable free, CAPTCHA-like spam protection that protects data privacy.','wpforms-lite' ),
'link' => wpforms_utm_link( 'https://wpforms.com/docs/setting-up-cloudflare-turnstile/', $utm_medium, 'Cloudflare Turnstile Feature' ),
'link_text' => $get_started_button_text,
'show' => $captcha_settings['provider'] !== 'turnstile',
],
'akismet' => [
'logo' => WPFORMS_PLUGIN_URL . 'assets/images/anti-spam/akismet.svg',
'title' => 'Akismet',
'description' => __( 'Integrate the powerful spam-fighting service trusted by millions of sites.','wpforms-lite' ),
'link' => wpforms_utm_link( 'https://wpforms.com/docs/setting-up-akismet-anti-spam-protection/', $utm_medium, 'Akismet Feature' ),
'link_text' => $get_started_button_text,
'show' => ! Akismet::is_installed(),
],
];
return wpforms_render(
'builder/antispam/also-available',
[ 'blocks' => $blocks ],
true
);
}
}
@@ -0,0 +1,61 @@
<?php
namespace WPForms\Admin\Builder;
/**
* Context Menu class.
*
* @since 1.8.6
*/
class ContextMenu {
/**
* Init class.
*
* @since 1.8.6
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.8.6
*/
protected function hooks() {
add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] );
add_action( 'wpforms_admin_page', [ $this, 'output' ], 20 );
}
/**
* Enqueue assets.
*
* @since 1.8.6
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-builder-context-menu',
WPFORMS_PLUGIN_URL . "assets/js/admin/builder/context-menu{$min}.js",
[ 'wpforms-builder' ],
WPFORMS_VERSION,
true
);
}
/**
* Output context menu markup.
*
* @since 1.8.6
*/
public function output() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render( 'builder/field-context-menu' );
}
}
@@ -0,0 +1,64 @@
<?php
namespace WPForms\Admin\Builder;
use WPForms\Helpers\CacheBase;
/**
* Form Builder Help Cache.
*
* @since 1.8.2
*/
class HelpCache extends CacheBase {
/**
* Remote source URL.
*
* @since 1.9.3
*
* @var string
*/
const REMOTE_SOURCE = 'https://wpformsapi.com/feeds/v1/docs/';
/**
* Determine if the class is allowed to load.
*
* @since 1.8.2
*
* @return bool
*/
protected function allow_load() {
if ( wp_doing_cron() || wpforms_doing_wp_cli() ) {
return true;
}
if ( ! wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] ) ) {
return false;
}
return wpforms_is_admin_page( 'builder' );
}
/**
* Setup settings and other things.
*
* @since 1.8.2
*/
protected function setup() {
return [
'remote_source' => self::REMOTE_SOURCE,
'cache_file' => 'docs.json',
/**
* Allow modifying Help Docs cache TTL (time to live).
*
* @since 1.6.3
*
* @param int $cache_ttl Cache TTL in seconds. Defaults to 1 week.
*/
'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_help_cache_ttl', WEEK_IN_SECONDS ),
'update_action' => 'wpforms_builder_help_cache_update',
];
}
}
@@ -0,0 +1,49 @@
<?php
namespace WPForms\Admin\Builder;
/**
* Image Upload functionality for the Form Builder Settings.
*
* @since 1.9.7.3
*/
class ImageUpload {
/**
* Initialize class.
*
* @since 1.9.7.3
*/
public function init(): void {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.9.7.3
*/
public function hooks(): void {
add_action( 'wpforms_builder_enqueues', [ $this, 'enqueues' ] );
}
/**
* Enqueue assets for the Form Builder.
*
* @since 1.9.7.3
*/
public function enqueues(): void {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-builder-settings-image-upload',
WPFORMS_PLUGIN_URL . "assets/js/admin/builder/image-upload{$min}.js",
[ 'wp-util', 'wpforms-builder-settings' ],
WPFORMS_VERSION,
true
);
}
}
@@ -0,0 +1,189 @@
<?php
namespace WPForms\Admin\Builder\Notifications\Advanced;
use WPForms_Builder_Panel_Settings;
use WPForms\Emails\Helpers;
use WPForms\Admin\Education\Helpers as EducationHelpers;
/**
* Email Template.
* This class will register the Email Template field in the "Notification" → "Advanced" settings.
* The Email Template field will allow users to override the default email template for a specific notification.
*
* @since 1.8.5
*/
class EmailTemplate {
/**
* Initialize class.
*
* @since 1.8.5
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.5
*/
private function hooks() {
add_action( 'wpforms_builder_enqueues', [ $this, 'builder_assets' ] );
add_action( 'wpforms_builder_print_footer_scripts', [ $this, 'builder_footer_scripts' ] );
add_filter( 'wpforms_lite_admin_education_builder_notifications_advanced_settings_content', [ $this, 'settings' ], 5, 3 );
add_filter( 'wpforms_pro_admin_builder_notifications_advanced_settings_content', [ $this, 'settings' ], 5, 3 );
}
/**
* Enqueue assets for the builder.
*
* @since 1.8.5
*/
public function builder_assets() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-builder-email-template',
WPFORMS_PLUGIN_URL . "assets/js/admin/builder/email-template{$min}.js",
[ 'jquery', 'jquery-confirm', 'wpforms-builder' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-builder-email-template',
'wpforms_builder_email_template',
[
'is_pro' => wpforms()->is_pro(),
'templates' => Helpers::get_email_template_choices( false ),
]
);
}
/**
* Output Email Template modal.
*
* @since 1.8.5
*/
public function builder_footer_scripts() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'builder/notifications/email-template-modal',
[
'pro_badge' => ! wpforms()->is_pro() ? EducationHelpers::get_badge( 'Pro' ) : '',
],
true
);
}
/**
* Add Email Template settings.
*
* @since 1.8.5
*
* @param string $content Notification → Advanced content.
* @param WPForms_Builder_Panel_Settings $settings Builder panel settings.
* @param int $id Notification id.
*
* @return string
*/
public function settings( $content, $settings, $id ) {
// Retrieve email template choices and disabled choices.
// A few of the email templates are only available in the Pro version and will be disabled for non-Pro users.
// The disabled choices will be added to the select field with a "(Pro)" label appended to the name.
list( $options, $disabled_options ) = $this->get_email_template_options();
// Add Email Template field.
$content .= wpforms_panel_field(
'select',
'notifications',
'template',
$settings->form_data,
esc_html__( 'Email Template', 'wpforms-lite' ),
[
'default' => '',
'options' => $options,
'disabled_options' => $disabled_options,
'class' => 'wpforms-panel-field-email-template-wrap',
'input_class' => 'wpforms-panel-field-email-template',
'parent' => 'settings',
'subsection' => $id,
'after' => $this->get_template_modal_link_content(),
'tooltip' => esc_html__( 'Override the default email template for this specific notification.', 'wpforms-lite' ),
],
false
);
return $content;
}
/**
* Get Email template choices.
*
* This function will return an array of email template choices and an array of disabled choices.
* The disabled choices are templates that are only available in the Pro version.
*
* @since 1.8.5
*
* @return array
*/
private function get_email_template_options() {
// Retrieve the available email template choices.
$choices = Helpers::get_email_template_choices( false );
// If there are no templates or the choices are not an array, return empty arrays.
if ( empty( $choices ) || ! is_array( $choices ) ) {
return [ [], [] ];
}
// Check if the Pro version is active.
$is_pro = wpforms()->is_pro();
// Initialize arrays for options and disabled options.
$options = [];
$disabled_options = [];
// Iterate through the templates and build the $options array.
foreach ( $choices as $key => $choice ) {
$value = esc_attr( $key );
$name = esc_html( $choice['name'] );
$is_disabled = ! $is_pro && isset( $choice['is_pro'] ) && $choice['is_pro'];
// If the option is disabled for non-Pro users, add it to the disabled options array.
if ( $is_disabled ) {
$disabled_options[] = $value;
}
// Build the $options array with appropriate labels.
// Pro badge labels are not meant to be translated.
$options[ $key ] = $is_disabled ? sprintf( '%s (Pro)', $name ) : $name;
}
// Add an empty option to the beginning of the $options array.
// This is a placeholder option that will be replaced with the default template name.
$options = array_merge( [ '' => esc_html__( 'Default Template', 'wpforms-lite' ) ], $options );
// Return the options and disabled options arrays.
return [ $options, $disabled_options ];
}
/**
* Get Email template modal link content.
*
* @since 1.8.5
*
* @return string
*/
private function get_template_modal_link_content() {
return wpforms_render( 'builder/notifications/email-template-link' );
}
}
@@ -0,0 +1,143 @@
<?php
namespace WPForms\Admin\Builder;
/**
* Form Builder Keyboard Shortcuts modal content.
*
* @since 1.6.9
*/
class Shortcuts {
/**
* Initialize class.
*
* @since 1.6.9
*/
public function init(): void {
// Terminate initialization if not in the builder.
if ( ! wpforms_is_admin_page( 'builder' ) ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.9
*/
private function hooks(): void {
add_filter( 'wpforms_builder_strings', [ $this, 'builder_strings' ] );
add_action( 'wpforms_admin_page', [ $this, 'output' ], 30 );
}
/**
* Get a shortcut list.
*
* @since 1.6.9
*
* @return array
*/
private function get_list(): array {
return [
'left' => [
'ctrl s' => __( 'Save Form', 'wpforms-lite' ),
'ctrl p' => __( 'Preview Form', 'wpforms-lite' ),
'ctrl b' => __( 'Embed Form', 'wpforms-lite' ),
'ctrl f' => __( 'Search Fields', 'wpforms-lite' ),
'ctrl c' => __( 'Copy Fields', 'wpforms-lite' ),
'ctrl v' => __( 'Paste Fields', 'wpforms-lite' ),
'd' => __( 'Duplicate Fields', 'wpforms-lite' ),
],
'right' => [
'ctrl z' => __( 'Undo', 'wpforms-lite' ),
'ctrl shift z' => __( 'Redo', 'wpforms-lite' ),
'ctrl h' => __( 'Open Help', 'wpforms-lite' ),
'ctrl t' => __( 'Toggle Sidebar', 'wpforms-lite' ), // It is 'alt s' on Windows/Linux, dynamically changed in the modal in admin-builder.js openKeyboardShortcutsModal().
'ctrl e' => __( 'View Entries', 'wpforms-lite' ),
'ctrl q' => __( 'Close Builder', 'wpforms-lite' ),
'delete' => __( 'Delete Fields', 'wpforms-lite' ),
],
];
}
/**
* Add Form builder strings.
*
* @since 1.6.9
*
* @param array|mixed $strings Form Builder strings.
*
* @return array
*/
public function builder_strings( $strings ): array {
$strings = (array) $strings;
$strings['shortcuts_modal_title'] = esc_html__( 'Keyboard Shortcuts', 'wpforms-lite' );
$strings['shortcuts_modal_msg'] = esc_html__( 'Handy shortcuts for common actions in the builder.', 'wpforms-lite' );
return $strings;
}
/**
* Generate and output shortcuts modal content as the wp.template.
*
* @since 1.6.9
*/
public function output(): void {
echo '
<script type="text/html" id="tmpl-wpforms-builder-keyboard-shortcuts">
<div class="wpforms-columns wpforms-columns-2">';
foreach ( $this->get_list() as $list ) {
echo "<ul class='wpforms-column'>";
foreach ( $list as $key => $label ) {
$key_parts = explode( ' ', $key );
if ( count( $key_parts ) > 1 ) {
printf(
'<li>
%1$s
<span class="shortcut-key shortcut-key-%2$s">
<i>%3$s</i><i>%4$s</i><i>%5$s</i>
</span>
</li>',
esc_html( $label ),
esc_html( str_replace( ' ', '-', $key ) ),
esc_html( $key_parts[0] ),
esc_html( $key_parts[1] ?? '' ),
esc_html( $key_parts[2] ?? '' )
);
} else {
// Single key like 'delete' or 'd'.
printf(
'<li>
%1$s
<span class="shortcut-key shortcut-key-%2$s">
<i>%2$s</i>
</span>
</li>',
esc_html( $label ),
esc_html( $key )
);
}
}
echo '</ul>';
}
echo '
</div>
</script>';
}
}
@@ -0,0 +1,247 @@
<?php
namespace WPForms\Admin\Builder;
use WPForms\Helpers\CacheBase;
use WPForms\Tasks\Actions\AsyncRequestTask;
/**
* Single template cache handler.
*
* @since 1.6.8
*/
class TemplateSingleCache extends CacheBase {
/**
* Template Id (hash).
*
* @since 1.6.8
*
* @var string
*/
private $id;
/**
* License data (`key` and `type`).
*
* @since 1.6.8
*
* @var array
*/
private $license;
/**
* Determine if the class is allowed to load.
*
* @since 1.6.8
*
* @return bool
*/
protected function allow_load() {
$has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] );
$allowed_requests = wpforms_is_admin_ajax() || wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_page( 'templates' );
$allow = wp_doing_cron() || wpforms_doing_wp_cli() || ( $has_permissions && $allowed_requests );
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Whether to allow to load this class.
*
* @since 1.7.2
*
* @param bool $allow True or false.
*/
return (bool) apply_filters( 'wpforms_admin_builder_templatesinglecache_allow_load', $allow );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Re-initialize object with the particular template.
*
* @since 1.6.8
*
* @param string $template_id Template ID (hash).
* @param array $license License data.
*
* @return TemplateSingleCache
*/
public function instance( $template_id, $license ) {
$this->id = $template_id;
$this->license = $license;
$this->init();
return $this;
}
/**
* Provide settings.
*
* @since 1.6.8
*
* @return array Settings array.
*/
protected function setup() {
return [
// Remote source URL.
'remote_source' => $this->remote_source(),
// Cache file.
'cache_file' => $this->get_cache_file_name(),
// This filter is documented in wpforms/src/Admin/Builder/TemplatesCache.php.
// phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName, WPForms.Comments.PHPDocHooks.RequiredHookDocumentation
'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_templates_cache_ttl', WEEK_IN_SECONDS ),
];
}
/**
* Generate single template remote URL.
*
* @since 1.6.8
*
* @param bool $cache True if the cache arg should be appended to the URL.
*
* @return string
*/
private function remote_source( $cache = false ) {
if ( ! isset( $this->license['key'] ) ) {
return '';
}
$args = [
'license' => $this->license['key'],
'site' => site_url(),
];
if ( $cache ) {
$args['cache'] = 1;
}
return add_query_arg(
$args,
'https://wpforms.com/templates/api/get/' . $this->id
);
}
/**
* Get cached data.
*
* @since 1.8.2
*
* @return array Cached data.
*/
public function get() {
$data = parent::get();
if ( ! $this->updated ) {
$this->update_usage_tracking();
}
return $data;
}
/**
* Sends a request to update the form template usage tracking database.
*
* @since 1.7.5
*/
private function update_usage_tracking() {
$tasks = wpforms()->obj( 'tasks' );
if ( ! $tasks ) {
return;
}
$url = $this->remote_source( true );
$args = [ 'blocking' => false ];
$tasks->create( AsyncRequestTask::ACTION )->async()->params( $url, $args )->register();
}
/**
* Get cache directory path.
*
* @since 1.6.8
*/
protected function get_cache_dir() {
return parent::get_cache_dir() . 'templates/';
}
/**
* Generate single template cache file name.
*
* @since 1.6.8
*
* @return string.
*/
private function get_cache_file_name() {
return sanitize_key( $this->id ) . '.json';
}
/**
* Prepare data to store in a local cache.
*
* @since 1.6.8
*
* @param array $data Raw data received by the remote request.
*
* @return array Prepared data for caching.
*/
protected function prepare_cache_data( $data ): array {
if (
empty( $data ) ||
! is_array( $data ) ||
empty( $data['status'] ) ||
$data['status'] !== 'success' ||
empty( $data['data'] )
) {
return [];
}
$cache_data = $data['data'];
$cache_data['data'] = empty( $cache_data['data'] ) ? [] : $cache_data['data'];
$cache_data['data']['settings'] = empty( $cache_data['data']['settings'] ) ? [] : $cache_data['data']['settings'];
$cache_data['data']['settings']['ajax_submit'] = '1';
// Strip the word "Template" from the end of the template name and form title setting.
$cache_data['name'] = preg_replace( '/\sTemplate$/', '', $cache_data['name'] );
$cache_data['data']['settings']['form_title'] = $cache_data['name'];
// Unset `From Name` field of the notification settings.
// By default, the builder will use the `blogname` option value.
unset( $cache_data['data']['settings']['notifications'][1]['sender_name'] );
return $cache_data;
}
/**
* Wipe cache of an empty templates.
*
* @since 1.7.5
*/
public function wipe_empty_templates_cache() {
$cache_dir = $this->get_cache_dir();
$files = glob( $cache_dir . '*.json' );
foreach ( $files as $filename ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$content = file_get_contents( $filename );
if ( empty( $content ) || trim( $content ) === '[]' ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
unlink( $filename );
}
}
}
}
@@ -0,0 +1,267 @@
<?php
namespace WPForms\Admin\Builder;
use WPForms\Helpers\CacheBase;
use WPForms\Helpers\File;
/**
* Form templates cache handler.
*
* @since 1.6.8
*/
class TemplatesCache extends CacheBase {
/**
* Templates list content cache files.
*
* @since 1.8.6
*
* @var array
*/
const CONTENT_CACHE_FILES = [
'admin-page' => 'templates-admin-page.html',
'builder' => 'templates-builder.html',
];
/**
* List of plugins that can use the templates cache.
*
* @since 1.8.7
*
* @var array
*/
const PLUGINS = [
'wpforms',
'wpforms-lite',
];
/**
* Determine if the class is allowed to load.
*
* @since 1.6.8
*
* @return bool
*/
protected function allow_load(): bool {
$has_permissions = wpforms_current_user_can( [ 'create_forms', 'edit_forms' ] );
$allowed_requests = wpforms_is_admin_ajax() ||
wpforms_is_admin_page( 'builder' ) ||
wpforms_is_admin_page( 'templates' ) ||
wpforms_is_admin_page( 'tools', 'action-scheduler' );
$allow = wp_doing_cron() || wpforms_doing_wp_cli() || ( $has_permissions && $allowed_requests );
/**
* Whether to load this class.
*
* @since 1.7.2
*
* @param bool $allow True or false.
*/
return (bool) apply_filters( 'wpforms_admin_builder_templatescache_allow_load', $allow ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Initialize the class.
*
* @since 1.8.7
*/
public function init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
parent::init();
// Upgrade cached templates data after the plugin update.
add_action( 'upgrader_process_complete', [ $this, 'upgrade_templates' ] );
}
/**
* Upgrade cached templates data after the plugin update.
*
* @since 1.8.7
*
* @param object $upgrader WP_Upgrader instance.
*/
public function upgrade_templates( $upgrader ) {
if ( $this->allow_update_cache( $upgrader ) ) {
$this->update( true );
}
}
/**
* Determine if allowed to update the cache.
*
* @since 1.8.7
*
* @param object $upgrader WP_Upgrader instance.
*
* @return bool
*/
private function allow_update_cache( $upgrader ): bool {
$result = $upgrader->result ?? null;
// Check if plugin was updated.
if ( ! $result ) {
return false;
}
// Check if updated plugin is WPForms.
if ( ! in_array( $result['destination_name'], self::PLUGINS, true ) ) {
return false;
}
return true;
}
/**
* Provide settings.
*
* @since 1.6.8
*
* @return array Settings array.
*/
protected function setup() {
return [
// Remote source URL.
'remote_source' => 'https://wpforms.com/templates/api/get/',
// Cache file.
'cache_file' => 'templates.json',
/**
* Time-to-live of the templates cache files in seconds.
*
* This applies to `uploads/wpforms/cache/templates.json`
* and all *.json files in `uploads/wpforms/cache/templates/` directory.
*
* @since 1.6.8
*
* @param integer $cache_ttl Cache time-to-live, in seconds.
* Default value: WEEK_IN_SECONDS.
*/
'cache_ttl' => (int) apply_filters( 'wpforms_admin_builder_templates_cache_ttl', WEEK_IN_SECONDS ),
// Scheduled update action.
'update_action' => 'wpforms_admin_builder_templates_cache_update',
];
}
/**
* Prepare data to store in a local cache.
*
* @since 1.6.8
*
* @param array $data Raw data received by the remote request.
*
* @return array Prepared data for caching.
*/
protected function prepare_cache_data( $data ): array {
if (
empty( $data ) ||
! is_array( $data ) ||
empty( $data['status'] ) ||
$data['status'] !== 'success' ||
empty( $data['data'] )
) {
return [];
}
$cache_data = $data['data'];
// Strip the word "Template" from the end of each template name.
foreach ( $cache_data['templates'] as $slug => $template ) {
$cache_data['templates'][ $slug ]['name'] = preg_replace( '/\sTemplate$/', '', $template['name'] );
}
return $cache_data;
}
/**
* Update the cache.
*
* @since 1.8.6
*
* @param bool $force Whether to force update the cache.
*
* @return bool
*/
public function update( bool $force = false ): bool {
$result = parent::update( $force );
if ( ! $result ) {
return false;
}
$this->wipe_content_cache();
return $result;
}
/**
* Get cached templates content.
*
* @since 1.8.6
*
* @return string
*/
public function get_content_cache(): string {
// phpcs:ignore Universal.Operators.DisallowShortTernary.Found
return File::get_contents( $this->get_content_cache_file() ) ?: '';
}
/**
* Save templates content cache.
*
* @since 1.8.6
*
* @param string|mixed $content Templates content.
*
* @return bool
*/
public function save_content_cache( $content ): bool {
return File::put_contents( $this->get_content_cache_file(), (string) $content );
}
/**
* Wipe cached templates content.
*
* @since 1.8.6
*/
public function wipe_content_cache() {
$cache_dir = $this->get_cache_dir();
// Delete the template content cache files. They will be regenerated on the first visit.
foreach ( self::CONTENT_CACHE_FILES as $file ) {
$cache_file = $cache_dir . $file;
if ( is_file( $cache_file ) && is_readable( $cache_file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
unlink( $cache_file );
}
}
}
/**
* Get templates content cache file path.
*
* @since 1.8.6
*
* @return string
*/
private function get_content_cache_file(): string {
$context = wpforms_is_admin_page( 'templates' ) ? 'admin-page' : 'builder';
return File::get_cache_dir() . self::CONTENT_CACHE_FILES[ $context ];
}
}
@@ -0,0 +1,772 @@
<?php
namespace WPForms\Admin;
/**
* Challenge and guide a user to set up a first form once WPForms is installed.
*
* @since 1.5.0
* @since 1.6.2 Challenge v2
*/
class Challenge {
/**
* Number of minutes to complete the Challenge.
*
* @since 1.5.0
*
* @var int
*/
protected $minutes = 5;
/**
* Initialize.
*
* @since 1.6.2
*/
public function init() {
if ( current_user_can( wpforms_get_capability_manage_options() ) ) {
$this->hooks();
}
}
/**
* Hooks.
*
* @since 1.5.0
*/
public function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'wpforms_builder_init', [ $this, 'init_challenge' ] );
add_action( 'admin_footer', [ $this, 'challenge_html' ] );
add_action( 'wpforms_welcome_intro_after', [ $this, 'welcome_html' ] );
add_action( 'wp_ajax_wpforms_challenge_save_option', [ $this, 'save_challenge_option_ajax' ] );
add_action( 'wp_ajax_wpforms_challenge_send_contact_form', [ $this, 'send_contact_form_ajax' ] );
}
/**
* Check if the current page is related to Challenge.
*
* @since 1.5.0
*/
public function is_challenge_page() {
return wpforms_is_admin_page() ||
$this->is_builder_page() ||
$this->is_form_embed_page();
}
/**
* Check if the current page is a forms builder page related to Challenge.
*
* @since 1.5.0
*
* @return bool
*/
public function is_builder_page() {
if ( ! wpforms_is_admin_page( 'builder' ) ) {
return false;
}
if ( ! $this->challenge_active() && ! $this->challenge_inited() ) {
return false;
}
$step = (int) $this->get_challenge_option( 'step' );
$form_id = (int) $this->get_challenge_option( 'form_id' );
if ( $form_id && $step < 2 ) {
return false;
}
$current_form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$is_new_form = isset( $_GET['newform'] ) ? (int) $_GET['newform'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $is_new_form && $step !== 2 ) {
return false;
}
if ( ! $is_new_form && $form_id !== $current_form_id && $step >= 2 ) {
// In case if user skipped the Challenge by closing the browser window or exiting the builder,
// we need to set the previous Challenge as `canceled`.
// Otherwise, the Form Embed Wizard will think that the Challenge is active.
$this->set_challenge_option(
[
'status' => 'skipped',
'finished_date_gmt' => current_time( 'mysql', true ),
]
);
return false;
}
return true;
}
/**
* Check if the current page is a form embed page edit related to Challenge.
*
* @since 1.5.0
*
* @return bool
*/
public function is_form_embed_page() {
if ( ! function_exists( 'get_current_screen' ) || ! is_admin() || ! is_user_logged_in() ) {
return false;
}
$screen = get_current_screen();
if ( ! isset( $screen->id ) || $screen->id !== 'page' || ! $this->challenge_active() ) {
return false;
}
$step = $this->get_challenge_option( 'step' );
if ( ! in_array( $step, [ 3, 4, 5 ], true ) ) {
return false;
}
$embed_page = $this->get_challenge_option( 'embed_page' );
$is_embed_page = false;
if ( isset( $screen->action ) && $screen->action === 'add' && $embed_page === 0 ) {
$is_embed_page = true;
}
if ( isset( $_GET['post'] ) && $embed_page === (int) $_GET['post'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$is_embed_page = true;
}
if ( $is_embed_page && $step < 4 ) {
$this->set_challenge_option( [ 'step' => 4 ] );
}
return $is_embed_page;
}
/**
* Load scripts and styles.
*
* @since 1.5.0
*/
public function enqueue_scripts() {
if ( ! $this->challenge_can_start() && ! $this->challenge_active() ) {
return;
}
$min = wpforms_get_min_suffix();
if ( $this->is_challenge_page() ) {
wp_enqueue_style(
'wpforms-challenge',
WPFORMS_PLUGIN_URL . "assets/css/challenge{$min}.css",
[],
WPFORMS_VERSION
);
wp_enqueue_script(
'wpforms-challenge-admin',
WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-admin{$min}.js",
[ 'jquery' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-challenge-admin',
'wpforms_challenge_admin',
[
'nonce' => wp_create_nonce( 'wpforms_challenge_ajax_nonce' ),
'minutes_left' => absint( $this->minutes ),
'option' => $this->get_challenge_option(),
'frozen_tooltip' => esc_html__( 'Challenge is frozen.', 'wpforms-lite' ),
]
);
}
if ( $this->is_builder_page() || $this->is_form_embed_page() ) {
wp_enqueue_style(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css',
null,
'4.2.6'
);
wp_enqueue_script(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js',
[ 'jquery' ],
'4.2.6',
true
);
wp_enqueue_script(
'wpforms-challenge-core',
WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-core{$min}.js",
[ 'jquery', 'tooltipster', 'wpforms-challenge-admin', 'wpforms-generic-utils' ],
WPFORMS_VERSION,
true
);
}
if ( $this->is_builder_page() ) {
wp_enqueue_script(
'wpforms-challenge-builder',
WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-builder{$min}.js",
[ 'jquery', 'tooltipster', 'wpforms-challenge-core', 'wpforms-builder' ],
WPFORMS_VERSION,
true
);
}
if ( $this->is_form_embed_page() ) {
wp_enqueue_style(
'wpforms-font-awesome',
WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css',
null,
'7.0.1'
);
// FontAwesome v4 compatibility shims.
wp_enqueue_style(
'wpforms-font-awesome-v4-shim',
WPFORMS_PLUGIN_URL . 'assets/lib/font-awesome/css/v4-shims.min.css',
null,
'4.7.0'
);
wp_enqueue_script(
'wpforms-challenge-embed',
WPFORMS_PLUGIN_URL . "assets/js/admin/challenge/challenge-embed{$min}.js",
[ 'jquery', 'tooltipster', 'wpforms-challenge-core' ],
WPFORMS_VERSION,
true
);
}
}
/**
* Get 'wpforms_challenge' option schema.
*
* @since 1.5.0
*
* @return array
*/
public function get_challenge_option_schema() {
return [
'status' => '',
'step' => 0,
'user_id' => get_current_user_id(),
'form_id' => 0,
'embed_page' => 0,
'embed_page_title' => '',
'started_date_gmt' => '',
'finished_date_gmt' => '',
'seconds_spent' => 0,
'seconds_left' => 0,
'feedback_sent' => false,
'feedback_contact_me' => false,
'window_closed' => '',
];
}
/**
* Get Challenge parameter(s) from Challenge option.
*
* @since 1.5.0
*
* @param array|string|null $query Query using 'wpforms_challenge' schema keys.
*
* @return array|mixed
*/
public function get_challenge_option( $query = null ) {
if ( ! $query ) {
return get_option( 'wpforms_challenge' );
}
$return_single = false;
if ( ! is_array( $query ) ) {
$return_single = true;
$query = [ $query ];
}
$query = array_flip( $query );
$option = get_option( 'wpforms_challenge' );
if ( ! $option || ! is_array( $option ) ) {
return array_intersect_key( $this->get_challenge_option_schema(), $query );
}
$result = array_intersect_key( $option, $query );
if ( $return_single ) {
$result = reset( $result );
}
return $result;
}
/**
* Set Challenge parameter(s) to Challenge option.
*
* @since 1.5.0
*
* @param array $query Query using 'wpforms_challenge' schema keys.
*/
public function set_challenge_option( $query ) {
if ( empty( $query ) || ! is_array( $query ) ) {
return;
}
$schema = $this->get_challenge_option_schema();
$replace = array_intersect_key( $query, $schema );
if ( ! $replace ) {
return;
}
// Validate and sanitize the data.
foreach ( $replace as $key => $value ) {
if ( in_array( $key, [ 'step', 'user_id', 'form_id', 'embed_page', 'seconds_spent', 'seconds_left' ], true ) ) {
$replace[ $key ] = absint( $value );
continue;
}
if ( in_array( $key, [ 'feedback_sent', 'feedback_contact_me' ], true ) ) {
$replace[ $key ] = wp_validate_boolean( $value );
continue;
}
$replace[ $key ] = sanitize_text_field( $value );
}
$option = get_option( 'wpforms_challenge' );
$option = ! $option || ! is_array( $option ) ? $schema : $option;
update_option( 'wpforms_challenge', array_merge( $option, $replace ) );
}
/**
* Check if any forms are present on a site.
*
* @since 1.5.0
*
* @retun bool
*/
public function website_has_forms() {
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
return (bool) wpforms()->obj( 'form' )->get(
'',
[
'numberposts' => 1,
'nopaging' => false,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
]
);
}
/**
* Check if Challenge was started.
*
* @since 1.5.0
*
* @return bool
*/
public function challenge_started() {
return $this->get_challenge_option( 'status' ) === 'started';
}
/**
* Check if Challenge was initialized.
*
* @since 1.6.2
*
* @return bool
*/
public function challenge_inited() {
return $this->get_challenge_option( 'status' ) === 'inited';
}
/**
* Check if Challenge was paused.
*
* @since 1.6.2
*
* @return bool
*/
public function challenge_paused() {
return $this->get_challenge_option( 'status' ) === 'paused';
}
/**
* Check if Challenge was finished.
*
* @since 1.5.0
*
* @return bool
*/
public function challenge_finished() {
$status = $this->get_challenge_option( 'status' );
return in_array( $status, [ 'completed', 'canceled', 'skipped' ], true );
}
/**
* Check if Challenge is in progress.
*
* @since 1.5.0
*
* @return bool
*/
public function challenge_active() {
return ( $this->challenge_inited() || $this->challenge_started() || $this->challenge_paused() ) && ! $this->challenge_finished();
}
/**
* Force Challenge to start.
*
* @since 1.6.2
*
* @return bool
*/
public function challenge_force_start() {
/**
* Allow force start Challenge for testing purposes.
*
* @since 1.6.2.2
*
* @param bool $is_forced True if Challenge should be started. False by default.
*/
return (bool) apply_filters( 'wpforms_admin_challenge_force_start', false );
}
/**
* Check if Challenge can be started.
*
* @since 1.5.0
*
* @return bool
*/
public function challenge_can_start() {
static $can_start = null;
if ( $can_start !== null ) {
return $can_start;
}
if ( $this->challenge_force_skip() ) {
$can_start = false;
}
// Challenge is only available on WPForms admin pages or Builder page.
if ( ! wpforms_is_admin_page() && ! wpforms_is_admin_page( 'builder' ) ) {
$can_start = false;
// No need to check something else in this case.
return false;
}
// The challenge should not start if this is the Forms' Overview page.
if ( wpforms_is_admin_page( 'overview' ) ) {
$can_start = false;
// No need to check something else in this case.
return false;
}
// Force start the Challenge.
if ( $this->challenge_force_start() && ! $this->is_builder_page() && ! $this->is_form_embed_page() ) {
$can_start = true;
// No need to check something else in this case.
return true;
}
if ( $this->challenge_finished() ) {
$can_start = false;
}
if ( $this->website_has_forms() ) {
$can_start = false;
}
if ( $can_start === null ) {
$can_start = true;
}
return $can_start;
}
/**
* Start the Challenge in Form Builder.
*
* @since 1.5.0
*/
public function init_challenge() {
if ( ! $this->challenge_can_start() ) {
return;
}
$this->set_challenge_option(
wp_parse_args(
[ 'status' => 'inited' ],
$this->get_challenge_option_schema()
)
);
}
/**
* Include Challenge HTML.
*
* @since 1.5.0
*/
public function challenge_html() {
if ( $this->challenge_force_skip() || ( $this->challenge_finished() && ! $this->challenge_force_start() ) ) {
return;
}
if ( wpforms_is_admin_page() && ! wpforms_is_admin_page( 'getting-started' ) && $this->challenge_can_start() ) {
// Before showing the Challenge in the `start` state we should reset the option.
// In this way we ensure the Challenge will not appear somewhere in the builder where it is not should be.
$this->set_challenge_option( [ 'status' => '' ] );
$this->challenge_modal_html( 'start' );
}
if ( $this->is_builder_page() ) {
$this->challenge_modal_html( 'progress' );
$this->challenge_builder_templates_html();
}
if ( $this->is_form_embed_page() ) {
$this->challenge_modal_html( 'progress' );
$this->challenge_embed_templates_html();
}
}
/**
* Include Challenge main modal window HTML.
*
* @since 1.5.0
*
* @param string $state State of Challenge ('start' or 'progress').
*/
public function challenge_modal_html( $state ) {
echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'admin/challenge/modal',
[
'state' => $state,
'step' => $this->get_challenge_option( 'step' ),
'minutes' => $this->minutes,
],
true
);
}
/**
* Include Challenge HTML templates specific to Form Builder.
*
* @since 1.5.0
*/
public function challenge_builder_templates_html() {
echo wpforms_render( 'admin/challenge/builder' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Include Challenge HTML templates specific to form embed page.
*
* @since 1.5.0
*/
public function challenge_embed_templates_html() {
/**
* Filter the content of the Challenge Congrats popup footer.
*
* @since 1.7.4
*
* @param string $footer Footer markup.
*/
$congrats_popup_footer = apply_filters( 'wpforms_admin_challenge_embed_template_congrats_popup_footer', '' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/challenge/embed',
[
'minutes' => $this->minutes,
'congrats_popup_footer' => $congrats_popup_footer,
],
true
);
}
/**
* Include Challenge CTA on WPForms welcome activation screen.
*
* @since 1.5.0
*/
public function welcome_html() {
if ( $this->challenge_can_start() ) {
echo wpforms_render( 'admin/challenge/welcome' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Save Challenge data via AJAX.
*
* @since 1.5.0
*/
public function save_challenge_option_ajax() {
check_admin_referer( 'wpforms_challenge_ajax_nonce' );
if ( empty( $_POST['option_data'] ) ) {
wp_send_json_error();
}
$schema = $this->get_challenge_option_schema();
$query = [];
foreach ( $schema as $key => $value ) {
if ( isset( $_POST['option_data'][ $key ] ) ) {
$query[ $key ] = sanitize_text_field( wp_unslash( $_POST['option_data'][ $key ] ) );
}
}
if ( empty( $query ) ) {
wp_send_json_error();
}
if ( ! empty( $query['status'] ) && $query['status'] === 'started' ) {
$query['started_date_gmt'] = current_time( 'mysql', true );
}
if ( ! empty( $query['status'] ) && in_array( $query['status'], [ 'completed', 'canceled', 'skipped' ], true ) ) {
$query['finished_date_gmt'] = current_time( 'mysql', true );
}
if ( ! empty( $query['status'] ) && $query['status'] === 'skipped' ) {
$query['started_date_gmt'] = current_time( 'mysql', true );
$query['finished_date_gmt'] = $query['started_date_gmt'];
}
$this->set_challenge_option( $query );
wp_send_json_success();
}
/**
* Send contact form to wpforms.com via AJAX.
*
* @since 1.5.0
*/
public function send_contact_form_ajax() {
check_admin_referer( 'wpforms_challenge_ajax_nonce' );
$url = 'https://wpforms.com/wpforms-challenge-feedback/';
$message = ! empty( $_POST['contact_data']['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['contact_data']['message'] ) ) : '';
$email = '';
if (
( ! empty( $_POST['contact_data']['contact_me'] ) && $_POST['contact_data']['contact_me'] === 'true' )
|| wpforms()->is_pro()
) {
$current_user = wp_get_current_user();
$email = $current_user->user_email;
$this->set_challenge_option( [ 'feedback_contact_me' => true ] );
}
if ( empty( $message ) && empty( $email ) ) {
wp_send_json_error();
}
$data = [
'body' => [
'wpforms' => [
'id' => 296355,
'submit' => 'wpforms-submit',
'fields' => [
2 => $message,
3 => $email,
4 => $this->get_challenge_license_type(),
5 => wpforms()->version,
6 => wpforms_get_license_key(),
],
],
],
];
$response = wp_remote_post( $url, $data );
if ( is_wp_error( $response ) ) {
wp_send_json_error();
}
$this->set_challenge_option( [ 'feedback_sent' => true ] );
wp_send_json_success();
}
/**
* Get the current WPForms license type as it pertains to the challenge feedback form.
*
* @since 1.8.1
*
* @return string The currently active license type.
*/
private function get_challenge_license_type() {
$license_type = wpforms_get_license_type();
if ( $license_type === false ) {
$license_type = wpforms()->is_pro() ? 'Unknown' : 'Lite';
}
return ucfirst( $license_type );
}
/**
* Force WPForms Challenge to skip.
*
* @since 1.7.6
*
* @return bool
*/
private function challenge_force_skip() {
return defined( 'WPFORMS_SKIP_CHALLENGE' ) && WPFORMS_SKIP_CHALLENGE;
}
}
@@ -0,0 +1,303 @@
<?php
namespace WPForms\Admin\Dashboard;
/**
* Class Widget.
*
* @since 1.7.3
*/
abstract class Widget {
/**
* Instance slug.
*
* @since 1.7.4
*
* @var string
*/
const SLUG = 'dash_widget';
/**
* Save a widget meta for a current user using AJAX.
*
* @since 1.7.4
*/
public function save_widget_meta_ajax() {
check_ajax_referer( 'wpforms_' . static::SLUG . '_nonce' );
$meta = ! empty( $_POST['meta'] ) ? sanitize_key( $_POST['meta'] ) : '';
$value = ! empty( $_POST['value'] ) ? absint( $_POST['value'] ) : 0;
$this->widget_meta( 'set', $meta, $value );
exit();
}
/**
* Get/set a widget meta.
*
* @since 1.7.4
*
* @param string $action Possible value: 'get' or 'set'.
* @param string $meta Meta name.
* @param int $value Value to set.
*
* @return mixed
*/
protected function widget_meta( $action, $meta, $value = 0 ) {
$allowed_actions = [ 'get', 'set' ];
if ( ! in_array( $action, $allowed_actions, true ) ) {
return false;
}
$defaults = [
'timespan' => $this->get_timespan_default(),
'active_form_id' => 0,
'hide_recommended_block' => 0,
'hide_graph' => 0,
'color_scheme' => 1, // 1 - wpforms, 2 - wp
'graph_style' => 2, // 1 - bar, 2 - line
];
if ( ! array_key_exists( $meta, $defaults ) ) {
return false;
}
$meta_key = 'wpforms_' . static::SLUG . '_' . $meta;
$user_id = get_current_user_id();
if ( $action === 'get' ) {
$meta_value = absint( get_user_meta( $user_id, $meta_key, true ) );
// Return a default value from $defaults if $meta_value is empty.
return empty( $meta_value ) ? $defaults[ $meta ] : $meta_value;
}
$value = absint( $value );
if ( $action === 'set' && ! empty( $value ) ) {
return update_user_meta( $user_id, $meta_key, $value );
}
if ( $action === 'set' && empty( $value ) ) {
return delete_user_meta( $user_id, $meta_key );
}
return false;
}
/**
* Get the default timespan option.
*
* @since 1.7.4
*
* @return int|null
*/
protected function get_timespan_default() {
$options = $this->get_timespan_options();
$default = reset( $options );
return is_numeric( $default ) ? $default : null;
}
/**
* Get timespan options (in days).
*
* @since 1.7.4
*
* @return array
*/
protected function get_timespan_options(): array {
$default = [ 7, 30 ];
$options = $default;
// Apply deprecated filters.
if ( function_exists( 'apply_filters_deprecated' ) ) {
// phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
$options = apply_filters_deprecated( 'wpforms_dash_widget_chart_timespan_options', [ $options ], '5.0', 'wpforms_dash_widget_timespan_options' );
$options = apply_filters_deprecated( 'wpforms_dash_widget_forms_list_timespan_options', [ $options ], '5.0', 'wpforms_dash_widget_timespan_options' );
// phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
} else {
// phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
$options = apply_filters( 'wpforms_dash_widget_chart_timespan_options', $options );
$options = apply_filters( 'wpforms_dash_widget_forms_list_timespan_options', $options );
// phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
}
if ( ! is_array( $options ) ) {
$options = $default;
}
$widget_slug = static::SLUG;
// phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
$options = apply_filters( "wpforms_{$widget_slug}_timespan_options", $options );
// phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
if ( ! is_array( $options ) ) {
return [];
}
$options = array_filter( $options, 'is_numeric' );
return empty( $options ) ? $default : $options;
}
/**
* Widget settings HTML.
*
* @since 1.7.4
*
* @param bool $enabled Is form fields should be enabled.
*/
protected function widget_settings_html( $enabled = true ) {
$graph_style = $this->widget_meta( 'get', 'graph_style' );
$color_scheme = $this->widget_meta( 'get', 'color_scheme' );
echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'admin/dashboard/widget/settings',
[
'graph_style' => $graph_style,
'color_scheme' => $color_scheme,
'enabled' => $enabled,
],
true
);
}
/**
* Return randomly chosen one of the recommended plugins.
*
* @since 1.7.3
*
* @return array
*/
final protected function get_recommended_plugin(): array {
$plugins = [
'google-analytics-for-wordpress/googleanalytics.php' => [
'name' => __( 'MonsterInsights', 'wpforms-lite' ),
'slug' => 'google-analytics-for-wordpress',
'more' => 'https://www.monsterinsights.com/',
'pro' => [
'file' => 'google-analytics-premium/googleanalytics-premium.php',
],
],
'all-in-one-seo-pack/all_in_one_seo_pack.php' => [
'name' => __( 'AIOSEO', 'wpforms-lite' ),
'slug' => 'all-in-one-seo-pack',
'more' => 'https://aioseo.com/',
'pro' => [
'file' => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
],
],
'coming-soon/coming-soon.php' => [
'name' => __( 'SeedProd', 'wpforms-lite' ),
'slug' => 'coming-soon',
'more' => 'https://www.seedprod.com/',
'pro' => [
'file' => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
],
],
'wp-mail-smtp/wp_mail_smtp.php' => [
'name' => __( 'WP Mail SMTP', 'wpforms-lite' ),
'slug' => 'wp-mail-smtp',
'more' => 'https://wpmailsmtp.com/',
'pro' => [
'file' => 'wp-mail-smtp-pro/wp_mail_smtp.php',
],
],
];
$installed = get_plugins();
foreach ( $plugins as $id => $plugin ) {
if ( isset( $installed[ $id ] ) ) {
unset( $plugins[ $id ] );
}
if ( isset( $plugin['pro']['file'], $installed[ $plugin['pro']['file'] ] ) ) {
unset( $plugins[ $id ] );
}
}
return $plugins ? $plugins[ array_rand( $plugins ) ] : [];
}
/**
* Timespan select HTML.
*
* @since 1.7.4
*
* @param int $active_form_id Currently preselected form ID.
* @param bool $enabled If the select menu items should be enabled.
*/
protected function timespan_select_html( $active_form_id, $enabled = true ) {
?>
<select id="wpforms-dash-widget-timespan" class="wpforms-dash-widget-select-timespan" title="<?php esc_attr_e( 'Select timespan', 'wpforms-lite' ); ?>"
<?php echo ! empty( $active_form_id ) ? 'data-active-form-id="' . absint( $active_form_id ) . '"' : ''; ?>>
<?php $this->timespan_options_html( $this->get_timespan_options(), $enabled ); ?>
</select>
<?php
}
/**
* Timespan select options HTML.
*
* @since 1.7.4
*
* @param array $options Timespan options (in days).
* @param bool $enabled If the select menu items should be enabled.
*/
protected function timespan_options_html( $options, $enabled = true ) {
$timespan = $this->widget_meta( 'get', 'timespan' );
foreach ( $options as $option ) :
?>
<option value="<?php echo absint( $option ); ?>" <?php selected( $timespan, absint( $option ) ); ?> <?php disabled( ! $enabled ); ?>>
<?php /* translators: %d - number of days. */ ?>
<?php echo esc_html( sprintf( _n( 'Last %d day', 'Last %d days', absint( $option ), 'wpforms-lite' ), absint( $option ) ) ); ?>
</option>
<?php
endforeach;
}
/**
* Check if the current page is a dashboard page.
*
* @since 1.8.3
*
* @return bool
*/
protected function is_dashboard_page(): bool {
global $pagenow;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $pagenow === 'index.php' && empty( $_GET['page'] );
}
/**
* Check if is a dashboard widget ajax request.
*
* @since 1.8.3
*
* @return bool
*/
protected function is_dashboard_widget_ajax_request(): bool {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return wpforms_is_admin_ajax() && isset( $_REQUEST['action'] ) && strpos( sanitize_key( $_REQUEST['action'] ), 'wpforms_dash_widget' ) !== false;
}
}
@@ -0,0 +1,174 @@
<?php
namespace WPForms\Admin\Education;
use WPForms\Admin\Addons\Addons;
use WPForms\Requirements\Requirements;
/**
* Base class for all "addon item" type Education features.
*
* @since 1.6.6
*/
abstract class AddonsItemBase implements EducationInterface {
/**
* Instance of the Education\Core class.
*
* @since 1.6.6
*
* @var Core
*/
protected $education;
/**
* Instance of the Education\Addons class.
*
* @since 1.6.6
*
* @var Addons
*/
protected $addons;
/**
* Template name for rendering single addon item.
*
* @since 1.6.6
*
* @var string
*/
protected $single_addon_template;
/**
* Indicate if the current Education feature is allowed to load.
* Should be called from the child feature class.
*
* @since 1.6.6
*
* @return bool
*
* @noinspection PhpMissingReturnTypeInspection
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
abstract public function allow_load();
/**
* Init.
*
* @since 1.6.6
*
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
// Store the instance of the Education core class.
$this->education = wpforms()->obj( 'education' );
// Store the instance of the Education\Addons class.
$this->addons = wpforms()->obj( 'addons' );
// Define hooks.
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.6
*/
abstract public function hooks();
/**
* Display single addon item.
*
* @since 1.6.6
*
* @param array $addon Addon data.
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingParamTypeInspection
*/
protected function display_single_addon( $addon ) {
/**
* Filter to disallow addons to be displayed in the Education feature.
*
* @since 1.8.2
*
* @param bool $display Whether to hide the addon.
* @param array $slug Addon data.
*/
$is_disallowed = (bool) apply_filters( 'wpforms_admin_education_addons_item_base_display_single_addon_hide', false, $addon );
if ( empty( $addon ) || $is_disallowed ) {
return;
}
echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->single_addon_template,
$addon,
true
);
}
/**
* Prepare field data-attributes for the education actions.
* E.g., install, activate, incompatible.
*
* @since 1.9.4
*
* @param array $addon Current addon information.
*
* @return array
*/
protected function prepare_field_action_data( array $addon ): array {
if ( empty( $addon['plugin_allow'] ) ) {
return [];
}
if ( $addon['action'] === 'install' ) {
return [
'data' => [
'action' => 'install',
'name' => $addon['modal_name'],
'url' => $addon['url'],
'nonce' => wp_create_nonce( 'wpforms-admin' ),
'license' => $addon['license_level'],
],
'class' => 'education-modal',
];
}
if ( $addon['action'] === 'activate' ) {
return [
'data' => [
'action' => 'activate',
'name' => sprintf( /* translators: %s - addon name. */
esc_html__( '%s addon', 'wpforms-lite' ),
$addon['name']
),
'path' => $addon['path'],
'nonce' => wp_create_nonce( 'wpforms-admin' ),
],
'class' => 'education-modal',
];
}
if ( $addon['action'] === 'incompatible' ) {
return [
'data' => [
'action' => 'incompatible',
'message' => Requirements::get_instance()->get_notice( $addon['path'] ),
],
'class' => 'education-modal',
];
}
return [];
}
}
@@ -0,0 +1,62 @@
<?php
namespace WPForms\Admin\Education;
/**
* Base class for all "addons list" type Education features.
*
* @since 1.6.6
*/
abstract class AddonsListBase extends AddonsItemBase {
/**
* Display addons.
*
* @since 1.6.6
*/
public function display_addons() {
array_map( [ $this, 'display_single_addon' ], (array) $this->get_addons() );
}
/**
* Get addons.
*
* @since 1.6.6
*
* @return array Addons array.
*/
abstract protected function get_addons();
/**
* Ensure that we do not display activated addon items if those addons are not allowed according to the current license.
*
* @since 1.6.6
*
* @param string $hook Hook name.
*/
protected function filter_not_allowed_addons( $hook ) {
$edu_addons = wp_list_pluck( $this->get_addons(), 'slug' );
foreach ( $edu_addons as $i => $addon ) {
$edu_addons[ $i ] = strtolower( preg_replace( '/[^a-zA-Z0-9]+/', '', $addon ) );
}
if ( empty( $GLOBALS['wp_filter'][ $hook ]->callbacks ) ) {
return;
}
foreach ( $GLOBALS['wp_filter'][ $hook ]->callbacks as $priority => $hooks ) {
foreach ( $hooks as $name => $arr ) {
$class = ! empty( $arr['function'][0] ) && is_object( $arr['function'][0] ) ? strtolower( get_class( $arr['function'][0] ) ) : '';
$class = explode( '\\', $class )[0];
$class = preg_replace( '/[^a-zA-Z0-9]+/', '', $class );
if ( in_array( $class, $edu_addons, true ) ) {
unset( $GLOBALS['wp_filter'][ $hook ]->callbacks[ $priority ][ $name ] );
}
}
}
}
}
@@ -0,0 +1,250 @@
<?php
namespace WPForms\Admin\Education\Admin;
use WP_Post;
use WPForms\Admin\Education\EducationInterface;
/**
* Admin/EditPost Education feature.
*
* @since 1.8.1
*/
class EditPost implements EducationInterface {
/**
* Determine if the website has some forms.
*
* @since 1.8.1
*
* @var bool
*/
private $has_forms;
/**
* Indicate if edit post education is allowed to load.
*
* @since 1.8.1
*
* @return bool
*/
public function allow_load() {
if ( ! is_admin() ) {
return false;
}
if ( ! wpforms_current_user_can( 'view_forms' ) ) {
return false;
}
// Skip it if it's the Challenge flow.
if ( wpforms()->obj( 'challenge' )->is_form_embed_page() ) {
return false;
}
$form_embed_wizard = wpforms()->obj( 'form_embed_wizard' );
// Skip it if it's the Form Embed Wizard flow.
if ( $form_embed_wizard->is_form_embed_page( 'edit' ) && $form_embed_wizard->get_meta() ) {
return false;
}
$user_id = get_current_user_id();
$dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true );
return empty( $dismissed['edu-edit-post-notice'] );
}
/**
* Initialize.
*
* @since 1.8.1
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
$this->has_forms = (bool) wpforms()->obj( 'form' )->get(
'',
[
'numberposts' => 1,
'nopaging' => false,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
]
);
$this->hooks();
}
/**
* Add hooks.
*
* @since 1.8.1
*/
private function hooks() {
add_action( 'edit_form_after_title', [ $this, 'classic_editor_notice' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
}
/**
* Is gutenberg Editor.
*
* @since 1.8.1
*
* @return bool
*/
private function is_gutenberg_editor() {
return (bool) get_current_screen()->is_block_editor();
}
/**
* Enqueue styles.
*
* @since 1.8.1
*/
public function enqueue_styles() {
$min = wpforms_get_min_suffix();
wp_enqueue_style(
'wpforms-edit-post-education',
WPFORMS_PLUGIN_URL . "assets/css/admin/edit-post-education{$min}.css",
[],
WPFORMS_VERSION
);
}
/**
* Enqueue scripts.
*
* @since 1.8.1
*/
public function enqueue_scripts() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-edit-post-education',
WPFORMS_PLUGIN_URL . "assets/js/admin/education/edit-post.es5{$min}.js",
[ 'jquery', 'underscore' ],
WPFORMS_VERSION,
true
);
$strings = [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'education_nonce' => wp_create_nonce( 'wpforms-education' ),
];
if ( $this->is_gutenberg_editor() ) {
$strings = array_merge( $strings, $this->get_gutenberg_strings() );
}
wp_localize_script(
'wpforms-edit-post-education',
'wpforms_edit_post_education',
$strings
);
}
/**
* Get Gutenberg i18n strings.
*
* @since 1.8.1
*
* @return array
*/
private function get_gutenberg_strings() {
$strings = [
'is_gutenberg' => true,
'gutenberg_notice' => [
'template' => $this->get_gutenberg_notice_template(),
'button' => __( 'Get Started', 'wpforms-lite' ),
],
];
if ( ! $this->has_forms ) {
$strings['gutenberg_notice']['url'] = add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) );
return $strings;
}
$strings['gutenberg_guide'] = [
[
'image' => WPFORMS_PLUGIN_URL . '/assets/images/edit-post-education-page-1.png',
'title' => __( 'Easily add your contact form', 'wpforms-lite' ),
'content' => __( 'Oh hey, it looks like you\'re working on a contact page. Don\'t forget to embed your contact form. Click the plus icon above and search for WPForms.', 'wpforms-lite' ),
],
[
'image' => WPFORMS_PLUGIN_URL . '/assets/images/edit-post-education-page-2.png',
'title' => __( 'Embed your form', 'wpforms-lite' ),
'content' => __( 'Then click on the WPForms block to embed your desired contact form.', 'wpforms-lite' ),
],
];
return $strings;
}
/**
* Add notice to classic editor.
*
* @since 1.8.1
*
* @param WP_Post $post Add notice to classic editor.
*/
public function classic_editor_notice( $post ) {
$message = $this->has_forms
? __( 'Don\'t forget to embed your contact form. Simply click the Add Form button below.', 'wpforms-lite' )
: sprintf( /* translators: %1$s - link to create a new form. */
__( 'Did you know that with <a href="%1$s" target="_blank" rel="noopener noreferrer">WPForms</a>, you can create an easy-to-use contact form in a matter of minutes?', 'wpforms-lite' ),
esc_url( add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) ) )
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'education/admin/edit-post/classic-notice',
[
'message' => $message,
],
true
);
}
/**
* Get Gutenberg notice template.
*
* @since 1.8.1
*
* @return string
*/
private function get_gutenberg_notice_template() {
$message = $this->has_forms
? __( 'You\'ve already created a form, now add it to the page so your customers can get in touch.', 'wpforms-lite' )
: sprintf( /* translators: %1$s - link to create a new form. */
__( 'Did you know that with <a href="%1$s" target="_blank" rel="noopener noreferrer">WPForms</a>, you can create an easy-to-use contact form in a matter of minutes?', 'wpforms-lite' ),
esc_url( add_query_arg( 'page', 'wpforms-overview', admin_url( 'admin.php' ) ) )
);
return wpforms_render(
'education/admin/edit-post/notice',
[
'message' => $message,
],
true
);
}
}
@@ -0,0 +1,152 @@
<?php
namespace WPForms\Admin\Education\Admin\Settings;
use WPForms\Admin\Education\AddonsItemBase;
/**
* Admin/Settings/Geolocation Education feature for Lite and Pro.
*
* @since 1.6.6
*/
class Geolocation extends AddonsItemBase {
/**
* Slug.
*
* @since 1.6.6
*/
const SLUG = 'geolocation';
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
add_filter( 'wpforms_settings_defaults', [ $this, 'add_sections' ] );
}
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load() {
return wpforms_is_admin_page( 'settings', 'geolocation' );
}
/**
* Enqueues.
*
* @since 1.6.6
*/
public function enqueues() {
// Lity - lightbox for images.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
}
/**
* Preview of education features for customers with not enough permissions.
*
* @since 1.6.6
*
* @param array $settings Settings sections.
*
* @return array
*/
public function add_sections( $settings ) {
$addon = $this->addons->get_addon( 'geolocation' );
if (
empty( $addon ) ||
empty( $addon['status'] ) ||
empty( $addon['action'] )
) {
return $settings;
}
$settings[ self::SLUG ][ self::SLUG . '-page' ] = [
'id' => self::SLUG . '-page',
'content' => wpforms_render( 'education/admin/page', $this->template_data(), true ),
'type' => 'content',
'no_label' => true,
'class' => [ 'wpforms-education-container-page' ],
];
return $settings;
}
/**
* Get the template data.
*
* @since 1.8.6
*
* @return array
*/
private function template_data(): array {
$addon = $this->addons->get_addon( 'geolocation' );
$images_url = WPFORMS_PLUGIN_URL . 'assets/images/geolocation-education/';
$params = [
'features' => [
__( 'City', 'wpforms-lite' ),
__( 'Latitude/Longitude', 'wpforms-lite' ),
__( 'Google Places API', 'wpforms-lite' ),
__( 'Country', 'wpforms-lite' ),
__( 'Address Autocomplete', 'wpforms-lite' ),
__( 'Mapbox API', 'wpforms-lite' ),
__( 'Postal/Zip Code', 'wpforms-lite' ),
__( 'Embedded Map in Forms', 'wpforms-lite' ),
],
'images' => [
[
'url' => $images_url . 'entry-location.jpg',
'url2x' => $images_url . 'entry-location@2x.jpg',
'title' => __( 'Location Info in Entries', 'wpforms-lite' ),
],
[
'url' => $images_url . 'address-autocomplete.jpg',
'url2x' => $images_url . 'address-autocomplete@2x.jpg',
'title' => __( 'Address Autocomplete Field', 'wpforms-lite' ),
],
[
'url' => $images_url . 'smart-address-field.jpg',
'url2x' => $images_url . 'smart-address-field@2x.jpg',
'title' => __( 'Smart Address Field', 'wpforms-lite' ),
],
],
'utm_medium' => 'Settings - Geolocation',
'utm_content' => 'Geolocation Addon',
'heading_title' => __( 'Geolocation', 'wpforms-lite' ),
'heading_description' => sprintf(
'<p>%1$s</p>',
__( 'Do you want to learn more about visitors who fill out your online forms? Our geolocation addon allows you to collect and store your website visitors geolocation data along with their form submission. This insight can help you to be better informed and turn more leads into customers. Furthermore, add a smart address field that autocompletes using the Google Places API.', 'wpforms-lite' )
),
'badge' => __( 'Pro', 'wpforms-lite' ),
'features_description' => __( 'Powerful location-based insights and features…', 'wpforms-lite' ),
];
return array_merge( $params, $addon );
}
}
@@ -0,0 +1,67 @@
<?php
namespace WPForms\Admin\Education\Admin\Settings;
use \WPForms\Admin\Education\AddonsListBase;
/**
* Base class for Admin/Integrations feature for Lite and Pro.
*
* @since 1.6.6
*/
class Integrations extends AddonsListBase {
/**
* Template for rendering single addon item.
*
* @since 1.6.6
*
* @var string
*/
protected $single_addon_template = 'education/admin/settings/integrations-item';
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_action( 'wpforms_settings_providers', [ $this, 'filter_addons' ], 1 );
add_action( 'wpforms_settings_providers', [ $this, 'display_addons' ], 500 );
}
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load() {
return wpforms_is_admin_page( 'settings', 'integrations' );
}
/**
* Get addons for the Settings/Integrations tab.
*
* @since 1.6.6
*
* @return array Addons data.
*/
protected function get_addons() {
return $this->addons->get_by_path( 'settings_integrations.category', 'crm|email-marketing|integration' );
}
/**
* Ensure that we do not display activated addon items if those addons are not allowed according to the current license.
*
* @since 1.6.6
*/
public function filter_addons() {
$this->filter_not_allowed_addons( 'wpforms_settings_providers' );
}
}
@@ -0,0 +1,67 @@
<?php
namespace WPForms\Admin\Education\Admin\Settings;
use WPForms\Admin\Education\EducationInterface;
/**
* SMTP education notice.
*
* @since 1.8.1
*/
class SMTP implements EducationInterface {
/**
* Indicate if Education core is allowed to load.
*
* @since 1.8.1
*
* @return bool
*/
public function allow_load() {
if ( ! wpforms_can_install( 'plugin' ) || ! wpforms_can_activate( 'plugin' ) ) {
return false;
}
$user_id = get_current_user_id();
$dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true );
if ( ! empty( $dismissed['edu-smtp-notice'] ) ) {
return false;
}
$active_plugins = get_option( 'active_plugins', [] );
$allowed_plugins = [
'wp-mail-smtp/wp_mail_smtp.php',
'wp-mail-smtp-pro/wp_mail_smtp.php',
];
return ! array_intersect( $active_plugins, $allowed_plugins );
}
/**
* Init.
*
* @since 1.8.1
*/
public function init() {
}
/**
* Get notice template.
*
* @since 1.8.1
*
* @return string
*/
public function get_template() {
if ( ! $this->allow_load() ) {
return '';
}
return wpforms_render( 'education/admin/settings/smtp-notice' );
}
}
@@ -0,0 +1,156 @@
<?php
namespace WPForms\Admin\Education\Admin\Tools;
/**
* Entry Automation Education class.
*
* @since 1.9.6.1
*/
class EntryAutomation {
/**
* Education init.
*
* @since 1.9.6.1
*/
public function init(): void {
$this->hooks();
}
/**
* Load hooks.
*
* @since 1.9.6.1
*/
private function hooks(): void {
add_action( 'wpforms_admin_tools_views_entry_automation_display', [ $this, 'display' ] );
}
/**
* Get the template data.
*
* @since 1.9.6.1
*
* @return array
*/
private function get_template_data(): array {
$images_url = WPFORMS_PLUGIN_URL . 'assets/images/entry-automation/';
$utm_medium = 'Tools - Entry Automation';
$utm_content = 'Entry Automation Addon';
$addon = wpforms()->obj( 'addons' )->get_addon( 'entry-automation' );
$upgrade_link = $addon['action'] === 'upgrade'
? sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */
' <strong><a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-upgrade-link">%2$s</a></strong>',
esc_url( wpforms_admin_upgrade_link( $utm_medium, $utm_content ) ),
esc_html__( 'Upgrade to WPForms Elite', 'wpforms-lite' )
)
: '';
$params = [
'features' => [
__( 'Automated Task Scheduling', 'wpforms-lite' ),
__( 'Scheduled Exports', 'wpforms-lite' ),
__( 'Task Chaining', 'wpforms-lite' ),
__( 'Automated Deletions', 'wpforms-lite' ),
__( 'Enhanced Data Management', 'wpforms-lite' ),
__( 'Robust Failsafes', 'wpforms-lite' ),
],
'images' => [
[
'url' => $images_url . 'education.png',
'url2x' => $images_url . 'education.png',
'title' => '',
],
],
'utm_medium' => $utm_medium,
'utm_content' => $utm_content,
'upgrade_link_text' => esc_html__( 'Upgrade to WPForms Elite', 'wpforms-lite' ),
'heading_title' => __( 'Tired of manually exporting and deleting entries? Wish you could schedule these actions for optimal efficiency?', 'wpforms-lite' ),
/* translators: %1$s - WPForms.com Upgrade page URL. */
'heading_description' => '<p>' . esc_html__( 'Entry Automation introduces powerful, automated task chaining, allowing you to seamlessly schedule exports and deletions, ensuring your data is managed precisely how you need it. Chain multiple tasks together for complex workflows export to CSV, then automatically delete after a specified period, for example. We\'ve built robust failsafes to guarantee data integrity, so you can automate with confidence, knowing your valuable entries are always protected. Take back your time and let our addon handle the heavy lifting, keeping your WPForms data organized and secure, automatically.', 'wpforms-lite' ) . '</p>'
. '<p>' . wp_kses(
$upgrade_link,
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
'class' => [],
],
'strong' => [],
]
) . '</p>',
'features_description' => __( 'Powerful Automation Features', 'wpforms-lite' ),
];
return isset( $addon ) ? array_merge( $params, $addon ) : $params;
}
/**
* Check if the addon is active.
*
* @since 1.9.6.1
*
* @return bool
*/
private function is_addon_active(): bool {
/**
* Check if the addon is active.
*
* @since 1.9.6.1
*
* @param bool $is_active Whether the addon is active.
*/
return (bool) apply_filters(
'wpforms_admin_education_admin_tools_entry_automation_is_addon_active',
wpforms()->obj( 'addons' )->is_active( 'entry-automation' )
);
}
/**
* Display education content.
*
* @since 1.9.6.1
*/
public function display(): void {
// Display the education content only if the addon is not active.
if ( $this->is_addon_active() ) {
return;
}
$this->enqueue();
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render( 'education/admin/page', $this->get_template_data(), true );
}
/**
* Enqueue scripts and styles.
*
* @since 1.9.6.1
*/
private function enqueue(): void {
// Lity.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
}
}
@@ -0,0 +1,307 @@
<?php
namespace WPForms\Admin\Education\Builder;
use WPForms\Admin\Education\AddonsItemBase;
use WPForms\Admin\Education\Helpers;
use WPForms\Integrations\AI\Helpers as AIHelpers;
/**
* Builder/Calculations Education feature for Lite and Pro.
*
* @since 1.8.4.1
*/
class Calculations extends AddonsItemBase {
/**
* Support calculations in these field types.
*
* @since 1.8.4.1
*
* @var array
*/
public const ALLOWED_FIELD_TYPES = [ 'text', 'textarea', 'number', 'hidden', 'payment-single' ];
/**
* Field types that should display educational notice in the basic field options tab.
*
* @since 1.8.4.1
*
* @var array
*/
public const BASIC_OPTIONS_NOTICE_FIELD_TYPES = [ 'number', 'payment-single' ];
/**
* Indicate if the current Education feature is allowed to load.
*
* @since 1.8.4.1
*
* @return bool
*
* @noinspection PhpMissingReturnTypeInspection
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function allow_load() {
return wpforms_is_admin_page( 'builder' ) || wpforms_is_admin_ajax();
}
/**
* Hooks.
*
* @since 1.8.4.1
*
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function hooks() {
add_action( 'wpforms_field_options_bottom_basic-options', [ $this, 'basic_options' ], 20, 2 );
add_action( 'wpforms_field_options_bottom_advanced-options', [ $this, 'advanced_options' ], 20, 2 );
}
/**
* Display notice on basic options.
*
* @since 1.8.4.1
*
* @param array $field Field data.
* @param object $instance Builder instance.
*
* @noinspection HtmlUnknownTarget
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingParamTypeInspection
* @noinspection HtmlUnknownAnchorTarget
*/
public function basic_options( $field, $instance ) {
// Display notice in basic options only in numbers and payment-single fields.
if ( ! in_array( $field['type'], self::BASIC_OPTIONS_NOTICE_FIELD_TYPES, true ) ) {
return;
}
$dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true );
$form_id = $instance->form_id ?? 0;
$dismiss_section = "builder-form-$form_id-field-options-calculations-notice";
// Check whether it is dismissed.
if ( ! empty( $dismissed[ 'edu-' . $dismiss_section ] ) ) {
return;
}
// Display notice only if Calculations addon is released (available in the `addons.json` file).
$addon = $this->addons->get_addon( 'calculations' );
if ( ! $addon ) {
return;
}
if (
AIHelpers::is_disabled() ||
(
wpforms_version_compare(
$addon['version'] ?? '1.5.0',
'1.5.0',
'<='
)
)
) {
$this->print_standard_education( $dismiss_section );
return;
}
$badge = esc_html__( 'NEW FEATURE', 'wpforms-lite' );
$notice_header = esc_html__( 'AI Calculations Are Here!', 'wpforms-lite' );
$notice = sprintf(
wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */
__( 'Easily create advanced calculations with WPForms AI. Head over to the <a href="#advanced-tab">Advanced Tab</a> to get started or read <a href="%1$s" target="_blank" rel="noopener noreferrer">our documentation</a> to learn more.', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/generating-calculation-formulas-with-wpforms-ai/', 'Calculations Education', 'Calculations Documentation' ) )
);
printf(
'<div class="wpforms-alert-ai wpforms-alert wpforms-educational-alert wpforms-calculations wpforms-field-educational-alert wpforms-dismiss-container">
<span class="wpforms-badge wpforms-badge-sm wpforms-badge-block wpforms-badge-purple wpforms-badge-rounded">
%5$s
</span>
<button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button>
<h3>%4$s</h3>
<p>%3$s</p>
</div>',
esc_html__( 'Dismiss this notice.', 'wpforms-lite' ),
esc_attr( $dismiss_section ),
$notice, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$notice_header, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$badge // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Print standard education notice.
*
* @since 1.9.4
*
* @param string $dismiss_section Dismiss section.
*
* @noinspection HtmlUnknownAnchorTarget
* @noinspection HtmlUnknownTarget
*/
private function print_standard_education( string $dismiss_section ): void {
$notice = sprintf(
wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */
__( 'Easily perform calculations based on user input. Head over to the <a href="#advanced-tab">Advanced Tab</a> to get started or read <a href="%1$s" target="_blank" rel="noopener noreferrer">our documentation</a> to learn more.', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/calculations-addon/', 'Calculations Education', 'Calculations Documentation' ) )
);
printf(
'<div class="wpforms-alert-info wpforms-alert wpforms-educational-alert wpforms-calculations wpforms-field-educational-alert wpforms-dismiss-container">
<button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button>
<p>%3$s</p>
</div>',
esc_html__( 'Dismiss this notice.', 'wpforms-lite' ),
esc_attr( $dismiss_section ),
$notice // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Display advanced options.
*
* @since 1.8.4.1
*
* @param array $field Field data.
* @param object $instance Builder instance.
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingParamTypeInspection
*/
public function advanced_options( $field, $instance ) {
if ( ! in_array( $field['type'], self::ALLOWED_FIELD_TYPES, true ) ) {
return;
}
$addon = $this->addons->get_addon( 'calculations' );
if ( ! $this->is_edu_required_by_status( $addon ) ) {
return;
}
$row_args = $this->get_row_attributes( $addon );
$row_args['content'] = $instance->field_element(
'toggle',
$field,
$this->get_field_attributes( $addon ),
false
);
$instance->field_element( 'row', $field, $row_args );
}
/**
* Get row attributes.
*
* @since 1.8.4.1
*
* @param array $addon Addon data.
*
* @return array
*/
private function get_row_attributes( array $addon ): array {
$data = $this->prepare_field_action_data( $addon );
$default = [
'slug' => 'calculation_is_enabled',
];
if ( ! empty( $data ) ) {
return wp_parse_args( $data, $default );
}
return wp_parse_args(
[
'data' => [
'action' => 'upgrade',
'name' => esc_html__( 'Calculations', 'wpforms-lite' ),
'utm-content' => 'Enable Calculations',
'license' => $addon['license_level'],
],
'class' => 'education-modal',
],
$default
);
}
/**
* Get attributes for the Enable Calculation field.
*
* @since 1.8.4.1
*
* @param array $addon Addon data.
*
* @return array
*/
private function get_field_attributes( array $addon ): array {
$default = [
'slug' => 'calculation_is_enabled',
'value' => '0',
'desc' => esc_html__( 'Enable Calculation', 'wpforms-lite' ),
];
if ( $addon['plugin_allow'] ) {
return $default;
}
return wp_parse_args(
[
'desc' => sprintf(
'%1$s%2$s',
esc_html__( 'Enable Calculation', 'wpforms-lite' ),
Helpers::get_badge( $addon['license_level'], 'sm', 'inline', 'slate' )
),
'attrs' => [
'disabled' => 'disabled',
],
],
$default
);
}
/**
* Determine if we require displaying educational items according to the addon status.
*
* @since 1.8.4.1
*
* @param array $addon Addon data.
*
* @return bool
*/
private function is_edu_required_by_status( array $addon ): bool {
return ! (
empty( $addon ) ||
empty( $addon['action'] ) ||
empty( $addon['status'] ) || (
$addon['status'] === 'active' && $addon['action'] !== 'upgrade'
)
);
}
}
@@ -0,0 +1,192 @@
<?php
namespace WPForms\Admin\Education\Builder;
use \WPForms\Admin\Education\EducationInterface;
/**
* Builder/ReCaptcha Education feature.
*
* @since 1.6.6
*/
class Captcha implements EducationInterface {
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*/
public function allow_load() {
return wp_doing_ajax();
}
/**
* Init.
*
* @since 1.6.6
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
// Define hooks.
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_action( 'wp_ajax_wpforms_update_field_captcha', [ $this, 'captcha_field_callback' ] );
}
/**
* Targeting on hCaptcha/reCAPTCHA field button in the builder.
*
* @since 1.6.6
*/
public function captcha_field_callback() {
// Run a security check.
check_ajax_referer( 'wpforms-builder', 'nonce' );
// Check for form ID.
if ( empty( $_POST['id'] ) ) {
wp_send_json_error( esc_html__( 'No form ID found.', 'wpforms-lite' ) );
}
$form_id = absint( $_POST['id'] );
// Check for permissions.
if ( ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) {
wp_send_json_error( esc_html__( 'You do not have permission.', 'wpforms-lite' ) );
}
// Get an actual form data.
$form_data = wpforms()->obj( 'form' )->get( $form_id, [ 'content_only' => true ] );
// Check that CAPTCHA is configured in the settings.
$captcha_settings = wpforms_get_captcha_settings();
$captcha_name = $this->get_captcha_name( $captcha_settings );
if ( empty( $form_data ) || empty( $captcha_name ) ) {
wp_send_json_error( esc_html__( 'Something wrong. Please try again later.', 'wpforms-lite' ) );
}
// Prepare a result array.
$data = $this->get_captcha_result_mockup( $captcha_settings );
if ( empty( $captcha_settings['site_key'] ) || empty( $captcha_settings['secret_key'] ) ) {
// If CAPTCHA is not configured in the WPForms plugin settings.
$data['current'] = 'not_configured';
} elseif ( ! isset( $form_data['settings']['recaptcha'] ) || $form_data['settings']['recaptcha'] !== '1' ) {
// If CAPTCHA is configured in WPForms plugin settings, but wasn't set in form settings.
$data['current'] = 'configured_not_enabled';
} else {
// If CAPTCHA is configured in WPForms plugin and form settings.
$data['current'] = 'configured_enabled';
}
wp_send_json_success( $data );
}
/**
* Retrieve the CAPTCHA name.
*
* @since 1.6.6
*
* @param array $settings The CAPTCHA settings.
*
* @return string
*/
private function get_captcha_name( $settings ) {
if ( empty( $settings['provider'] ) ) {
return '';
}
if ( empty( $settings['site_key'] ) && empty( $settings['secret_key'] ) ) {
return esc_html__( 'CAPTCHA', 'wpforms-lite' );
}
if ( $settings['provider'] === 'hcaptcha' ) {
return esc_html__( 'hCaptcha', 'wpforms-lite' );
}
if ( $settings['provider'] === 'turnstile' ) {
return esc_html__( 'Cloudflare Turnstile', 'wpforms-lite' );
}
$recaptcha_names = [
'v2' => esc_html__( 'Google Checkbox v2 reCAPTCHA', 'wpforms-lite' ),
'invisible' => esc_html__( 'Google Invisible v2 reCAPTCHA', 'wpforms-lite' ),
'v3' => esc_html__( 'Google v3 reCAPTCHA', 'wpforms-lite' ),
];
return isset( $recaptcha_names[ $settings['recaptcha_type'] ] ) ? $recaptcha_names[ $settings['recaptcha_type'] ] : '';
}
/**
* Get CAPTCHA callback result mockup.
*
* @since 1.6.6
*
* @param array $settings The CAPTCHA settings.
*
* @return array
*/
private function get_captcha_result_mockup( $settings ) {
$captcha_name = $this->get_captcha_name( $settings );
if ( empty( $captcha_name ) ) {
wp_send_json_error( esc_html__( 'Something wrong. Please, try again later.', 'wpforms-lite' ) );
}
return [
'current' => false,
'cases' => [
'not_configured' => [
'title' => esc_html__( 'Heads up!', 'wpforms-lite' ),
'content' => sprintf(
wp_kses( /* translators: %1$s - CAPTCHA settings page URL, %2$s - WPForms.com doc URL. */
__( 'Please complete the setup in your <a href="%1$s" target="_blank">WPForms Settings</a>, and check out <a href="%2$s" target="_blank" rel="noopener noreferrer">our guide</a> to learn about available CAPTCHA solutions.', 'wpforms-lite' ),
[
'a' => [
'href' => true,
'rel' => true,
'target' => true,
],
]
),
esc_url( admin_url( 'admin.php?page=wpforms-settings&view=captcha' ) ),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setup-captcha-wpforms/', 'builder-modal', 'Captcha Documentation' ) )
),
],
'configured_not_enabled' => [
'title' => false,
/* translators: %s - CAPTCHA name. */
'content' => sprintf( esc_html__( '%s has been enabled for this form. Don\'t forget to save your form!', 'wpforms-lite' ), $captcha_name ),
],
'configured_enabled' => [
'title' => false,
/* translators: %s - CAPTCHA name. */
'content' => sprintf( esc_html__( 'Are you sure you want to disable %s for this form?', 'wpforms-lite' ), $captcha_name ),
'cancel' => true,
],
],
'provider' => $settings['provider'],
];
}
}
@@ -0,0 +1,75 @@
<?php
namespace WPForms\Admin\Education\Builder;
use WPForms\Admin\Education\AddonsItemBase;
use WPForms\Admin\Education\Fields as EducationFields;
/**
* Base class for Builder/Fields Education feature.
*
* @since 1.6.6
*/
abstract class Fields extends AddonsItemBase {
/**
* Instance of the Education\Fields class.
*
* @since 1.6.6
*
* @var EducationFields
*/
protected $fields;
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load(): bool {
return wp_doing_ajax() || wpforms_is_admin_page( 'builder' );
}
/**
* Init.
*
* @since 1.6.6
*/
public function init(): void {
parent::init();
// Store the instance of the Education\Fields class.
$this->fields = wpforms()->obj( 'education_fields' );
}
/**
* Print the form preview notice.
*
* @since 1.9.4
*
* @param array $texts Notice texts.
*/
protected function print_form_preview_notice( $texts ): void {
printf(
'<div class="wpforms-alert %1$s wpforms-alert-dismissible wpforms-pro-fields-notice wpforms-dismiss-container">
<div class="wpforms-alert-message">
<h3>%2$s</h3>
<p>%3$s</p>
</div>
<div class="wpforms-alert-buttons">
<button type="button" class="wpforms-dismiss-button" data-section="%4$s" title="%5$s" />
</div>
</div>',
esc_attr( $texts['class'] ),
esc_html( $texts['title'] ),
$texts['content'], // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
esc_html( $texts['dismiss_section'] ),
esc_attr__( 'Dismiss this notice', 'wpforms-lite' )
);
}
}
@@ -0,0 +1,154 @@
<?php
namespace WPForms\Admin\Education\Builder;
use WPForms\Admin\Education\AddonsItemBase;
use WPForms\Admin\Education\Helpers;
/**
* Builder/Geolocation Education feature for Lite and Pro.
*
* @since 1.6.6
*/
class Geolocation extends AddonsItemBase {
/**
* Indicate if the current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingReturnTypeInspection
*/
public function allow_load() {
return wpforms_is_admin_page( 'builder' ) || wp_doing_ajax();
}
/**
* Hooks.
*
* @since 1.6.6
*
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function hooks() {
add_action( 'wpforms_field_options_bottom_advanced-options', [ $this, 'geolocation_options' ], 10, 2 );
}
/**
* Display geolocation options.
*
* @since 1.6.6
*
* @param array $field Field data.
* @param object $instance Builder instance.
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingParamTypeInspection
*/
public function geolocation_options( $field, $instance ) {
if ( ! in_array( $field['type'], [ 'text', 'address' ], true ) ) {
return;
}
$addon = $this->addons->get_addon( 'geolocation' );
if (
empty( $addon ) ||
empty( $addon['action'] ) ||
empty( $addon['status'] ) || (
$addon['status'] === 'active' &&
$addon['action'] !== 'upgrade'
)
) {
return;
}
$row_args = $this->get_address_autocomplete_row_attributes( $addon );
$row_args['content'] = $instance->field_element(
'toggle',
$field,
$this->get_address_autocomplete_field_attributes( $addon ),
false
);
$instance->field_element( 'row', $field, $row_args );
}
/**
* Get attributes for address autocomplete row.
*
* @since 1.6.6
*
* @param array $addon Current addon information.
*
* @return array
*/
private function get_address_autocomplete_row_attributes( array $addon ): array {
$data = $this->prepare_field_action_data( $addon );
$default = [
'slug' => 'enable_address_autocomplete',
];
if ( ! empty( $data ) ) {
return wp_parse_args( $data, $default );
}
return wp_parse_args(
[
'data' => [
'action' => 'upgrade',
'name' => esc_html__( 'Address Autocomplete', 'wpforms-lite' ),
'utm-content' => 'Address Autocomplete',
'licence' => 'pro',
'message' => esc_html__( 'We\'re sorry, Address Autocomplete is part of the Geolocation Addon and not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ),
],
'class' => 'education-modal',
],
$default
);
}
/**
* Get attributes for address autocomplete field.
*
* @since 1.6.6
*
* @param array $addon Current addon information.
*
* @return array
*/
private function get_address_autocomplete_field_attributes( array $addon ): array {
$default = [
'slug' => 'enable_address_autocomplete',
'value' => '0',
'desc' => esc_html__( 'Enable Address Autocomplete', 'wpforms-lite' ),
];
if ( $addon['plugin_allow'] ) {
return $default;
}
return wp_parse_args(
[
'desc' => sprintf(
'%1$s%2$s',
esc_html__( 'Enable Address Autocomplete', 'wpforms-lite' ),
Helpers::get_badge( 'Pro', 'sm', 'inline', 'slate' )
),
'attrs' => [
'disabled' => 'disabled',
],
],
$default
);
}
}
@@ -0,0 +1,246 @@
<?php
namespace WPForms\Admin\Education\Builder;
use WPForms\Admin\Education\Helpers;
/**
* PDF educational popup.
*
* @since 1.9.7.3
*/
class PDF {
/**
* Addon slug.
*
* @since 1.9.7.3
*
* @var string
*/
private $slug = 'wpforms-pdf';
/**
* Addon data.
*
* @since 1.9.7.3
*
* @var array
*/
private $addon_data;
/**
* Initialize.
*
* @since 1.9.7.3
*/
public function init(): void {
$this->addon_data = $this->get_addon_data();
if ( ! $this->should_show_popup() ) {
return;
}
$this->hooks();
}
/**
* Should show popup.
*
* @since 1.9.7.3
*
* @return bool
*/
private function should_show_popup(): bool {
if ( ! wpforms_is_admin_page( 'builder' ) && ! wpforms_is_admin_ajax() ) {
return false;
}
if ( ! current_user_can( wpforms_get_capability_manage_options() ) ) {
return false;
}
$challenge = wpforms()->obj( 'challenge' );
if ( ! $challenge || $challenge->challenge_active() ) {
return false;
}
return $this->is_popup_visible();
}
/**
* Is popup visible.
*
* @since 1.9.7.3
*
* @return bool
*/
private function is_popup_visible(): bool {
$action = $this->addon_data['action'] ?? 'install';
if (
empty( $this->addon_data ) ||
( $action === 'install' && empty( $this->addon_data['url'] ) ) // The install action requires a valid URL.
) {
return false;
}
$meta = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true );
return empty( $meta['edu-builder-pdf'] );
}
/**
* Hooks.
*
* @since 1.9.7.3
*/
private function hooks(): void {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_filter( 'wpforms_builder_output_before_toolbar', [ $this, 'popup_html' ] );
}
/**
* Enqueue scripts.
*
* @since 1.9.7.3
*/
public function enqueue_scripts(): void {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-pdf-popup',
WPFORMS_PLUGIN_URL . "assets/js/admin/education/pdf$min.js",
[],
WPFORMS_VERSION,
true
);
}
/**
* Popup HTML.
*
* @since 1.9.7.3
*
* @param string|mixed $html HTML.
*
* @return string
* @noinspection HtmlUnknownTarget
*/
public function popup_html( $html ): string {
$html = (string) $html;
$popup = sprintf(
'<div id="wpforms-pdf-popup" class="wpforms-pdf-popup wpforms-hidden wpforms-dismiss-container" role="dialog" aria-modal="true" aria-labelledby="wpforms-pdf-popup-title" aria-describedby="wpforms-pdf-popup-description">
<div class="wpforms-pdf-popup-content">
<div class="icon">
<img src="%1$s" alt="PDF Icon">
</div>
<div class="close-popup wpforms-dismiss-button dashicons-no-alt" data-section="builder-pdf"></div>
<div class="badge">%2$s</div>
<h2 id="wpforms-pdf-popup-title">%3$s</h2>
<p id="wpforms-pdf-popup-description">%4$s</p>
%5$s
</div>
</div>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/pdf-education/pdf.svg' ),
__( 'NEW FEATURE', 'wpforms-lite' ),
__( 'PDF Addon', 'wpforms-lite' ),
__( 'Easily turn form entry data into beautifully designed PDFs and attach them to notifications.', 'wpforms-lite' ),
$this->get_button_html()
);
return $popup . $html;
}
/**
* Get button HTML.
*
* @since 1.9.7.3
*
* @return string
* @noinspection HtmlUnknownAttribute
*/
private function get_button_html(): string {
$addon = $this->addon_data;
$action = $addon['action'] ?? 'switch';
[ $button_label, $button_utm, $button_class, $button_attr ] = $this->get_button_data( $action, $addon );
return sprintf(
'<button class="wpforms-btn wpforms-btn-sm wpforms-btn-orange %1$s" data-action="%2$s" %4$s data-license="%5$s" data-utm-content="%6$s">%3$s</button>',
esc_attr( $button_class ),
esc_attr( $action ),
esc_html( $button_label ),
$button_attr,
esc_attr( $addon['license_level'] ?? 'pro' ),
esc_attr( $button_utm )
);
}
/**
* Get addon data.
*
* @since 1.9.7.3
*
* @return array
*/
private function get_addon_data(): array {
/**
* Filter the slug for the PDF educational popup.
*
* @since 1.9.7.3
*
* @param string $slug The slug for the PDF educational popup.
*/
$slug = apply_filters( 'wpforms_admin_education_builder_pdf_get_addon_data_slug', $this->slug );
$addons = Helpers::get_edu_addons();
return $addons[ $slug ] ?? [];
}
/**
* Get button data.
*
* @since 1.9.7.3
*
* @param string $action Action type (switch, upgrade, etc.).
* @param array $addon Addon data.
*
* @return array
*/
protected function get_button_data( string $action, array $addon ): array {
$button_label = $action === 'upgrade'
? esc_html__( 'Upgrade to Pro', 'wpforms-lite' )
: esc_html__( 'Try it Out', 'wpforms-lite' );
$button_utm = 'PDF Addon Pop-up';
$button_class = 'education-action-button';
$button_attr = '';
if ( $action === 'switch' ) {
$button_class = 'education-modal education-switch-button';
$button_attr = 'data-target="wpforms-pdf"';
} elseif ( $action !== 'upgrade' ) {
$button_class = 'education-modal';
$button_attr = sprintf(
'data-nonce="%1$s" data-path="%2$s" data-url="%3$s" data-message="" data-name="%4$s"',
esc_attr( wp_create_nonce( 'wpforms-admin' ) ),
$addon['path'] ?? '',
$addon['url'] ?? '',
esc_html__( 'WPForms PDF Addon', 'wpforms-lite' )
);
}
return [ $button_label, $button_utm, $button_class, $button_attr ];
}
}
@@ -0,0 +1,69 @@
<?php
namespace WPForms\Admin\Education\Builder;
use \WPForms\Admin\Education\AddonsListBase;
/**
* Base class for Builder/Settings, Builder/Providers, Builder/Payments Education features.
*
* @since 1.6.6
*/
abstract class Panel extends AddonsListBase {
/**
* Panel slug. Should be redefined in the real Builder/Panel class.
*
* @since 1.6.6
*
* @return string
**/
abstract protected function get_name();
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load() {
// Load only in the Form Builder.
return wpforms_is_admin_page( 'builder' ) && ! empty( $this->get_name() );
}
/**
* Get addons for the current panel.
*
* @since 1.6.6
*/
protected function get_addons() {
return $this->addons->get_by_path( 'form_builder.category', $this->get_name() );
}
/**
* Template name for rendering single addon item.
*
* @since 1.6.6
*
* @return string
*/
protected function get_single_addon_template() {
return 'education/builder/' . $this->get_name() . '-item';
}
/**
* Display addons.
*
* @since 1.6.6
*/
public function display_addons() {
$this->single_addon_template = $this->get_single_addon_template();
parent::display_addons();
}
}
@@ -0,0 +1,70 @@
<?php
namespace WPForms\Admin\Education\Builder;
use \WPForms\Admin\Education;
/**
* Builder/Payments Education feature.
*
* @since 1.6.6
*/
class Payments extends Education\Builder\Panel {
/**
* Panel slug.
*
* @since 1.6.6
*
* @return string
**/
protected function get_name() {
return 'payments';
}
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_action( 'wpforms_payments_panel_sidebar', [ $this, 'filter_addons' ], 1 );
add_action( 'wpforms_payments_panel_sidebar', [ $this, 'display_addons' ], 500 );
}
/**
* Get addons for the Payments panel.
*
* @since 1.7.7.2
*
* @return array
*/
protected function get_addons() {
return $this->addons->get_by_path( 'form_builder.category', $this->get_name() );
}
/**
* Template name for rendering single addon item.
*
* @since 1.6.6
*
* @return string
*/
protected function get_single_addon_template() {
return 'education/builder/providers-item';
}
/**
* Ensure that we do not display activated addon items if those addons are not allowed according to the current license.
*
* @since 1.6.6
*/
public function filter_addons() {
$this->filter_not_allowed_addons( 'wpforms_payments_panel_sidebar' );
}
}
@@ -0,0 +1,71 @@
<?php
namespace WPForms\Admin\Education\Builder;
use \WPForms\Admin\Education;
/**
* Builder/Providers Education feature.
*
* @since 1.6.6
*/
class Providers extends Education\Builder\Panel {
/**
* Panel slug.
*
* @since 1.6.6
*
* @return string
**/
protected function get_name() {
return 'providers';
}
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_action( 'wpforms_providers_panel_sidebar', [ $this, 'filter_addons' ], 1 );
add_action( 'wpforms_providers_panel_sidebar', [ $this, 'display_addons' ], 500 );
}
/**
* Ensure that we do not display activated addon items if those addons are not allowed according to the current license.
*
* @since 1.6.6
*/
public function filter_addons() {
$this->filter_not_allowed_addons( 'wpforms_providers_panel_sidebar' );
}
/**
* Get addons for the Marketing panel.
*
* @since 1.7.7.2
*/
protected function get_addons() {
$addons = parent::get_addons();
/**
* Google Sheets uses Providers API. All providers are automatically
* added to the Marketing tab in the builder. We don't need the addon
* on the Marketing tab because the addon is already added to
* the builder's Settings tab.
*/
foreach ( $addons as $key => $addon ) {
if ( isset( $addon['slug'] ) && $addon['slug'] === 'wpforms-google-sheets' ) {
unset( $addons[ $key ] );
break;
}
}
return $addons;
}
}
@@ -0,0 +1,214 @@
<?php
namespace WPForms\Admin\Education\Builder;
use WPForms\Admin\Education\AddonsItemBase;
use WPForms\Admin\Education\Helpers;
/**
* Builder/Quiz Education feature for Lite and Pro.
*
* @since 1.9.9
*/
class Quiz extends AddonsItemBase {
/**
* Indicate if the current Education feature is allowed to load.
*
* @since 1.9.9
*
* @return bool
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingReturnTypeInspection
*/
public function allow_load() {
return wpforms_is_admin_page( 'builder' ) || wp_doing_ajax();
}
/**
* Hooks.
*
* @since 1.9.9
*
* @noinspection ReturnTypeCanBeDeclaredInspection
*/
public function hooks() {
add_action( 'wpforms_field_options_before_description', [ $this, 'quiz_fields' ], 10, 2 );
}
/**
* Display the Enable Quiz option.
*
* @since 1.9.9
*
* @param array $field Field data.
* @param object $instance Builder instance.
*
* @noinspection ReturnTypeCanBeDeclaredInspection
* @noinspection PhpMissingParamTypeInspection
* @noinspection HtmlUnknownTarget
*/
public function quiz_fields( $field, $instance ) {
if ( ! in_array( $field['type'], [ 'radio', 'checkbox', 'select' ], true ) ) {
return;
}
$addon = $this->addons->get_addon( 'quiz' );
if (
empty( $addon ) ||
empty( $addon['action'] ) ||
empty( $addon['status'] ) || (
$addon['status'] === 'active' &&
$addon['action'] !== 'upgrade'
)
) {
return;
}
$form_id = ! empty( $instance->form_id ) ? (int) $instance->form_id : 0;
$row_args = $this->get_enable_quiz_row_attributes( $addon, $form_id );
$row_args['content'] = $instance->field_element(
'toggle',
$field,
$this->get_enable_quiz_field_attributes( $addon ),
false
);
$instance->field_element(
'row',
$field,
$row_args
);
$dismissed = get_user_meta( get_current_user_id(), 'wpforms_dismissed', true );
$dismiss_section = "builder-form-$form_id-field-options-quiz-notice";
// Check whether it is dismissed.
if ( ! empty( $dismissed[ 'edu-' . $dismiss_section ] ) ) {
return;
}
$badge = esc_html__( 'NEW FEATURE', 'wpforms-lite' );
$notice_header = esc_html__( 'Turn Your Form Into a Quiz', 'wpforms-lite' );
$notice = sprintf(
wp_kses( /* translators: %1$s - link to the WPForms.com doc article. */
__( 'Easily create interactive quizzes. Add true or false, multiple choice, or checkbox questions. Set correct answers and automatically score submissions. <a href="%1$s" target="_blank" rel="noopener noreferrer">Learn more about the Quiz Addon</a>', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/quiz-addon/', 'Quiz Education', 'Quiz Documentation' ) )
);
printf(
'<div class="wpforms-alert wpforms-alert-info wpforms-educational-alert wpforms-field-educational-alert wpforms-dismiss-container">
<span class="wpforms-badge wpforms-badge-sm wpforms-badge-block wpforms-badge-green wpforms-badge-rounded">
%5$s
</span>
<button type="button" class="wpforms-dismiss-button" title="%1$s" data-section="%2$s"></button>
<h3>%4$s</h3>
<p>%3$s</p>
</div>',
esc_html__( 'Dismiss this notice.', 'wpforms-lite' ),
esc_attr( $dismiss_section ),
$notice, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$notice_header, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$badge // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Get attributes for the `Enable Quiz` field option row.
*
* @since 1.9.9
*
* @param array $addon Current addon information.
* @param int $form_id Form ID.
*
* @return array
*/
private function get_enable_quiz_row_attributes( array $addon, int $form_id ): array {
$data = $this->prepare_field_action_data( $addon );
$default = [
'slug' => 'enable_quiz',
];
if ( ! empty( $data ) ) {
$data = wp_parse_args( $data, $default );
$data['data']['redirect-url'] = add_query_arg(
[
'page' => 'wpforms-builder',
'view' => 'settings',
'form_id' => $form_id,
'section' => 'quiz',
],
admin_url( 'admin.php' )
);
return $data;
}
return wp_parse_args(
[
'data' => [
'action' => 'upgrade',
'name' => esc_html__( 'Quiz Addon', 'wpforms-lite' ),
'utm-content' => 'Quiz Addon',
'licence' => 'pro',
'message' => esc_html__( 'We\'re sorry, Enable Quiz is part of the Quiz Addon and not available on your plan. Please upgrade to the PRO plan to unlock all these awesome features.', 'wpforms-lite' ),
],
'class' => 'education-modal',
],
$default
);
}
/**
* Get attributes for the `Enable Quiz` field option.
*
* @since 1.9.9
*
* @param array $addon Current addon information.
*
* @return array
*/
private function get_enable_quiz_field_attributes( array $addon ): array {
$default = [
'slug' => 'enable_quiz',
'value' => '0',
'desc' => esc_html__( 'Include in Quiz Scoring', 'wpforms-lite' ),
];
if ( $addon['plugin_allow'] ) {
return $default;
}
return wp_parse_args(
[
'desc' => sprintf(
'%1$s%2$s',
esc_html__( 'Include in Quiz Scoring', 'wpforms-lite' ),
Helpers::get_badge( 'Pro', 'sm', 'inline', 'slate' )
),
'attrs' => [
'disabled' => 'disabled',
],
],
$default
);
}
}
@@ -0,0 +1,69 @@
<?php
namespace WPForms\Admin\Education\Builder;
use \WPForms\Admin\Education;
/**
* Builder/Settings Education feature.
*
* @since 1.6.6
*/
class Settings extends Education\Builder\Panel {
/**
* Panel slug.
*
* @since 1.6.6
*
* @return string
**/
protected function get_name() {
return 'settings';
}
/**
* Hooks.
*
* @since 1.6.6
*/
public function hooks() {
add_filter( 'wpforms_builder_settings_sections', [ $this, 'filter_addons' ], 1 );
add_action( 'wpforms_builder_after_panel_sidebar', [ $this, 'display' ], 100, 2 );
}
/**
* Display settings addons.
*
* @since 1.6.6
*
* @param object $form Current form.
* @param string $panel Panel slug.
*/
public function display( $form, $panel ) {
if ( empty( $form ) || $this->get_name() !== $panel ) {
return;
}
$this->display_addons();
}
/**
* Ensure that we do not display activated addon items if those addons are not allowed according to the current license.
*
* @since 1.6.6
*
* @param array $sections Settings sections.
*
* @return array
*/
public function filter_addons( $sections ) {
$this->filter_not_allowed_addons( 'wpforms_builder_settings_sections' );
return $sections;
}
}
@@ -0,0 +1,143 @@
<?php
namespace WPForms\Admin\Education;
/**
* Education core.
*
* @since 1.6.6
*/
class Core {
use StringsTrait;
/**
* Indicate if Education core is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load(): bool {
return wp_doing_ajax() || wpforms_is_admin_page() || wpforms_is_admin_page( 'builder' );
}
/**
* Init.
*
* @since 1.6.6
*/
public function init() {
// Only proceed if allowed.
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.6
*/
protected function hooks() {
if ( wp_doing_ajax() ) {
add_action( 'wp_ajax_wpforms_education_dismiss', [ $this, 'ajax_dismiss' ] );
return;
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
}
/**
* Load enqueues.
*
* @since 1.6.6
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-admin-education-core',
WPFORMS_PLUGIN_URL . "assets/js/admin/education/core{$min}.js",
[ 'jquery', 'jquery-confirm' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-admin-education-core',
'wpforms_education',
$this->get_js_strings()
);
}
/**
* Ajax handler for the education dismisses buttons.
*
* @since 1.6.6
*/
public function ajax_dismiss() {
// Run a security check.
check_ajax_referer( 'wpforms-education', 'nonce' );
// Section is the identifier of the education feature.
// For example, in Builder/DidYouKnow feature used 'builder-did-you-know-notifications'
// and 'builder-did-you-know-confirmations'.
$section = ! empty( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
if ( empty( $section ) ) {
wp_send_json_error(
[ 'error' => esc_html__( 'Please specify a section.', 'wpforms-lite' ) ]
);
}
// Check for permissions.
if ( ! $this->current_user_can() ) {
wp_send_json_error(
[ 'error' => esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) ]
);
}
$user_id = get_current_user_id();
$dismissed = get_user_meta( $user_id, 'wpforms_dismissed', true );
if ( empty( $dismissed ) ) {
$dismissed = [];
}
$dismissed[ 'edu-' . $section ] = time();
update_user_meta( $user_id, 'wpforms_dismissed', $dismissed );
wp_send_json_success();
}
/**
* Whether the current user can perform an action.
*
* @since 1.8.0
*
* @return bool
*/
private function current_user_can(): bool {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$page = ! empty( $_POST['page'] ) ? sanitize_key( $_POST['page'] ) : '';
// key is the same as $current_screen->id and the JS global 'pagenow', value - capability name(s).
$caps = [
'toplevel_page_wpforms-overview' => [ 'view_forms' ],
'wpforms_page_wpforms-builder' => [ 'edit_forms' ],
'wpforms_page_wpforms-entries' => [ 'view_entries' ],
];
return isset( $caps[ $page ] ) ? wpforms_current_user_can( $caps[ $page ] ) : wpforms_current_user_can();
}
}
@@ -0,0 +1,27 @@
<?php
namespace WPForms\Admin\Education;
/**
* Interface EducationInterface defines required methods for Education features to work properly.
*
* @since 1.6.6
*/
interface EducationInterface {
/**
* Indicate if current Education feature is allowed to load.
*
* @since 1.6.6
*
* @return bool
*/
public function allow_load();
/**
* Init.
*
* @since 1.6.6
*/
public function init();
}
@@ -0,0 +1,415 @@
<?php
namespace WPForms\Admin\Education;
/**
* Fields data holder.
*
* @since 1.6.6
*/
class Fields {
/**
* All fields data.
*
* @since 1.6.6
*
* @var array
*/
protected $fields;
/**
* All fields data.
*
* @since 1.6.6
*
* @return array All possible fields.
*/
private function get_all(): array {
if ( ! empty( $this->fields ) ) {
return $this->fields;
}
$this->fields = [
[
'icon' => 'fa-phone',
'name' => esc_html__( 'Phone', 'wpforms-lite' ),
'name_en' => 'Phone',
'type' => 'phone',
'group' => 'fancy',
'order' => '50',
],
[
'icon' => 'fa-map-marker',
'name' => esc_html__( 'Address', 'wpforms-lite' ),
'name_en' => 'Address',
'type' => 'address',
'group' => 'fancy',
'order' => '70',
],
[
'icon' => 'fa-map-location-dot',
'name' => esc_html__( 'Map', 'wpforms-lite' ),
'name_en' => 'Map',
'type' => 'Map',
'group' => 'fancy',
'addon' => 'wpforms-geolocation',
'order' => '75',
],
[
'icon' => 'fa-calendar-o',
'name' => esc_html__( 'Date / Time', 'wpforms-lite' ),
'name_en' => 'Date / Time',
'type' => 'date-time',
'group' => 'fancy',
'order' => '80',
],
[
'icon' => 'fa-link',
'name' => esc_html__( 'Website / URL', 'wpforms-lite' ),
'name_en' => 'Website / URL',
'type' => 'url',
'group' => 'fancy',
'order' => '90',
],
[
'icon' => 'fa-upload',
'name' => esc_html__( 'File Upload', 'wpforms-lite' ),
'name_en' => 'File Upload',
'type' => 'file-upload',
'group' => 'fancy',
'order' => '100',
],
[
'icon' => 'fa-camera',
'name' => esc_html__( 'Camera', 'wpforms-lite' ),
'name_en' => 'Camera',
'type' => 'camera',
'group' => 'fancy',
'order' => '105',
],
[
'icon' => 'fa-lock',
'name' => esc_html__( 'Password', 'wpforms-lite' ),
'name_en' => 'Password',
'type' => 'password',
'group' => 'fancy',
'order' => '95',
],
[
'icon' => 'fa-columns',
'name' => esc_html__( 'Layout', 'wpforms-lite' ),
'name_en' => 'Layout',
'type' => 'layout',
'group' => 'fancy',
'order' => '140',
],
[
'icon' => 'fa-list',
'name' => esc_html__( 'Repeater', 'wpforms-lite' ),
'name_en' => 'Repeater',
'type' => 'repeater',
'group' => 'fancy',
'order' => '150',
],
[
'icon' => 'fa-files-o',
'name' => esc_html__( 'Page Break', 'wpforms-lite' ),
'name_en' => 'Page Break',
'type' => 'pagebreak',
'group' => 'fancy',
'order' => '160',
],
[
'icon' => 'fa-arrows-h',
'name' => esc_html__( 'Section Divider', 'wpforms-lite' ),
'name_en' => 'Section Divider',
'type' => 'divider',
'group' => 'fancy',
'order' => '170',
],
[
'icon' => 'fa-pencil-square-o',
'name' => esc_html__( 'Rich Text', 'wpforms-lite' ),
'name_en' => 'Rich Text',
'type' => 'richtext',
'group' => 'fancy',
'order' => '170',
],
[
'icon' => 'fa-file-image-o',
'name' => esc_html__( 'Content', 'wpforms-lite' ),
'name_en' => 'Content',
'type' => 'content',
'group' => 'fancy',
'order' => '180',
],
[
'icon' => 'fa-code',
'name' => esc_html__( 'HTML', 'wpforms-lite' ),
'name_en' => 'HTML',
'type' => 'html',
'group' => 'fancy',
'order' => '185',
],
[
'icon' => 'fa-file-text-o',
'name' => esc_html__( 'Entry Preview', 'wpforms-lite' ),
'name_en' => 'Entry Preview',
'type' => 'entry-preview',
'group' => 'fancy',
'order' => '190',
],
[
'icon' => 'fa-star',
'name' => esc_html__( 'Rating', 'wpforms-lite' ),
'name_en' => 'Rating',
'type' => 'rating',
'group' => 'fancy',
'order' => '310',
],
[
'icon' => 'fa-eye-slash',
'name' => esc_html__( 'Hidden Field', 'wpforms-lite' ),
'name_en' => 'Hidden Field',
'type' => 'hidden',
'group' => 'fancy',
'order' => '98',
],
[
'icon' => 'fa-question-circle',
'name' => esc_html__( 'Custom Captcha', 'wpforms-lite' ),
'keywords' => esc_html__( 'spam, math, maths, question', 'wpforms-lite' ),
'name_en' => 'Custom Captcha',
'type' => 'captcha',
'group' => 'fancy',
'addon' => 'wpforms-captcha',
'order' => '300',
],
[
'icon' => 'fa-pencil',
'name' => esc_html__( 'Signature', 'wpforms-lite' ),
'keywords' => esc_html__( 'user, e-signature', 'wpforms-lite' ),
'name_en' => 'Signature',
'type' => 'signature',
'group' => 'fancy',
'addon' => 'wpforms-signatures',
'order' => '200',
],
[
'icon' => 'fa-ellipsis-h',
'name' => esc_html__( 'Likert Scale', 'wpforms-lite' ),
'keywords' => esc_html__( 'survey, rating scale', 'wpforms-lite' ),
'name_en' => 'Likert Scale',
'type' => 'likert_scale',
'group' => 'fancy',
'addon' => 'wpforms-surveys-polls',
'order' => '400',
],
[
'icon' => 'fa-tachometer',
'name' => esc_html__( 'Net Promoter Score', 'wpforms-lite' ),
'keywords' => esc_html__( 'survey, nps', 'wpforms-lite' ),
'name_en' => 'Net Promoter Score',
'type' => 'net_promoter_score',
'group' => 'fancy',
'addon' => 'wpforms-surveys-polls',
'order' => '410',
],
[
'icon' => 'fa-credit-card',
'name' => esc_html__( 'Authorize.Net', 'wpforms-lite' ),
'keywords' => esc_html__( 'store, ecommerce, credit card, pay, payment, debit card', 'wpforms-lite' ),
'name_en' => 'Authorize.Net',
'type' => 'authorize_net',
'group' => 'payment',
'addon' => 'wpforms-authorize-net',
'order' => '95',
],
[
'icon' => 'fa-ticket',
'name' => esc_html__( 'Coupon', 'wpforms-lite' ),
'keywords' => esc_html__( 'discount, sale', 'wpforms-lite' ),
'name_en' => 'Coupon',
'type' => 'payment-coupon',
'group' => 'payment',
'addon' => 'wpforms-coupons',
'order' => '100',
],
];
$captcha = $this->get_captcha();
if ( ! empty( $captcha ) ) {
$this->fields[] = $captcha;
}
return $this->fields;
}
/**
* Get Captcha field data.
*
* @since 1.6.6
*
* @return array Captcha field data.
*/
private function get_captcha(): array {
$captcha_settings = wpforms_get_captcha_settings();
if ( empty( $captcha_settings['provider'] ) ) {
return [];
}
$captcha = [
'hcaptcha' => [
'name' => 'hCaptcha',
'icon' => 'fa-question-circle-o',
],
'recaptcha' => [
'name' => 'reCAPTCHA',
'icon' => 'fa-google',
],
'turnstile' => [
'name' => 'Turnstile',
'icon' => 'fa-question-circle-o',
],
];
if ( ! empty( $captcha_settings['site_key'] ) || ! empty( $captcha_settings['secret_key'] ) ) {
$captcha_name = $captcha[ $captcha_settings['provider'] ]['name'];
$captcha_icon = $captcha[ $captcha_settings['provider'] ]['icon'];
} else {
$captcha_name = 'CAPTCHA';
$captcha_icon = 'fa-question-circle-o';
}
return [
'icon' => $captcha_icon,
'name' => $captcha_name,
'name_en' => $captcha_name,
'keywords' => esc_html__( 'captcha, spam, antispam', 'wpforms-lite' ),
'type' => 'captcha_' . $captcha_settings['provider'],
'group' => 'standard',
'order' => 180,
'class' => 'not-draggable',
];
}
/**
* Get filtered fields data.
*
* Usage:
* get_filtered( [ 'group' => 'payment' ] ) - fields from the 'payment' group.
* get_filtered( [ 'addon' => 'surveys-polls' ] ) - fields of the addon 'surveys-polls'.
* get_filtered( [ 'type' => 'payment-total' ] ) - field 'payment-total'.
*
* @since 1.6.6
*
* @param array $args Arguments array.
*
* @return array Fields data filtered according to given arguments.
*/
private function get_filtered( array $args = [] ): array {
$default_args = [
'group' => '',
'addon' => '',
'type' => '',
];
$args = array_filter( wp_parse_args( $args, $default_args ) );
$fields = $this->get_all();
$filtered_fields = [];
foreach ( $args as $prop => $prop_val ) {
foreach ( $fields as $field ) {
if ( ! empty( $field[ $prop ] ) && $field[ $prop ] === $prop_val ) {
$filtered_fields[] = $field;
}
}
}
return $filtered_fields;
}
/**
* Get fields by group.
*
* @since 1.6.6
*
* @param string $group Fields group (standard, fancy or payment).
*
* @return array.
*/
public function get_by_group( string $group ): array {
return $this->get_filtered( [ 'group' => $group ] );
}
/**
* Get fields by addon.
*
* @since 1.6.6
*
* @param string $addon Addon slug.
*
* @return array.
*/
public function get_by_addon( string $addon ): array {
return $this->get_filtered( [ 'addon' => $addon ] );
}
/**
* Get field by type.
*
* @since 1.6.6
*
* @param string $type Field type.
*
* @return array Single field data. Empty array if field is not available.
*/
public function get_field( string $type ): array {
$fields = $this->get_filtered( [ 'type' => $type ] );
return ! empty( $fields[0] ) ? $fields[0] : [];
}
/**
* Set key value of each field (conditionally).
*
* @since 1.6.6
*
* @param array $fields Fields data.
* @param string $key Key.
* @param string $value Value.
* @param string $condition Condition.
*
* @return array Updated field data.
*/
public function set_values( array $fields, string $key, string $value, string $condition ): array {
if ( empty( $fields ) || empty( $key ) ) {
return $fields;
}
foreach ( $fields as $f => $field ) {
switch ( $condition ) {
case 'empty':
$fields[ $f ][ $key ] = empty( $field[ $key ] ) ? $value : $field[ $key ];
break;
default:
$fields[ $f ][ $key ] = $value;
}
}
return $fields;
}
}
@@ -0,0 +1,172 @@
<?php
namespace WPForms\Admin\Education;
/**
* Helpers class.
*
* @since 1.8.5
*/
class Helpers {
/**
* Get badge HTML.
*
* @since 1.8.5
* @since 1.8.6 Added `$icon` parameter.
*
* @param string $text Badge text.
* @param string $size Badge size.
* @param string $position Badge position.
* @param string $color Badge color.
* @param string $shape Badge shape.
* @param string $icon Badge icon name in Font Awesome "format", e.g. `fa-check`, defaults to empty string.
*
* @return string
*/
public static function get_badge(
string $text,
string $size = 'sm',
string $position = 'inline',
string $color = 'titanium',
string $shape = 'rounded',
string $icon = ''
): string {
if ( ! empty( $icon ) ) {
$icon = self::get_inline_icon( $icon );
}
return sprintf(
'<span class="wpforms-badge wpforms-badge-%1$s wpforms-badge-%2$s wpforms-badge-%3$s wpforms-badge-%4$s">%5$s%6$s</span>',
esc_attr( $size ),
esc_attr( $position ),
esc_attr( $color ),
esc_attr( $shape ),
wp_kses(
$icon,
[
'i' => [
'class' => [],
'aria-hidden' => [],
],
]
),
esc_html( $text )
);
}
/**
* Print badge HTML.
*
* @since 1.8.5
* @since 1.8.6 Added `$icon` parameter.
*
* @param string $text Badge text.
* @param string $size Badge size.
* @param string $position Badge position.
* @param string $color Badge color.
* @param string $shape Badge shape.
* @param string $icon Badge icon name in Font Awesome "format", e.g. `fa-check`, defaults to empty string.
*/
public static function print_badge(
string $text,
string $size = 'sm',
string $position = 'inline',
string $color = 'titanium',
string $shape = 'rounded',
string $icon = ''
) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo self::get_badge( $text, $size, $position, $color, $shape, $icon );
}
/**
* Get addon badge HTML.
*
* @since 1.8.9
*
* @param array $addon Addon data.
*
* @return string
*/
public static function get_addon_badge( array $addon ): string {
// List of possible badges.
$badges = [
'recommended' => [
'text' => esc_html__( 'Recommended', 'wpforms-lite' ),
'color' => 'green',
'icon' => 'fa-star',
],
'new' => [
'text' => esc_html__( 'New', 'wpforms-lite' ),
'color' => 'blue',
],
'featured' => [
'text' => esc_html__( 'Featured', 'wpforms-lite' ),
'color' => 'orange',
],
];
$badge = [];
// Get first badge that exists.
foreach ( $badges as $key => $value ) {
if ( ! empty( $addon[ $key ] ) ) {
$badge = $value;
break;
}
}
if ( empty( $badge ) ) {
return '';
}
return self::get_badge( $badge['text'], 'sm', 'inline', $badge['color'], 'rounded', $badge['icon'] ?? '' );
}
/**
* Get inline icon HTML.
*
* @since 1.8.6
*
* @param string $name Font Awesome icon name, e.g. `fa-check`.
*
* @return string HTML markup for the icon element.
*/
public static function get_inline_icon( string $name ): string {
return sprintf( '<i class="fa %1$s" aria-hidden="true"></i>', esc_attr( $name ) );
}
/**
* Get available education addons.
*
* @since 1.9.4
*
* @return array
*/
public static function get_edu_addons(): array {
static $addons = null;
if ( $addons !== null ) {
return $addons;
}
$addons_obj = wpforms()->obj( 'addons' );
if ( ! $addons_obj ) {
$addons = [];
return $addons;
}
$addons = $addons_obj->get_available();
return $addons;
}
}
@@ -0,0 +1,157 @@
<?php
namespace WPForms\Admin\Education\Pointers;
use WPForms\Integrations\Stripe;
use WPForms\Admin\Payments\Views\Overview\Page as PaymentsPage;
use WPForms\Migrations\Base as MigrationsBase;
/**
* Education class for handling Payments education pointer functionality.
*
* This class extends the abstract Pointers class and provides functionality
* specific to the Payments feature in WPForms.
*
* @since 1.8.8
*/
class Payment extends Pointer {
/**
* Unique ID for the pointer.
*
* @since 1.8.8
*
* @var string
*/
protected $pointer_id = 'admin_menu_payments';
/**
* Selector for the pointer.
*
* @since 1.8.8
*
* @var string
*/
protected $selector = '[href$="-payments"]';
/**
* Make sure that the pointer is visible across other dashboard pages.
*
* @since 1.8.8
*
* @var bool
*/
protected $top_level_visible = true;
/**
* Determine if the Payments feature pointer is allowed to load.
*
* Checks various conditions to determine if the Payments feature pointer
* should be allowed to load for the current user.
*
* @since 1.8.8
*
* @return bool
*/
protected function allow_load(): bool {
// Bail early if the user doesn't have a Lite, Basic, or Plus license.
if ( ! in_array( $this->get_license_type(), [ 'lite', 'basic', 'plus' ], true ) ) {
return false;
}
// Bail early if it has been less than 90 days since activation or the installation wasn't upgraded.
if (
! get_option( MigrationsBase::PREVIOUS_CORE_VERSION_OPTION_NAME ) ||
wpforms_get_activated_timestamp() > ( time() - 90 * DAY_IN_SECONDS )
) {
return false;
}
// Bail early if a Stripe account is connected.
if ( Stripe\Helpers::has_stripe_keys() ) {
return false;
}
// Bail early if the user doesn't have the capability to manage options.
if ( ! wpforms_current_user_can() ) {
return false;
}
// Bail early if there are no published forms.
$forms_obj = wpforms()->obj( 'form' );
return $forms_obj && $forms_obj->forms_exist();
}
/**
* Enqueue assets for the pointer.
*
* @since 1.8.8
*/
public function enqueue_assets() {
// Enqueue the pointer static assets.
parent::enqueue_assets();
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-education-pointers-payment',
WPFORMS_PLUGIN_URL . "assets/js/admin/education/pointers/payment{$min}.js",
[ 'wp-pointer' ],
WPFORMS_VERSION,
true
);
$admin_l10n = [
'pointer' => sanitize_key( $this->pointer_id ),
'nonce' => sanitize_text_field( $this->get_nonce_token() ),
];
wp_localize_script(
'wpforms-education-pointers-payment',
'wpforms_education_pointers_payment',
$admin_l10n
);
}
/**
* Set arguments for the Payments feature pointer.
*
* @since 1.8.8
*
* @noinspection HtmlUnknownTarget
*/
protected function set_args() {
$this->args['title'] = __( 'Payment and Donation Forms are here!', 'wpforms-lite' );
$this->args['message'] = sprintf( /* translators: %1$s - Payments page URL. */
__(
'Now available for you: create forms that accept credit cards, Apple Pay, and Google Pay payments. Visit our new <a href="%1$s" id="wpforms-education-pointers-payments">Payments area</a> to get started.',
'wpforms-lite'
),
esc_url( PaymentsPage::get_url() )
);
}
/**
* Retrieve the current installation license type in the lowercase.
* If no license type is found, defaults to 'lite'.
*
* @since 1.8.8
*
* @return string
*/
private function get_license_type(): string {
$type = wpforms_get_license_type();
// Set the default to 'lite' if no license type is detected.
if ( empty( $type ) ) {
$type = 'lite';
}
return $type;
}
}
@@ -0,0 +1,445 @@
<?php
namespace WPForms\Admin\Education\Pointers;
/**
* Abstract class representing Pointers functionality.
*
* This abstract class provides a foundation for implementing pointers in WPForms.
* Child classes should extend this class and implement the necessary methods to set properties and allow loading.
*
* The class separates concerns by implementing methods for different functionalities such as initializing pointers,
* handling interactions, printing scripts, etc., which enhances code maintainability and security.
* Additionally, the class is designed to be abstract, allowing for customization and extension while enforcing certain security measures in child classes.
*
* @since 1.8.8
*/
abstract class Pointer {
/**
* Unique ID for the pointer.
*
* @since 1.8.8
*
* @var string
*/
protected $pointer_id;
/**
* Selector for the pointer.
*
* @since 1.8.8
*
* @var string
*/
protected $selector;
/**
* Arguments for the pointer.
*
* @since 1.8.8
*
* @var array
*/
protected $args;
/**
* Top-level menu selector.
*
* @since 1.8.8
*
* @var string
*/
private $top_level_menu = '#toplevel_page_wpforms-overview';
/**
* Determines whether the pointer should be visible outside the "WPForms" primary menu.
* Note that setting this property to true will display the pointer on other dashboard pages as well.
*
* @since 1.8.8
*
* @var string
*/
protected $top_level_visible = false;
/**
* Option name for storing interactions with pointers.
*
* @since 1.8.8
*/
private const OPTION_NAME = 'wpforms_pointers';
/**
* Initialize the pointer.
*
* @since 1.8.8
*/
public function init(): void {
// If loading is not allowed, or if the pointer is already dismissed, return.
if ( ! $this->allow_display() || ! $this->allow_load() ) {
return;
}
// Set initial arguments.
$this->set_initial_args();
// Register hooks.
$this->hooks();
}
/**
* Check if the pointer is already dismissed or interacted with.
*
* @since 1.8.8
*
* @return bool
*/
private function allow_display(): bool {
// If the pointer ID is empty, return.
// Check if announcements are allowed to be displayed.
if ( empty( $this->pointer_id ) || wpforms_setting( 'hide-announcements' ) ) {
return false;
}
// Get pointers.
$pointers = (array) get_option( self::OPTION_NAME, [] );
// Check if the pointer ID exists in the engagement list.
if ( isset( $pointers['engagement'] ) && in_array( $this->pointer_id, (array) $pointers['engagement'], true ) ) {
return false;
}
// Check if the pointer ID exists in the dismissed list.
if ( isset( $pointers['dismiss'] ) && in_array( $this->pointer_id, (array) $pointers['dismiss'], true ) ) {
return false;
}
return true;
}
/**
* Register hooks for the pointer.
*
* @since 1.8.8
*/
private function hooks(): void {
// Enqueue assets.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
// Print the pointer script.
add_action( 'admin_print_footer_scripts', [ $this, 'print_script' ] );
// Add Ajax callback for the engagement.
add_action( 'wp_ajax_wpforms_education_pointers_engagement', [ $this, 'engagement_callback' ] );
// Add Ajax callback for dismissing the pointer.
add_action( 'wp_ajax_wpforms_education_pointers_dismiss', [ $this, 'dismiss_callback' ] );
}
/**
* Enqueue assets for the pointer.
*
* @since 1.8.8
*/
public function enqueue_assets() {
// Enqueue the pointer CSS.
wp_enqueue_style( 'wp-pointer' );
// Enqueue the pointer script.
wp_enqueue_script( 'wp-pointer' );
}
/**
* Print the pointer script.
*
* @since 1.8.8
*/
public function print_script(): void {
// Encode the $args array into JSON format.
$encoded_args = $this->get_prepared_args();
if ( empty( $encoded_args ) ) {
return;
}
// Sanitize pointer ID and selector.
$pointer_id = sanitize_text_field( $this->pointer_id );
$selector = sanitize_text_field( $this->get_selector() );
// Get the admin-ajax URL.
$ajaxurl = esc_url_raw( admin_url( 'admin-ajax.php' ) );
// Create nonce for the pointer.
$nonce = sanitize_text_field( $this->get_nonce_token() );
// Menu flyout selector.
$menu_flyout = "{$this->top_level_menu}:not(.wp-menu-open)";
// Inline CSS style id.
$inline_css_id = "wpforms-{$pointer_id}-inline-css";
// The type of echo being used in this PHP code is a HEREDOC syntax.
// HEREDOC allows you to create strings that span multiple lines without
// needing to concatenate them with dots (.) as you would with double quotes.
// phpcs:disable
echo <<<HTML
<script type="text/javascript">
( function( $ ) {
let options = $encoded_args, setup;
if ( ! options ) {
return;
}
options = $.extend( options, {
show: function() {
if ( ! $( '#$inline_css_id' ).length && $( '$menu_flyout' ).length ) {
$( '<style id="$inline_css_id">' ).text( '$menu_flyout:after, $menu_flyout .wp-submenu-wrap{ display: none }' ).appendTo( 'head' );
}
},
close: function() {
$( '#$inline_css_id' ).remove();
$.post(
'$ajaxurl',
{
pointer_id: '$pointer_id',
_ajax_nonce: '$nonce',
action: 'wpforms_education_pointers_dismiss',
}
);
}
} );
setup = function() {
$( '$selector' ).first().pointer( options ).pointer( 'open' );
};
if ( options.position && options.position.defer_loading ) {
$( window ).on( 'load.wp-pointers', setup );
} else {
$( function() {
setup();
} );
}
} )( jQuery );
</script>
HTML;
// phpcs:enable
}
/**
* Callback function for engaging with a pointer.
*
* This function is triggered via AJAX when a user interacts with a pointer, indicating engagement.
*
* @since 1.8.8
*/
public function engagement_callback(): void {
check_ajax_referer( $this->pointer_id, '_ajax_nonce' );
if ( ! wpforms_current_user_can() ) {
wp_send_json_error();
}
[ $pointer_id, $pointers ] = $this->handle_pointer_interaction();
// Add the current pointer to the engagement list.
$pointers['engagement'][] = $pointer_id;
// Update the pointer state.
update_option( self::OPTION_NAME, $pointers );
// Indicate that the pointer was engaged.
wp_send_json_success();
}
/**
* Ajax callback for dismissing the pointer.
*
* @since 1.8.8
*/
public function dismiss_callback(): void {
check_ajax_referer( $this->pointer_id, '_ajax_nonce' );
if ( ! wpforms_current_user_can() ) {
wp_send_json_error();
}
[ $pointer_id, $pointers ] = $this->handle_pointer_interaction();
// Add the current pointer to the dismissed list.
$pointers['dismiss'][] = $pointer_id;
// Update the pointer state.
update_option( self::OPTION_NAME, $pointers );
// Indicate that the pointer was dismissed.
wp_send_json_success();
}
/**
* Get nonce for the pointer.
*
* @since 1.8.8
*
* @return string
*/
protected function get_nonce_token(): string {
return wp_create_nonce( $this->pointer_id );
}
/**
* Handle pointer interaction via AJAX.
*
* @since 1.8.8
*
* @return array Pointer ID and pointers state.
*/
private function handle_pointer_interaction(): array {
// Check if the request is valid.
check_ajax_referer( $this->pointer_id );
// Get the pointer ID from the request.
$pointer_id = isset( $_POST['pointer_id'] ) ? sanitize_key( $_POST['pointer_id'] ) : '';
// If the pointer ID is empty, return an error response.
if ( empty( $pointer_id ) ) {
wp_send_json_error();
}
// Get the current pointers state.
$pointers = (array) get_option(
self::OPTION_NAME,
[
'engagement' => [],
'dismiss' => [],
]
);
return [ $pointer_id, $pointers ];
}
/**
* Set initial arguments to use in a pointer.
*
* @since 1.8.8
*/
private function set_initial_args(): void {
// Set default arguments.
$this->args = [
'content' => '',
'pointerWidth' => 395,
'position' => [
'edge' => 'left',
'align' => 'center',
],
];
// Set additional arguments for the pointer.
$this->set_args();
}
/**
* Retrieves the selector based on conditions.
*
* @since 1.8.8
*
* @return string
*/
private function get_selector(): string {
// If the sublevel menu is defined, and it's an admin page, return the combined selector.
if ( ! empty( $this->selector ) && wpforms_is_admin_page() ) {
return "{$this->top_level_menu} {$this->selector}";
}
// Default returns the top-level menu.
return $this->top_level_menu;
}
/**
* Prepare and encode args for the pointer.
*
* @since 1.8.8
*
* @return string
*/
private function get_prepared_args(): string {
// Retrieve title and message from an argument array, fallback to empty strings if not set.
$title = $this->args['title'] ?? '';
$message = $this->args['message'] ?? '';
// Return early if both title and message are empty.
if ( empty( $message ) ) {
return '';
}
// Pointer markup uses <h3> tag for the title and <p> tag for the message.
$content = ! empty( $title ) ? sprintf( '<h3>%s</h3>', esc_html( $title ) ) : '';
$content .= sprintf( '<p style="font-size:14px">%s</p>', wp_kses( $message, $this->get_allowed_html() ) );
$this->args['content'] = $content;
// Unset title and message to clean up an argument array.
unset( $this->args['title'], $this->args['message'] );
// If RTL and position edge are 'left', switch it to 'right'.
if ( ! empty( $this->args['position']['edge'] ) && $this->args['position']['edge'] === 'left' && is_rtl() ) {
$this->args['position']['edge'] = 'right';
}
// Encode arguments array to JSON.
return wp_json_encode( $this->args );
}
/**
* Get allowed HTML tags for wp_kses.
*
* @since 1.8.8
*
* @return array
*/
private function get_allowed_html(): array {
return [
'a' => [
'id' => [],
'class' => [],
'href' => [],
'target' => [],
'rel' => [],
],
'strong' => [],
'em' => [],
'br' => [],
];
}
/**
* Check if loading of the pointer is allowed.
*
* @since 1.8.8
*
* @return bool
*/
abstract protected function allow_load(): bool;
/**
* Set arguments for the pointer.
*
* @since 1.8.8
*/
abstract protected function set_args();
}
@@ -0,0 +1,184 @@
<?php
namespace WPForms\Admin\Education;
/**
* Strings trait.
*
* @since 1.8.8
*/
trait StringsTrait {
/**
* Localize common strings.
*
* @since 1.6.6
*
* @return array
*/
protected function get_js_strings(): array {
$strings = [];
$name = '%name%';
$strings['ok'] = esc_html__( 'Ok', 'wpforms-lite' );
$strings['cancel'] = esc_html__( 'Cancel', 'wpforms-lite' );
$strings['close'] = esc_html__( 'Close', 'wpforms-lite' );
$strings['ajax_url'] = admin_url( 'admin-ajax.php' );
$strings['nonce'] = wp_create_nonce( 'wpforms-education' );
$strings['activate_prompt'] = '<p>' . esc_html(
sprintf( /* translators: %s - addon name. */
__( 'The %s is installed but not activated. Would you like to activate it?', 'wpforms-lite' ),
$name
)
) . '</p>';
$strings['activate_confirm'] = esc_html__( 'Yes, Activate', 'wpforms-lite' );
$strings['addon_activated'] = esc_html__( 'Addon activated', 'wpforms-lite' );
$strings['plugin_activated'] = esc_html__( 'Plugin activated', 'wpforms-lite' );
$strings['activating'] = esc_html__( 'Activating', 'wpforms-lite' );
$strings['install_prompt'] = '<p>' . esc_html(
sprintf( /* translators: %s - addon name. */
__( 'The %s is not installed. Would you like to install and activate it?', 'wpforms-lite' ),
$name
)
) . '</p>';
$strings['install_confirm'] = esc_html__( 'Yes, Install and Activate', 'wpforms-lite' );
$strings['installing'] = esc_html__( 'Installing', 'wpforms-lite' );
$strings['save_prompt'] = esc_html__( 'Almost done! Would you like to save and refresh the form builder?', 'wpforms-lite' );
$strings['save_confirm'] = esc_html__( 'Yes, save and refresh', 'wpforms-lite' );
$strings['saving'] = esc_html__( 'Saving ...', 'wpforms-lite' );
// Check if the user can install addons.
// Includes license check.
$can_install_addons = wpforms_can_install( 'addon' );
// Check if the user can install plugins.
// Only checks if the user has the capability.
// Needed to display the correct message for non-admin users.
$can_install_plugins = current_user_can( 'install_plugins' );
$strings['can_install_addons'] = $can_install_addons && $can_install_plugins;
if ( ! $can_install_addons ) {
$strings['install_prompt'] = '<p>' . esc_html(
sprintf( /* translators: %s - addon name. */
__( 'The %s is not installed. Please install and activate it to use this feature.', 'wpforms-lite' ),
$name
)
) . '</p>';
}
if ( ! $can_install_plugins ) {
/* translators: %s - addon name. */
$strings['install_prompt'] = '<p>' . esc_html(
sprintf( /* translators: %s - addon name. */
__( 'The %s is not installed. Please contact the site administrator.', 'wpforms-lite' ),
$name
)
) . '</p>';
}
// Check if the user can activate plugins.
$can_activate_plugins = current_user_can( 'activate_plugins' );
$strings['can_activate_addons'] = $can_activate_plugins;
if ( ! $can_activate_plugins ) {
/* translators: %s - addon name. */
$strings['activate_prompt'] = '<p>' . esc_html( sprintf( __( 'The %s is not activated. Please contact the site administrator.', 'wpforms-lite' ), $name ) ) . '</p>';
}
$upgrade_utm_medium = wpforms_is_admin_page() ? 'Settings - Integration' : 'Builder - Settings';
if ( wpforms_is_block_editor() ) {
$upgrade_utm_medium = 'gutenberg';
}
$strings['upgrade'] = [
'pro' => $this->get_upgrade_strings( 'Pro', $name, $upgrade_utm_medium ),
'elite' => $this->get_upgrade_strings( 'Elite', $name, $upgrade_utm_medium ),
];
$strings['upgrade_bonus'] = wpautop(
wp_kses(
__( '<strong>Bonus:</strong> WPForms Lite users get <span>50% off</span> regular price, automatically applied at checkout.', 'wpforms-lite' ),
[
'strong' => [],
'span' => [],
]
)
);
$strings['thanks_for_interest'] = esc_html__( 'Thanks for your interest in WPForms Pro!', 'wpforms-lite' );
/**
* Filters the education strings.
*
* @since 1.6.6
*
* @param array $strings Education strings.
*
* @return array
*/
return (array) apply_filters( 'wpforms_admin_education_strings', $strings );
}
/**
* Get upgrade strings.
*
* @since 1.8.8
*
* @param string $level Upgrade level.
* @param string $name Addon name.
* @param string $upgrade_utm_medium UTM medium for the upgrade link.
*
* @return array
* @noinspection HtmlUnknownTarget
*/
private function get_upgrade_strings( string $level, string $name, string $upgrade_utm_medium ): array {
// phpcs:ignore WPForms.Formatting.EmptyLineAfterFunctionDeclaration.AddEmptyLineAfterFunctionDeclaration
return [
'title' => esc_html(
sprintf( /* translators: %s - level name, either Pro or Elite. */
__( 'is a %s Feature', 'wpforms-lite' ),
$level
)
),
'title_plural' => esc_html(
sprintf( /* translators: %s - level name, either Pro or Elite. */
__( 'are a %s Feature', 'wpforms-lite' ),
$level
)
),
'message' => '<p>' . esc_html(
sprintf( /* translators: %1$s - addon name, %2$s - level name, either Pro or Elite. */
__( 'We\'re sorry, the %1$s is not available on your plan. Please upgrade to the %2$s plan to unlock all these awesome features.', 'wpforms-lite' ),
$name,
$level
)
) . '</p>',
'message_plural' => '<p>' . esc_html(
sprintf( /* translators: %1$s - addon name, %2$s - level name, either Pro or Elite. */
__( 'We\'re sorry, %1$s are not available on your plan. Please upgrade to the %2$s plan to unlock all these awesome features.', 'wpforms-lite' ),
$name,
$level
)
) . '</p>',
'doc' => sprintf(
'<a href="%1$s" target="_blank" rel="noopener noreferrer" class="already-purchased">%2$s</a>',
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/upgrade-wpforms-lite-paid-license/#installing-wpforms', $upgrade_utm_medium, '%name%' ) ),
esc_html__( 'Already purchased?', 'wpforms-lite' )
),
'button' => esc_html(
sprintf( /* translators: %s - level name, either Pro or Elite. */
__( 'Upgrade to %s', 'wpforms-lite' ),
$level
)
),
'url' => wpforms_admin_upgrade_link( $upgrade_utm_medium ),
'url_template' => wpforms_is_admin_page( 'templates' ) ? wpforms_admin_upgrade_link( 'Form Templates Subpage' ) : wpforms_admin_upgrade_link( 'builder-modal-template' ),
'url_themes' => wpforms_admin_upgrade_link( 'Builder Themes' ),
'modal' => wpforms_get_upgrade_modal_text( strtolower( $level ) ),
];
}
}
@@ -0,0 +1,141 @@
<?php
namespace WPForms\Admin;
/**
* Admin Flyout Menu.
*
* @since 1.5.7
*/
class FlyoutMenu {
/**
* Constructor.
*
* @since 1.5.7
*/
public function __construct() {
if ( ! \wpforms_is_admin_page() || \wpforms_is_admin_page( 'builder' ) ) {
return;
}
if ( ! \apply_filters( 'wpforms_admin_flyoutmenu', true ) ) {
return;
}
// Check if WPForms Challenge can be displayed.
if ( wpforms()->obj( 'challenge' )->challenge_can_start() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.5.7
*/
public function hooks() {
add_action( 'admin_footer', [ $this, 'output' ] );
}
/**
* Output menu.
*
* @since 1.5.7
*/
public function output() {
printf(
'<div id="wpforms-flyout">
<div id="wpforms-flyout-items">
%1$s
</div>
<a href="#" class="wpforms-flyout-button wpforms-flyout-head">
<div class="wpforms-flyout-label">%2$s</div>
<img src="%3$s" alt="%2$s" data-active="%4$s" />
</a>
</div>',
$this->get_items_html(), // phpcs:ignore
\esc_attr__( 'See Quick Links', 'wpforms-lite' ),
\esc_url( \WPFORMS_PLUGIN_URL . 'assets/images/admin-flyout-menu/sullie-default.svg' ),
\esc_url( \WPFORMS_PLUGIN_URL . 'assets/images/admin-flyout-menu/sullie-active.svg' )
);
}
/**
* Generate menu items HTML.
*
* @since 1.5.7
*
* @return string Menu items HTML.
*/
private function get_items_html() {
$items = array_reverse( $this->menu_items() );
$items_html = '';
foreach ( $items as $item_key => $item ) {
$items_html .= sprintf(
'<a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-flyout-button wpforms-flyout-item wpforms-flyout-item-%2$d"%5$s%6$s>
<div class="wpforms-flyout-label">%3$s</div>
<i class="fa %4$s"></i>
</a>',
\esc_url( $item['url'] ),
(int) $item_key,
\esc_html( $item['title'] ),
\sanitize_html_class( $item['icon'] ),
! empty( $item['bgcolor'] ) ? ' style="background-color: ' . \esc_attr( $item['bgcolor'] ) . '"' : '',
! empty( $item['hover_bgcolor'] ) ? ' onMouseOver="this.style.backgroundColor=\'' . \esc_attr( $item['hover_bgcolor'] ) . '\'" onMouseOut="this.style.backgroundColor=\'' . \esc_attr( $item['bgcolor'] ) . '\'"' : ''
);
}
return $items_html;
}
/**
* Menu items data.
*
* @since 1.5.7
*/
private function menu_items() {
$is_pro = wpforms()->is_pro();
$utm_campaign = $is_pro ? 'plugin' : 'liteplugin';
$items = [
[
'title' => \esc_html__( 'Upgrade to WPForms Pro', 'wpforms-lite' ),
'url' => wpforms_admin_upgrade_link( 'Flyout Menu', 'Upgrade to WPForms Pro' ),
'icon' => 'fa-star',
'bgcolor' => '#E1772F',
'hover_bgcolor' => '#ff8931',
],
[
'title' => \esc_html__( 'Support & Docs', 'wpforms-lite' ),
'url' => 'https://wpforms.com/docs/?utm_source=WordPress&utm_medium=Flyout Menu&utm_campaign=' . $utm_campaign . '&utm_content=Support',
'icon' => 'fa-life-ring',
],
[
'title' => \esc_html__( 'Join Our Community', 'wpforms-lite' ),
'url' => 'https://www.facebook.com/groups/wpformsvip/',
'icon' => 'fa-comments',
],
[
'title' => \esc_html__( 'Suggest a Feature', 'wpforms-lite' ),
'url' => 'https://wpforms.com/features/suggest/?utm_source=WordPress&utm_medium=Flyout Menu&utm_campaign=' . $utm_campaign . '&utm_content=Feature',
'icon' => 'fa-lightbulb-o',
],
];
if ( $is_pro ) {
array_shift( $items );
}
return \apply_filters( 'wpforms_admin_flyout_menu_items', $items );
}
}
@@ -0,0 +1,480 @@
<?php
namespace WPForms\Admin;
use WP_Post;
/**
* Embed Form in a Page wizard.
*
* @since 1.6.2
*/
class FormEmbedWizard {
/**
* Max search results count of 'Select Page' dropdown.
*
* @since 1.7.9
*
* @var int
*/
const MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT = 20;
/**
* Post statuses of pages in 'Select Page' dropdown.
*
* @since 1.7.9
*
* @var string[]
*/
const POST_STATUSES_OF_DROPDOWN_PAGES = [ 'publish', 'pending' ];
/**
* Initialize class.
*
* @since 1.6.2
*/
public function init() {
// Form Embed Wizard should load only in the Form Builder and on the Edit/Add Page screen.
if (
! wpforms_is_admin_page( 'builder' ) &&
! wpforms_is_admin_ajax() &&
! $this->is_form_embed_page()
) {
return;
}
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.6.2
* @since 1.7.9 Add hook for searching pages in embed wizard via AJAX.
*/
public function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
add_action( 'admin_footer', [ $this, 'output' ] );
add_filter( 'default_title', [ $this, 'embed_page_title' ], 10, 2 );
add_filter( 'default_content', [ $this, 'embed_page_content' ], 10, 2 );
add_action( 'wp_ajax_wpforms_admin_form_embed_wizard_embed_page_url', [ $this, 'get_embed_page_url_ajax' ] );
add_action( 'wp_ajax_wpforms_admin_form_embed_wizard_search_pages_choicesjs', [ $this, 'get_search_result_pages_ajax' ] );
}
/**
* Enqueue assets.
*
* @since 1.6.2
* @since 1.7.9 Add 'underscore' as dependency.
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
if ( $this->is_form_embed_page() && $this->get_meta() && ! $this->is_challenge_active() ) {
wp_enqueue_style(
'wpforms-admin-form-embed-wizard',
WPFORMS_PLUGIN_URL . "assets/css/form-embed-wizard{$min}.css",
[],
WPFORMS_VERSION
);
wp_enqueue_style(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css',
null,
'4.2.6'
);
wp_enqueue_script(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js',
[ 'jquery' ],
'4.2.6',
true
);
}
wp_enqueue_script(
'wpforms-admin-form-embed-wizard',
WPFORMS_PLUGIN_URL . "assets/js/admin/form-embed-wizard{$min}.js",
[ 'jquery', 'underscore' ],
WPFORMS_VERSION,
false
);
wp_localize_script(
'wpforms-admin-form-embed-wizard',
'wpforms_admin_form_embed_wizard',
[
'nonce' => wp_create_nonce( 'wpforms_admin_form_embed_wizard_nonce' ),
'is_edit_page' => (int) $this->is_form_embed_page( 'edit' ),
'video_url' => esc_url(
sprintf(
'https://youtube.com/embed/%s?rel=0&showinfo=0',
wpforms_is_gutenberg_active() ? '_29nTiDvmLw' : 'IxGVz3AjEe0'
)
),
]
);
}
/**
* Output HTML.
*
* @since 1.6.2
*/
public function output() {
// We don't need to output tooltip if Challenge is active.
if ( $this->is_form_embed_page() && $this->is_challenge_active() ) {
$this->delete_meta();
return;
}
// We don't need to output tooltip if it's not an embed flow.
if ( $this->is_form_embed_page() && ! $this->get_meta() ) {
return;
}
$template = $this->is_form_embed_page() ? 'admin/form-embed-wizard/tooltip' : 'admin/form-embed-wizard/popup';
$args = [];
if ( ! $this->is_form_embed_page() ) {
$args['user_can_edit_pages'] = current_user_can( 'edit_pages' );
$args['dropdown_pages'] = $this->get_select_dropdown_pages_html();
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render( $template, $args );
$this->delete_meta();
}
/**
* Check if Challenge is active.
*
* @since 1.6.4
*
* @return boolean
*/
public function is_challenge_active() {
static $challenge_active = null;
if ( $challenge_active === null ) {
$challenge = wpforms()->obj( 'challenge' );
$challenge_active = method_exists( $challenge, 'challenge_active' ) ? $challenge->challenge_active() : false;
}
return $challenge_active;
}
/**
* Check if the current page is a form embed page.
*
* @since 1.6.2
*
* @param string $type Type of the embed page to check. Can be '', 'add' or 'edit'. By default is empty string.
*
* @return boolean
*/
public function is_form_embed_page( $type = '' ) {
global $pagenow;
$type = $type === 'add' || $type === 'edit' ? $type : '';
if (
$pagenow !== 'post.php' &&
$pagenow !== 'post-new.php'
) {
return false;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = empty( $_GET['post'] ) ? 0 : (int) $_GET['post'];
$post_type = empty( $_GET['post_type'] ) ? '' : sanitize_key( $_GET['post_type'] );
$action = empty( $_GET['action'] ) ? 'add' : sanitize_key( $_GET['action'] );
// phpcs:enable
if ( $pagenow === 'post-new.php' &&
( empty( $post_type ) || $post_type !== 'page' )
) {
return false;
}
if (
$pagenow === 'post.php' &&
( empty( $post_id ) || get_post_type( $post_id ) !== 'page' )
) {
return false;
}
$meta = $this->get_meta();
$embed_page = ! empty( $meta['embed_page'] ) ? (int) $meta['embed_page'] : 0;
if ( 'add' === $action && 0 === $embed_page && $type !== 'edit' ) {
return true;
}
if ( ! empty( $post_id ) && $embed_page === $post_id && $type !== 'add' ) {
return true;
}
return false;
}
/**
* Set user's embed meta data.
*
* @since 1.6.2
*
* @param array $data Data array to set.
*/
public function set_meta( $data ) {
update_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard', $data );
}
/**
* Get user's embed meta data.
*
* @since 1.6.2
*
* @return array User's embed meta data.
*/
public function get_meta() {
return get_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard', true );
}
/**
* Delete user's embed meta data.
*
* @since 1.6.2
*/
public function delete_meta() {
delete_user_meta( get_current_user_id(), 'wpforms_admin_form_embed_wizard' );
}
/**
* Get embed page URL via AJAX.
*
* @since 1.6.2
*/
public function get_embed_page_url_ajax() {
// Run a security check.
check_admin_referer( 'wpforms_admin_form_embed_wizard_nonce' );
// Check for permissions.
if ( ! wpforms_current_user_can( 'edit_forms' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
$page_id = ! empty( $_POST['pageId'] ) ? absint( $_POST['pageId'] ) : 0;
$meta = $this->prepare_meta_data( $page_id );
$this->set_meta( $meta );
// Update challenge option to properly continue challenge on the embed page.
$this->update_challenge_option( $meta );
wp_send_json_success( $meta['url'] );
}
/**
* Prepare meta data for the embed page.
*
* @since 1.9.4
*
* @param int $page_id Page ID.
*
* @return array
*/
private function prepare_meta_data( int $page_id ): array {
if ( ! empty( $page_id ) ) {
$url = get_edit_post_link( $page_id, '' );
$meta = [
'embed_page' => $page_id,
];
} else {
$url = add_query_arg( 'post_type', 'page', admin_url( 'post-new.php' ) );
$meta = [
'embed_page' => 0,
'embed_page_title' => ! empty( $_POST['pageTitle'] ) ? sanitize_text_field( wp_unslash( $_POST['pageTitle'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Missing
];
}
$meta['form_id'] = ! empty( $_POST['formId'] ) ? absint( $_POST['formId'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$meta['url'] = $url;
return $meta;
}
/**
* Update challenge option to properly continue challenge on the embed page.
*
* @since 1.9.4
*
* @param array $meta Meta data.
*/
private function update_challenge_option( array $meta ): void {
if ( $this->is_challenge_active() ) {
$challenge = wpforms()->obj( 'challenge' );
if ( $challenge && method_exists( $challenge, 'set_challenge_option' ) ) {
$challenge->set_challenge_option( [ 'embed_page' => $meta['embed_page'] ] );
}
}
}
/**
* Set default title for the new page.
*
* @since 1.6.2
*
* @param string $post_title Default post title.
* @param \WP_Post $post Post object.
*
* @return string New default post title.
*/
public function embed_page_title( $post_title, $post ) {
$meta = $this->get_meta();
$this->delete_meta();
return empty( $meta['embed_page_title'] ) ? $post_title : $meta['embed_page_title'];
}
/**
* Embed the form to the new page.
*
* @since 1.6.2
*
* @param string $post_content Default post content.
* @param \WP_Post $post Post object.
*
* @return string Embedding string (shortcode or GB component code).
*/
public function embed_page_content( $post_content, $post ) {
$meta = $this->get_meta();
$form_id = ! empty( $meta['form_id'] ) ? $meta['form_id'] : 0;
$page_id = ! empty( $meta['embed_page'] ) ? $meta['embed_page'] : 0;
if ( ! empty( $page_id ) || empty( $form_id ) ) {
return $post_content;
}
if ( wpforms_is_gutenberg_active() ) {
$pattern = '<!-- wp:wpforms/form-selector {"formId":"%d"} /-->';
} else {
$pattern = '[wpforms id="%d" title="false" description="false"]';
}
return sprintf( $pattern, absint( $form_id ) );
}
/**
* Generate select with pages which are available to edit for current user.
*
* @since 1.6.6
* @since 1.7.9 Refactor to use ChoicesJS instead of `wp_dropdown_pages()`.
*
* @return string
*/
private function get_select_dropdown_pages_html() {
$dropdown_pages = wpforms_search_posts(
'',
[
'count' => self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT,
'post_status' => self::POST_STATUSES_OF_DROPDOWN_PAGES,
]
);
if ( empty( $dropdown_pages ) ) {
return '';
}
$total_pages = 0;
$wp_count_pages = (array) wp_count_posts( 'page' );
foreach ( $wp_count_pages as $page_status => $pages_count ) {
if ( in_array( $page_status, self::POST_STATUSES_OF_DROPDOWN_PAGES, true ) ) {
$total_pages += $pages_count;
}
}
// Include so we can use `\wpforms_settings_select_callback()`.
require_once WPFORMS_PLUGIN_DIR . 'includes/admin/settings-api.php';
return wpforms_settings_select_callback(
[
'id' => 'form-embed-wizard-choicesjs-select-pages',
'type' => 'select',
'choicesjs' => true,
'options' => wp_list_pluck( $dropdown_pages, 'post_title', 'ID' ),
'data' => [
'use_ajax' => $total_pages > self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT,
],
]
);
}
/**
* Get search result pages for ChoicesJS via AJAX.
*
* @since 1.7.9
*/
public function get_search_result_pages_ajax() {
// Run a security check.
if ( ! check_ajax_referer( 'wpforms_admin_form_embed_wizard_nonce', false, false ) ) {
wp_send_json_error(
[
'msg' => esc_html__( 'Your session expired. Please reload the builder.', 'wpforms-lite' ),
]
);
}
// Check for permissions.
if ( ! wpforms_current_user_can( 'edit_forms' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
if ( ! array_key_exists( 'search', $_GET ) ) {
wp_send_json_error(
[
'msg' => esc_html__( 'Incorrect usage of this operation.', 'wpforms-lite' ),
]
);
}
$result_pages = wpforms_search_pages_for_dropdown(
sanitize_text_field( wp_unslash( $_GET['search'] ) ),
[
'count' => self::MAX_SEARCH_RESULTS_DROPDOWN_PAGES_COUNT,
'post_status' => self::POST_STATUSES_OF_DROPDOWN_PAGES,
]
);
if ( empty( $result_pages ) ) {
wp_send_json_error( [] );
}
wp_send_json_success( $result_pages );
}
}
@@ -0,0 +1,103 @@
<?php
namespace WPForms\Admin\Forms\Ajax;
use WPForms\Admin\Forms\Table\Facades;
/**
* Columns AJAX actions on Forms Overview list page.
*
* @since 1.8.6
*/
class Columns {
/**
* Determine if the class is allowed to load.
*
* @since 1.8.6
*
* @return bool
*/
private function allow_load(): bool {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';
// Load only in the case of AJAX calls on Forms Overview page.
return wpforms_is_admin_ajax() && strpos( $action, 'wpforms_admin_forms_overview_' ) === 0;
}
/**
* Initialize class.
*
* @since 1.8.6
*/
public function init(): void {
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.6
*/
private function hooks(): void {
add_action( 'wp_ajax_wpforms_admin_forms_overview_save_columns_order', [ $this, 'save_order' ] );
}
/**
* Save columns' order.
*
* @since 1.8.6
*/
public function save_order(): void {
check_ajax_referer( 'wpforms-admin', 'nonce' );
if ( ! wpforms_current_user_can( 'view_forms' ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to perform this action.', 'wpforms-lite' ) );
}
$data = $this->get_prepared_data();
// Prepare the new columns' order.
$columns = [];
foreach ( $data['columns'] as $column ) {
$columns[] = str_replace( '-foot', '', $column );
}
$result = Facades\Columns::sanitize_and_save_columns( $columns );
if ( $result === false ) {
wp_send_json_error( esc_html__( 'Cannot save columns order.', 'wpforms-lite' ) );
}
wp_send_json_success();
}
/**
* Get prepared data before perform ajax action.
*
* @since 1.8.6
*
* @return array
*/
private function get_prepared_data(): array {
// Run a security check.
if ( ! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ) {
wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) );
}
return [
'columns' => ! empty( $_POST['columns'] ) ? map_deep( (array) wp_unslash( $_POST['columns'] ), 'sanitize_key' ) : [],
];
}
}
@@ -0,0 +1,273 @@
<?php
namespace WPForms\Admin\Forms\Ajax;
use WPForms_Form_Handler;
/**
* Tags AJAX actions on All Forms page.
*
* @since 1.7.5
*/
class Tags {
/**
* Determine if the new tag was added during processing submitted tags.
*
* @since 1.7.5
*
* @var bool
*/
private $is_new_tag_added;
/**
* Determine if the class is allowed to load.
*
* @since 1.7.5
*
* @return bool
*/
private function allow_load() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';
// Load only in the case of AJAX calls on Forms Overview page.
return wp_doing_ajax() && strpos( $action, 'wpforms_admin_forms_overview_' ) === 0;
}
/**
* Initialize class.
*
* @since 1.7.5
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.5
*/
private function hooks() {
add_action( 'wp_ajax_wpforms_admin_forms_overview_save_tags', [ $this, 'save_tags' ] );
add_action( 'wp_ajax_wpforms_admin_forms_overview_delete_tags', [ $this, 'delete_tags' ] );
}
/**
* Save tags.
*
* @since 1.7.5
*/
public function save_tags() {
$data = $this->get_prepared_data( 'save' );
$tags_ids = $this->get_processed_tags( $data['tags'] );
$tags_labels = wp_list_pluck( $data['tags'], 'label' );
// Set tags to each form.
$this->set_tags_to_forms( $data['forms'], $tags_ids, $tags_labels );
$tags_obj = wpforms()->obj( 'forms_tags' );
$terms = get_the_terms( array_pop( $data['forms'] ), WPForms_Form_Handler::TAGS_TAXONOMY );
$tags_data = $tags_obj->get_tags_data( $terms );
if ( ! empty( $this->is_new_tag_added ) ) {
$tags_data['all_tags_choices'] = $tags_obj->get_all_tags_choices();
}
wp_send_json_success( $tags_data );
}
/**
* Delete tags.
*
* @since 1.7.5
*/
public function delete_tags(): void {
$form_obj = wpforms()->obj( 'form' );
$data = $this->get_prepared_data( 'delete' );
$deleted = 0;
$labels = [];
foreach ( $data['tags'] as $tag_id ) {
$term = get_term_by( 'term_id', $tag_id, WPForms_Form_Handler::TAGS_TAXONOMY, ARRAY_A );
$labels[] = $term['name'];
// Delete tag (term).
if ( wp_delete_term( $tag_id, WPForms_Form_Handler::TAGS_TAXONOMY ) === true ) {
++$deleted;
}
}
// Get forms marked by the tags.
$args = [
'fields' => 'ids',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => [
[
'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY,
'field' => 'term_id',
'terms' => array_map( 'absint', $data['tags'] ),
],
],
];
$forms = $form_obj ? (array) $form_obj->get( 0, $args ) : [];
// Remove tags from the settings of the forms.
foreach ( $forms as $form_id ) {
$form_data = $form_obj->get( $form_id, [ 'content_only' => true ] );
if (
empty( $form_data['settings']['form_tags'] ) ||
! is_array( $form_data['settings']['form_tags'] )
) {
continue;
}
$form_data['settings']['form_tags'] = array_diff( $form_data['settings']['form_tags'], $labels );
$form_obj->update( $form_id, $form_data );
}
wp_send_json_success( [ 'deleted' => $deleted ] );
}
/**
* Get processed tags.
*
* @since 1.7.5
*
* @param array $tags_data Submitted tags data.
*
* @return array Tags IDs list.
*/
public function get_processed_tags( $tags_data ) {
if ( ! is_array( $tags_data ) ) {
return [];
}
$tags_ids = [];
// Process the tags' data.
foreach ( $tags_data as $tag ) {
$term = get_term( $tag['value'], WPForms_Form_Handler::TAGS_TAXONOMY );
// In the case when the term is not found, we should create the new term.
if ( empty( $term ) || is_wp_error( $term ) ) {
$new_term = wp_insert_term( sanitize_text_field( $tag['label'] ), WPForms_Form_Handler::TAGS_TAXONOMY );
$tag['value'] = ! is_wp_error( $new_term ) && isset( $new_term['term_id'] ) ? $new_term['term_id'] : 0;
$this->is_new_tag_added = $this->is_new_tag_added || $tag['value'] > 0;
}
if ( ! empty( $tag['value'] ) ) {
$tags_ids[] = absint( $tag['value'] );
}
}
return $tags_ids;
}
/**
* Get prepared data before perform ajax action.
*
* @since 1.7.5
*
* @param string $action Action: `save` OR `delete`.
*
* @return array
*/
private function get_prepared_data( string $action ): array {
// Run a security check.
if ( ! check_ajax_referer( 'wpforms-admin-forms-overview-nonce', 'nonce', false ) ) {
wp_send_json_error( esc_html__( 'Most likely, your session expired. Please reload the page.', 'wpforms-lite' ) );
}
// Check for permissions.
if ( ! wpforms_current_user_can( 'edit_others_forms' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
$data = [
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
'tags' => ! empty( $_POST['tags'] ) ? map_deep( (array) wp_unslash( $_POST['tags'] ), 'sanitize_text_field' ) : [],
];
if ( $action === 'save' ) {
$data['forms'] = $this->get_allowed_forms();
}
return $data;
}
/**
* Get allowed forms.
*
* @since 1.7.5
*
* @return array Allowed form IDs.
*/
private function get_allowed_forms() {
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( empty( $_POST['forms'] ) ) {
wp_send_json_error( esc_html__( 'No forms selected when trying to add a tag to them.', 'wpforms-lite' ) );
}
$forms_all = array_filter( array_map( 'absint', (array) $_POST['forms'] ) );
$forms_allowed = [];
// phpcs:enable WordPress.Security.NonceVerification.Missing
foreach ( $forms_all as $form_id ) {
if ( wpforms_current_user_can( 'edit_form_single', $form_id ) ) {
$forms_allowed[] = $form_id;
}
}
if ( empty( $forms_allowed ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
return $forms_allowed;
}
/**
* Set tags to each form in the list.
*
* @since 1.7.5
*
* @param array $forms_ids Forms IDs list.
* @param array $tags_ids Tags IDs list.
* @param array $tags_labels Tags labels list.
*/
private function set_tags_to_forms( $forms_ids, $tags_ids, $tags_labels ) {
$form_obj = wpforms()->obj( 'form' );
foreach ( $forms_ids as $form_id ) {
wp_set_post_terms(
$form_id,
$tags_ids,
WPForms_Form_Handler::TAGS_TAXONOMY
);
// Store tags labels in the form settings.
$form_data = $form_obj->get( $form_id, [ 'content_only' => true ] );
$form_data['settings']['form_tags'] = $tags_labels;
$form_obj->update( $form_id, $form_data );
}
}
}
@@ -0,0 +1,492 @@
<?php
namespace WPForms\Admin\Forms;
use WPForms\Admin\Notice;
/**
* Bulk actions on All Forms page.
*
* @since 1.7.3
*/
class BulkActions {
/**
* Allowed actions.
*
* @since 1.7.3
*
* @const array
*/
const ALLOWED_ACTIONS = [
'trash',
'restore',
'delete',
'duplicate',
'empty_trash',
];
/**
* Forms ids.
*
* @since 1.7.3
*
* @var array
*/
private $ids;
/**
* Current action.
*
* @since 1.7.3
*
* @var string
*/
private $action;
/**
* Current view.
*
* @since 1.7.3
*
* @var string
*/
private $view;
/**
* Determine if the class is allowed to load.
*
* @since 1.7.3
*
* @return bool
*/
private function allow_load() {
// Load only on the `All Forms` admin page.
return wpforms_is_admin_page( 'overview' );
}
/**
* Initialize class.
*
* @since 1.7.3
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
$this->view = wpforms()->obj( 'forms_views' )->get_current_view();
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.3
*/
private function hooks() {
add_action( 'load-toplevel_page_wpforms-overview', [ $this, 'notices' ] );
add_action( 'load-toplevel_page_wpforms-overview', [ $this, 'process' ] );
add_filter( 'removable_query_args', [ $this, 'removable_query_args' ] );
}
/**
* Process the bulk actions.
*
* @since 1.7.3
*/
public function process() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$this->ids = isset( $_GET['form_id'] ) ? array_map( 'absint', (array) $_GET['form_id'] ) : [];
$this->action = isset( $_REQUEST['action'] ) ? sanitize_key( $_REQUEST['action'] ) : false;
if ( $this->action === '-1' ) {
$this->action = ! empty( $_REQUEST['action2'] ) ? sanitize_key( $_REQUEST['action2'] ) : false;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( empty( $this->ids ) || empty( $this->action ) ) {
return;
}
// Check exact action values.
if ( ! in_array( $this->action, self::ALLOWED_ACTIONS, true ) ) {
return;
}
if ( empty( $_GET['_wpnonce'] ) ) {
return;
}
// Check the nonce.
if (
! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bulk-forms' ) &&
! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_' . $this->action . '_form_nonce' )
) {
return;
}
// Finally, we can process the action.
$this->process_action();
}
/**
* Process action.
*
* @since 1.7.3
*
* @uses process_action_trash
* @uses process_action_restore
* @uses process_action_delete
* @uses process_action_duplicate
* @uses process_action_empty_trash
*/
private function process_action() {
$method = "process_action_{$this->action}";
// Check that we have a method for this action.
if ( ! method_exists( $this, $method ) ) {
return;
}
if ( empty( $this->ids ) || ! is_array( $this->ids ) ) {
return;
}
$query_args = [];
if ( count( $this->ids ) === 1 ) {
$query_args['type'] = wpforms_is_form_template( $this->ids[0] ) ? 'template' : 'form';
}
$result = [];
foreach ( $this->ids as $id ) {
$result[ $id ] = $this->$method( $id );
}
$count_result = count( array_keys( array_filter( $result ) ) );
// Empty trash action returns count of deleted forms.
if ( $method === 'process_action_empty_trash' ) {
$count_result = $result[1] ?? 0;
}
$query_args[ rtrim( $this->action, 'e' ) . 'ed' ] = $count_result;
// Unset get vars and perform redirect to avoid action reuse.
wp_safe_redirect(
add_query_arg(
$query_args,
remove_query_arg( [ 'action', 'action2', '_wpnonce', 'form_id', 'paged', '_wp_http_referer' ] )
)
);
exit;
}
/**
* Trash the form.
*
* @since 1.7.3
*
* @param int $id Form ID to trash.
*
* @return bool
*/
private function process_action_trash( $id ) {
return wpforms()->obj( 'form' )->update_status( $id, 'trash' );
}
/**
* Restore the form.
*
* @since 1.7.3
*
* @param int $id Form ID to restore from trash.
*
* @return bool
*/
private function process_action_restore( $id ) {
return wpforms()->obj( 'form' )->update_status( $id, 'publish' );
}
/**
* Delete the form.
*
* @since 1.7.3
*
* @param int $id Form ID to delete.
*
* @return bool
*/
private function process_action_delete( $id ) {
return wpforms()->obj( 'form' )->delete( $id );
}
/**
* Duplicate the form.
*
* @since 1.7.3
*
* @param int $id Form ID to duplicate.
*
* @return bool
*/
private function process_action_duplicate( $id ) {
if ( ! wpforms_current_user_can( 'create_forms' ) ) {
return false;
}
if ( ! wpforms_current_user_can( 'view_form_single', $id ) ) {
return false;
}
return wpforms()->obj( 'form' )->duplicate( $id );
}
/**
* Empty trash.
*
* @since 1.7.3
*
* @param int $id Form ID. This parameter is not used in this method,
* but we need to keep it here because all the `process_action_*` methods
* should be called with the $id parameter.
*
* @return bool
*/
private function process_action_empty_trash( $id ) {
// Empty trash is actually the "delete all forms in trash" action.
// So, after the execution we should display the same notice as for the `delete` action.
$this->action = 'delete';
return wpforms()->obj( 'form' )->empty_trash();
}
/**
* Define bulk actions available for forms overview table.
*
* @since 1.7.3
*
* @return array
*/
public function get_dropdown_items() {
$items = [];
if ( wpforms_current_user_can( 'delete_forms' ) ) {
if ( $this->view === 'trash' ) {
$items = [
'restore' => esc_html__( 'Restore', 'wpforms-lite' ),
'delete' => esc_html__( 'Delete Permanently', 'wpforms-lite' ),
];
} else {
$items = [
'trash' => esc_html__( 'Move to Trash', 'wpforms-lite' ),
];
}
}
// phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity
/**
* Filters the Bulk Actions dropdown items.
*
* @since 1.7.5
*
* @param array $items Dropdown items.
*/
$items = apply_filters( 'wpforms_admin_forms_bulk_actions_get_dropdown_items', $items );
// phpcs:enable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity
if ( empty( $items ) ) {
// We should have dummy item, otherwise, WP will hide the Bulk Actions Dropdown,
// which is not good from a design point of view.
return [
'' => '&mdash;',
];
}
return $items;
}
/**
* Admin notices.
*
* @since 1.7.3
*/
public function notices() {
// phpcs:disable WordPress.Security.NonceVerification
$results = [
'trashed' => ! empty( $_REQUEST['trashed'] ) ? sanitize_key( $_REQUEST['trashed'] ) : false,
'restored' => ! empty( $_REQUEST['restored'] ) ? sanitize_key( $_REQUEST['restored'] ) : false,
'deleted' => ! empty( $_REQUEST['deleted'] ) ? sanitize_key( $_REQUEST['deleted'] ) : false,
'duplicated' => ! empty( $_REQUEST['duplicated'] ) ? sanitize_key( $_REQUEST['duplicated'] ) : false,
'type' => ! empty( $_REQUEST['type'] ) ? sanitize_key( $_REQUEST['type'] ) : 'form',
];
// phpcs:enable WordPress.Security.NonceVerification
// Display notice in case of error.
if ( in_array( 'error', $results, true ) ) {
Notice::add(
esc_html__( 'Security check failed. Please try again.', 'wpforms-lite' ),
'error'
);
return;
}
$this->notices_success( $results );
}
/**
* Admin success notices.
*
* @since 1.7.3
*
* @param array $results Action results data.
*/
private function notices_success( array $results ) {
$type = $results['type'] ?? '';
if ( ! in_array( $type, [ 'form', 'template' ], true ) ) {
return;
}
$method = "get_notice_success_for_{$type}";
$actions = [ 'trashed', 'restored', 'deleted', 'duplicated' ];
foreach ( $actions as $action ) {
$count = (int) $results[ $action ];
if ( ! $count ) {
continue;
}
$notice = $this->$method( $action, $count );
if ( ! $notice ) {
continue;
}
Notice::add( $notice, 'info' );
}
}
/**
* Remove certain arguments from a query string that WordPress should always hide for users.
*
* @since 1.7.3
*
* @param array $removable_query_args An array of parameters to remove from the URL.
*
* @return array Extended/filtered array of parameters to remove from the URL.
*/
public function removable_query_args( $removable_query_args ) {
$removable_query_args[] = 'trashed';
$removable_query_args[] = 'restored';
$removable_query_args[] = 'deleted';
$removable_query_args[] = 'duplicated';
return $removable_query_args;
}
/**
* Get notice success message for form.
*
* @since 1.9.2.3
*
* @param string $action Action type.
* @param int $count Count of forms.
*
* @return string
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_notice_success_for_form( string $action, int $count ): string {
switch ( $action ) {
case 'restored':
/* translators: %1$d - restored forms count. */
$notice = _n( '%1$d form was successfully restored.', '%1$d forms were successfully restored.', $count, 'wpforms-lite' );
break;
case 'deleted':
/* translators: %1$d - deleted forms count. */
$notice = _n( '%1$d form was successfully permanently deleted.', '%1$d forms were successfully permanently deleted.', $count, 'wpforms-lite' );
break;
case 'duplicated':
/* translators: %1$d - duplicated forms count. */
$notice = _n( '%1$d form was successfully duplicated.', '%1$d forms were successfully duplicated.', $count, 'wpforms-lite' );
break;
case 'trashed':
/* translators: %1$d - trashed forms count. */
$notice = _n( '%1$d form was successfully moved to Trash.', '%1$d forms were successfully moved to Trash.', $count, 'wpforms-lite' );
break;
default:
// phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement
return '';
}
return sprintf( $notice, $count );
}
/**
* Get notice success message for template.
*
* @since 1.9.2.3
*
* @param string $action Action type.
* @param int $count Count of forms.
*
* @return string
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_notice_success_for_template( string $action, int $count ): string {
switch ( $action ) {
case 'restored':
/* translators: %1$d - restored templates count. */
$notice = _n( '%1$d template was successfully restored.', '%1$d templates were successfully restored.', $count, 'wpforms-lite' );
break;
case 'deleted':
/* translators: %1$d - deleted templates count. */
$notice = _n( '%1$d template was successfully permanently deleted.', '%1$d templates were successfully permanently deleted.', $count, 'wpforms-lite' );
break;
case 'duplicated':
/* translators: %1$d - duplicated templates count. */
$notice = _n( '%1$d template was successfully duplicated.', '%1$d templates were successfully duplicated.', $count, 'wpforms-lite' );
break;
case 'trashed':
/* translators: %1$d - trashed templates count. */
$notice = _n( '%1$d template was successfully moved to Trash.', '%1$d templates were successfully moved to Trash.', $count, 'wpforms-lite' );
break;
default:
// phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement
return '';
}
return sprintf( $notice, $count );
}
}
@@ -0,0 +1,648 @@
<?php
namespace WPForms\Admin\Forms;
use WP_List_Table;
use WP_Post;
use WP_Screen;
use WPForms\Admin\Forms\Table\Facades\Columns;
use WPForms\Forms\Locator;
use WPForms\Integrations\LiteConnect\LiteConnect;
use WPForms\Integrations\LiteConnect\Integration as LiteConnectIntegration;
// IMPORTANT NOTICE:
// This line is needed to prevent fatal errors in the third-party plugins.
// We know about Jetpack (probably others also) can load WP classes during cron jobs or something similar.
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
/**
* Generate the table on the plugin overview page.
*
* @since 1.8.6
*/
class ListTable extends WP_List_Table {
/**
* Number of forms to show per page.
*
* @since 1.8.6
*
* @var int
*/
public $per_page;
/**
* Number of forms in different views.
*
* @since 1.8.6
*
* @var array
*/
private $count;
/**
* Current view.
*
* @since 1.8.6
*
* @var string
*/
private $view;
/**
* Primary class constructor.
*
* @since 1.8.6
*/
public function __construct() {
// Utilize the parent constructor to build the main class properties.
parent::__construct(
[
'singular' => 'form',
'plural' => 'forms',
'ajax' => false,
]
);
$this->hooks();
// Determine the current view.
$this->view = wpforms()->obj( 'forms_views' )->get_current_view();
/**
* Filters the default number of forms to show per page.
*
* @since 1.0.0
*
* @param int $forms_per_page Number of forms to show per page.
*/
$this->per_page = (int) apply_filters( 'wpforms_overview_per_page', 20 ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Register hooks.
*
* @since 1.8.6
*/
private function hooks() {
add_filter( 'default_hidden_columns', [ $this, 'default_hidden_columns' ], 10, 2 );
}
/**
* Get the instance of a class and store it in itself.
*
* @since 1.8.6
*/
public static function get_instance() {
static $instance;
if ( ! $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Retrieve the table columns.
*
* @since 1.8.6
*
* @return array $columns Array of all the list table columns.
*/
public function get_columns() {
return Columns::get_list_table_columns();
}
/**
* Render the checkbox column.
*
* @since 1.8.6
*
* @param WP_Post $form Form.
*
* @return string
*/
public function column_cb( $form ) {
return '<input type="checkbox" name="form_id[]" value="' . absint( $form->ID ) . '" />';
}
/**
* Render the columns.
*
* @since 1.8.6
*
* @param WP_Post $form CPT object as a form representation.
* @param string $column_name Column Name.
*
* @return string
*/
public function column_default( $form, $column_name ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity
switch ( $column_name ) {
case 'id':
$value = $form->ID;
break;
case 'shortcode':
$value = '[wpforms id="' . $form->ID . '"]';
if ( wpforms_is_form_template( $form->ID ) ) {
$value = __( 'N/A', 'wpforms-lite' );
}
break;
// This slug is not changed to 'date' for backward compatibility.
case 'created':
if ( gmdate( 'Ymd', strtotime( $form->post_date ) ) === gmdate( 'Ymd', strtotime( $form->post_modified ) ) ) {
$value = wp_kses(
sprintf( /* translators: %1$s - Post created date. */
__( 'Created<br/>%1$s', 'wpforms-lite' ),
esc_html( wpforms_datetime_format( $form->post_date ) )
),
[ 'br' => [] ]
);
} else {
$value = wp_kses(
sprintf( /* translators: %1$s - Post modified date. */
__( 'Last Modified<br/>%1$s', 'wpforms-lite' ),
esc_html( wpforms_datetime_format( $form->post_modified ) )
),
[ 'br' => [] ]
);
}
break;
case 'entries':
$value = sprintf(
'<span class="wpforms-lite-connect-entries-count"><a href="%s" data-title="%s">%s%d</a></span>',
esc_url( admin_url( 'admin.php?page=wpforms-entries' ) ),
esc_attr__( 'Entries are securely backed up in the cloud. Upgrade to restore.', 'wpforms-lite' ),
'<svg viewBox="0 0 16 12"><path d="M10.8 2c1.475 0 2.675 1.175 2.775 2.625C15 5.125 16 6.475 16 8a3.6 3.6 0 0 1-3.6 3.6H4a3.98 3.98 0 0 1-4-4 4.001 4.001 0 0 1 2.475-3.7A4.424 4.424 0 0 1 6.8.4c1.4 0 2.625.675 3.425 1.675C10.4 2.025 10.6 2 10.8 2ZM4 10.4h8.4a2.4 2.4 0 0 0 0-4.8.632.632 0 0 0-.113.013.678.678 0 0 1-.112.012c.125-.25.225-.525.225-.825 0-.875-.725-1.6-1.6-1.6a1.566 1.566 0 0 0-1.05.4 3.192 3.192 0 0 0-2.95-2 3.206 3.206 0 0 0-3.2 3.2v.05A2.757 2.757 0 0 0 1.2 7.6 2.795 2.795 0 0 0 4 10.4Zm6.752-4.624a.64.64 0 1 0-.905-.905L6.857 7.86 5.38 6.352a.64.64 0 1 0-.914.896l1.93 1.97a.64.64 0 0 0 .91.004l3.446-3.446Z"/></svg>',
LiteConnectIntegration::get_form_entries_count( $form->ID )
);
break;
case 'modified':
$value = get_post_modified_time( get_option( 'date_format' ), false, $form );
break;
case 'author':
$value = '';
$author = get_userdata( $form->post_author );
if ( ! $author ) {
break;
}
$value = $author->display_name;
$user_edit_url = get_edit_user_link( $author->ID );
if ( ! empty( $user_edit_url ) ) {
$value = '<a href="' . esc_url( $user_edit_url ) . '">' . esc_html( $value ) . '</a>';
}
break;
case 'php':
$value = '<code style="display:block;font-size:11px;">if( function_exists( \'wpforms_get\' ) ){ wpforms_get( ' . $form->ID . ' ); }</code>';
break;
default:
$value = '';
}
/**
* Filters the Forms Overview list table culumn value.
*
* @since 1.0.0
*
* @param string $value Column value.
* @param WP_Post $form CPT object as a form representation.
* @param string $column_name Column Name.
*/
return apply_filters( 'wpforms_overview_table_column_value', $value, $form, $column_name ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Filter the default list of hidden columns.
*
* @since 1.8.6
*
* @param string[] $hidden Array of IDs of columns hidden by default.
* @param WP_Screen $screen WP_Screen object of the current screen.
*
* @return string[]
*/
public function default_hidden_columns( $hidden, $screen ) {
if ( $screen->id !== 'toplevel_page_wpforms-overview' ) {
return $hidden;
}
if ( Columns::has_selected_columns() ) {
return [];
}
return [
'tags',
'author',
Locator::COLUMN_NAME,
];
}
/**
* Render the form name column with action links.
*
* @since 1.8.6
*
* @param WP_Post $form Form.
*
* @return string
*/
public function column_name( $form ) {
$title = $this->get_column_name_title( $form );
$states = _post_states( $form, false );
$actions = $this->get_column_name_row_actions( $form );
// Build the row action links and return the value.
return $title . $states . $actions;
}
/**
* Render the form tags column.
*
* @since 1.8.6
*
* @param WP_Post $form Form.
*
* @return string
*/
public function column_tags( $form ) {
return wpforms()->obj( 'forms_tags' )->column_tags( $form );
}
/**
* Get the form name HTML for the form name column.
*
* @since 1.8.6
*
* @param WP_Post $form Form object.
*
* @return string
*/
protected function get_column_name_title( $form ) {
$title = ! empty( $form->post_title ) ? $form->post_title : $form->post_name;
$name = sprintf(
'<span><strong>%s</strong></span>',
esc_html( $title )
);
if ( $this->view === 'trash' ) {
return $name;
}
if ( wpforms_current_user_can( 'view_form_single', $form->ID ) ) {
$name = sprintf(
'<a href="%s" title="%s" class="row-title" target="_blank" rel="noopener noreferrer"><strong>%s</strong></a>',
esc_url( wpforms_get_form_preview_url( $form->ID ) ),
esc_attr__( 'View preview', 'wpforms-lite' ),
esc_html( $title )
);
}
if ( wpforms_current_user_can( 'view_entries_form_single', $form->ID ) ) {
$name = sprintf(
'<a href="%s" title="%s"><strong>%s</strong></a>',
esc_url(
add_query_arg(
[
'view' => 'list',
'form_id' => $form->ID,
],
admin_url( 'admin.php?page=wpforms-entries' )
)
),
esc_attr__( 'View entries', 'wpforms-lite' ),
esc_html( $title )
);
}
if ( wpforms_current_user_can( 'edit_form_single', $form->ID ) ) {
$name = sprintf(
'<a href="%s" title="%s"><strong>%s</strong></a>',
esc_url(
add_query_arg(
[
'view' => 'fields',
'form_id' => $form->ID,
],
admin_url( 'admin.php?page=wpforms-builder' )
)
),
esc_attr__( 'Edit This Form', 'wpforms-lite' ),
esc_html( $title )
);
}
return $name;
}
/**
* Get the row actions HTML for the form name column.
*
* @since 1.8.6
*
* @param WP_Post $form Form object.
*
* @return string
*/
protected function get_column_name_row_actions( $form ) {
// phpcs:disable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Filters row action links on the 'All Forms' admin page.
*
* @since 1.0.0
*
* @param array $row_actions An array of action links for a given form.
* @param WP_Post $form Form object.
*/
return $this->row_actions( apply_filters( 'wpforms_overview_row_actions', [], $form ) );
// phpcs:enable WPForms.Comments.PHPDocHooks.RequiredHookDocumentation, WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Define bulk actions available for our table listing.
*
* @since 1.8.6
*
* @return array
*/
public function get_bulk_actions() {
return wpforms()->obj( 'forms_bulk_actions' )->get_dropdown_items();
}
/**
* Generate the table navigation above or below the table.
*
* @since 1.8.6
*
* @param string $which The location of the table navigation: 'top' or 'bottom'.
*/
protected function display_tablenav( $which ) {
// If there are some forms just call the parent method.
if ( $this->has_items() ) {
parent::display_tablenav( $which );
return;
}
// Otherwise, display bulk actions menu and "0 items" on the right (pagination).
?>
<div class="tablenav <?php echo esc_attr( $which ); ?>">
<div class="alignleft actions bulkactions">
<?php $this->bulk_actions( $which ); ?>
</div>
<?php
$this->extra_tablenav( $which );
if ( $which === 'top' ) {
$this->pagination( $which );
}
?>
<br class="clear" />
</div>
<?php
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @since 1.8.6
*
* @param string $which The location of the table navigation: 'top' or 'bottom'.
*/
protected function extra_tablenav( $which ) {
wpforms()->obj( 'forms_tags' )->extra_tablenav( $which, $this );
wpforms()->obj( 'forms_views' )->extra_tablenav( $which );
}
/**
* Message to be displayed when there are no forms.
*
* @since 1.8.6
*/
public function no_items() {
wpforms()->obj( 'forms_views' )->get_current_view() === 'templates' ?
esc_html_e( 'No form templates found.', 'wpforms-lite' ) :
esc_html_e( 'No forms found.', 'wpforms-lite' );
}
/**
* Fetch and set up the final data for the table.
*
* @since 1.8.6
*/
public function prepare_items() {
// Set up the columns.
$columns = $this->get_columns();
// Hidden columns (none).
$hidden = get_hidden_columns( $this->screen );
// Define which columns can be sorted - form name, author, date.
$sortable = [
'id' => [ 'ID', false ],
'name' => [ 'title', false ],
'author' => [ 'author', false ],
'created' => [ 'date', false ],
];
// Set column headers.
$this->_column_headers = [ $columns, $hidden, $sortable ];
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$page = $this->get_pagenum();
$order = isset( $_GET['order'] ) && $_GET['order'] === 'asc' ? 'ASC' : 'DESC';
$orderby = isset( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'ID';
$per_page = $this->get_items_per_page( 'wpforms_forms_per_page', $this->per_page );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( $orderby === 'date' ) {
$orderby = [
'modified' => $order,
'date' => $order,
];
}
$args = [
'orderby' => $orderby,
'order' => $order,
'nopaging' => false,
'posts_per_page' => $per_page,
'paged' => $page,
'no_found_rows' => false,
'post_status' => 'publish',
];
/**
* Filters the `get_posts()` arguments while preparing items for the forms overview table.
*
* @since 1.7.3
*
* @param array $args Arguments array.
*/
$args = (array) apply_filters( 'wpforms_overview_table_prepare_items_args', $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
// Giddy up.
$this->items = wpforms()->obj( 'form' )->get( '', $args );
$per_page = $args['posts_per_page'] ?? $this->get_items_per_page( 'wpforms_forms_per_page', $this->per_page );
$this->update_count( $args );
$count_current_view = empty( $this->count[ $this->view ] ) ? 0 : $this->count[ $this->view ];
// Finalize pagination.
$this->set_pagination_args(
[
'total_items' => $count_current_view,
'per_page' => $per_page,
'total_pages' => (int) ceil( $count_current_view / $per_page ),
]
);
}
/**
* Calculate and update form counts.
*
* @since 1.8.6
*
* @param array $args Get forms arguments.
*/
private function update_count( $args ) {
/**
* Allow counting forms filtered by a given search criteria.
*
* If result will not contain `all` key, count All Forms without filtering will be performed.
*
* @since 1.7.2
*
* @param array $count Contains counts of forms in different views.
* @param array $args Arguments of the `get_posts`.
*/
$this->count = (array) apply_filters( 'wpforms_overview_table_update_count', [], $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
// We do not need to perform all forms count if we have the result already.
if ( isset( $this->count['all'] ) ) {
return;
}
// Count all forms.
$this->count['all'] = wpforms_current_user_can( 'wpforms_view_others_forms' )
? (int) wp_count_posts( 'wpforms' )->publish
: (int) count_user_posts( get_current_user_id(), 'wpforms', true );
/**
* Filters forms count data after counting all forms.
*
* This filter executes only if the result of `wpforms_overview_table_update_count` filter
* doesn't contain `all` key.
*
* @since 1.7.3
*
* @param array $count Contains counts of forms in different views.
* @param array $args Arguments of the `get_posts`.
*/
$this->count = (array) apply_filters( 'wpforms_overview_table_update_count_all', $this->count, $args ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Display the pagination.
*
* @since 1.8.6
*
* @param string $which The location of the table pagination: 'top' or 'bottom'.
*/
protected function pagination( $which ) {
if ( $this->has_items() ) {
parent::pagination( $which );
return;
}
printf(
'<div class="tablenav-pages one-page">
<span class="displaying-num">%s</span>
</div>',
esc_html__( '0 items', 'wpforms-lite' )
);
}
/**
* Extending the `display_rows()` method in order to add hooks.
*
* @since 1.8.6
*/
public function display_rows() {
/**
* Fires before displaying the table rows.
*
* @since 1.5.6.2
*
* @param ListTable $list_table_obj ListTable instance.
*/
do_action( 'wpforms_admin_overview_before_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
parent::display_rows();
/**
* Fires after displaying the table rows.
*
* @since 1.5.6.2
*
* @param ListTable $list_table_obj ListTable instance.
*/
do_action( 'wpforms_admin_overview_after_rows', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Forms search markup.
*
* @since 1.8.6
*
* @param string $text The 'submit' button label.
* @param string $input_id ID attribute value for the search input field.
*/
public function search_box( $text, $input_id ) {
wpforms()->obj( 'forms_search' )->search_box( $text, $input_id );
}
/**
* Get the list of views available on forms overview table.
*
* @since 1.8.6
*/
protected function get_views() {
return wpforms()->obj( 'forms_views' )->get_views();
}
}
@@ -0,0 +1,361 @@
<?php
namespace WPForms\Admin\Forms;
use WPForms\Admin\Forms\Table\Facades\Columns;
use WPForms\Admin\Traits\HasScreenOptions;
/**
* Primary overview page inside the admin which lists all forms.
*
* @since 1.8.6
*/
class Page {
use HasScreenOptions;
/**
* Overview Table instance.
*
* @since 1.8.6
*
* @var ListTable
*/
private $overview_table;
/**
* Primary class constructor.
*
* @since 1.8.6
*/
public function __construct() {
$this->screen_options_id = 'wpforms_forms_overview_screen_options';
$this->screen_options = [
'pagination' => [
'heading' => esc_html__( 'Pagination', 'wpforms-lite' ),
'options' => [
[
'label' => esc_html__( 'Number of forms per page:', 'wpforms-lite' ),
'option' => 'per_page',
'default' => wpforms()->obj( 'form' )->get_count_per_page(),
'type' => 'number',
'args' => [
'min' => 1,
'max' => 999,
'step' => 1,
'maxlength' => 3,
],
],
],
],
'view' => [
'heading' => esc_html__( 'View', 'wpforms-lite' ),
'options' => [
[
'label' => esc_html__( 'Show form templates', 'wpforms-lite' ),
'option' => 'show_form_templates',
'default' => true,
'type' => 'checkbox',
'checked' => true,
],
],
],
];
$this->init_screen_options( wpforms_is_admin_page( 'overview' ) );
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.6
*/
private function hooks() {
// Reset columns settings.
add_filter( 'manage_toplevel_page_wpforms-overview_columns', [ $this, 'screen_settings_columns' ] );
// Rewrite forms per page value from Form Overview page screen options.
add_filter( 'wpforms_forms_per_page', [ $this, 'get_wpforms_forms_per_page' ] );
}
/**
* Check if the template visibility option is enabled.
*
* @since 1.8.8
*
* @return bool
*/
public function overview_show_form_templates() {
return get_user_option( $this->screen_options_id . '_view_show_form_templates' );
}
/**
* Get forms per page value from Form Overview page screen options.
*
* @since 1.8.8
*
* @return int
*/
public function get_wpforms_forms_per_page() {
return get_user_option( $this->screen_options_id . '_pagination_per_page' );
}
/**
* Determine if the user is viewing the overview page, if so, party on.
*
* @since 1.8.6
*/
public function init() { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
// Only load if we are actually on the overview page.
if ( ! wpforms_is_admin_page( 'overview' ) ) {
return;
}
// Avoid recursively include _wp_http_referer in the REQUEST_URI.
$this->remove_referer();
add_action( 'current_screen', [ $this, 'init_overview_table' ], 5 );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
add_action( 'wpforms_admin_page', [ $this, 'field_column_setting' ] );
/**
* Fires after the form overview page initialization.
*
* @since 1.0.0
*/
do_action( 'wpforms_overview_init' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Init overview table class.
*
* @since 1.8.6
*/
public function init_overview_table() {
$this->overview_table = ListTable::get_instance();
}
/**
* Remove previous `_wp_http_referer` variable from the REQUEST_URI.
*
* @since 1.8.6
*/
private function remove_referer() {
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$_SERVER['REQUEST_URI'] = remove_query_arg( '_wp_http_referer', wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
}
/**
* Add per-page screen option to the Forms table.
*
* @since 1.8.6
*
* @depecated 1.8.8 Use HasScreenOptions trait instead.
*/
public function screen_options() {
_deprecated_function( __METHOD__, '1.8.8 of the WPForms plugin' );
}
/**
* Filter screen settings columns data.
*
* @since 1.8.6
*
* @param array $columns Columns.
*
* @return array
* @noinspection PhpMissingParamTypeInspection
*/
public function screen_settings_columns( $columns ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
return [];
}
/**
* Enqueue assets for the overview page.
*
* @since 1.8.6
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-htmx',
WPFORMS_PLUGIN_URL . 'assets/lib/htmx.min.js',
[],
WPFORMS_VERSION,
true
);
wp_enqueue_script(
'wpforms-admin-forms-overview',
WPFORMS_PLUGIN_URL . "assets/js/admin/forms/overview{$min}.js",
[ 'jquery', 'underscore', 'wpforms-htmx' ],
WPFORMS_VERSION,
true
);
wp_enqueue_style(
'wpforms-admin-list-table-ext',
WPFORMS_PLUGIN_URL . "assets/css/admin-list-table-ext{$min}.css",
[],
WPFORMS_VERSION
);
wp_enqueue_script(
'wpforms-admin-list-table-ext',
WPFORMS_PLUGIN_URL . "assets/js/admin/share/list-table-ext{$min}.js",
[ 'jquery', 'jquery-ui-sortable', 'underscore', 'wpforms-admin', 'wpforms-multiselect-checkboxes' ],
WPFORMS_VERSION,
true
);
/**
* Fires after enqueue the forms overview page assets.
*
* @since 1.0.0
*/
do_action( 'wpforms_overview_enqueue' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Determine if it is an empty state.
*
* @since 1.8.6
*/
private function is_empty_state() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
return empty( $this->overview_table->items ) &&
! isset( $_GET['search']['term'] ) &&
! isset( $_GET['status'] ) &&
! isset( $_GET['tags'] ) &&
array_sum( wpforms()->obj( 'forms_views' )->get_count() ) === 0;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* Build the output for the overview page.
*
* @since 1.8.6
*/
public function output() {
?>
<div id="wpforms-overview" class="wrap wpforms-admin-wrap">
<h1 class="page-title">
<?php esc_html_e( 'Forms Overview', 'wpforms-lite' ); ?>
<?php if ( wpforms_current_user_can( 'create_forms' ) ) : ?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforms-builder&view=setup' ) ); ?>" class="page-title-action wpforms-btn add-new-h2 wpforms-btn-orange" data-action="add">
<svg viewBox="0 0 14 14" class="page-title-action-icon">
<path d="M14 5.385v3.23H8.615V14h-3.23V8.615H0v-3.23h5.385V0h3.23v5.385H14Z"/>
</svg>
<span class="page-title-action-text"><?php esc_html_e( 'Add New', 'wpforms-lite' ); ?></span>
</a>
<?php endif; ?>
</h1>
<div class="wpforms-admin-content">
<?php
$this->overview_table->prepare_items();
/**
* Fires before forms overview list table output.
*
* @since 1.6.0.1
*/
do_action( 'wpforms_admin_overview_before_table' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
if ( $this->is_empty_state() ) {
// Output no forms screen.
echo wpforms_render( 'admin/empty-states/no-forms' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
?>
<form id="wpforms-overview-table" method="get" action="<?php echo esc_url( admin_url( 'admin.php?page=wpforms-overview' ) ); ?>">
<input type="hidden" name="post_type" value="wpforms" />
<input type="hidden" name="page" value="wpforms-overview" />
<?php
$this->overview_table->search_box( esc_html__( 'Search Forms', 'wpforms-lite' ), 'wpforms-overview-search' );
$this->overview_table->views();
$this->overview_table->display();
?>
</form>
<?php } ?>
</div>
</div>
<?php
}
/**
* Settings for field column personalization.
*
* @since 1.8.6
*/
public function field_column_setting() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->get_columns_multiselect();
}
/**
* Get columns multiselect menu.
*
* @since 1.8.6
*
* @return string HTML menu markup.
*/
private function get_columns_multiselect(): string {
$columns = Columns::get_columns();
$selected_keys = Columns::get_selected_columns_keys();
$options = '';
$html = '
<div id="wpforms-list-table-ext-edit-columns-select-container" class="wpforms-hidden wpforms-forms-overview-page">
<form method="post" action="">
<input type="hidden" name="action" value="wpforms_admin_forms_overview_save_columns_order"/>
<select name="fields[]"
id="wpforms-forms-table-edit-columns-select"
class="wpforms-forms-table-edit-columns-select wpforms-list-table-ext-edit-columns-select"
multiple="multiple">
<optgroup label="' . esc_html__( 'Columns', 'wpforms-lite' ) . '">
%s
</optgroup>
</select>
</form>
</div>
';
foreach ( $columns as $column ) {
$selected = in_array( $column->get_id(), $selected_keys, true ) ? 'selected' : '';
$disabled = $column->is_readonly() ? 'disabled="true"' : '';
$options .= sprintf( '<option value="%s" %s %s>%s</option>', esc_attr( $column->get_id() ), $selected, $disabled, esc_html( $column->get_label() ) );
}
return sprintf( $html, $options );
}
}
@@ -0,0 +1,268 @@
<?php
namespace WPForms\Admin\Forms;
/**
* Search Forms feature.
*
* @since 1.7.2
*/
class Search {
/**
* Current search term.
*
* @since 1.7.2
*
* @var string
*/
private $term;
/**
* Current search term escaped.
*
* @since 1.7.2
*
* @var string
*/
private $term_escaped;
/**
* Determine if the class is allowed to load.
*
* @since 1.7.2
*
* @return bool
*/
private function allow_load() {
// Load only on the `All Forms` admin page and only if the search should be performed.
return wpforms_is_admin_page( 'overview' ) && $this->is_search();
}
/**
* Initialize class.
*
* @since 1.7.2
*/
public function init() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$this->term = isset( $_GET['search']['term'] ) ? sanitize_text_field( wp_unslash( $_GET['search']['term'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$this->term_escaped = isset( $_GET['search']['term'] ) ? esc_html( wp_unslash( $_GET['search']['term'] ) ) : '';
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.2
*/
private function hooks() {
// Use filter to add the search term to the get forms arguments.
add_filter( 'wpforms_get_multiple_forms_args', [ $this, 'get_forms_args' ] );
// Encapsulate search into posts_where.
add_action( 'wpforms_form_handler_get_multiple_before_get_posts', [ $this, 'before_get_posts' ] );
add_action( 'wpforms_form_handler_get_multiple_after_get_posts', [ $this, 'after_get_posts' ], 10, 2 );
}
/**
* Determine whether a search is performing.
*
* @since 1.7.2
*
* @return bool
*/
private function is_search() {
return ! wpforms_is_empty_string( $this->term_escaped );
}
/**
* Pass the search term to the arguments array.
*
* @since 1.7.2
*
* @param array $args Get posts arguments.
*
* @return array
*/
public function get_forms_args( $args ) {
if ( is_numeric( $this->term ) ) {
$args['post__in'] = [ absint( $this->term ) ];
} else {
$args['search']['term'] = $this->term;
$args['search']['term_escaped'] = $this->term_escaped;
}
return $args;
}
/**
* Before get_posts() call routine.
*
* @since 1.7.2
*
* @param array $args Arguments of the `get_posts()`.
*/
public function before_get_posts( $args ) {
// The `posts_where` hook is very general and has broad usage across the WP core and tons of plugins.
// Therefore, in order to do not break something,
// we should add this hook right before the call of `get_posts()` inside \WPForms_Form_Handler::get_multiple().
add_filter( 'posts_where', [ $this, 'search_by_term_where' ], 10, 2 );
}
/**
* After get_posts() call routine.
*
* @since 1.7.2
*
* @param array $args Arguments of the get_posts().
* @param array $forms Forms data. Result of getting multiple forms.
*/
public function after_get_posts( $args, $forms ) {
// The `posts_where` hook is very general and has broad usage across the WP core and tons of plugins.
// Therefore, in order to do not break something,
// we should remove this hook right after the call of `get_posts()` inside \WPForms_Form_Handler::get_multiple().
remove_filter( 'posts_where', [ $this, 'search_by_term_where' ] );
}
/**
* Modify the WHERE clause of the SQL query in order to search forms by given term.
*
* @since 1.7.2
*
* @param string $where WHERE clause.
* @param \WP_Query $wp_query The WP_Query instance.
*
* @return string
*/
public function search_by_term_where( $where, $wp_query ) {
if ( is_numeric( $this->term ) ) {
return $where;
}
global $wpdb;
// When user types only HTML tag (<section> for example), the sanitized term we will be empty.
// In this case, it's better to return an empty result set than all the forms. It's not the same as the empty search term.
if ( wpforms_is_empty_string( $this->term ) && ! wpforms_is_empty_string( $this->term_escaped ) ) {
$where .= ' AND 1<>1';
}
if ( wpforms_is_empty_string( $this->term ) ) {
return $where;
}
// Prepare the WHERE clause to search form title and description.
$where .= $wpdb->prepare(
" AND (
$wpdb->posts.post_title LIKE %s OR
$wpdb->posts.post_excerpt LIKE %s
)",
'%' . $wpdb->esc_like( esc_html( $this->term ) ) . '%',
'%' . $wpdb->esc_like( $this->term ) . '%'
);
return $where;
}
/**
* Forms search markup.
*
* @since 1.7.2
*
* @param string $text The 'submit' button label.
* @param string $input_id ID attribute value for the search input field.
*/
public function search_box( $text, $input_id ) {
$search_term = wpforms_is_empty_string( $this->term ) ? $this->term_escaped : $this->term;
// Display search reset block.
$this->search_reset_block( $search_term );
// Display search box.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/forms/search-box',
[
'term_input_id' => $input_id . '-term',
'text' => $text,
'search_term' => $search_term,
],
true
);
}
/**
* Forms search reset block.
*
* @since 1.7.2
*
* @param string $search_term Current search term.
*/
private function search_reset_block( $search_term ) {
if ( wpforms_is_empty_string( $search_term ) ) {
return;
}
$views = wpforms()->obj( 'forms_views' );
$count = $views->get_count();
$view = $views->get_current_view();
$count['all'] = ! empty( $count['all'] ) ? $count['all'] : 0;
$message = sprintf(
wp_kses( /* translators: %1$d - number of forms found, %2$s - search term. */
_n(
'Found <strong>%1$d form</strong> containing <em>"%2$s"</em>',
'Found <strong>%1$d forms</strong> containing <em>"%2$s"</em>',
(int) $count['all'],
'wpforms-lite'
),
[
'strong' => [],
'em' => [],
]
),
(int) $count['all'],
esc_html( $search_term )
);
/**
* Filters the message in the search reset block.
*
* @since 1.7.3
*
* @param string $message Message text.
* @param string $search_term Search term.
* @param array $count Count forms in different views.
* @param string $view Current view.
*/
$message = apply_filters( 'wpforms_admin_forms_search_search_reset_block_message', $message, $search_term, $count, $view );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/forms/search-reset',
[
'message' => $message,
],
true
);
}
}
@@ -0,0 +1,12 @@
<?php
namespace WPForms\Admin\Forms\Table\DataObjects;
use WPForms\Admin\Base\Tables\DataObjects\ColumnBase;
/**
* Column data object.
*
* @since 1.8.6
*/
class Column extends ColumnBase {}
@@ -0,0 +1,315 @@
<?php
namespace WPForms\Admin\Forms\Table\Facades;
use WPForms\Admin\Base\Tables\Facades\ColumnsBase;
use WPForms\Admin\Forms\Table\DataObjects\Column;
use WPForms\Integrations\LiteConnect\LiteConnect;
/**
* Column facade class.
*
* Hides the complexity of columns' collection behind a simple interface.
*
* @since 1.8.6
*/
class Columns extends ColumnsBase {
/**
* Saved columns order user meta name.
*
* @since 1.8.6
*/
const COLUMNS_USER_META_NAME = 'wpforms_overview_table_columns';
/**
* Legacy saved columns order user meta name.
*
* @since 1.8.6
*/
const LEGACY_COLUMNS_USER_META_NAME = 'managetoplevel_page_wpforms-overviewcolumnshidden';
/**
* Get columns.
*
* Returns all possible columns for the Forms table.
*
* @since 1.8.6
*
* @return Column[] Array of columns as objects.
*/
protected static function get_all(): array {
static $columns = null;
if ( ! $columns ) {
$columns = self::get_columns();
}
return $columns;
}
/**
* Get forms' list table columns.
*
* @since 1.8.6
*
* @return Column[] Array of columns as objects.
*/
public static function get_columns(): array {
$columns_data = [
'id' => [
'label' => esc_html__( 'ID', 'wpforms-lite' ),
],
'name' => [
'label' => esc_html__( 'Name', 'wpforms-lite' ),
'readonly' => true,
],
'tags' => [
'label' => esc_html__( 'Tags', 'wpforms-lite' ),
],
'author' => [
'label' => esc_html__( 'Author', 'wpforms-lite' ),
],
'shortcode' => [
'label' => esc_html__( 'Shortcode', 'wpforms-lite' ),
],
'created' => [
'label' => esc_html__( 'Date', 'wpforms-lite' ),
],
'entries' => [
'label' => esc_html__( 'Entries', 'wpforms-lite' ),
],
];
// In Lite, we should not show Entries column if Lite Connect is not enabled.
if ( ! wpforms()->is_pro() && ! ( LiteConnect::is_allowed() && LiteConnect::is_enabled() ) ) {
unset( $columns_data['entries'] );
}
/**
* Filters the forms overview table columns data.
*
* @since 1.8.6
*
* @param array[] $columns Columns data.
*/
$columns_data = apply_filters( 'wpforms_admin_forms_table_facades_columns_data', $columns_data );
$columns_data = self::set_columns_data_defaults( $columns_data );
$columns = [];
foreach ( $columns_data as $id => $column ) {
$columns[ $id ] = new Column( $id, $column );
}
return $columns;
}
/**
* Get columns' keys for the columns which user selected to be displayed.
*
* It returns an array of keys in the order they should be displayed.
* It returns draggable and non-draggable columns.
*
* @since 1.8.6
*
* @return array
*/
public static function get_selected_columns_keys(): array {
$user_id = get_current_user_id();
$user_meta_columns = get_user_meta( $user_id, self::COLUMNS_USER_META_NAME, true );
$user_meta_legacy_columns_hidden = get_user_meta( $user_id, self::LEGACY_COLUMNS_USER_META_NAME, true );
$user_meta_columns = $user_meta_columns ? $user_meta_columns : [];
$user_meta_legacy_columns_hidden = $user_meta_legacy_columns_hidden ? $user_meta_legacy_columns_hidden : [];
// Make form id column hidden by default.
if ( empty( $user_meta_columns ) ) {
$user_meta_legacy_columns_hidden[] = 'id';
}
// Always include readonly columns.
$user_meta_columns = array_unique( array_merge( $user_meta_columns, self::get_readonly_columns_keys() ) );
if ( ! empty( $user_meta_columns ) && empty( $user_meta_legacy_columns_hidden ) ) {
return $user_meta_columns;
}
// If custom order is not saved, let's check if there is a legacy user meta-option.
// It is a kind of migration from legacy user meta-option to the new one.
$user_meta_columns = array_diff( array_keys( self::get_all() ), $user_meta_legacy_columns_hidden );
// Update user meta option.
if ( update_user_meta( $user_id, self::COLUMNS_USER_META_NAME, $user_meta_columns ) ) {
// Remove legacy user meta-option.
delete_user_meta( $user_id, self::LEGACY_COLUMNS_USER_META_NAME );
}
return $user_meta_columns;
}
/**
* Get draggable columns ordered keys.
*
* It will return custom order if user has already saved it, otherwise it will return default order.
*
* @since 1.8.6
*
* @return array
*/
private static function get_draggable_ordered_keys(): array {
// First, let's check if user has already saved custom order.
$custom_order = self::get_selected_columns_keys();
$all_columns = self::get_all();
if ( $custom_order ) {
// If a user has saved custom order, let's filter out columns which are not draggable.
return array_filter(
$custom_order,
static function ( $column ) use ( $all_columns ) {
return isset( $all_columns[ $column ] ) && $all_columns[ $column ]->is_draggable();
}
);
}
// If a user has not saved custom order, let's use the default order.
$draggable = array_filter(
$all_columns,
static function ( $column ) {
return $column->is_draggable();
}
);
return array_keys( $draggable );
}
/**
* Save columns keys array into user meta.
*
* @since 1.8.6
*
* @param array $columns_keys Array of columns keys in desired display order.
*
* @return bool
*/
public static function sanitize_and_save_columns( array $columns_keys ): bool {
$columns_keys = array_map( [ __CLASS__, 'sanitize_column_key' ], $columns_keys );
$columns_keys = array_filter( $columns_keys, [ __CLASS__, 'validate_column_key' ] );
// Add readonly columns.
$columns_keys = array_unique( array_merge( $columns_keys, self::get_readonly_columns_keys() ) );
$user_id = get_current_user_id();
$user_meta_columns = get_user_meta( $user_id, self::COLUMNS_USER_META_NAME, true );
// If user has already saved custom order, let's check if it has been changed.
if ( $user_meta_columns === $columns_keys ) {
return true;
}
// Update user meta option.
return update_user_meta( $user_id, self::COLUMNS_USER_META_NAME, $columns_keys );
}
/**
* Sanitize column key.
*
* @since 1.8.6
*
* @param string $key Column key.
*
* @return string
*/
public static function sanitize_column_key( string $key ): string {
return sanitize_key( $key );
}
/**
* Get columns' data ready to use in the list table object.
*
* @since 1.8.6
*
* @return array
*/
public static function get_list_table_columns(): array {
$columns = [
'cb' => '<input type="checkbox" />',
];
$order = self::get_draggable_ordered_keys();
$all_columns = self::get_all();
foreach ( $order as $column_id ) {
$columns[ $column_id ] = $all_columns[ $column_id ]->get_label_html();
}
/**
* Filters the forms overview table columns.
*
* @since 1.0.0
*
* @param array $columns Columns data.
*/
$columns = apply_filters( 'wpforms_overview_table_columns', $columns ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
// Add empty column for the cog icon.
$columns['cog'] = '';
return $columns;
}
/**
* Get readonly columns keys.
*
* @since 1.8.6
*
* @return array
*/
private static function get_readonly_columns_keys(): array {
$readonly = array_filter(
self::get_all(),
static function ( $column ) {
return $column->is_readonly();
}
);
return array_keys( $readonly );
}
/**
* Set columns data defaults.
*
* @since 1.8.6
*
* @param array $columns_data Columns data.
*
* @return array
*/
private static function set_columns_data_defaults( array $columns_data ): array {
return array_map(
static function ( $column ) {
$column['type'] = $column['type'] ?? '';
$column['draggable'] = $column['draggable'] ?? true;
$column['label_html'] = $column['label_html'] ?? '';
$column['readonly'] = $column['readonly'] ?? false;
return $column;
},
$columns_data
);
}
}
@@ -0,0 +1,613 @@
<?php
namespace WPForms\Admin\Forms;
use WP_Post;
use WPForms_Form_Handler;
/**
* Tags on All Forms page.
*
* @since 1.7.5
*/
class Tags {
/**
* Current tags filter.
*
* @since 1.7.5
*
* @var array
*/
private $tags_filter;
/**
* Current view slug.
*
* @since 1.7.5
*
* @var string
*/
private $current_view;
/**
* Base URL.
*
* @since 1.7.5
*
* @var string
*/
private $base_url;
/**
* Determine if the class is allowed to load.
*
* @since 1.7.5
*
* @return bool
*/
private function allow_load() {
// Load only on the `All Forms` admin page.
return wpforms_is_admin_page( 'overview' );
}
/**
* Initialize class.
*
* @since 1.7.5
*/
public function init() {
// In case of AJAX call we need to initialize base URL only.
if ( wp_doing_ajax() ) {
$this->base_url = admin_url( 'admin.php?page=wpforms-overview' );
}
if ( ! $this->allow_load() ) {
return;
}
$this->update_view_vars();
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.5
*/
private function hooks() {
add_action( 'init', [ $this, 'update_tags_filter' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
add_action( 'wpforms_admin_overview_before_rows', [ $this, 'bulk_edit_tags' ] );
add_filter( 'wpforms_get_multiple_forms_args', [ $this, 'get_forms_args' ] );
add_filter( 'wpforms_admin_forms_bulk_actions_get_dropdown_items', [ $this, 'add_bulk_action' ], 10, 2 );
add_filter( 'wpforms_overview_table_columns', [ $this, 'filter_columns' ] );
}
/**
* Init view-related variables.
*
* @since 1.7.5
*/
private function update_view_vars() {
$views_object = wpforms()->obj( 'forms_views' );
$this->current_view = $views_object->get_current_view();
$view_config = $views_object->get_view_by_slug( $this->current_view );
$this->base_url = remove_query_arg(
[ 'tags', 'search', 'action', 'action2', '_wpnonce', 'form_id', 'paged', '_wp_http_referer' ],
$views_object->get_base_url()
);
// Base URL should contain variable according to the current view.
if (
isset( $view_config['get_var'], $view_config['get_var_value'] ) && $this->current_view !== 'all'
) {
$this->base_url = add_query_arg( $view_config['get_var'], $view_config['get_var_value'], $this->base_url );
}
// Base URL fallback.
$this->base_url = empty( $this->base_url ) ? admin_url( 'admin.php?page=wpforms-overview' ) : $this->base_url;
}
/**
* Update tags filter value.
*
* @since 1.7.5
*/
public function update_tags_filter() {
// Do not need to update this property while doing AJAX.
if ( wp_doing_ajax() ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$tags = isset( $_GET['tags'] ) ? sanitize_text_field( wp_unslash( rawurldecode( $_GET['tags'] ) ) ) : '';
$tags_slugs = explode( ',', $tags );
$tags_filter = array_filter(
self::get_all_tags_choices(),
static function( $tag ) use ( $tags_slugs ) {
return in_array( trim( rawurldecode( $tag['slug'] ) ), $tags_slugs, true );
}
);
$this->tags_filter = array_map( 'absint', wp_list_pluck( $tags_filter, 'value' ) );
}
/**
* Enqueue assets.
*
* @since 1.7.5
*/
public function enqueues() {
wp_enqueue_script(
'wpforms-admin-forms-overview-choicesjs',
WPFORMS_PLUGIN_URL . 'assets/lib/choices.min.js',
[],
'10.2.0',
true
);
wp_localize_script(
'wpforms-admin-forms-overview-choicesjs',
'wpforms_admin_forms_overview',
[
'choicesjs_config' => self::get_choicesjs_config(),
'edit_tags_form' => $this->get_column_tags_form(),
'all_tags_choices' => self::get_all_tags_choices(),
'strings' => $this->get_localize_strings(),
]
);
}
/**
* Get Choices.js configuration.
*
* @since 1.7.5
*/
public static function get_choicesjs_config() {
return [
'removeItemButton' => true,
'shouldSort' => false,
'loadingText' => esc_html__( 'Loading...', 'wpforms-lite' ),
'noResultsText' => esc_html__( 'No results found', 'wpforms-lite' ),
'noChoicesText' => esc_html__( 'No tags to choose from', 'wpforms-lite' ),
'searchEnabled' => true,
'searchChoices' => true,
'searchFloor' => 1,
'searchResultLimit' => 100,
'searchFields' => [ 'label' ],
'allowHTML' => true,
// These `fuseOptions` options enable the search of chars not only from the beginning of the tags.
'fuseOptions' => [
'threshold' => 0.1,
'distance' => 1000,
'location' => 2,
],
];
}
/**
* Get all tags (terms) as items for Choices.js.
*
* @since 1.7.5
*
* @return array
*/
public static function get_all_tags_choices() {
static $choices = null;
if ( is_array( $choices ) ) {
return $choices;
}
$choices = [];
$tags = get_terms(
[
'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY,
'hide_empty' => false,
]
);
foreach ( $tags as $tag ) {
$choices[] = [
'value' => (string) $tag->term_id,
'slug' => $tag->slug,
'label' => sanitize_term_field( 'name', $tag->name, $tag->term_id, WPForms_Form_Handler::TAGS_TAXONOMY, 'display' ),
'count' => (int) $tag->count,
];
}
return $choices;
}
/**
* Determine if the Tags column is hidden.
*
* @since 1.7.5
*
* @return bool
*/
private function is_tags_column_hidden() {
$overview_table = ListTable::get_instance();
$columns = $overview_table->__call( 'get_column_info', [] );
return isset( $columns[1] ) && in_array( 'tags', $columns[1], true );
}
/**
* Get localize strings.
*
* @since 1.7.5
*/
private function get_localize_strings() {
return [
'nonce' => wp_create_nonce( 'wpforms-admin-forms-overview-nonce' ),
'is_tags_column_hidden' => $this->is_tags_column_hidden(),
'base_url' => admin_url( 'admin.php?' . wp_parse_url( $this->base_url, PHP_URL_QUERY ) ),
'add_new_tag' => esc_html__( 'Press Enter or "," key to add new tag', 'wpforms-lite' ),
'error' => esc_html__( 'Something wrong. Please try again later.', 'wpforms-lite' ),
'all_tags' => esc_html__( 'All Tags', 'wpforms-lite' ),
'bulk_edit_one_form' => wp_kses(
__( '<strong>1 form</strong> selected for Bulk Edit.', 'wpforms-lite' ),
[ 'strong' => [] ]
),
'bulk_edit_n_forms' => wp_kses( /* translators: %d - number of forms selected for Bulk Edit. */
__( '<strong>%d forms</strong> selected for Bulk Edit.', 'wpforms-lite' ),
[ 'strong' => [] ]
),
'manage_tags_title' => esc_html__( 'Manage Tags', 'wpforms-lite' ),
'manage_tags_desc' => esc_html__( 'Delete tags that you\'re no longer using. Deleting a tag will remove it from a form, but will not delete the form itself.', 'wpforms-lite' ),
'manage_tags_save' => esc_html__( 'Delete Tags', 'wpforms-lite' ),
'manage_tags_one_tag' => wp_kses(
__( 'You have <strong>1 tag</strong> selected for deletion.', 'wpforms-lite' ),
[ 'strong' => [] ]
),
'manage_tags_n_tags' => wp_kses( /* translators: %d - number of forms selected for Bulk Edit. */
__( 'You have <strong>%d tags</strong> selected for deletion.', 'wpforms-lite' ),
[ 'strong' => [] ]
),
'manage_tags_no_tags' => wp_kses(
__( 'There are no tags to delete.<br>Please create at least one by adding it to any form.', 'wpforms-lite' ),
[ 'br' => [] ]
),
'manage_tags_one_deleted' => esc_html__( '1 tag was successfully deleted.', 'wpforms-lite' ),
/* translators: %d - number of deleted tags. */
'manage_tags_n_deleted' => esc_html__( '%d tags were successfully deleted.', 'wpforms-lite' ),
'manage_tags_result_title' => esc_html__( 'Almost done!', 'wpforms-lite' ),
'manage_tags_result_text' => esc_html__( 'In order to update the tags in the forms list, please refresh the page.', 'wpforms-lite' ),
'manage_tags_btn_refresh' => esc_html__( 'Refresh', 'wpforms-lite' ),
];
}
/**
* Determine if tags are editable.
*
* @since 1.7.5
*
* @param int|null $form_id Form ID.
*
* @return bool
*/
private function is_editable( $form_id = null ) {
if ( $this->current_view === 'trash' ) {
return false;
}
if ( ! empty( $form_id ) && ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) {
return false;
}
if ( empty( $form_id ) && ! wpforms_current_user_can( 'edit_forms' ) ) {
return false;
}
return true;
}
/**
* Generate Tags column markup.
*
* @since 1.7.5
*
* @param WP_Post $form Form.
*
* @return string
*/
public function column_tags( $form ) {
$terms = get_the_terms( $form->ID, WPForms_Form_Handler::TAGS_TAXONOMY );
$data = $this->get_tags_data( $terms );
return $this->get_column_tags_links( $data['tags_links'], $data['tags_ids'], $form->ID ) . $this->get_column_tags_form( $data['tags_options'] );
}
/**
* Generate tags data.
*
* @since 1.7.5
*
* @param array $terms Tags terms.
*
* @return array
*/
public function get_tags_data( $terms ) {
if ( ! is_array( $terms ) ) {
$taxonomy_object = get_taxonomy( WPForms_Form_Handler::TAGS_TAXONOMY );
return [
'tags_links' => sprintf(
'<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
esc_html( isset( $taxonomy_object->labels->no_terms ) ? $taxonomy_object->labels->no_terms : '—' )
),
'tags_ids' => '',
'tags_options' => '',
];
}
$tags_links = [];
$tags_ids = [];
$tags_options = [];
$terms = empty( $terms ) ? [] : (array) $terms;
foreach ( $terms as $tag ) {
$filter_url = add_query_arg(
'tags',
rawurlencode( $tag->slug ),
$this->base_url
);
$tags_links[] = sprintf(
'<a href="%1$s">%2$s</a>',
esc_url( $filter_url ),
esc_html( $tag->name )
);
$tags_ids[] = $tag->term_id;
$tags_options[] = sprintf(
'<option value="%1$s" selected>%2$s</option>',
esc_attr( $tag->term_id ),
esc_html( $tag->name )
);
}
return [
/* translators: used between list items, there is a space after the comma. */
'tags_links' => implode( __( ', ', 'wpforms-lite' ), $tags_links ),
'tags_ids' => implode( ',', array_filter( $tags_ids ) ),
'tags_options' => implode( '', $tags_options ),
];
}
/**
* Get form tags links list markup.
*
* @since 1.7.5
*
* @param string $tags_links Tags links.
* @param string $tags_ids Tags IDs.
* @param int $form_id Form ID.
*
* @return string
*/
private function get_column_tags_links( $tags_links = '', $tags_ids = '', $form_id = 0 ) {
$edit_link = '';
if ( $this->is_editable( $form_id ) ) {
$edit_link = sprintf(
'<a href="#" class="wpforms-column-tags-edit">%s</a>',
esc_html__( 'Edit', 'wpforms-lite' )
);
}
return sprintf(
'<div class="wpforms-column-tags-links" data-form-id="%1$d" data-is-editable="%2$s" data-tags="%3$s">
<div class="wpforms-column-tags-links-list">%4$s</div>
%5$s
</div>',
absint( $form_id ),
$this->is_editable( $form_id ) ? '1' : '0',
esc_attr( $tags_ids ),
$tags_links,
$edit_link
);
}
/**
* Get edit tags form markup in the Tags column.
*
* @since 1.7.5
*
* @param string $tags_options Tags options.
*
* @return string
*/
private function get_column_tags_form( $tags_options = '' ) {
return sprintf(
'<div class="wpforms-column-tags-form wpforms-hidden">
<select multiple>%1$s</select>
<i class="dashicons dashicons-dismiss wpforms-column-tags-edit-cancel" title="%2$s"></i>
<i class="dashicons dashicons-yes-alt wpforms-column-tags-edit-save" title="%3$s"></i>
<i class="wpforms-spinner spinner wpforms-hidden"></i>
</div>',
$tags_options,
esc_attr__( 'Cancel', 'wpforms-lite' ),
esc_attr__( 'Save changes', 'wpforms-lite' )
);
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @since 1.7.5
*
* @param string $which The location of the table navigation: 'top' or 'bottom'.
* @param ListTable $overview_table Instance of the ListTable class.
*/
public function extra_tablenav( $which, $overview_table ) {
if ( $this->current_view === 'trash' ) {
return;
}
$all_tags = self::get_all_tags_choices();
$is_column_hidden = $this->is_tags_column_hidden();
$is_hidden = $is_column_hidden || empty( $all_tags );
$tags_options = '';
if ( $this->is_filtered() ) {
$tags = get_terms(
[
'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY,
'hide_empty' => false,
'include' => $this->tags_filter,
]
);
foreach ( $tags as $tag ) {
$tags_options .= sprintf(
'<option value="%1$s" selected>%2$s</option>',
esc_attr( $tag->term_id ),
esc_html( $tag->name )
);
}
}
printf(
'<div class="wpforms-tags-filter %1$s">
<select multiple size="1" data-tags-filter="1">
<option placeholder>%2$s</option>
%3$s
</select>
<button type="button" class="button">%4$s</button>
</div>
<button type="button" class="button wpforms-manage-tags %1$s">%5$s</button>',
esc_attr( $is_hidden ? 'wpforms-hidden' : '' ),
esc_html( empty( $tags_options ) ? __( 'All Tags', 'wpforms-lite' ) : '' ),
wp_kses(
$tags_options,
[
'option' => [
'value' => [],
'selected' => [],
],
]
),
esc_attr__( 'Filter', 'wpforms-lite' ),
esc_attr__( 'Manage Tags', 'wpforms-lite' )
);
}
/**
* Render and display Bulk Edit Tags form.
*
* @since 1.7.5
*
* @param ListTable $list_table Overview list table object.
*/
public function bulk_edit_tags( $list_table ) {
$columns = $list_table->get_columns();
echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'admin/forms/bulk-edit-tags',
[
'columns' => count( $columns ),
],
true
);
}
/**
* Determine whether a filtering is performing.
*
* @since 1.7.5
*
* @return bool
*/
private function is_filtered() {
return ! empty( $this->tags_filter );
}
/**
* Pass the tags to the arguments array.
*
* @since 1.7.5
*
* @param array $args Get posts arguments.
*
* @return array
*/
public function get_forms_args( $args ) {
if ( $args['post_status'] === 'trash' || ! $this->is_filtered() ) {
return $args;
}
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
$args['tax_query'] = [
[
'taxonomy' => WPForms_Form_Handler::TAGS_TAXONOMY,
'field' => 'term_id',
'terms' => $this->tags_filter,
],
];
return $args;
}
/**
* Add item to Bulk Actions dropdown.
*
* @since 1.7.5
*
* @param array $items Dropdown items.
*
* @return array
*/
public function add_bulk_action( $items ) {
if ( $this->is_editable() ) {
$items['edit_tags'] = esc_html__( 'Edit Tags', 'wpforms-lite' );
}
return $items;
}
/**
* Filter list table columns.
*
* @since 1.7.5
*
* @param string[] $columns Array of columns.
*
* @return array
*/
public function filter_columns( $columns ) {
if ( $this->current_view === 'trash' ) {
unset( $columns['tags'] );
}
return $columns;
}
}
@@ -0,0 +1,440 @@
<?php
namespace WPForms\Admin\Forms;
use WP_Post;
use WPForms\Admin\Notice;
use WPForms\Pro\Tasks\Actions\PurgeTemplateEntryTask;
/**
* User Templates class.
*
* @since 1.8.8
*/
class UserTemplates {
/**
* Initialize class.
*
* @since 1.8.8
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.8
*/
public function hooks() {
add_action( 'init', [ $this, 'register_post_type' ] );
// Add template states.
add_filter( 'display_post_states', [ $this, 'add_template_states' ], 10, 2 );
// Modify get form args on the overview page.
add_filter( 'wpforms_get_form_args', [ $this, 'add_template_post_type' ] );
// Modify Show Templates user option.
add_filter( 'get_user_option_wpforms_forms_overview_show_form_templates', [ $this, 'get_forms_overview_show_form_templates_option' ] );
// Disable payment processing for user templates.
add_filter( 'wpforms_process_before_form_data', [ $this, 'process_before_form_data' ] );
// Add user templates to the form templates list.
add_filter( 'wpforms_form_templates', [ $this, 'add_form_templates' ] );
// AJAX handler for deleting user templates.
add_action( 'wp_ajax_wpforms_user_template_remove', [ $this, 'ajax_remove_user_template' ] );
// Disable Lite Connect integration for user templates on form submission.
add_action( 'wpforms_process', [ $this, 'process_entry' ], 10, 3 );
if ( wpforms()->is_pro() ) {
// Add notices about entry(ies) being purged.
add_action( 'admin_notices', [ $this, 'get_template_entries_notice' ] );
add_action( 'admin_notices', [ $this, 'get_template_entry_notice' ] );
// Add purge entry task.
add_filter( 'wpforms_tasks_get_tasks', [ $this, 'add_purge_entry_task' ] );
// Disable edit entry for templates.
add_filter( 'wpforms_current_user_can', [ $this, 'disable_edit_entry' ], 10, 3 );
}
}
/**
* Register the `wpforms-template` post type.
*
* @since 1.8.8
*/
public function register_post_type() {
/**
* Filter the arguments for the `wpforms-template` post type.
*
* @since 1.8.8
*
* @param array $args Post type arguments.
*/
$args = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'wpforms_template_post_type_args',
[
'label' => 'WPForms Template',
'public' => false,
'exclude_from_search' => true,
'show_ui' => false,
'show_in_admin_bar' => false,
'rewrite' => false,
'query_var' => false,
'can_export' => false,
'supports' => [ 'title', 'author', 'revisions' ],
'capability_type' => 'wpforms_form', // Not using 'capability_type' anywhere. It just has to be custom for security reasons.
'map_meta_cap' => false, // Don't let WP to map meta caps to have a granular control over this process via 'map_meta_cap' filter.
]
);
register_post_type( 'wpforms-template', $args );
}
/**
* Add template states.
*
* @since 1.8.8
*
* @param array $post_states Array of post states.
* @param WP_Post $post Post object.
*
* @return array
*/
public function add_template_states( $post_states, $post ) {
if ( ! ( wpforms_is_admin_page( 'overview' ) || wpforms_is_admin_page( 'entries' ) ) ) {
return $post_states;
}
// No need to show template states on the templates page.
if ( wpforms_is_admin_page( 'overview' ) && wpforms()->obj( 'forms_views' )->get_current_view() === 'templates' ) {
return $post_states;
}
if ( $post->post_type === 'wpforms-template' ) {
$post_states['wpforms_template'] = __( 'Template', 'wpforms-lite' );
}
return $post_states;
}
/**
* Disable edit entry for templates.
*
* @since 1.8.8
*
* @param bool $user_can Whether the current user can perform the given capability.
* @param string $caps Capability name.
* @param int $id The ID of the object to check against.
*
* @return bool Whether the current user can perform the given capability.
*/
public function disable_edit_entry( bool $user_can, $caps, $id ): bool {
if ( $caps === 'edit_entries_form_single' && wpforms_is_form_template( $id ) ) {
$user_can = false;
}
return $user_can;
}
/**
* Display admin notice for the entries page.
*
* @since 1.8.8
*/
public function get_template_entries_notice() {
if ( ! wpforms_is_admin_page( 'entries', 'list' ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$form_id = ! empty( $_REQUEST['form_id'] ) ? absint( $_REQUEST['form_id'] ) : 0;
// The notice should be displayed only for form templates.
if ( ! wpforms_is_form_template( $form_id ) ) {
return;
}
// If there are no entries, we don't need to display the notice on the empty state page.
$entries = wpforms()->obj( 'entry' )->get_entries(
[
'form_id' => $form_id,
'limit' => 1,
]
);
if ( empty( $entries ) ) {
return;
}
/** This filter is documented in wpforms/src/Pro/Tasks/Actions/PurgeTemplateEntryTask.php */
$delay = (int) apply_filters( 'wpforms_pro_tasks_actions_purge_template_entry_task_delay', DAY_IN_SECONDS ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
Notice::warning(
sprintf(
/* translators: %s - delay in formatted time. */
esc_html__( 'Form template entries are for testing purposes and will be automatically deleted after %s.', 'wpforms-lite' ),
// The `- 1` hack is to avoid the "1 day" message in favor of "24 hours".
human_time_diff( time(), time() + $delay - 1 )
)
);
}
/**
* Display admin notice for the entry page.
*
* @since 1.8.8
*/
public function get_template_entry_notice() {
if ( ! wpforms_is_admin_page( 'entries', 'details' ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$entry_id = ! empty( $_REQUEST['entry_id'] ) ? absint( $_REQUEST['entry_id'] ) : 0;
$entry = wpforms()->obj( 'entry' )->get( $entry_id );
// If entry does not exist, we don't need to display the notice on the empty state page.
if ( empty( $entry ) ) {
return;
}
// The notice should be displayed only for form template entry.
if ( ! wpforms_is_form_template( $entry->form_id ) ) {
return;
}
$meta = wpforms()->obj( 'entry_meta' )->get_meta(
[
'entry_id' => absint( $entry_id ),
'type' => 'purge_template_entry_task',
'number' => 1,
]
);
if ( empty( $meta ) ) {
return;
}
$task = wpforms_json_decode( $meta[0]->data,true );
if ( empty( $task['timestamp'] ) ) {
return;
}
Notice::warning(
sprintf(
/* translators: %s - delay in formatted time. */
esc_html__( 'Form template entries are for testing purposes. This entry will be automatically deleted in %s.', 'wpforms-lite' ),
human_time_diff( time(), $task['timestamp'] )
)
);
}
/**
* Get the Show Templates user option.
*
* If the user has not set the Show Templates screen option, it will default to showing templates.
* In this case, we want to show templates by default.
*
* @since 1.8.8
*
* @return bool Whether to show templates by default.
*/
public function get_forms_overview_show_form_templates_option(): bool {
$screen_options = get_user_option( 'wpforms_forms_overview_options' );
$result = $screen_options['wpforms_forms_overview_show_form_templates'] ?? true;
return $result !== '0'; // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement
}
/**
* Add `wpforms-template` post type to the form args.
*
* @since 1.8.8
*
* @param array $args Form arguments.
*
* @return array
*/
public function add_template_post_type( array $args ): array {
// Only add the post type to the form args on the overview page.
if ( ! wpforms_is_admin_page( 'overview' ) ) {
return $args;
}
// Only add the template post type if the Show Templates screen option is enabled
// and `post_type` is not already set.
if ( ! isset( $args['post_type'] ) && wpforms()->obj( 'forms_overview' )->overview_show_form_templates() ) {
$args['post_type'] = wpforms()->obj( 'form' )::POST_TYPES;
}
return $args;
}
/**
* Add user templates to the form templates list.
*
* @since 1.8.8
*
* @param array $templates Form templates.
*
* @return array Form templates.
*/
public function add_form_templates( array $templates ): array {
$user_templates = wpforms()->obj( 'form' )->get( '', [ 'post_type' => 'wpforms-template' ] );
if ( empty( $user_templates ) ) {
return $templates;
}
foreach ( $user_templates as $template ) {
$template_data = wpforms_decode( $template->post_content );
$edit_url = add_query_arg(
[
'page' => 'wpforms-builder',
'form_id' => $template->ID,
],
admin_url( 'admin.php' )
);
$create_url = add_query_arg(
[
'page' => 'wpforms-builder',
'form_id' => $template->ID,
'action' => 'template_to_form',
'_wpnonce' => wp_create_nonce( 'wpforms_template_to_form_form_nonce' ),
],
admin_url( 'admin.php' )
);
$templates[] = [
'name' => $template->post_title,
'slug' => 'wpforms-user-template-' . $template->ID,
'action_text' => wpforms_is_admin_page( 'builder' ) || wp_doing_ajax() ? esc_html__( 'Use Template', 'wpforms-lite' ) : esc_html__( 'Create Form', 'wpforms-lite' ),
'edit_action_text' => esc_html__( 'Edit Template', 'wpforms-lite' ),
'description' => ! empty( $template_data['settings']['template_description'] ) ? $template_data['settings']['template_description'] : '',
'source' => 'wpforms-user-template',
'create_url' => $create_url,
'edit_url' => $edit_url,
'categories' => [ 'user' ],
'has_access' => true,
'data' => $template_data,
'post_id' => $template->ID,
];
}
return $templates;
}
/**
* AJAX handler for removing user templates.
*
* @since 1.8.8
*/
public function ajax_remove_user_template(): void {
// Run a security check.
check_ajax_referer( 'wpforms-form-templates', 'nonce' );
$template_id = isset( $_POST['template'] ) ? absint( $_POST['template'] ) : 0;
if ( ! $template_id ) {
wp_send_json_error();
}
// Check for permissions for the specific template.
if ( ! wpforms_current_user_can( 'delete_form_single', $template_id ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to delete this template.', 'wpforms-lite' ) );
}
// Verify the post exists and is a template.
$template = get_post( $template_id );
if ( ! $template || $template->post_type !== 'wpforms-template' ) {
wp_send_json_error( esc_html__( 'Template not found.', 'wpforms-lite' ) );
}
// Delete the template.
$result = wp_delete_post( $template_id, true );
if ( ! $result ) {
wp_send_json_error( esc_html__( 'Failed to delete the template.', 'wpforms-lite' ) );
}
wp_send_json_success();
}
/**
* Add purge entry task.
*
* @since 1.8.8
*
* @param array $tasks Task class list.
*/
public function add_purge_entry_task( $tasks ) {
$tasks[] = PurgeTemplateEntryTask::class;
return $tasks;
}
/**
* Modify the form data before it is processed to disable payment processing.
*
* @since 1.8.8
*
* @param array $form_data Form data.
*
* @return array
*/
public function process_before_form_data( $form_data ) {
if ( ! isset( $form_data['id'] ) ) {
return $form_data;
}
if ( wpforms_is_form_template( $form_data['id'] ) ) {
$form_data['payments'] = [];
}
return $form_data;
}
/**
* Disable Lite Connect integration for user templates while processing submission.
*
* @since 1.8.8
*
* @param array $fields Form fields.
* @param array $entry Form entry.
* @param array $form_data Form data.
*/
public function process_entry( array $fields, array $entry, array $form_data ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
if ( ! wpforms_is_form_template( $form_data['id'] ) ) {
return;
}
add_filter( 'wpforms_integrations_lite_connect_is_allowed', '__return_false' );
}
}
@@ -0,0 +1,797 @@
<?php
namespace WPForms\Admin\Forms;
use WP_Post;
/**
* List table views.
*
* @since 1.7.3
*/
class Views {
/**
* Current view slug.
*
* @since 1.7.3
*
* @var string
*/
private $current_view;
/**
* Views settings.
*
* @since 1.7.3
*
* @var array
*/
private $views;
/**
* Count forms in different views.
*
* @since 1.7.3
*
* @var array
*/
private $count;
/**
* Base URL.
*
* @since 1.7.3
*
* @var string
*/
private $base_url;
/**
* Show form templates.
*
* @since 1.8.8
*
* @var bool
*/
private $show_form_templates;
/**
* Views configuration.
*
* @since 1.7.3
*/
private function configuration() {
if ( ! empty( $this->views ) ) {
return;
}
// Define views.
$views = [
'all' => [
'title' => __( 'All', 'wpforms-lite' ),
'get_var' => '',
'get_var_value' => '',
],
'trash' => [
'title' => __( 'Trash', 'wpforms-lite' ),
'get_var' => 'status',
'get_var_value' => 'trash',
'args' => [
'post_status' => 'trash',
],
],
];
$this->show_form_templates = wpforms()->obj( 'forms_overview' )->overview_show_form_templates();
// Add Forms and Templates views if Show Templates setting is enabled.
if ( $this->show_form_templates ) {
$views = wpforms_array_insert(
$views,
[
'forms' => [
'title' => __( 'Forms', 'wpforms-lite' ),
'get_var' => 'type',
'get_var_value' => 'form',
'args' => [
'post_type' => 'wpforms',
],
],
'templates' => [
'title' => __( 'Templates', 'wpforms-lite' ),
'get_var' => 'type',
'get_var_value' => 'template',
'args' => [
'post_type' => 'wpforms-template',
],
],
],
'all'
);
}
// phpcs:disable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity
/**
* Filters configuration of the Forms Overview table views.
*
* @since 1.7.3
*
* @param array $views {
* Views array.
*
* @param array $view {
* Each view is the array with three elements:
*
* @param string $title View title.
* @param string $get_var URL query variable name.
* @param string $get_var_value URL query variable value.
* @param array $args Additional arguments to be passed to `wpforms()->obj( 'form' )->get()` method.
* }
* ...
* }
*/
$this->views = apply_filters( 'wpforms_admin_forms_views_configuration', $views );
// phpcs:enable WPForms.Comments.ParamTagHooks.InvalidParamTagsQuantity
}
/**
* Determine if the class is allowed to load.
*
* @since 1.7.3
*
* @return bool
*/
private function allow_load(): bool {
// Load only on the `All Forms` admin page.
return wpforms_is_admin_page( 'overview' );
}
/**
* Initialize class.
*
* @since 1.7.3
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
$this->configuration();
$this->update_current_view();
$this->update_base_url();
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.3
*/
private function hooks() {
add_filter( 'wpforms_overview_table_update_count', [ $this, 'update_count' ], 10, 2 );
add_filter( 'wpforms_overview_table_update_count_all', [ $this, 'update_count' ], 10, 2 );
add_filter( 'wpforms_overview_table_prepare_items_args', [ $this, 'prepare_items_args' ], 100 );
add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_all' ], 9, 2 );
add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_trash' ], PHP_INT_MAX, 2 );
add_filter( 'wpforms_admin_forms_search_search_reset_block_message', [ $this, 'search_reset_message' ], 10, 4 );
}
/**
* Determine and save current view slug.
*
* @since 1.7.3
*/
private function update_current_view() {
if ( ! is_array( $this->views ) ) {
return;
}
$this->current_view = 'all';
foreach ( $this->views as $slug => $view ) {
if (
// phpcs:disable WordPress.Security.NonceVerification.Recommended
isset( $_GET[ $view['get_var'] ] ) &&
$view['get_var_value'] === sanitize_key( $_GET[ $view['get_var'] ] )
// phpcs:enable WordPress.Security.NonceVerification.Recommended
) {
$this->current_view = $slug;
return;
}
}
}
/**
* Update Base URL.
*
* @since 1.7.3
*/
private function update_base_url() {
if ( ! is_array( $this->views ) ) {
return;
}
$get_vars = wp_list_pluck( $this->views, 'get_var' );
$get_vars = array_merge(
$get_vars,
[
'paged',
'trashed',
'restored',
'deleted',
'duplicated',
]
);
$this->base_url = remove_query_arg( $get_vars );
}
/**
* Get current view slug.
*
* @since 1.7.3
*
* @return string
*/
public function get_current_view(): string {
return $this->current_view;
}
/**
* Get base URL.
*
* @since 1.7.5
*
* @return string
*/
public function get_base_url(): string {
return $this->base_url;
}
/**
* Get view configuration by slug.
*
* @since 1.7.5
*
* @param string $slug View slug.
*
* @return array
*/
public function get_view_by_slug( string $slug ): array {
return $this->views[ $slug ] ?? []; // phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.RemoveEmptyLineBeforeReturnStatement
}
/**
* Update count.
*
* @since 1.7.3
*
* @param array $count Number of forms in different views.
* @param array $args Get forms arguments.
*
* @return array
*/
public function update_count( $count, $args ) {
$defaults = [
'nopaging' => true,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'fields' => 'ids',
'post_status' => 'publish',
'post_type' => wpforms()->obj( 'form' )::POST_TYPES,
];
$args = array_merge( $args, $defaults );
$count['all'] = $this->get_all_items_count( $args );
$count['trash'] = $this->get_trashed_forms_count( $args );
// Count forms and templates separately only if Show Templates screen setting is enabled.
if ( $this->show_form_templates ) {
$count['forms'] = $this->get_forms_count( $args );
$count['templates'] = $this->get_form_templates_count( $args );
}
// Store in class property for further use.
$this->count = $count;
return $count;
}
/**
* Get count of all items.
*
* May include only forms or both forms and form templates, depending on the
* Screen Options settings whether to show form templates or not.
*
* @since 1.8.8
*
* @param array $args Get forms arguments.
*
* @return int Number of forms and templates.
*/
private function get_all_items_count( array $args ): int {
if ( ! $this->show_form_templates ) {
$args['post_type'] = 'wpforms';
}
$all_items = wpforms()->obj( 'form' )->get( '', $args );
return is_array( $all_items ) ? count( $all_items ) : 0;
}
/**
* Get count of forms.
*
* @since 1.8.8
*
* @param array $args Get forms arguments.
*
* @return int Number of published forms.
*/
private function get_forms_count( array $args ): int {
$args['post_type'] = 'wpforms';
$forms = wpforms()->obj( 'form' )->get( '', $args );
return is_array( $forms ) ? count( $forms ) : 0;
}
/**
* Get count of form templates.
*
* @since 1.8.8
*
* @param array $args Get forms arguments.
*
* @return int Number of published templates.
*/
private function get_form_templates_count( array $args ): int {
$args['post_type'] = 'wpforms-template';
$templates = wpforms()->obj( 'form' )->get( '', $args );
return is_array( $templates ) ? count( $templates ) : 0;
}
/**
* Get count of trashed items.
*
* May include only forms or both forms and form templates, depending on the
* Screen Options settings whether to show form templates or not.
*
* @since 1.8.8
*
* @param array $args Get forms arguments.
*
* @return int Number of trashed forms.
*/
private function get_trashed_forms_count( array $args ): int {
if ( ! $this->show_form_templates ) {
$args['post_type'] = 'wpforms';
}
$args['post_status'] = 'trash';
$forms = wpforms()->obj( 'form' )->get( '', $args );
return is_array( $forms ) ? count( $forms ) : 0;
}
/**
* Get counts of forms in different views.
*
* @since 1.7.3
*
* @return array
*/
public function get_count(): array {
return $this->count;
}
/**
* Prepare items arguments for list table.
*
* @since 1.8.8
*
* @param array $args Get multiple forms arguments.
*
* @return array
*/
public function prepare_items_args( $args ): array {
$view_args = $this->views[ $this->current_view ]['args'] ?? [];
if ( ! empty( $view_args ) ) {
$args = array_merge( $args, $view_args );
}
return $args;
}
/**
* Get forms from Trash when preparing items for list table.
*
* @since 1.7.3
*
* @depecated 1.8.8 The `prepare_items_args()` now handles all cases, uses `$this->views`.
*
* @param array $args Get multiple forms arguments.
*
* @return array
*/
public function prepare_items_trash( $args ) {
_deprecated_function( __METHOD__, '1.8.8 of the WPForms plugin' );
return $args;
}
/**
* Generate views items.
*
* @since 1.7.3
*
* @return array
*/
public function get_views(): array {
if ( ! is_array( $this->views ) ) {
return [];
}
$views = [];
foreach ( $this->views as $slug => $view ) {
if (
$slug === 'trash' &&
$this->current_view !== 'trash' &&
empty( $this->count[ $slug ] )
) {
continue;
}
$views[ $slug ] = $this->get_view_markup( $slug );
}
/**
* Filters the Forms Overview table views links.
*
* @since 1.7.3
*
* @param array $views Views links.
* @param array $count Count forms in different views.
*/
return apply_filters( 'wpforms_admin_forms_views_get_views', $views, $this->count );
}
/**
* Generate single view item.
*
* @since 1.7.3
*
* @param string $slug View slug.
*
* @return string
*/
private function get_view_markup( string $slug ): string {
if ( empty( $this->views[ $slug ] ) ) {
return '';
}
$view = $this->views[ $slug ];
return sprintf(
'<a href="%1$s"%2$s>%3$s&nbsp;<span class="count">(%4$d)</span></a>',
$slug === 'all' ? esc_url( $this->base_url ) : esc_url( add_query_arg( $view['get_var'], $view['get_var_value'], $this->base_url ) ),
$this->current_view === $slug ? ' class="current"' : '',
esc_html( $view['title'] ),
empty( $this->count[ $slug ] ) ? 0 : absint( $this->count[ $slug ] )
);
}
/**
* Row actions for views "All", "Forms", "Templates".
*
* @since 1.7.3
*
* @param array $row_actions Row actions.
* @param WP_Post $form Form object.
*
* @return array
*/
public function row_actions_all( $row_actions, $form ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
// Modify row actions only for these views.
$allowed_views = [ 'all', 'forms', 'templates' ];
if ( ! in_array( $this->current_view, $allowed_views, true ) ) {
return $row_actions;
}
$is_form_template = wpforms_is_form_template( $form );
$row_actions = [];
// Edit.
if ( wpforms_current_user_can( 'edit_form_single', $form->ID ) ) {
$row_actions['edit'] = sprintf(
'<a href="%s" title="%s">%s</a>',
esc_url(
add_query_arg(
[
'view' => 'fields',
'form_id' => $form->ID,
],
admin_url( 'admin.php?page=wpforms-builder' )
)
),
$is_form_template ? esc_attr__( 'Edit this template', 'wpforms-lite' ) : esc_attr__( 'Edit this form', 'wpforms-lite' ),
esc_html__( 'Edit', 'wpforms-lite' )
);
}
// Entries.
if ( wpforms_current_user_can( 'view_entries_form_single', $form->ID ) ) {
$row_actions['entries'] = sprintf(
'<a href="%s" title="%s">%s</a>',
esc_url(
add_query_arg(
[
'view' => 'list',
'form_id' => $form->ID,
],
admin_url( 'admin.php?page=wpforms-entries' )
)
),
esc_attr__( 'View entries', 'wpforms-lite' ),
esc_html__( 'Entries', 'wpforms-lite' )
);
}
// Payments.
if (
wpforms_current_user_can( wpforms_get_capability_manage_options(), $form->ID ) &&
wpforms()->obj( 'payment' )->get_by( 'form_id', $form->ID )
) {
$row_actions['payments'] = sprintf(
'<a href="%s" title="%s">%s</a>',
esc_url(
add_query_arg(
[
'page' => 'wpforms-payments',
'form_id' => $form->ID,
],
admin_url( 'admin.php' )
)
),
esc_attr__( 'View payments', 'wpforms-lite' ),
esc_html__( 'Payments', 'wpforms-lite' )
);
}
// Preview.
if ( wpforms_current_user_can( 'view_form_single', $form->ID ) ) {
$row_actions['preview_'] = sprintf(
'<a href="%s" title="%s" target="_blank" rel="noopener noreferrer">%s</a>',
esc_url( wpforms_get_form_preview_url( $form->ID ) ),
esc_attr__( 'View preview', 'wpforms-lite' ),
esc_html__( 'Preview', 'wpforms-lite' )
);
}
// Duplicate.
if ( wpforms_current_user_can( 'create_forms' ) && wpforms_current_user_can( 'view_form_single', $form->ID ) ) {
$row_actions['duplicate'] = sprintf(
'<a href="%1$s" title="%2$s" data-type="%3$s">%4$s</a>',
esc_url(
wp_nonce_url(
add_query_arg(
[
'action' => 'duplicate',
'form_id' => $form->ID,
],
$this->base_url
),
'wpforms_duplicate_form_nonce'
)
),
$is_form_template ? esc_attr__( 'Duplicate this template', 'wpforms-lite' ) : esc_attr__( 'Duplicate this form', 'wpforms-lite' ),
$is_form_template ? 'template' : 'form',
esc_html__( 'Duplicate', 'wpforms-lite' )
);
}
// Trash.
if ( wpforms_current_user_can( 'delete_form_single', $form->ID ) ) {
$query_arg = [
'action' => 'trash',
'form_id' => $form->ID,
];
if ( $this->current_view !== 'all' ) {
$query_arg['type'] = $this->current_view === 'templates' ? 'template' : 'form';
}
$row_actions['trash'] = sprintf(
'<a href="%s" title="%s">%s</a>',
esc_url(
wp_nonce_url(
add_query_arg( $query_arg, $this->base_url ),
'wpforms_trash_form_nonce'
)
),
$is_form_template ? esc_attr__( 'Move this form template to trash', 'wpforms-lite' ) : esc_attr__( 'Move this form to trash', 'wpforms-lite' ),
esc_html__( 'Trash', 'wpforms-lite' )
);
}
return $row_actions;
}
/**
* Row actions for view "Trash".
*
* @since 1.7.3
*
* @param array $row_actions Row actions.
* @param WP_Post $form Form object.
*
* @return array
*/
public function row_actions_trash( $row_actions, $form ) {
if (
$this->current_view !== 'trash' ||
! wpforms_current_user_can( 'delete_form_single', $form->ID )
) {
return $row_actions;
}
$is_form_template = wpforms_is_form_template( $form );
$row_actions = [];
// Restore form.
$row_actions['restore'] = sprintf(
'<a href="%s" title="%s">%s</a>',
esc_url(
wp_nonce_url(
add_query_arg(
[
'action' => 'restore',
'form_id' => $form->ID,
'status' => 'trash',
],
$this->base_url
),
'wpforms_restore_form_nonce'
)
),
$is_form_template ? esc_attr__( 'Restore this template', 'wpforms-lite' ) : esc_attr__( 'Restore this form', 'wpforms-lite' ),
esc_html__( 'Restore', 'wpforms-lite' )
);
// Delete permanently.
$row_actions['delete'] = sprintf(
'<a href="%1$s" title="%2$s" data-type="%3$s">%4$s</a>',
esc_url(
wp_nonce_url(
add_query_arg(
[
'action' => 'delete',
'form_id' => $form->ID,
'status' => 'trash',
],
$this->base_url
),
'wpforms_delete_form_nonce'
)
),
$is_form_template ? esc_attr__( 'Delete this template permanently', 'wpforms-lite' ) : esc_attr__( 'Delete this form permanently', 'wpforms-lite' ),
$is_form_template ? 'template' : 'form',
esc_html__( 'Delete Permanently', 'wpforms-lite' )
);
return $row_actions;
}
/**
* Search reset message.
*
* @since 1.7.3
*
* @param string $message Search reset block message.
* @param string $search_term Search term.
* @param array $count Count forms in different views.
* @param string $current_view Current view.
*
* @return string
*/
public function search_reset_message( $message, $search_term, $count, $current_view ) {
if ( $current_view !== 'trash' ) {
return $message;
}
$count['trash'] = ! empty( $count['trash'] ) ? $count['trash'] : 0;
return sprintf(
wp_kses( /* translators: %1$d - number of forms found in the trash, %2$s - search term. */
_n(
'Found <strong>%1$d form</strong> in <em>the trash</em> containing <em>"%2$s"</em>',
'Found <strong>%1$d forms</strong> in <em>the trash</em> containing <em>"%2$s"</em>',
(int) $count['trash'],
'wpforms-lite'
),
[
'strong' => [],
'em' => [],
]
),
(int) $count['trash'],
esc_html( $search_term )
);
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @since 1.7.3
*
* @param string $which The location of the table navigation: 'top' or 'bottom'.
*/
public function extra_tablenav( $which ) {
if ( ! wpforms_current_user_can( 'delete_form_single' ) ) {
return;
}
if ( $this->current_view !== 'trash' ) {
return;
}
// Preserve current view after applying bulk action.
echo '<input type="hidden" name="status" value="trash">';
// Display Empty Trash button.
printf(
'<a href="%1$s" class="button delete-all">%2$s</a>',
esc_url(
wp_nonce_url(
add_query_arg(
[
'action' => 'empty_trash',
'form_id' => 1, // Technically, `empty_trash` is one of the bulk actions, therefore we need to provide fake form_id to proceed.
'status' => 'trash',
],
$this->base_url
),
'wpforms_empty_trash_form_nonce'
)
),
esc_html__( 'Empty Trash', 'wpforms-lite' )
);
}
}
@@ -0,0 +1,138 @@
<?php
namespace WPForms\Admin\Helpers;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
/**
* Chart dataset processing helper methods.
*
* @since 1.8.2
*/
class Chart {
/**
* Default date format.
*
* @since 1.8.2
*/
const DATE_FORMAT = 'Y-m-d';
/**
* Default date-time format.
*
* @since 1.8.2
*/
const DATETIME_FORMAT = 'Y-m-d H:i:s';
/**
* Processes the provided dataset to make sure the formatting needed for the "Chart.js" instance is provided.
*
* @since 1.8.2
*
* @param array $query Dataset retrieved from the database.
* @param DateTimeImmutable $start_date Start date for the timespan.
* @param DateTimeImmutable $end_date End date for the timespan.
*
* @return array
*/
public static function process_chart_dataset_data( $query, $start_date, $end_date ) {
// Bail early if the given query contains no records to iterate.
if ( ! is_array( $query ) || empty( $query ) ) {
return [ 0, [] ];
}
$dataset = [];
$timezone = wp_timezone(); // Retrieve the timezone object for the site.
$mysql_timezone = timezone_open( 'UTC' ); // In the database, all datetime are stored in UTC.
foreach ( $query as $row ) {
$row_day = isset( $row['day'] ) ? sanitize_text_field( $row['day'] ) : '';
$row_count = isset( $row['count'] ) ? abs( (float) $row['count'] ) : 0;
// Skip the rest of the current iteration if the date (day) is unavailable.
if ( empty( $row_day ) ) {
continue;
}
// Since we wont need the initial datetime instances after the query,
// there is no need to create immutable date objects.
$row_datetime = date_create_from_format( self::DATETIME_FORMAT, $row_day, $mysql_timezone );
// Skip the rest of the current iteration if the date creation function fails.
if ( ! $row_datetime ) {
continue;
}
$row_datetime->setTimezone( $timezone );
$row_date_formatted = $row_datetime->format( self::DATE_FORMAT );
// We must take into account entries submitted at different hours of the day,
// because it is possible that more than one entry could be submitted on a given day.
if ( ! isset( $dataset[ $row_date_formatted ] ) ) {
$dataset[ $row_date_formatted ] = $row_count;
continue;
}
$dataset_count = $dataset[ $row_date_formatted ];
$dataset[ $row_date_formatted ] = $dataset_count + $row_count;
}
return self::format_chart_dataset_data( $dataset, $start_date, $end_date );
}
/**
* Format given forms dataset to ensure correct data structure is parsed for serving the "chart.js" instance.
* i.e., [ '2023-02-11' => [ 'day' => '2023-02-11', 'count' => 12 ] ].
*
* @since 1.8.2
*
* @param array $dataset Dataset for the chart.
* @param DateTimeImmutable $start_date Start date for the timespan.
* @param DateTimeImmutable $end_date End date for the timespan.
*
* @return array
*/
private static function format_chart_dataset_data( $dataset, $start_date, $end_date ) {
// In the event that there is no dataset to process, leave early.
if ( empty( $dataset ) ) {
return [ 0, [] ];
}
$interval = new DateInterval( 'P1D' ); // Variable that store the date interval of period 1 day.
$period = new DatePeriod( $start_date, $interval, $end_date ); // Used for iteration between start and end date period.
$data = []; // Placeholder for the actual chart dataset data.
$total_entries = 0;
$has_non_zero_count = false;
// Use loop to store date into array.
foreach ( $period as $date ) {
$date_formatted = $date->format( self::DATE_FORMAT );
$count = isset( $dataset[ $date_formatted ] ) ? (float) $dataset[ $date_formatted ] : 0;
$total_entries += $count;
$data[ $date_formatted ] = [
'day' => $date_formatted,
'count' => $count,
];
// This check helps determine whether there is at least one non-zero count value in the dataset being processed.
// It's used to optimize the function's behavior and to decide whether to include certain data in the returned result.
if ( $count > 0 && ! $has_non_zero_count ) {
$has_non_zero_count = true;
}
}
return [
$total_entries,
$has_non_zero_count ? $data : [], // We will return an empty array to indicate that there is no data to display.
];
}
}
@@ -0,0 +1,461 @@
<?php
namespace WPForms\Admin\Helpers;
use DateTimeImmutable;
/**
* Timespan and popover date-picker helper methods.
*
* @since 1.8.2
*/
class Datepicker {
/**
* Number of timespan days by default.
* "Last 30 Days", by default.
*
* @since 1.8.2
*/
const TIMESPAN_DAYS = '30';
/**
* Timespan (date range) delimiter.
*
* @since 1.8.2
*/
const TIMESPAN_DELIMITER = ' - ';
/**
* Default date format.
*
* @since 1.8.2
*/
const DATE_FORMAT = 'Y-m-d';
/**
* Default date-time format.
*
* @since 1.8.2
*/
const DATETIME_FORMAT = 'Y-m-d H:i:s';
/**
* Sets the timespan (or date range) selected.
*
* Includes:
* 1. Start date object in WP timezone.
* 2. End date object in WP timezone.
* 3. Number of "Last X days", if applicable, otherwise returns "custom".
* 4. Label associated with the selected date filter choice. @see "get_date_filter_choices".
*
* @since 1.8.2
*
* @return array
*/
public static function process_timespan() {
$dates = (string) filter_input( INPUT_GET, 'date', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
// Return default timespan if dates are empty.
if ( empty( $dates ) ) {
return self::get_timespan_dates( self::TIMESPAN_DAYS );
}
$dates = self::maybe_validate_string_timespan( $dates );
list( $start_date, $end_date ) = explode( self::TIMESPAN_DELIMITER, $dates );
// Return default timespan if start date is more recent than end date.
if ( strtotime( $start_date ) > strtotime( $end_date ) ) {
return self::get_timespan_dates( self::TIMESPAN_DAYS );
}
$timezone = wp_timezone(); // Retrieve the timezone string for the site.
$start_date = date_create_immutable( $start_date, $timezone );
$end_date = date_create_immutable( $end_date, $timezone );
// Return default timespan if date creation fails.
if ( ! $start_date || ! $end_date ) {
return self::get_timespan_dates( self::TIMESPAN_DAYS );
}
// Set time to 0:0:0 for start date and 23:59:59 for end date.
$start_date = $start_date->setTime( 0, 0, 0 );
$end_date = $end_date->setTime( 23, 59, 59 );
$days_diff = '';
$current_date = date_create_immutable( 'now', $timezone )->setTime( 23, 59, 59 );
// Calculate days difference only if end date is equal to current date.
if ( ! $current_date->diff( $end_date )->format( '%a' ) ) {
$days_diff = $end_date->diff( $start_date )->format( '%a' );
}
list( $days, $timespan_label ) = self::get_date_filter_choices( $days_diff );
return [
$start_date, // WP timezone.
$end_date, // WP timezone.
$days, // e.g., 22.
$timespan_label, // e.g., Custom.
];
}
/**
* Sets the timespan (or date range) for performing mysql queries.
*
* Includes:
* 1. Start date object in WP timezone.
* 2. End date object in WP timezone.
*
* @param null|array $timespan Given timespan (dates) preferably in WP timezone.
*
* @since 1.8.2
*
* @return array
*/
public static function process_timespan_mysql( $timespan = null ) {
// Retrieve and validate timespan if none is given.
if ( empty( $timespan ) || ! is_array( $timespan ) ) {
$timespan = self::process_timespan();
}
list( $start_date, $end_date ) = $timespan; // Ideally should be in WP timezone.
// If the time period is not a date object, return empty values.
if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) {
return [ '', '' ];
}
// If given timespan is already in UTC timezone, return as it is.
if ( date_timezone_get( $start_date )->getName() === 'UTC' && date_timezone_get( $end_date )->getName() === 'UTC' ) {
return [
$start_date, // UTC timezone.
$end_date, // UTC timezone.
];
}
$mysql_timezone = timezone_open( 'UTC' );
return [
$start_date->setTimezone( $mysql_timezone ), // UTC timezone.
$end_date->setTimezone( $mysql_timezone ), // UTC timezone.
];
}
/**
* Helper method to generate WP and UTC based date-time instances.
*
* Includes:
* 1. Start date object in WP timezone.
* 2. End date object in WP timezone.
* 3. Start date object in UTC timezone.
* 4. End date object in UTC timezone.
*
* @since 1.8.2
*
* @param string $dates Given timespan (dates) in string. i.e. "2023-01-16 - 2023-02-15".
*
* @return bool|array
*/
public static function process_string_timespan( $dates ) {
$dates = self::maybe_validate_string_timespan( $dates );
list( $start_date, $end_date ) = explode( self::TIMESPAN_DELIMITER, $dates );
// Return false if the start date is more recent than the end date.
if ( strtotime( $start_date ) > strtotime( $end_date ) ) {
return false;
}
$timezone = wp_timezone(); // Retrieve the timezone object for the site.
$start_date = date_create_immutable( $start_date, $timezone );
$end_date = date_create_immutable( $end_date, $timezone );
// Return false if the date creation fails.
if ( ! $start_date || ! $end_date ) {
return false;
}
// Set the time to 0:0:0 for the start date and 23:59:59 for the end date.
$start_date = $start_date->setTime( 0, 0, 0 );
$end_date = $end_date->setTime( 23, 59, 59 );
// Since we will need the initial datetime instances after the query,
// we need to return new objects when modifications made.
// Convert the dates to UTC timezone.
$mysql_timezone = timezone_open( 'UTC' );
$utc_start_date = $start_date->setTimezone( $mysql_timezone );
$utc_end_date = $end_date->setTimezone( $mysql_timezone );
return [
$start_date, // WP timezone.
$end_date, // WP timezone.
$utc_start_date, // UTC timezone.
$utc_end_date, // UTC timezone.
];
}
/**
* Sets the timespan (or date range) for performing mysql queries.
*
* Includes:
* 1. A list of date filter options for the datepicker module.
* 2. Currently selected filter or date range values. Last "X" days, or i.e. Feb 8, 2023 - Mar 9, 2023.
* 3. Assigned timespan dates.
*
* @param null|array $timespan Given timespan (dates) preferably in WP timezone.
*
* @since 1.8.2
*
* @return array
*/
public static function process_datepicker_choices( $timespan = null ) {
// Retrieve and validate timespan if none is given.
if ( empty( $timespan ) || ! is_array( $timespan ) ) {
$timespan = self::process_timespan();
}
list( $start_date, $end_date, $days ) = $timespan;
$filters = self::get_date_filter_choices();
$selected = isset( $filters[ $days ] ) ? $days : 'custom';
$value = self::concat_dates( $start_date, $end_date );
$chosen_filter = $selected === 'custom' ? $value : $filters[ $selected ];
$choices = [];
foreach ( $filters as $choice => $label ) {
$timespan_dates = self::get_timespan_dates( $choice );
$checked = checked( $selected, $choice, false );
$choices[] = sprintf(
'<label class="%s">%s<input type="radio" aria-hidden="true" name="timespan" value="%s" %s></label>',
$checked ? 'is-selected' : '',
esc_html( $label ),
esc_attr( self::concat_dates( ...$timespan_dates ) ),
esc_attr( $checked )
);
}
return [
$choices,
$chosen_filter,
$value,
];
}
/**
* Based on the specified date-time range, calculates the comparable prior time period to estimate trends.
*
* Includes:
* 1. Start date object in the given (original) timezone.
* 2. End date object in the given (original) timezone.
*
* @since 1.8.2
* @since 1.8.8 Added $days_diff optional parameter.
*
* @param DateTimeImmutable $start_date Start date for the timespan.
* @param DateTimeImmutable $end_date End date for the timespan.
* @param null|int $days_diff Optional. Number of days in the timespan. If provided, it won't be calculated.
*
* @return bool|array
*/
public static function get_prev_timespan_dates( $start_date, $end_date, $days_diff = null ) {
if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) {
return false;
}
// Calculate $days_diff if not provided.
if ( ! is_numeric( $days_diff ) ) {
$days_diff = $end_date->diff( $start_date )->format( '%a' );
}
// If $days_diff is non-positive, set $days_modifier to 1; otherwise, use $days_diff.
$days_modifier = max( (int) $days_diff, 1 );
return [
$start_date->modify( "-{$days_modifier} day" ),
$start_date->modify( '-1 second' ),
];
}
/**
* Get the site's date format from WordPress settings and convert it to a format compatible with Moment.js.
*
* @since 1.8.5.4
*
* @return string
*/
public static function get_wp_date_format_for_momentjs() {
// Get the date format from WordPress settings.
$date_format = get_option( 'date_format', 'F j, Y' );
// Define a mapping of PHP date format characters to Moment.js format characters.
$format_mapping = [
'd' => 'DD',
'D' => 'ddd',
'j' => 'D',
'l' => 'dddd',
'S' => '', // PHP's S (English ordinal suffix) is not directly supported in Moment.js.
'w' => 'd',
'z' => '', // PHP's z (Day of the year) is not directly supported in Moment.js.
'W' => '', // PHP's W (ISO-8601 week number of year) is not directly supported in Moment.js.
'F' => 'MMMM',
'm' => 'MM',
'M' => 'MMM',
'n' => 'M',
't' => '', // PHP's t (Number of days in the given month) is not directly supported in Moment.js.
'L' => '', // PHP's L (Whether it's a leap year) is not directly supported in Moment.js.
'o' => 'YYYY',
'Y' => 'YYYY',
'y' => 'YY',
'a' => 'a',
'A' => 'A',
'B' => '', // PHP's B (Swatch Internet time) is not directly supported in Moment.js.
'g' => 'h',
'G' => 'H',
'h' => 'hh',
'H' => 'HH',
'i' => 'mm',
's' => 'ss',
'u' => '', // PHP's u (Microseconds) is not directly supported in Moment.js.
'e' => '', // PHP's e (Timezone identifier) is not directly supported in Moment.js.
'I' => '', // PHP's I (Whether or not the date is in daylight saving time) is not directly supported in Moment.js.
'O' => '', // PHP's O (Difference to Greenwich time (GMT) without colon) is not directly supported in Moment.js.
'P' => '', // PHP's P (Difference to Greenwich time (GMT) with colon) is not directly supported in Moment.js.
'T' => '', // PHP's T (Timezone abbreviation) is not directly supported in Moment.js.
'Z' => '', // PHP's Z (Timezone offset in seconds) is not directly supported in Moment.js.
'c' => 'YYYY-MM-DD', // PHP's c (ISO 8601 date) is not directly supported in Moment.js.
'r' => 'ddd, DD MMM YYYY', // PHP's r (RFC 2822 formatted date) is not directly supported in Moment.js.
'U' => '', // PHP's U (Seconds since the Unix Epoch) is not directly supported in Moment.js.
];
// Convert PHP format to JavaScript format.
$momentjs_format = strtr( $date_format, $format_mapping );
// Use 'MMM D, YYYY' as a fallback if the conversion is not available.
return empty( $momentjs_format ) ? 'MMM D, YYYY' : $momentjs_format;
}
/**
* The number of days is converted to the start and end date range.
*
* @since 1.8.2
*
* @param string $days Timespan days.
*
* @return array
*/
private static function get_timespan_dates( $days ) {
list( $timespan_key, $timespan_label ) = self::get_date_filter_choices( $days );
// Bail early, if the given number of days is NOT a number nor a numeric string.
if ( ! is_numeric( $days ) ) {
return [ '', '', $timespan_key, $timespan_label ];
}
$end_date = date_create_immutable( 'now', wp_timezone() );
$start_date = $end_date;
if ( (int) $days > 0 ) {
$start_date = $start_date->modify( "-{$days} day" );
}
$start_date = $start_date->setTime( 0, 0, 0 );
$end_date = $end_date->setTime( 23, 59, 59 );
return [
$start_date, // WP timezone.
$end_date, // WP timezone.
$timespan_key, // i.e. 30.
$timespan_label, // i.e. Last 30 days.
];
}
/**
* Check the delimiter to see if the end date is specified.
* We can assume that the start and end dates are the same if the end date is missing.
*
* @since 1.8.2
*
* @param string $dates Given timespan (dates) in string. i.e. "2023-01-16 - 2023-02-15" or "2023-01-16".
*
* @return string
*/
private static function maybe_validate_string_timespan( $dates ) {
// "-" (en dash) is used as a delimiter for the datepicker module.
if ( strpos( $dates, self::TIMESPAN_DELIMITER ) !== false ) {
return $dates;
}
return $dates . self::TIMESPAN_DELIMITER . $dates;
}
/**
* Returns a list of date filter options for the datepicker module.
*
* @since 1.8.2
*
* @param string|null $key Optional. Key associated with available filters.
*
* @return array
*/
private static function get_date_filter_choices( $key = null ) {
// Available date filters.
$choices = [
'0' => esc_html__( 'Today', 'wpforms-lite' ),
'1' => esc_html__( 'Yesterday', 'wpforms-lite' ),
'7' => esc_html__( 'Last 7 days', 'wpforms-lite' ),
'30' => esc_html__( 'Last 30 days', 'wpforms-lite' ),
'90' => esc_html__( 'Last 90 days', 'wpforms-lite' ),
'365' => esc_html__( 'Last 1 year', 'wpforms-lite' ),
'custom' => esc_html__( 'Custom', 'wpforms-lite' ),
];
// Bail early, and return the full list of options.
if ( is_null( $key ) ) {
return $choices;
}
// Return the "Custom" filter if the given key is not found.
$key = isset( $choices[ $key ] ) ? $key : 'custom';
return [ $key, $choices[ $key ] ];
}
/**
* Concatenate given dates into a single string. i.e. "2023-01-16 - 2023-02-15".
*
* @since 1.8.2
*
* @param DateTimeImmutable $start_date Start date.
* @param DateTimeImmutable $end_date End date.
* @param int|string $fallback Fallback value if dates are not valid.
*
* @return string
*/
private static function concat_dates( $start_date, $end_date, $fallback = '' ) {
// Bail early, if the given dates are not valid.
if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) {
return $fallback;
}
return implode(
self::TIMESPAN_DELIMITER,
[
$start_date->format( self::DATE_FORMAT ),
$end_date->format( self::DATE_FORMAT ),
]
);
}
}
@@ -0,0 +1,89 @@
<?php
namespace WPForms\Admin;
/**
* Class Loader gives ability to track/load all admin modules.
*
* @since 1.5.0
*/
class Loader {
/**
* Get the instance of a class and store it in itself.
*
* @since 1.5.0
*/
public static function get_instance() {
static $instance;
if ( ! $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Loader constructor.
*
* @since 1.5.0
*/
public function __construct() {
$core_class_names = [
'Connect',
'FlyoutMenu',
'Builder\LicenseAlert',
'Builder\Builder',
'Pages\Community',
'Pages\SMTP',
'Pages\Analytics',
'Entries\PrintPreview',
];
$class_names = \apply_filters( 'wpforms_admin_classes_available', $core_class_names );
foreach ( $class_names as $class_name ) {
$this->register_class( $class_name );
}
}
/**
* Register a new class.
*
* @since 1.5.0
*
* @param string $class_name Class name to register.
*/
public function register_class( $class_name ) {
$class_name = sanitize_text_field( $class_name );
// Load Lite class if exists.
if ( class_exists( 'WPForms\Lite\Admin\\' . $class_name ) && ! wpforms()->is_pro() ) {
$class_name = 'WPForms\Lite\Admin\\' . $class_name;
new $class_name();
return;
}
// Load Pro class if exists.
if ( class_exists( 'WPForms\Pro\Admin\\' . $class_name ) && wpforms()->is_pro() ) {
$class_name = 'WPForms\Pro\Admin\\' . $class_name;
new $class_name();
return;
}
// Load general class if neither Pro nor Lite class exists.
if ( class_exists( __NAMESPACE__ . '\\' . $class_name ) ) {
$class_name = __NAMESPACE__ . '\\' . $class_name;
new $class_name();
}
}
}
@@ -0,0 +1,402 @@
<?php
namespace WPForms\Admin;
/**
* Dismissible admin notices.
*
* @since 1.6.7.1
*
* @example Dismissible - global:
* \WPForms\Admin\Notice::error(
* 'Fatal error!',
* [
* 'dismiss' => \WPForms\Admin\Notice::DISMISS_GLOBAL,
* 'slug' => 'fatal_error_3678975',
* ]
* );
*
* @example Dismissible - per user:
* \WPForms\Admin\Notice::warning(
* 'Do something please.',
* [
* 'dismiss' => \WPForms\Admin\Notice::DISMISS_USER,
* 'slug' => 'do_something_1238943',
* ]
* );
*
* @example Dismissible - global, add custom class to output and disable auto paragraph in text:
* \WPForms\Admin\Notice::error(
* 'Fatal error!',
* [
* 'dismiss' => \WPForms\Admin\Notice::DISMISS_GLOBAL,
* 'slug' => 'fatal_error_348975',
* 'autop' => false,
* 'class' => 'some-additional-class',
* ]
* );
*
* @example Not dismissible:
* \WPForms\Admin\Notice::success( 'Everything is good!' );
*/
class Notice {
/**
* Not dismissible.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_NONE means that the notice is not dismissible.
*
* @since 1.6.7.1
*/
const DISMISS_NONE = 0;
/**
* Dismissible global.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_GLOBAL means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed for all users.
*
* @since 1.6.7.1
*/
const DISMISS_GLOBAL = 1;
/**
* Dismissible per user.
*
* Constant attended to use as the value of the $args['dismiss'] argument.
* DISMISS_USER means that the notice will have the dismiss button, and after clicking this button, the notice will be dismissed only for the current user..
*
* @since 1.6.7.1
*/
const DISMISS_USER = 2;
/**
* Order of notices by type.
*
* @since 1.9.1
*/
const ORDER = [
'error',
'warning',
'info',
'success',
];
/**
* Added notices.
*
* @since 1.6.7.1
*
* @var array
*/
private $notices = [];
/**
* Init.
*
* @since 1.6.7.1
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.6.7.1
*/
public function hooks() {
add_action( 'admin_notices', [ $this, 'display' ], PHP_INT_MAX );
add_action( 'wp_ajax_wpforms_notice_dismiss', [ $this, 'dismiss_ajax' ] );
}
/**
* Enqueue assets.
*
* @since 1.6.7.1
*/
private function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-admin-notices',
WPFORMS_PLUGIN_URL . "assets/js/admin/notices{$min}.js",
[ 'jquery' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-admin-notices',
'wpforms_admin_notices',
[
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wpforms-admin' ),
]
);
}
/**
* Display the notices.
*
* @since 1.6.7.1
*/
public function display() {
$dismissed_notices = get_user_meta( get_current_user_id(), 'wpforms_admin_notices', true );
$dismissed_notices = is_array( $dismissed_notices ) ? $dismissed_notices : [];
$dismissed_notices = array_merge( $dismissed_notices, (array) get_option( 'wpforms_admin_notices', [] ) );
$dismissed_notices = array_filter( wp_list_pluck( $dismissed_notices, 'dismissed' ) );
$this->notices = array_diff_key( $this->notices, $dismissed_notices );
$output = implode( '', array_column( $this->sort_notices(), 'data' ) );
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// Enqueue script only when it's needed.
if ( strpos( $output, 'is-dismissible' ) !== false ) {
$this->enqueues();
}
}
/**
* Sort notices by type.
*
* @since 1.9.1
*
* @return array Notices.
*/
private function sort_notices(): array {
$ordered_notices = [];
foreach ( self::ORDER as $order ) {
foreach ( $this->notices as $key => $notice ) {
if ( $notice['type'] === $order ) {
$ordered_notices[ $key ] = $notice;
unset( $this->notices[ $key ] );
}
}
}
// Notices that are not in the self::ORDER array merged to the end of array.
return array_merge( $ordered_notices, $this->notices );
}
/**
* Add notice to the registry.
*
* @since 1.6.7.1
*
* @param string $message Message to display.
* @param string $type Type of the notice. Can be [ '' (default) | 'info' | 'error' | 'success' | 'warning' ].
* @param array $args The array of additional arguments. Please see the $defaults array below.
*/
public static function add( $message, $type = '', $args = [] ) {
static $uniq_id = 0;
$defaults = [
'dismiss' => self::DISMISS_NONE, // Dismissible level: one of the self::DISMISS_* const. By default notice is not dismissible.
'slug' => '', // Slug. Should be unique if dismissible is not equal self::DISMISS_NONE.
'autop' => true, // `false` if not needed to pass message through wpautop().
'class' => '', // Additional CSS class.
];
$args = wp_parse_args( $args, $defaults );
$dismissible = (int) $args['dismiss'];
$dismissible = $dismissible > self::DISMISS_USER ? self::DISMISS_USER : $dismissible;
$class = $dismissible > self::DISMISS_NONE ? ' is-dismissible' : '';
$global = ( $dismissible === self::DISMISS_GLOBAL ) ? 'global-' : '';
$slug = sanitize_key( $args['slug'] );
++$uniq_id;
$uniq_id += ( $uniq_id === (int) $slug ) ? 1 : 0;
$notice = [
'type' => $type,
];
$id = 'wpforms-notice-' . $global;
$id .= empty( $slug ) ? $uniq_id : $slug;
$type = ! empty( $type ) ? 'notice-' . esc_attr( sanitize_key( $type ) ) : '';
$class = empty( $args['class'] ) ? $class : $class . ' ' . esc_attr( sanitize_key( $args['class'] ) );
$message = $args['autop'] ? wpautop( $message ) : $message;
$notice['data'] = sprintf(
'<div class="notice wpforms-notice %s%s" id="%s">%s</div>',
esc_attr( $type ),
esc_attr( $class ),
esc_attr( $id ),
$message
);
if ( empty( $slug ) ) {
wpforms()->obj( 'notice' )->notices[] = $notice;
} else {
wpforms()->obj( 'notice' )->notices[ $slug ] = $notice;
}
}
/**
* Add info notice.
*
* @since 1.6.7.1
*
* @param string $message Message to display.
* @param array $args Array of additional arguments. Details in the self::add() method.
*/
public static function info( $message, $args = [] ) {
self::add( $message, 'info', $args );
}
/**
* Add error notice.
*
* @since 1.6.7.1
*
* @param string $message Message to display.
* @param array $args Array of additional arguments. Details in the self::add() method.
*/
public static function error( $message, $args = [] ) {
self::add( $message, 'error', $args );
}
/**
* Add success notice.
*
* @since 1.6.7.1
*
* @param string $message Message to display.
* @param array $args Array of additional arguments. Details in the self::add() method.
*/
public static function success( $message, $args = [] ) {
self::add( $message, 'success', $args );
}
/**
* Add warning notice.
*
* @since 1.6.7.1
*
* @param string $message Message to display.
* @param array $args Array of additional arguments. Details in the self::add() method.
*/
public static function warning( $message, $args = [] ) {
self::add( $message, 'warning', $args );
}
/**
* AJAX routine that updates dismissed notices meta data.
*
* @since 1.6.7.1
*/
public function dismiss_ajax() {
// Run a security check.
check_ajax_referer( 'wpforms-admin', 'nonce' );
// Sanitize POST data.
$post = array_map( 'sanitize_key', wp_unslash( $_POST ) );
// Update notices meta data.
if ( strpos( $post['id'], 'global-' ) !== false ) {
// Check for permissions.
if ( ! wpforms_current_user_can() ) {
wp_send_json_error();
}
$notices = $this->dismiss_global( $post['id'] );
$level = self::DISMISS_GLOBAL;
} else {
$notices = $this->dismiss_user( $post['id'] );
$level = self::DISMISS_USER;
}
/**
* Allows developers to apply additional logic to the dismissing notice process.
* Executes after updating option or user meta (according to the notice level).
*
* @since 1.6.7.1
*
* @param string $notice_id Notice ID (slug).
* @param integer $level Notice level.
* @param array $notices Dismissed notices.
*/
do_action( 'wpforms_admin_notice_dismiss_ajax', $post['id'], $level, $notices );
if ( ! wpforms_debug() ) {
wp_send_json_success();
}
wp_send_json_success(
[
'id' => $post['id'],
'time' => time(),
'level' => $level,
'notices' => $notices,
]
);
}
/**
* AJAX sub-routine that updates dismissed notices option.
*
* @since 1.6.7.1
*
* @param string $id Notice Id.
*
* @return array Notices.
*/
private function dismiss_global( $id ) {
$id = str_replace( 'global-', '', $id );
$notices = get_option( 'wpforms_admin_notices', [] );
$notices[ $id ] = [
'time' => time(),
'dismissed' => true,
];
update_option( 'wpforms_admin_notices', $notices, true );
return $notices;
}
/**
* AJAX sub-routine that updates dismissed notices user meta.
*
* @since 1.6.7.1
*
* @param string $id Notice Id.
*
* @return array Notices.
*/
private function dismiss_user( $id ) {
$user_id = get_current_user_id();
$notices = get_user_meta( $user_id, 'wpforms_admin_notices', true );
$notices = ! is_array( $notices ) ? [] : $notices;
$notices[ $id ] = [
'time' => time(),
'dismissed' => true,
];
update_user_meta( $user_id, 'wpforms_admin_notices', $notices );
return $notices;
}
}
@@ -0,0 +1,776 @@
<?php
namespace WPForms\Admin\Notifications;
use WPForms\Migrations\Base as MigrationsBase;
/**
* Class EventDriven.
*
* @since 1.7.5
*/
class EventDriven {
/**
* WPForms version when the Event Driven feature has been introduced.
*
* @since 1.7.5
*
* @var string
*/
public const FEATURE_INTRODUCED = '1.7.5';
/**
* Expected date format for notifications.
*
* @since 1.7.5
*
* @var string
*/
private const DATE_FORMAT = 'Y-m-d H:i:s';
/**
* Common UTM parameters.
*
* @since 1.7.5
*
* @var array
*/
private const UTM_PARAMS = [
'utm_source' => 'WordPress',
'utm_medium' => 'Event Notification',
];
/**
* Common targets for date logic.
*
* Available items:
* - upgraded (upgraded to the latest version)
* - activated
* - forms_first_created
* - X.X.X.X (upgraded to a specific version)
* - pro (activated/installed)
* - lite (activated/installed)
*
* @since 1.7.5
*
* @var array
*/
private const DATE_LOGIC = [ 'upgraded', 'activated', 'forms_first_created' ];
/**
* Timestamps.
*
* @since 1.7.5
*
* @var array
*/
private $timestamps = [];
/**
* Initialize class.
*
* @since 1.7.5
*/
public function init(): void {
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Indicate if this is allowed to load.
*
* @since 1.7.5
*
* @return bool
*/
private function allow_load(): bool {
$notifications_obj = wpforms()->obj( 'notifications' );
$has_access = $notifications_obj && $notifications_obj->has_access();
return $has_access || wp_doing_cron();
}
/**
* Hooks.
*
* @since 1.7.5
*/
private function hooks(): void {
add_filter( 'wpforms_admin_notifications_update_data', [ $this, 'update_events' ] );
}
/**
* Add Event Driven notifications before saving them in a database.
*
* @since 1.7.5
*
* @param array|mixed $data Notification data.
*
* @return array
*/
public function update_events( $data ): array {
$data = (array) $data;
$updated = [];
/**
* Allow developers to turn on debug mode: store all notifications and then show all of them.
*
* @since 1.7.5
*
* @param bool $is_debug True if it's a debug mode. Default: false.
*/
$is_debug = (bool) apply_filters( 'wpforms_admin_notifications_event_driven_update_events_debug', false );
$wpforms_notifications = wpforms()->obj( 'notifications' );
foreach ( $this->get_notifications() as $slug => $notification ) {
$is_processed = ! empty( $data['events'][ $slug ]['start'] );
$is_conditional_ok = ! ( isset( $notification['condition'] ) && $notification['condition'] === false );
// If it's a debug mode, OR valid notification has been already processed - skip running logic checks and save it.
if (
$is_debug ||
(
$is_processed &&
$is_conditional_ok &&
$wpforms_notifications &&
$wpforms_notifications->is_valid( $data['events'][ $slug ] )
)
) {
unset( $notification['date_logic'], $notification['offset'], $notification['condition'] );
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$notification['start'] = $is_debug ? date( self::DATE_FORMAT ) : $data['events'][ $slug ]['start'];
$updated[ $slug ] = $notification;
continue;
}
// Ignore if a condition is not passed conditional checks.
if ( ! $is_conditional_ok ) {
continue;
}
$timestamp = $this->get_timestamp_by_date_logic(
$this->prepare_date_logic( $notification )
);
if ( empty( $timestamp ) ) {
continue;
}
// Probably, notification should be visible after some time.
$offset = empty( $notification['offset'] ) ? 0 : absint( $notification['offset'] );
// Set a start date when the notification will be shown.
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$notification['start'] = date( self::DATE_FORMAT, $timestamp + $offset );
// Ignore if notification data is not valid.
if ( ! $wpforms_notifications->is_valid( $notification ) ) {
continue;
}
// Remove unnecessary values, mark the notification as active, and save it.
unset( $notification['date_logic'], $notification['offset'], $notification['condition'] );
$updated[ $slug ] = $notification;
}
$data['events'] = $updated;
return $data;
}
/**
* Prepare and retrieve date logic.
*
* @since 1.7.5
*
* @param array|mixed $notification Notification data.
*
* @return array
*/
private function prepare_date_logic( $notification ): array {
$date_logic = empty( $notification['date_logic'] ) || ! is_array( $notification['date_logic'] )
? self::DATE_LOGIC
: $notification['date_logic'];
return array_filter( array_filter( $date_logic, 'is_string' ) );
}
/**
* Retrieve a notification timestamp based on date logic.
*
* @since 1.7.5
*
* @param array $args Date logic.
*
* @return int
*/
private function get_timestamp_by_date_logic( array $args ): int {
foreach ( $args as $target ) {
if ( ! empty( $this->timestamps[ $target ] ) ) {
return $this->timestamps[ $target ];
}
$timestamp = (int) call_user_func(
$this->get_timestamp_callback( $target ),
$target
);
if ( ! empty( $timestamp ) ) {
$this->timestamps[ $target ] = $timestamp;
return $timestamp;
}
}
return 0;
}
/**
* Retrieve a callback that determines the necessary timestamp.
*
* @since 1.7.5
*
* @param string $target Date logic target.
*
* @return callable
*/
private function get_timestamp_callback( string $target ) {
$raw_target = $target;
// As $target should be a part of name for callback method,
// this regular expression allows lowercase characters, numbers, and underscore.
$target = strtolower( preg_replace( '/[^a-z0-9_]/', '', $target ) );
// Basic callback.
$callback = [ $this, 'get_timestamp_' . $target ];
// Determine if a special version number is passed.
// Uses the regular expression to check a SemVer string.
// @link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.
if ( preg_match( '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.([1-9\d*]))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $raw_target ) ) {
$callback = [ $this, 'get_timestamp_upgraded' ];
}
// If a callback is callable, return it. Otherwise, return fallback.
return is_callable( $callback ) ? $callback : '__return_zero';
}
/**
* Retrieve a timestamp when WPForms was upgraded.
*
* @since 1.7.5
*
* @param string $version WPForms version.
*
* @return int|false Unix timestamp. False on failure.
*/
private function get_timestamp_upgraded( string $version ) {
if ( $version === 'upgraded' ) {
$version = WPFORMS_VERSION;
}
$timestamp = wpforms_get_upgraded_timestamp( $version );
if ( $timestamp === false ) {
return false;
}
// Return a current timestamp if no luck to return a migration's timestamp.
return $timestamp <= 0 ? time() : $timestamp;
}
/**
* Retrieve a timestamp when WPForms was first installed/activated.
*
* @since 1.7.5
*
* @return int|false Unix timestamp. False on failure.
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_timestamp_activated() {
return wpforms_get_activated_timestamp();
}
/**
* Retrieve a timestamp when Lite was first installed.
*
* @since 1.7.5
*
* @return int|false Unix timestamp. False on failure.
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_timestamp_lite() {
$activated = (array) get_option( 'wpforms_activated', [] );
return ! empty( $activated['lite'] ) ? absint( $activated['lite'] ) : false;
}
/**
* Retrieve a timestamp when Pro was first installed.
*
* @since 1.7.5
*
* @return int|false Unix timestamp. False on failure.
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_timestamp_pro() {
$activated = (array) get_option( 'wpforms_activated', [] );
return ! empty( $activated['pro'] ) ? absint( $activated['pro'] ) : false;
}
/**
* Retrieve a timestamp when a first form was created.
*
* @since 1.7.5
*
* @return int|false Unix timestamp. False on failure.
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function get_timestamp_forms_first_created() {
$timestamp = get_option( 'wpforms_forms_first_created' );
return ! empty( $timestamp ) ? absint( $timestamp ) : false;
}
/**
* Retrieve a number of entries.
*
* @since 1.7.5
*
* @return int
*/
private function get_entry_count(): int {
static $count;
if ( is_int( $count ) ) {
return $count;
}
global $wpdb;
$count = 0;
$entry_handler = wpforms()->obj( 'entry' );
$entry_meta_handler = wpforms()->obj( 'entry_meta' );
if ( ! $entry_handler || ! $entry_meta_handler ) {
return $count;
}
$query = "SELECT COUNT( $entry_handler->primary_key )
FROM $entry_handler->table_name
WHERE $entry_handler->primary_key
NOT IN (
SELECT entry_id
FROM $entry_meta_handler->table_name
WHERE type = 'backup_id'
);";
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
$count = (int) $wpdb->get_var( $query );
return $count;
}
/**
* Retrieve forms.
*
* @since 1.7.5
*
* @param int $posts_per_page The number of forms to return.
*
* @return array
* @noinspection PhpSameParameterValueInspection
*/
private function get_forms( int $posts_per_page ): array {
$form_obj = wpforms()->obj( 'form' );
$forms = $form_obj
? $form_obj->get(
'',
[
'posts_per_page' => $posts_per_page,
'nopaging' => false,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'cap' => false,
]
)
: null;
return ! empty( $forms ) ? (array) $forms : [];
}
/**
* Determine if the user has at least 1 form.
*
* @since 1.7.5
*
* @return bool
*/
private function has_form(): bool {
return ! empty( $this->get_forms( 1 ) );
}
/**
* Determine if it is a new user.
*
* @since 1.7.5
*
* @return bool
*/
private function is_new_user(): bool {
// Check if this is an update or first install.
return ! get_option( MigrationsBase::PREVIOUS_CORE_VERSION_OPTION_NAME );
}
/**
* Determine if it's an English site.
*
* @since 1.7.5
*
* @return bool
* @noinspection PhpUnusedPrivateMethodInspection
*/
private function is_english_site(): bool {
static $result;
if ( is_bool( $result ) ) {
return $result;
}
$locales = array_unique(
array_map(
[ $this, 'language_to_iso' ],
[ get_locale(), get_user_locale() ]
)
);
$result = count( $locales ) === 1 && $locales[0] === 'en';
return $result;
}
/**
* Convert language to ISO.
*
* @since 1.7.5
*
* @param string|mixed $lang Language value.
*
* @return string
*/
private function language_to_iso( $lang ): string {
$lang = (string) $lang;
return $lang === '' ? $lang : explode( '_', $lang )[0];
}
/**
* Retrieve a modified URL query string.
*
* @since 1.7.5
*
* @param array $args An associative array of query variables.
* @param string $url A URL to act upon.
*
* @return string
*/
private function add_query_arg( array $args, string $url ): string {
return add_query_arg(
array_merge( $this->get_utm_params(), array_map( 'rawurlencode', $args ) ),
$url
);
}
/**
* Retrieve UTM parameters for Event Driven notifications links.
*
* @since 1.7.5
*
* @return array
*/
private function get_utm_params(): array {
static $utm_params;
if ( ! $utm_params ) {
$utm_params = [
'utm_source' => self::UTM_PARAMS['utm_source'],
'utm_medium' => rawurlencode( self::UTM_PARAMS['utm_medium'] ),
'utm_campaign' => wpforms()->is_pro() ? 'plugin' : 'liteplugin',
];
}
return $utm_params;
}
/**
* Retrieve Event Driven notifications.
*
* @since 1.7.5
*
* @return array
*/
private function get_notifications(): array {
return [
'welcome-message' => [
'id' => 'welcome-message',
'title' => esc_html__( 'Welcome to WPForms!', 'wpforms-lite' ),
'content' => sprintf( /* translators: %s - number of templates. */
esc_html__( 'Were grateful that you chose WPForms for your website! Now that youve installed the plugin, youre less than 5 minutes away from publishing your first form. To make it easy, weve got %s form templates to get you started!', 'wpforms-lite' ),
'2100+'
),
'btns' => [
'main' => [
'url' => admin_url( 'admin.php?page=wpforms-builder' ),
'text' => esc_html__( 'Start Building', 'wpforms-lite' ),
],
'alt' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Welcome Read the Guide' ],
'https://wpforms.com/docs/creating-first-form/'
),
'text' => esc_html__( 'Read the Guide', 'wpforms-lite' ),
],
],
'type' => [
'lite',
'basic',
'plus',
'pro',
'agency',
'elite',
'ultimate',
],
// Immediately after activation (new users only, not upgrades).
'condition' => $this->is_new_user(),
],
'wp-mail-smtp-education' => [
'id' => 'wp-mail-smtp-education',
'title' => esc_html__( 'Dont Miss Your Form Notification Emails!', 'wpforms-lite' ),
'content' => esc_html__( 'Did you know that many WordPress sites are not properly configured to send emails? With the free WP Mail SMTP plugin, you can easily optimize your site to send emails, avoid the spam folder, and make sure your emails land in the recipients inbox every time.', 'wpforms-lite' ),
'btns' => [
'main' => [
'url' => admin_url( 'admin.php?page=wpforms-smtp' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
'alt' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'WP Mail SMTP Learn More' ],
'https://wpforms.com/docs/how-to-set-up-smtp-using-the-wp-mail-smtp-plugin/'
),
'text' => esc_html__( 'Learn More', 'wpforms-lite' ),
],
],
// 3 days after activation/upgrade.
'offset' => 3 * DAY_IN_SECONDS,
'condition' => ! function_exists( 'wp_mail_smtp' ),
],
'join-vip-circle' => [
'id' => 'join-vip-circle',
'title' => esc_html__( 'Want to Be a VIP? Join Now!', 'wpforms-lite' ),
'content' => esc_html__( 'Running a WordPress site can be challenging. But help is just around the corner! Our Facebook group contains tons of tips and help to get your business growing! When you join our VIP Circle, youll get instant access to tips, tricks, and answers from a community of loyal WPForms users. Best of all, membership is 100% free!', 'wpforms-lite' ),
'btns' => [
'main' => [
'url' => 'https://www.facebook.com/groups/wpformsvip/',
'text' => esc_html__( 'Join Now', 'wpforms-lite' ),
],
],
// 30 days after activation/upgrade.
'offset' => 30 * DAY_IN_SECONDS,
],
'survey-reports' => [
'id' => 'survey-reports',
'title' => esc_html__( 'Want to Know What Your Customers Really Think?', 'wpforms-lite' ),
'content' => esc_html__( 'Nothing beats real feedback from your customers and visitors. Thats why many small businesses love our awesome Surveys and Polls addon. Instantly unlock full survey reporting right in your WordPress dashboard. And dont forget: building a survey is easy with our pre-made templates, so you could get started within a few minutes!', 'wpforms-lite' ),
'btns' => [
'main' => [
'license' => [
'lite' => [
'url' => $this->add_query_arg(
[
'utm_content' => 'Surveys and Polls Upgrade Lite',
'utm_locale' => wpforms_sanitize_key( get_locale() ),
],
'https://wpforms.com/lite-upgrade/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'basic' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Surveys and Polls Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'plus' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Surveys and Polls Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'pro' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
'elite' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
],
],
'alt' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Surveys and Polls Learn More' ],
'https://wpforms.com/docs/how-to-install-and-use-the-surveys-and-polls-addon/'
),
'text' => esc_html__( 'Learn More', 'wpforms-lite' ),
],
],
// 45 days after activation/upgrade.
'offset' => 45 * DAY_IN_SECONDS,
'condition' => ! defined( 'WPFORMS_SURVEYS_POLLS_VERSION' ),
],
'form-abandonment' => [
'id' => 'form-abandonment',
'title' => esc_html__( 'Get More Leads From Your Forms!', 'wpforms-lite' ),
'content' => esc_html__( 'Are your forms converting fewer visitors than you hoped? Often, visitors quit forms partway through. That can prevent you from getting all the leads you deserve to capture. With our Form Abandonment addon, you can capture partial entries even if your visitor didnt hit Submit! From there, its easy to follow up with leads and turn them into loyal customers.', 'wpforms-lite' ),
'btns' => [
'main' => [
'license' => [
'lite' => [
'url' => $this->add_query_arg(
[
'utm_content' => 'Form Abandonment Upgrade Lite',
'utm_locale' => wpforms_sanitize_key( get_locale() ),
],
'https://wpforms.com/lite-upgrade/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'basic' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Form Abandonment Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'plus' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Form Abandonment Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'pro' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
'elite' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
],
],
'alt' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'Form Abandonment Learn More' ],
'https://wpforms.com/docs/how-to-install-and-use-form-abandonment-with-wpforms/'
),
'text' => esc_html__( 'Learn More', 'wpforms-lite' ),
],
],
// 60 days after activation/upgrade.
'offset' => 60 * DAY_IN_SECONDS,
'condition' => ! defined( 'WPFORMS_FORM_ABANDONMENT_VERSION' ),
],
'ideas' => [
'id' => 'ideas',
'title' => esc_html__( 'Whats Your Dream WPForms Feature?', 'wpforms-lite' ),
'content' => esc_html__( 'If you could add just one feature to WPForms, what would it be? We want to know! Our team is busy surveying valued customers like you as we plan the year ahead. Wed love to know which features would take your business to the next level! Do you have a second to share your idea with us?', 'wpforms-lite' ),
'btns' => [
'main' => [
'url' => 'https://wpforms.com/share-your-idea/',
'text' => esc_html__( 'Share Your Idea', 'wpforms-lite' ),
],
],
// 120 days after activation/upgrade.
'offset' => 120 * DAY_IN_SECONDS,
'condition' => $this->has_form(),
],
'user-insights' => [
'id' => 'user-insights',
'title' => esc_html__( 'Congratulations! You Just Got Your 100th Form Entry!', 'wpforms-lite' ),
'content' => esc_html__( 'You just hit 100 entries&hellip; and this is just the beginning! Now its time to dig into the data and figure out what makes your visitors tick. The User Journey addon shows you what your visitors looked at before submitting your form. Now you can easily find which areas of your site are triggering form conversions.', 'wpforms-lite' ),
'btns' => [
'main' => [
'license' => [
'lite' => [
'url' => $this->add_query_arg(
[
'utm_content' => 'User Journey Upgrade Lite',
'utm_locale' => wpforms_sanitize_key( get_locale() ),
],
'https://wpforms.com/lite-upgrade/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'basic' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'User Journey Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'plus' => [
'url' => $this->add_query_arg(
[ 'utm_content' => 'User Journey Upgrade Basic' ],
'https://wpforms.com/account/licenses/'
),
'text' => esc_html__( 'Upgrade Now', 'wpforms-lite' ),
],
'pro' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
'elite' => [
'url' => admin_url( 'admin.php?page=wpforms-addons' ),
'text' => esc_html__( 'Install Now', 'wpforms-lite' ),
],
],
],
],
'condition' => ! defined( 'WPFORMS_USER_JOURNEY_VERSION' ) && $this->get_entry_count() >= 100,
],
];
}
}
@@ -0,0 +1,793 @@
<?php
namespace WPForms\Admin\Notifications;
/**
* Notifications.
*
* @since 1.7.5
*/
class Notifications {
/**
* Source of notifications content.
*
* @since 1.7.5
*
* @var string
*/
const SOURCE_URL = 'https://wpformsapi.com/feeds/v1/notifications';
/**
* Array of license types, that are considered being Elite level.
*
* @since 1.7.5
*
* @var array
*/
const LICENSES_ELITE = [ 'agency', 'ultimate', 'elite' ];
/**
* Option value.
*
* @since 1.7.5
*
* @var bool|array
*/
public $option = false;
/**
* Current license type.
*
* @since 1.7.5
*
* @var string
*/
private $license_type;
/**
* Initialize class.
*
* @since 1.7.5
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.7.5
*/
public function hooks() {
add_action( 'wpforms_admin_notifications_update', [ $this, 'update' ] );
if ( ! wpforms_is_admin_ajax() && ! is_admin() ) {
return;
}
add_action( 'wpforms_overview_enqueue', [ $this, 'enqueues' ] );
add_action( 'wpforms_admin_overview_before_table', [ $this, 'output' ] );
add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 );
add_action( 'wp_ajax_wpforms_notification_dismiss', [ $this, 'dismiss' ] );
}
/**
* Check if user has access and is enabled.
*
* @since 1.7.5
* @since 1.8.2 Added AS task support.
*
* @return bool
*/
public function has_access() {
$has_access = ! wpforms_setting( 'hide-announcements', false );
if ( ! wp_doing_cron() && ! wpforms_doing_wp_cli() ) {
$has_access = $has_access && wpforms_current_user_can( 'view_forms' );
}
/**
* Allow modifying state if a user has access.
*
* @since 1.6.0
*
* @param bool $access True if user has access.
*/
return (bool) apply_filters( 'wpforms_admin_notifications_has_access', $has_access );
}
/**
* Get option value.
*
* @since 1.7.5
*
* @param bool $cache Reference property cache if available.
*
* @return array
*/
public function get_option( $cache = true ) {
if ( $this->option && $cache ) {
return $this->option;
}
$option = (array) get_option( 'wpforms_notifications', [] );
$this->option = [
'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
];
return $this->option;
}
/**
* Fetch notifications from feed.
*
* @since 1.7.5
*
* @return array
*/
public function fetch_feed() {
$response = wp_remote_get(
self::SOURCE_URL,
[
'timeout' => 10,
'user-agent' => wpforms_get_default_user_agent(),
]
);
if ( is_wp_error( $response ) ) {
return [];
}
$body = wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
return [];
}
return $this->verify( json_decode( $body, true ) );
}
/**
* Verify notification data before it is saved.
*
* @since 1.7.5
*
* @param array $notifications Array of notifications items to verify.
*
* @return array
*/
public function verify( $notifications ) {
$data = [];
if ( ! is_array( $notifications ) || empty( $notifications ) ) {
return $data;
}
foreach ( $notifications as $notification ) {
// Ignore if one of the conditional checks is true:
//
// 1. notification message is empty.
// 2. license type does not match.
// 3. notification is expired.
// 4. notification has already been dismissed.
// 5. notification existed before installing WPForms.
// (Prevents bombarding the user with notifications after activation).
if (
empty( $notification['content'] ) ||
! $this->is_license_type_match( $notification ) ||
$this->is_expired( $notification ) ||
$this->is_dismissed( $notification ) ||
$this->is_existed( $notification )
) {
continue;
}
$data[] = $notification;
}
return $data;
}
/**
* Verify saved notification data for active notifications.
*
* @since 1.7.5
*
* @param array $notifications Array of notifications items to verify.
*
* @return array
*/
public function verify_active( $notifications ) {
if ( ! is_array( $notifications ) || empty( $notifications ) ) {
return [];
}
$current_timestamp = time();
// Remove notifications that are not active.
foreach ( $notifications as $key => $notification ) {
if (
( ! empty( $notification['start'] ) && $current_timestamp < strtotime( $notification['start'] ) ) ||
( ! empty( $notification['end'] ) && $current_timestamp > strtotime( $notification['end'] ) )
) {
unset( $notifications[ $key ] );
}
}
return $notifications;
}
/**
* Get notification data.
*
* @since 1.7.5
*
* @return array
*/
public function get() {
if ( ! $this->has_access() ) {
return [];
}
$option = $this->get_option();
// Update notifications using async task.
if ( empty( $option['update'] ) || time() > $option['update'] + DAY_IN_SECONDS ) {
$tasks = wpforms()->obj( 'tasks' );
if ( ! $tasks->is_scheduled( 'wpforms_admin_notifications_update' ) !== false ) {
$tasks
->create( 'wpforms_admin_notifications_update' )
->async()
->params()
->register();
}
}
$feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : [];
$events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : [];
return array_merge( $feed, $events );
}
/**
* Get notification count.
*
* @since 1.7.5
*
* @return int
*/
public function get_count() {
return count( $this->get() );
}
/**
* Add a new Event Driven notification.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*/
public function add( $notification ) {
if ( ! $this->is_valid( $notification ) ) {
return;
}
$option = $this->get_option();
// Notification ID already exists.
if ( ! empty( $option['events'][ $notification['id'] ] ) ) {
return;
}
update_option(
'wpforms_notifications',
[
'update' => $option['update'],
'feed' => $option['feed'],
'events' => array_merge( $notification, $option['events'] ),
'dismissed' => $option['dismissed'],
]
);
}
/**
* Determine if notification data is valid.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return bool
*/
public function is_valid( $notification ) {
if ( empty( $notification['id'] ) ) {
return false;
}
return ! empty( $this->verify( [ $notification ] ) );
}
/**
* Determine if notification has already been dismissed.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return bool
*/
private function is_dismissed( $notification ) {
$option = $this->get_option();
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
return ! empty( $option['dismissed'] ) && in_array( $notification['id'], $option['dismissed'] );
}
/**
* Determine if license type is match.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return bool
*/
private function is_license_type_match( $notification ) {
// A specific license type is not required.
if ( empty( $notification['type'] ) ) {
return true;
}
return in_array( $this->get_license_type(), (array) $notification['type'], true );
}
/**
* Determine if notification is expired.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return bool
*/
private function is_expired( $notification ) {
return ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] );
}
/**
* Determine if notification existed before installing WPForms.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return bool
*/
private function is_existed( $notification ) {
$activated = wpforms_get_activated_timestamp();
return ! empty( $activated ) &&
! empty( $notification['start'] ) &&
$activated > strtotime( $notification['start'] );
}
/**
* Update notification data from feed.
*
* @since 1.7.5
* @since 1.7.8 Added `wp_cache_flush()` call when the option has been updated.
* @since 1.8.2 Don't fire the update action when it disabled or was fired recently.
*/
public function update() {
if ( ! $this->has_access() ) {
return;
}
$option = $this->get_option();
// Double-check the last update time to prevent multiple requests.
if ( ! empty( $option['update'] ) && time() < $option['update'] + DAY_IN_SECONDS ) {
return;
}
$data = [
'feed' => $this->fetch_feed(),
'events' => $option['events'],
'dismissed' => $option['dismissed'],
];
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Allow changing notification data before it will be updated in database.
*
* @since 1.7.5
*
* @param array $data New notification data.
*/
$data = (array) apply_filters( 'wpforms_admin_notifications_update_data', $data );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
$data['update'] = time();
update_option( 'wpforms_notifications', $data );
}
/**
* Remove notification data from database before a plugin is deactivated.
*
* @since 1.7.5
*
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network
* or just the current site. Multisite only. Default false.
*/
public function delete( $plugin, $network_deactivating ) {
$wpforms_plugins = [
'wpforms-lite/wpforms.php',
'wpforms/wpforms.php',
];
if ( ! in_array( $plugin, $wpforms_plugins, true ) ) {
return;
}
delete_option( 'wpforms_notifications' );
}
/**
* Enqueue assets on Form Overview admin page.
*
* @since 1.7.5
*/
public function enqueues() {
if ( ! $this->get_count() ) {
return;
}
$min = wpforms_get_min_suffix();
wp_enqueue_style(
'wpforms-admin-notifications',
WPFORMS_PLUGIN_URL . "assets/css/admin-notifications{$min}.css",
[ 'wpforms-lity' ],
WPFORMS_VERSION
);
wp_enqueue_script(
'wpforms-admin-notifications',
WPFORMS_PLUGIN_URL . "assets/js/admin/admin-notifications{$min}.js",
[ 'jquery', 'wpforms-lity' ],
WPFORMS_VERSION,
true
);
// Lity.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
[],
WPFORMS_VERSION
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
WPFORMS_VERSION,
true
);
}
/**
* Output notifications on Form Overview admin area.
*
* @since 1.7.5
*/
public function output() {
// Leave early if there are no forms.
if ( ! wpforms()->obj( 'form' )->forms_exist() ) {
return;
}
$notifications = $this->get();
if ( empty( $notifications ) ) {
return;
}
$notifications_html = '';
$current_class = ' current';
$content_allowed_tags = $this->get_allowed_tags();
foreach ( $notifications as $notification ) {
// Prepare required arguments.
$notification = wp_parse_args(
$notification,
[
'id' => 0,
'title' => '',
'content' => '',
'video' => '',
]
);
$title = $this->get_component_data( $notification['title'] );
$content = $this->get_component_data( $notification['content'] );
if ( ! $title && ! $content ) {
continue;
}
// Notification HTML.
$notifications_html .= sprintf(
'<div class="wpforms-notifications-message%5$s" data-message-id="%4$s">
<h3 class="wpforms-notifications-title">%1$s%6$s</h3>
<div class="wpforms-notifications-content">%2$s</div>
%3$s
</div>',
esc_html( $title ),
wp_kses( wpautop( $content ), $content_allowed_tags ),
$this->get_notification_buttons_html( $notification ),
esc_attr( $notification['id'] ),
esc_attr( $current_class ),
$this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
);
// Only first notification is current.
$current_class = '';
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/notifications',
[
'notifications' => [
'count' => count( $notifications ),
'html' => $notifications_html,
],
],
true
);
}
/**
* Get the allowed HTML tags and their attributes.
*
* @since 1.8.8
*
* @return array
*/
public function get_allowed_tags(): array {
return [
'br' => [],
'em' => [],
'strong' => [],
'span' => [
'style' => [],
],
'p' => [
'id' => [],
'class' => [],
],
'a' => [
'href' => [],
'target' => [],
'rel' => [],
],
];
}
/**
* Retrieve notification's buttons HTML.
*
* @since 1.7.5
*
* @param array $notification Notification data.
*
* @return string
*/
private function get_notification_buttons_html( $notification ) {
$html = '';
if ( empty( $notification['btns'] ) || ! is_array( $notification['btns'] ) ) {
return $html;
}
foreach ( $notification['btns'] as $btn_type => $btn ) {
$btn = $this->get_component_data( $btn );
if ( ! $btn ) {
continue;
}
$url = $this->prepare_btn_url( $btn );
$target = ! empty( $btn['target'] ) ? $btn['target'] : '_blank';
$target = ! empty( $url ) && strpos( $url, home_url() ) === 0 ? '_self' : $target;
$html .= sprintf(
'<a href="%1$s" class="button button-%2$s"%3$s>%4$s</a>',
esc_url( $url ),
$btn_type === 'main' ? 'primary' : 'secondary',
$target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '',
! empty( $btn['text'] ) ? esc_html( $btn['text'] ) : ''
);
}
return ! empty( $html ) ? sprintf( '<div class="wpforms-notifications-buttons">%s</div>', $html ) : '';
}
/**
* Retrieve notification's component data by a license type.
*
* @since 1.7.5
*
* @param mixed $data Component data.
*
* @return false|mixed
*/
private function get_component_data( $data ) {
if ( empty( $data['license'] ) ) {
return $data;
}
$license_type = $this->get_license_type();
if ( in_array( $license_type, self::LICENSES_ELITE, true ) ) {
$license_type = 'elite';
}
return ! empty( $data['license'][ $license_type ] ) ? $data['license'][ $license_type ] : false;
}
/**
* Retrieve the current installation license type (always lowercase).
*
* @since 1.7.5
*
* @return string
*/
private function get_license_type() {
if ( $this->license_type ) {
return $this->license_type;
}
$this->license_type = wpforms_get_license_type();
if ( ! $this->license_type ) {
$this->license_type = 'lite';
}
return $this->license_type;
}
/**
* Dismiss notification via AJAX.
*
* @since 1.7.5
*/
public function dismiss() {
// Check for required param, security and access.
if (
empty( $_POST['id'] ) ||
! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ||
! $this->has_access()
) {
wp_send_json_error();
}
$id = sanitize_key( $_POST['id'] );
$type = is_numeric( $id ) ? 'feed' : 'events';
$option = $this->get_option();
$option['dismissed'][] = $id;
$option['dismissed'] = array_unique( $option['dismissed'] );
// Remove notification.
if ( is_array( $option[ $type ] ) && ! empty( $option[ $type ] ) ) {
foreach ( $option[ $type ] as $key => $notification ) {
if ( (string) $notification['id'] === (string) $id ) {
unset( $option[ $type ][ $key ] );
break;
}
}
}
update_option( 'wpforms_notifications', $option );
wp_send_json_success();
}
/**
* Prepare button URL.
*
* @since 1.7.5
*
* @param array $btn Button data.
*
* @return string
*/
private function prepare_btn_url( $btn ) {
if ( empty( $btn['url'] ) ) {
return '';
}
$replace_tags = [
'{admin_url}' => admin_url(),
'{license_key}' => wpforms_get_license_key(),
];
return str_replace( array_keys( $replace_tags ), array_values( $replace_tags ), $btn['url'] );
}
/**
* Get the notification's video badge HTML.
*
* @since 1.7.5
*
* @param string $video_url Valid video URL.
*
* @return string
*/
private function get_video_badge_html( $video_url ) {
$video_url = wp_http_validate_url( $video_url );
if ( empty( $video_url ) ) {
return '';
}
$data_attr_lity = wp_is_mobile() ? '' : 'data-lity';
return sprintf(
'<a class="wpforms-notifications-badge" href="%1$s" %2$s>
<svg fill="none" viewBox="0 0 15 13" aria-hidden="true">
<path fill="#fff" d="M4 2.5h7v8H4z"/>
<path fill="#D63638" d="M14.2 10.5v-8c0-.4-.2-.8-.5-1.1-.3-.3-.7-.5-1.1-.5H2.2c-.5 0-.8.2-1.1.5-.4.3-.5.7-.5 1.1v8c0 .4.2.8.5 1.1.3.3.6.5 1 .5h10.5c.4 0 .8-.2 1.1-.5.3-.3.5-.7.5-1.1Zm-8.8-.8V3.3l4.8 3.2-4.8 3.2Z"/>
</svg>
%3$s
</a>',
esc_url( $video_url ),
esc_attr( $data_attr_lity ),
esc_html__( 'Watch Video', 'wpforms-lite' )
);
}
}
@@ -0,0 +1,166 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Community Sub-page.
*
* @since 1.5.6
*/
class Community {
/**
* Admin menu page slug.
*
* @since 1.5.6
*
* @var string
*/
const SLUG = 'wpforms-community';
/**
* Constructor.
*
* @since 1.5.6
*/
public function __construct() {
if ( \wpforms_current_user_can() ) {
$this->hooks();
}
}
/**
* Hooks.
*
* @since 1.5.6
*/
public function hooks() {
// Check what page we are on.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
// Only load if we are actually on the Community page.
if ( self::SLUG !== $page ) {
return;
}
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
// Hook for addons.
do_action( 'wpforms_admin_community_init' );
}
/**
* Page data.
*
* @since 1.5.6
*/
public function get_blocks_data() {
$type = wpforms()->is_pro() ? 'plugin' : 'liteplugin';
$data = [];
$data['vip_circle'] = [
'title' => esc_html__( 'WPForms VIP Circle Facebook Group', 'wpforms-lite' ),
'description' => esc_html__( 'Powered by the community, for the community. Anything and everything WPForms: Discussions. Questions. Tutorials. Insights and sneak peaks. Also, exclusive giveaways!', 'wpforms-lite' ),
'button_text' => esc_html__( 'Join WPForms VIP Circle', 'wpforms-lite' ),
'button_link' => 'https://www.facebook.com/groups/wpformsvip/',
'cover_bg_color' => '#E4F0F6',
'cover_img' => 'vip-circle.png',
'cover_img2x' => 'vip-circle@2x.png',
];
$data['announcements'] = [
'title' => esc_html__( 'WPForms Announcements', 'wpforms-lite' ),
'description' => esc_html__( 'Check out the latest releases from WPForms. Our team is always innovating to bring you powerful features and functionality that are simple to use. Every release is designed with you in mind!', 'wpforms-lite' ),
'button_text' => esc_html__( 'View WPForms Announcements', 'wpforms-lite' ),
'button_link' => 'https://wpforms.com/blog/?utm_source=WordPress&amp;utm_medium=Community&amp;utm_campaign=' . esc_attr( $type ) . '&amp;utm_content=Announcements',
'cover_bg_color' => '#EFF8E9',
'cover_img' => 'announcements.png',
'cover_img2x' => 'announcements@2x.png',
];
$data['youtube'] = [
'title' => esc_html__( 'WPForms YouTube Channel', 'wpforms-lite' ),
'description' => esc_html__( 'Take a visual dive into everything WPForms has to offer. From simple contact forms to advanced payment forms and email marketing integrations, our extensive video collection covers it all.', 'wpforms-lite' ),
'button_text' => esc_html__( 'Visit WPForms YouTube Channel', 'wpforms-lite' ),
'button_link' => 'https://www.youtube.com/c/wpformsplugin',
'cover_bg_color' => '#FFE6E6',
'cover_img' => 'youtube.png',
'cover_img2x' => 'youtube@2x.png',
];
$data['dev_docs'] = [
'title' => esc_html__( 'WPForms Developer Documentation', 'wpforms-lite' ),
'description' => esc_html__( 'Customize and extend WPForms with code. Our comprehensive developer resources include tutorials, snippets, and documentation on core actions, filters, functions, and more.', 'wpforms-lite' ),
'button_text' => esc_html__( 'View WPForms Dev Docs', 'wpforms-lite' ),
'button_link' => 'https://wpforms.com/developers/?utm_source=WordPress&amp;utm_medium=Community&amp;utm_campaign=' . esc_attr( $type ) . '&amp;utm_content=Developers',
'cover_bg_color' => '#EBEBEB',
'cover_img' => 'dev-docs.png',
'cover_img2x' => 'dev-docs@2x.png',
];
$data['wpbeginner'] = [
'title' => esc_html__( 'WPBeginner Engage Facebook Group', 'wpforms-lite' ),
'description' => esc_html__( 'Hang out with other WordPress experts and like minded website owners such as yourself! Hosted by WPBeginner, the largest free WordPress site for beginners.', 'wpforms-lite' ),
'button_text' => esc_html__( 'Join WPBeginner Engage', 'wpforms-lite' ),
'button_link' => 'https://www.facebook.com/groups/wpbeginner/',
'cover_bg_color' => '#FCEDE4',
'cover_img' => 'wpbeginner.png',
'cover_img2x' => 'wpbeginner@2x.png',
];
$data['suggest'] = [
'title' => esc_html__( 'Suggest a Feature', 'wpforms-lite' ),
'description' => esc_html__( 'Do you have an idea or suggestion for WPForms? If you have thoughts on features, integrations, addons, or improvements - we want to hear it! We appreciate all feedback and insight from our users.', 'wpforms-lite' ),
'button_text' => esc_html__( 'Suggest a Feature', 'wpforms-lite' ),
'button_link' => 'https://wpforms.com/features/suggest/?utm_source=WordPress&amp;utm_medium=Community&amp;utm_campaign=' . esc_attr( $type ) . '&amp;utm_content=Feature',
'cover_bg_color' => '#FFF9EF',
'cover_img' => 'suggest.png',
'cover_img2x' => 'suggest@2x.png',
];
return $data;
}
/**
* Generate and output page HTML.
*
* @since 1.5.6
*/
public function output() {
?>
<div id="wpforms-admin-community" class="wrap wpforms-admin-wrap">
<h1 class="page-title"><?php esc_html_e( 'Community', 'wpforms-lite' ); ?></h1>
<div class="items">
<?php
$data = $this->get_blocks_data();
foreach ( $data as $item ) {
printf(
'<div class="item">
<a href="%6$s" target="_blank" rel="noopener noreferrer" class="item-cover" style="background-color: %s;" title="%4$s"><img class="item-img" src="%s" srcset="%s 2x" alt="%4$s"/></a>
<h3 class="item-title">%s</h3>
<p class="item-description">%s</p>
<div class="item-footer">
<a class="wpforms-btn button-primary wpforms-btn-blue" href="%s" target="_blank" rel="noopener noreferrer">%s</a>
</div>
</div>',
esc_attr( $item['cover_bg_color'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/community/' . $item['cover_img'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/community/' . $item['cover_img2x'] ),
esc_html( $item['title'] ),
esc_html( $item['description'] ),
esc_url( $item['button_link'] ),
esc_html( $item['button_text'] )
);
}
?>
</div>
</div>
<?php
}
}
@@ -0,0 +1,90 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Constant Contact Sub-page.
*
* @since 1.7.3
*/
class ConstantContact {
/**
* Determine if the class is allowed to be loaded.
*
* @since 1.7.3
*/
private function allow_load() {
return wpforms_is_admin_page( 'page', 'constant-contact' );
}
/**
* Initialize class.
*
* @since 1.7.3
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.7.3
*/
private function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'wpforms_admin_page', [ $this, 'view' ] );
}
/**
* Enqueue JS and CSS files.
*
* @since 1.7.3
*/
public function enqueue_assets() {
// Lity.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
}
/**
* Page view.
*
* @since 1.7.3
*/
public function view() {
$sign_up_link = get_option( 'wpforms_constant_contact_signup', 'https://constant-contact.evyy.net/c/11535/341874/3411?sharedid=wpforms' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/pages/constant-contact',
[
'sign_up_link' => is_string( $sign_up_link ) ? $sign_up_link : '',
'wpbeginners_guide_link' => 'https://www.wpbeginner.com/beginners-guide/why-you-should-start-building-your-email-list-right-away',
],
true
);
}
}
@@ -0,0 +1,487 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Duplicator Subpage.
*
* @since 1.9.8.6
*/
class Duplicator extends Page {
/**
* Admin menu page slug.
*
* @since 1.9.8.6
*
* @var string
*/
public const SLUG = 'wpforms-duplicator';
/**
* Configuration.
*
* @since 1.9.8.6
*
* @var array
*/
protected $config = [
'lite_plugin' => 'duplicator/duplicator.php',
'lite_wporg_url' => 'https://wordpress.org/plugins/duplicator/',
'lite_download_url' => 'https://downloads.wordpress.org/plugin/duplicator.zip',
'pro_plugin' => 'duplicator-pro/duplicator-pro.php',
'duplicator_addon' => 'duplicator-pro/duplicator-pro.php',
'duplicator_addon_page' => 'https://duplicator.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=duplicator-page',
'duplicator_onboarding' => 'admin.php?page=duplicator',
];
/**
* Constructor.
*
* @since 1.9.8.6
*/
public function __construct() {
// Set the correct onboarding page based on the active version.
if ( $this->is_pro_active() ) {
$this->config['duplicator_onboarding'] = 'admin.php?page=duplicator-pro';
}
parent::__construct();
}
/**
* Get the plugin name for use in IDs, CSS classes, and config keys.
*
* @since 1.9.8.6
*
* @return string Plugin name.
*/
protected static function get_plugin_name(): string {
return 'duplicator';
}
/**
* Get heading title text.
*
* @since 1.9.8.6
*
* @return string Heading title.
*/
protected function get_heading_title(): string {
return esc_html__( 'WPForms Collects It. Duplicator Protects It.', 'wpforms-lite' );
}
/**
* Get heading alt text for logo.
*
* @since 1.9.8.6
*
* @return string Heading alt text.
*/
protected function get_heading_alt_text(): string {
return esc_attr__( 'WPForms ♥ Duplicator', 'wpforms-lite' );
}
/**
* Get heading description strings.
*
* @since 1.9.8.6
*
* @return array Array of description strings.
*/
protected function get_heading_strings(): array {
return [
esc_html__( 'Every form entry lives in your database. One bad update, one crash, and it\'s gone. Duplicator backs up your entire site automatically so you can restore everything with one click.', 'wpforms-lite' ),
esc_html__( 'Trusted by over 1.5 million websites.', 'wpforms-lite' ),
];
}
/**
* Get screenshot features list.
*
* @since 1.9.8.6
*
* @return array Array of feature strings.
*/
protected function get_screenshot_features(): array {
return [
'Back up your entire site automatically: forms, entries, everything.',
'Restore your site with one click if anything goes wrong.',
'Store backups safely in Google Drive, Dropbox, or Amazon S3.',
'Schedule daily backups so you never have to think about it.',
];
}
/**
* Get screenshot alt text.
*
* @since 1.9.8.6
*
* @return string Alt text for screenshot image.
*/
protected function get_screenshot_alt_text(): string {
return esc_attr__( 'Duplicator screenshot', 'wpforms-lite' );
}
/**
* Generate and output step 'Result' section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_result(): void {
$step = $this->get_data_step_result();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-result %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 3', 'wpforms-lite' ),
esc_html__( 'Set Up Scheduled Cloud Backups', 'wpforms-lite' ),
esc_html__( 'Keep your data safe forever with automatic daily backups to Google Drive, Dropbox, or Amazon S3.', 'wpforms-lite' ),
esc_attr( $step['button_class'] ),
esc_url( admin_url( $this->is_pro_active() ? 'admin.php?page=duplicator-pro-schedules' : 'admin.php?page=duplicator-schedules' ) ),
esc_html( $step['button_text'] )
);
}
/**
* Whether the plugin is finished setup or not.
*
* @since 1.9.8.6
*/
protected function is_plugin_finished_setup(): bool {
if ( ! $this->is_plugin_configured() ) {
return false;
}
$count = $this->get_package_count();
$schedule_count = 0;
if ( $count && class_exists( '\Duplicator\Models\ScheduleEntity' ) && $this->is_pro_active() ) {
$schedule_count = \Duplicator\Models\ScheduleEntity::count(); // phpcs:ignore WPForms.PHP.BackSlash.RemoveBackslash, WPForms.PHP.BackSlash.UseShortSyntax
}
return $count && $schedule_count;
}
/**
* Generate and output footer section HTML.
*
* @since 1.9.8.6
*/
protected function output_section_footer(): void {
printf(
'<section class="bottom">
<p>%s</p>
</section>',
esc_html__( 'Because the data you collect with WPForms is too valuable to lose.', 'wpforms-lite' )
);
}
/**
* Step 'Result' data.
*
* @since 1.9.8.6
*
* @return array Step data.
*/
protected function get_data_step_result(): array {
$count = $this->get_package_count();
$data = [
'section_class' => $count ? '' : 'grey',
'button_class' => ! $count ? 'grey disabled' : 'button-primary',
'icon' => 'step-3.svg',
'button_text' => esc_html__( 'Set Up Cloud Backups', 'wpforms-lite' ),
];
if ( $count && class_exists( '\Duplicator\Models\ScheduleEntity' ) && $this->is_pro_active() ) {
$schedule_count = \Duplicator\Models\ScheduleEntity::count(); // phpcs:ignore WPForms.PHP.BackSlash.RemoveBackslash, WPForms.PHP.BackSlash.UseShortSyntax
$data['section_class'] = '';
$data['button_class'] = 'button-primary';
if ( $schedule_count ) {
$data['icon'] = 'step-complete.svg';
$data['button_class'] = 'grey disabled';
$data['button_text'] = esc_html__( 'Cloud Backups Set Up', 'wpforms-lite' );
}
}
return $data;
}
/**
* Whether a plugin is configured or not.
*
* @since 1.9.8.6
*
* @return bool True if plugin is configured properly.
*/
protected function is_plugin_configured(): bool {
if ( ! $this->is_plugin_activated() ) {
return false;
}
$count = $this->get_package_count();
return $count > 0;
}
/**
* Get the number of packages in the database.
*
* @since 1.9.8.6
*
* @return int Number of packages.
*/
protected function get_package_count(): int {
/**
* Check if the plugin is available.
* Since we are using a direct query to the database to get the number of records instead of built-in methods,
* there is a chance of getting a non-zero value even if the plugin is turned off.
*/
if ( ! $this->is_plugin_available() ) {
return 0;
}
// Check if Duplicator has been configured with basic settings.
global $wpdb;
// Check for the Duplicator packages table.
$packages_table = $this->is_pro_active() ? $wpdb->prefix . 'duplicator_backups' : $wpdb->prefix . 'duplicator_packages';
// Use object caching to minimize direct DB queries here, as there is no core API
// to check custom plugin table existence or its contents.
$blog_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0;
$table_exists_cache_key = "wpforms_dup_table_exists_{$blog_id}";
$package_count_cache_key = "wpforms_dup_package_count_{$blog_id}";
$table_exists = wp_cache_get( $table_exists_cache_key, 'wpforms' );
if ( $table_exists === false ) {
// PHPCS: We must use a direct DB query because no WP API exists for custom tables.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $packages_table ) ) );
wp_cache_set( $table_exists_cache_key, $table_exists, 'wpforms', 60 );
}
$package_count = 0;
if ( $table_exists === $packages_table ) {
$package_count = wp_cache_get( $package_count_cache_key, 'wpforms' );
if ( $package_count === false ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$package_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$packages_table}" );
wp_cache_set( $package_count_cache_key, $package_count, 'wpforms', 60 );
}
}
return (int) $package_count;
}
/**
* Whether a plugin is active or not.
*
* @since 1.9.8.6
*
* @return bool True if the plugin is active.
*/
protected function is_plugin_activated(): bool {
return (
( defined( 'DUPLICATOR_VERSION' ) || class_exists( 'Duplicator\Plugin' ) || class_exists( 'Duplicator\Pro\Requirements' ) ) &&
(
is_plugin_active( $this->config['lite_plugin'] ) ||
is_plugin_active( $this->config['pro_plugin'] )
)
);
}
/**
* Whether a plugin is available (class/function exists).
*
* @since 1.9.8.6
*
* @return bool True if plugin is available.
*/
protected function is_plugin_available(): bool {
return class_exists( 'Duplicator\Plugin' ) || defined( 'DUPLICATOR_VERSION' ) || class_exists( 'DUP_PRO_Plugin' ) || defined( 'DUPLICATOR_PRO_VERSION' );
}
/**
* Whether pro version is active.
*
* @since 1.9.8.6
*
* @return bool True if pro version is active.
*/
protected function is_pro_active(): bool {
return class_exists( 'DUP_PRO_Plugin' ) || defined( 'DUPLICATOR_PRO_VERSION' );
}
/**
* Get the heading for the install step.
*
* @since 1.9.8.6
*
* @return string Install step heading.
*/
protected function get_install_heading(): string {
return esc_html__( 'Install and Activate Duplicator', 'wpforms-lite' );
}
/**
* Get the description for the install step.
*
* @since 1.9.8.6
*
* @return string Install step description.
*/
protected function get_install_description(): string {
return esc_html__( 'Your first step toward bulletproof backups.', 'wpforms-lite' );
}
/**
* Get the plugin title.
*
* @since 1.9.8.6
*
* @return string Plugin title.
*/
protected function get_plugin_title(): string {
return esc_html__( 'Duplicator', 'wpforms-lite' );
}
/**
* Get the install button text.
*
* @since 1.9.8.6
*
* @return string Install button text.
*/
protected function get_install_button_text(): string {
return esc_html__( 'Install Duplicator', 'wpforms-lite' );
}
/**
* Get the text when a plugin is installed and activated.
*
* @since 1.9.8.6
*
* @return string Installed & activated text.
*/
protected function get_installed_activated_text(): string {
return esc_html__( 'Duplicator Installed & Activated', 'wpforms-lite' );
}
/**
* Get the activate button text.
*
* @since 1.9.8.6
*
* @return string Activate button text.
*/
protected function get_activate_text(): string {
return esc_html__( 'Activate Duplicator', 'wpforms-lite' );
}
/**
* Get the heading for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step heading.
*/
protected function get_setup_heading(): string {
return esc_html__( 'Create Your First Backup', 'wpforms-lite' );
}
/**
* Get the description for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step description.
*/
protected function get_setup_description(): string {
return esc_html__( 'Back up your site — forms, entries, settings, everything — in just one click.', 'wpforms-lite' );
}
/**
* Get the setup button text.
*
* @since 1.9.8.6
*
* @return string Setup button text.
*/
protected function get_setup_button_text(): string {
return esc_html__( 'Create First Backup', 'wpforms-lite' );
}
/**
* Get the text when setup is completed.
*
* @since 1.9.8.6
*
* @return string Setup completed text.
*/
protected function get_setup_completed_text(): string {
return esc_html__( 'Backup Created', 'wpforms-lite' );
}
/**
* Get the text when a pro-version is installed and activated.
*
* @since 1.9.8.6
*
* @return string Pro installed and activated text.
*/
protected function get_pro_installed_activated_text(): string {
return esc_html__( 'Duplicator Pro Installed & Activated', 'wpforms-lite' );
}
}
@@ -0,0 +1,748 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Abstract class for admin pages.
*
* @since 1.9.8.6
*/
abstract class Page {
/**
* Admin menu page slug.
*
* @since 1.9.8.6
*
* @var string
*/
public const SLUG = '';
/**
* Configuration.
*
* @since 1.9.8.6
*
* @var array
*/
protected $config = [];
/**
* Runtime data used for generating page HTML.
*
* @since 1.9.8.6
*
* @var array
*/
protected $output_data = [];
/**
* Constructor.
*
* @since 1.9.8.6
*/
public function __construct() {
if ( ! wpforms_current_user_can() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.9.8.6
*/
public function hooks(): void {
$plugin = static::get_plugin_name();
if ( wp_doing_ajax() ) {
add_action( "wp_ajax_wpforms_page_check_{$plugin}_status", [ $this, 'ajax_check_plugin_status' ] );
add_action( 'wpforms_plugin_activated', [ $this, 'plugin_activated' ] );
}
// Check what page we are on.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
// Only load if we are actually on the correct page.
if ( $page !== static::SLUG ) {
return;
}
add_filter( 'wpforms_admin_header', '__return_false' );
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
/**
* Hook for addons.
*
* @since 1.9.8.6
*/
do_action( 'wpforms_admin_pages_page_' . static::get_plugin_name() . '_hooks' );
}
/**
* Enqueue JS and CSS files.
*
* @since 1.9.8.6
*/
public function enqueue_assets(): void {
$min = wpforms_get_min_suffix();
// Lity.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
// Custom styles for Lity image size limitation.
wp_add_inline_style(
'wpforms-lity',
'
.lity-image .lity-container {
max-width: 1040px !important;
}
.lity-image img {
max-width: 1040px !important;
width: 100%;
height: auto;
}
'
);
wp_enqueue_script(
'wpforms-admin-page-' . static::get_plugin_name(),
WPFORMS_PLUGIN_URL . "assets/js/admin/pages/common{$min}.js",
[ 'jquery' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-admin-page-' . static::get_plugin_name(),
'wpforms_pluginlanding',
$this->get_js_strings()
);
}
/**
* Generate and output page HTML.
*
* @since 1.9.8.6
*/
public function output(): void {
echo '<div id="wpforms-admin-' . esc_attr( static::get_plugin_name() ) . '" class="wrap wpforms-admin-wrap wpforms-admin-plugin-landing">';
$this->output_section_heading();
$this->output_section_screenshot();
$this->output_section_footer();
$this->output_section_step_install();
$this->output_section_step_setup();
$this->output_section_step_result();
echo '</div>';
}
/**
* Generate and output step 'Install' section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_install(): void {
$step = $this->get_data_step_install();
if ( empty( $step ) ) {
return;
}
$button_format = '<button class="button %3$s" data-plugin="%1$s" data-action="%4$s" data-provider="%5$s">%2$s</button>';
$button_allowed_html = [
'button' => [
'class' => true,
'data-plugin' => true,
'data-action' => true,
'data-provider' => true,
],
];
if (
! $this->output_data['plugin_installed'] &&
! $this->output_data['pro_plugin_installed'] &&
! wpforms_can_install( 'plugin' )
) {
$button_format = '<a class="link" href="%1$s" target="_blank" rel="nofollow noopener">%2$s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
$button_allowed_html = [
'a' => [
'class' => true,
'href' => true,
'target' => true,
'rel' => true,
],
'span' => [
'class' => true,
'aria-hidden' => true,
],
];
}
$plugin_attr = wpforms_is_url( $step['plugin'] ) ? esc_url( $step['plugin'] ) : esc_attr( $step['plugin'] );
$button = sprintf( $button_format, $plugin_attr, esc_html( $step['button_text'] ), esc_attr( $step['button_class'] ), esc_attr( $step['button_action'] ), esc_attr( static::get_plugin_name() ) );
printf(
'<section class="step step-install">
<aside class="num">
<img src="%1$s" alt="%2$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%3$s</h2>
<p>%4$s</p>
%5$s
</div>
</section>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 1', 'wpforms-lite' ),
esc_html( $step['heading'] ),
esc_html( $step['description'] ),
wp_kses( $button, $button_allowed_html )
);
}
/**
* Generate and output step 'Setup' section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_setup(): void {
$step = $this->get_data_step_setup();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-setup %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 2', 'wpforms-lite' ),
esc_html( $step['heading'] ),
esc_html( $step['description'] ),
esc_attr( $step['button_class'] ),
esc_url( admin_url( $this->config[ static::get_plugin_name() . '_onboarding' ] ) ),
esc_html( $step['button_text'] )
);
}
/**
* Generate and output footer section HTML.
*
* @since 1.9.8.6
*/
protected function output_section_footer(): void {
// Default implementation - can be overridden by child classes.
}
/**
* Step 'Install' data.
*
* @since 1.9.8.6
*
* @return array Step data.
*/
protected function get_data_step_install(): array {
$step = [];
$step['heading'] = $this->get_install_heading();
$step['description'] = $this->get_install_description();
$this->output_data['all_plugins'] = get_plugins();
$this->output_data['plugin_installed'] = array_key_exists( $this->config['lite_plugin'], $this->output_data['all_plugins'] );
$this->output_data['plugin_activated'] = false;
$this->output_data['pro_plugin_installed'] = array_key_exists( $this->config['pro_plugin'], $this->output_data['all_plugins'] );
$this->output_data['pro_plugin_activated'] = false;
if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] ) {
$step['icon'] = 'step-1.svg';
$step['button_text'] = $this->get_install_button_text();
$step['button_class'] = 'button-primary';
$step['button_action'] = 'install';
$step['plugin'] = $this->config['lite_download_url'];
if ( ! wpforms_can_install( 'plugin' ) ) {
$step['heading'] = $this->get_plugin_title();
$step['description'] = '';
$step['button_text'] = $this->get_plugin_title() . ' on WordPress.org';
$step['plugin'] = $this->config['lite_wporg_url'];
}
} else {
$this->output_data['plugin_activated'] =
is_plugin_active( $this->config['lite_plugin'] ) || is_plugin_active( $this->config['pro_plugin'] );
$step['icon'] = $this->output_data['plugin_activated'] ? 'step-complete.svg' : 'step-1.svg';
$step['button_text'] =
$this->output_data['plugin_activated']
? $this->get_installed_activated_text()
: $this->get_activate_text();
$step['button_class'] = $this->output_data['plugin_activated']
? 'grey disabled'
: 'button-primary';
$step['button_action'] = $this->output_data['plugin_activated'] ? '' : 'activate';
$step['plugin'] =
$this->output_data['pro_plugin_installed'] ? $this->config['pro_plugin'] : $this->config['lite_plugin'];
$step['is_pro'] = $this->output_data['pro_plugin_installed'];
}
return $step;
}
/**
* Step 'Setup' data.
*
* @since 1.9.8.6
*
* @return array Step data.
*/
protected function get_data_step_setup(): array {
$step = [];
$this->output_data['plugin_setup'] = false;
if ( $this->output_data['plugin_activated'] ) {
$this->output_data['plugin_setup'] = $this->is_plugin_configured();
}
$step['icon'] = 'step-2.svg';
$step['section_class'] = $this->output_data['plugin_activated'] ? '' : 'grey';
$step['heading'] = $this->get_setup_heading();
$step['description'] = $this->get_setup_description();
$step['button_text'] = $this->get_setup_button_text();
$step['button_class'] = 'grey disabled';
if ( $this->output_data['plugin_setup'] ) {
$step['icon'] = 'step-complete.svg';
$step['section_class'] = '';
$step['button_text'] = $this->get_setup_completed_text();
} else {
$step['button_class'] = $this->output_data['plugin_activated'] ? 'button-primary' : 'grey disabled';
}
return $step;
}
/**
* Ajax endpoint. Check plugin setup status.
* Used to properly init the step 2 section after completing step 1.
*
* @since 1.9.8.6
*/
public function ajax_check_plugin_status(): void {
// Security checks.
if (
! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ||
! wpforms_current_user_can()
) {
wp_send_json_error(
[ 'error' => esc_html__( 'You do not have permission.', 'wpforms-lite' ) ]
);
}
$result = [];
if ( ! $this->is_plugin_available() ) {
wp_send_json_error(
[ 'error' => esc_html__( 'Plugin unavailable.', 'wpforms-lite' ) ]
);
}
$result['setup_status'] = (int) $this->is_plugin_configured();
$result['license_level'] = 'lite';
$result['step3_button_url'] = $this->config[ static::get_plugin_name() . '_addon_page' ];
if ( $this->is_pro_active() ) {
$result['license_level'] = 'pro';
}
$result['result_status'] = $this->is_plugin_finished_setup();
$result['addon_installed'] = (int) array_key_exists( $this->config[ static::get_plugin_name() . '_addon' ], get_plugins() );
wp_send_json_success( $result );
}
/**
* Set the source of the plugin installation.
*
* @since 1.9.8.6
*
* @param string $plugin_basename The basename of the plugin.
*/
public function plugin_activated( string $plugin_basename ): void {
if ( $plugin_basename !== $this->config['lite_plugin'] ) {
return;
}
$source = wpforms()->is_pro() ? 'WPForms' : 'WPForms Lite';
update_option( static::get_plugin_name() . '_source', $source, false );
update_option( static::get_plugin_name() . '_date', time(), false );
}
/**
* JS strings.
*
* @since 1.9.8.6
*
* @return array Array of strings.
* @noinspection HtmlUnknownTarget
*/
protected function get_js_strings(): array {
$error_could_not_install = sprintf(
wp_kses( /* translators: %1$s - Lite plugin download URL. */
__( 'Could not install the plugin automatically. Please <a href="%1$s">download</a> it and install it manually.', 'wpforms-lite' ),
[
'a' => [
'href' => true,
],
]
),
esc_url( $this->config['lite_download_url'] ?? '' )
);
$error_could_not_activate = sprintf(
wp_kses( /* translators: %1$s - Plugins page URL. */
__( 'Could not activate the plugin. Please activate it on the <a href="%1$s">Plugins page</a>.', 'wpforms-lite' ),
[
'a' => [
'href' => true,
],
]
),
esc_url( admin_url( 'plugins.php' ) )
);
return [
'installing' => esc_html__( 'Installing...', 'wpforms-lite' ),
'activating' => esc_html__( 'Activating...', 'wpforms-lite' ),
'activated' => $this->get_installed_activated_text(),
'activated_pro' => $this->get_pro_installed_activated_text(),
'install_now' => esc_html__( 'Install Now', 'wpforms-lite' ),
'activate_now' => esc_html__( 'Activate Now', 'wpforms-lite' ),
'download_now' => esc_html__( 'Download Now', 'wpforms-lite' ),
'plugins_page' => esc_html__( 'Go to Plugins page', 'wpforms-lite' ),
'error_could_not_install' => $error_could_not_install,
'error_could_not_activate' => $error_could_not_activate,
static::get_plugin_name() . '_manual_install_url' => $this->config['lite_download_url'],
static::get_plugin_name() . '_manual_activate_url' => admin_url( 'plugins.php' ),
];
}
/**
* Get the plugin name for use in IDs, CSS classes, and config keys.
*
* @since 1.9.8.6
*
* @return string Plugin name.
*/
abstract protected static function get_plugin_name(): string;
/**
* Generate and output heading section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
public function output_section_heading(): void {
$strings = $this->get_heading_strings();
// Heading section.
printf(
'<section class="top">
<img class="img-top" src="%1$s" alt="%2$s"/>
<h1>%3$s</h1>
<p>%4$s</p>
</section>',
esc_url( $this->get_heading_image_url() ),
esc_attr( $this->get_heading_alt_text() ),
esc_html( $this->get_heading_title() ),
esc_html( implode( ' ', $strings ) )
);
}
/**
* Get heading image URL.
*
* @since 1.9.8.6
*
* @return string Heading image URL.
*/
protected function get_heading_image_url(): string {
return WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/wpforms-' . static::get_plugin_name() . '.svg';
}
/**
* Get heading title text.
*
* @since 1.9.8.6
*
* @return string Heading title.
*/
abstract protected function get_heading_title(): string;
/**
* Get heading alt text for logo.
*
* @since 1.9.8.6
*
* @return string Heading alt text.
*/
abstract protected function get_heading_alt_text(): string;
/**
* Get heading description strings.
*
* @since 1.9.8.6
*
* @return array Array of description strings.
*/
abstract protected function get_heading_strings(): array;
/**
* Generate and output screenshot section HTML.
*
* @since 1.9.8.6
*/
protected function output_section_screenshot(): void {
$features = $this->get_screenshot_features();
$list = '';
foreach ( $features as $feature ) {
$list .= '<li>' . esc_html( $feature ) . '</li>';
}
// Screenshot section.
printf(
'<section class="screenshot">
<div class="cont">
<img src="%1$s" alt="%2$s" srcset="%4$s 2x"/>
<a href="%3$s" class="hover" data-lity></a>
</div>
<ul>%5$s</ul>
</section>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-tnail.png' ),
esc_attr( $this->get_screenshot_alt_text() ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-full@2x.png' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . static::get_plugin_name() . '/screenshot-tnail@2x.png' ),
wp_kses( $list, [ 'li' => [] ] )
);
}
/**
* Get screenshot features list.
*
* @since 1.9.8.6
*
* @return array Array of feature strings.
*/
abstract protected function get_screenshot_features(): array;
/**
* Get screenshot alt text.
*
* @since 1.9.8.6
*
* @return string Alt text for screenshot image.
*/
abstract protected function get_screenshot_alt_text(): string;
/**
* Generate and output step 'Result' section HTML.
*
* @since 1.9.8.6
*/
abstract protected function output_section_step_result(): void;
/**
* Whether a plugin is configured or not.
*
* @since 1.9.8.6
*
* @return bool True if a plugin is configured properly.
*/
abstract protected function is_plugin_configured(): bool;
/**
* Whether a plugin is active or not.
*
* @since 1.9.8.6
*
* @return bool True if the plugin is active.
*/
abstract protected function is_plugin_activated(): bool;
/**
* Whether a plugin is finished setup or not.
*
* @since 1.9.8.6
*
* @return bool True if the plugin is finished setup.
*/
abstract protected function is_plugin_finished_setup(): bool;
/**
* Whether a plugin is available (class/function exists).
*
* @since 1.9.8.6
*
* @return bool True if a plugin is available.
*/
abstract protected function is_plugin_available(): bool;
/**
* Whether a pro-version is active.
*
* @since 1.9.8.6
*
* @return bool True if a pro-version is active.
*/
abstract protected function is_pro_active(): bool;
/**
* Get the heading for the installation step.
*
* @since 1.9.8.6
*
* @return string Install step heading.
*/
abstract protected function get_install_heading(): string;
/**
* Get the description for the installation step.
*
* @since 1.9.8.6
*
* @return string Install step description.
*/
abstract protected function get_install_description(): string;
/**
* Get the plugin title.
*
* @since 1.9.8.6
*
* @return string Plugin title.
*/
abstract protected function get_plugin_title(): string;
/**
* Get the installation button text.
*
* @since 1.9.8.6
*
* @return string Install button text.
*/
abstract protected function get_install_button_text(): string;
/**
* Get the text when a plugin is installed and activated.
*
* @since 1.9.8.6
*
* @return string Installed & activated text.
*/
abstract protected function get_installed_activated_text(): string;
/**
* Get the activate button text.
*
* @since 1.9.8.6
*
* @return string Activate button text.
*/
abstract protected function get_activate_text(): string;
/**
* Get the heading for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step heading.
*/
abstract protected function get_setup_heading(): string;
/**
* Get the description for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step description.
*/
abstract protected function get_setup_description(): string;
/**
* Get the setup button text.
*
* @since 1.9.8.6
*
* @return string Setup button text.
*/
abstract protected function get_setup_button_text(): string;
/**
* Get the text when setup is completed.
*
* @since 1.9.8.6
*
* @return string Setup completed text.
*/
abstract protected function get_setup_completed_text(): string;
/**
* Get the text when a pro-version is installed and activated.
*
* @since 1.9.8.6
*
* @return string Pro installed and activated text.
*/
abstract protected function get_pro_installed_activated_text(): string;
}
@@ -0,0 +1,481 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Privacy Compliance Subpage.
*
* @since 1.9.7.3
*/
class PrivacyCompliance extends Page {
/**
* Admin menu page slug.
*
* @since 1.9.7.3
*
* @var string
*/
public const SLUG = 'wpforms-wpconsent';
/**
* Configuration.
*
* @since 1.9.7.3
*
* @var array
*/
protected $config = [
'lite_plugin' => 'wpconsent-cookies-banner-privacy-suite/wpconsent.php',
'lite_wporg_url' => 'https://wordpress.org/plugins/wpconsent-cookies-banner-privacy-suite/',
'lite_download_url' => 'https://downloads.wordpress.org/plugin/wpconsent-cookies-banner-privacy-suite.zip',
'pro_plugin' => 'wpconsent-premium/wpconsent-premium.php',
'wpconsent_addon' => 'wpconsent-premium/wpconsent-premium.php',
'wpconsent_addon_page' => 'https://wpconsent.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=privacy-compliance-page',
'wpconsent_onboarding' => 'admin.php?page=wpconsent-onboarding',
];
/**
* Get the plugin name for use in IDs, CSS classes, and config keys.
*
* @since 1.9.7.3
*
* @return string Plugin name.
*/
protected static function get_plugin_name(): string {
return 'wpconsent';
}
/**
* Hooks.
*
* @since 1.9.7.3
*/
public function hooks(): void {
if ( wp_doing_ajax() ) {
remove_action( 'admin_init', 'wpconsent_maybe_redirect_onboarding', 9999 );
}
parent::hooks();
}
/**
* Get heading image URL.
*
* @since 1.9.7.3
*
* @return string Heading image URL.
*/
protected function get_heading_image_url(): string {
return WPFORMS_PLUGIN_URL . 'assets/images/wpconsent/wpforms-wpconsent.svg';
}
/**
* Get heading title text.
*
* @since 1.9.7.3
*
* @return string Heading title.
*/
protected function get_heading_title(): string {
return esc_html__( 'Make Your Website Privacy-Compliant in Minutes', 'wpforms-lite' );
}
/**
* Get heading alt text for logo.
*
* @since 1.9.7.3
*
* @return string Heading alt text.
*/
protected function get_heading_alt_text(): string {
return esc_attr__( 'WPForms ♥ WPConsent', 'wpforms-lite' );
}
/**
* Get heading description strings.
*
* @since 1.9.7.3
*
* @return array Array of description strings.
*/
protected function get_heading_strings(): array {
return [
esc_html__( 'Build trust with clear, compliant privacy practices. WPConsent adds clean, professional banners and handles the technical side for you.', 'wpforms-lite' ),
esc_html__( 'Built for transparency. Designed for ease.', 'wpforms-lite' ),
];
}
/**
* Get screenshot features list.
*
* @since 1.9.7.3
*
* @return array Array of feature strings.
*/
protected function get_screenshot_features(): array {
return [
esc_html__( 'A professional banner that fits your site.', 'wpforms-lite' ),
esc_html__( 'Tools like Google Analytics and Facebook Pixel paused until consent.', 'wpforms-lite' ),
esc_html__( 'Peace of mind knowing youre aligned with global laws.', 'wpforms-lite' ),
esc_html__( 'Self-hosted. Your data remains on your site.', 'wpforms-lite' ),
];
}
/**
* Get screenshot alt text.
*
* @since 1.9.7.3
*
* @return string Alt text for screenshot image.
*/
protected function get_screenshot_alt_text(): string {
return esc_attr__( 'WPConsent screenshot', 'wpforms-lite' );
}
/**
* Generate and output step 'Result' section HTML.
*
* @since 1.9.7.3
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_result(): void {
$step = $this->get_data_step_result();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-result %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 3', 'wpforms-lite' ),
esc_html__( 'Get Advanced Cookie Consent Features', 'wpforms-lite' ),
esc_html__( 'With WPConsent Pro you can access advanced features like geolocation, popup layout, records of consent, multilanguage support, and more.', 'wpforms-lite' ),
esc_attr( $step['button_class'] ),
esc_url( $step['button_url'] ),
esc_html( $step['button_text'] )
);
}
/**
* Step 'Result' data.
*
* @since 1.9.7.3
*
* @return array Step data.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function get_data_step_result(): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$step = [];
$step['icon'] = 'step-3.svg';
$step['section_class'] = $this->output_data['plugin_setup'] ? '' : 'grey';
$step['button_text'] = esc_html__( 'Learn More', 'wpforms-lite' );
$step['button_class'] = 'grey disabled';
$step['button_url'] = '';
$plugin_license_level = $this->get_license_level();
switch ( $plugin_license_level ) {
case 'lite':
$step['button_url'] = $this->config['wpconsent_addon_page'];
$step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled';
break;
case 'pro':
$addon_installed = array_key_exists( $this->config['wpconsent_addon'], $this->output_data['all_plugins'] );
$step['button_text'] =
$addon_installed
? esc_html__( 'WPConsent Pro Installed & Activated', 'wpforms-lite' )
: esc_html__( 'Install Now', 'wpforms-lite' );
$step['button_class'] = $this->output_data['plugin_setup'] ? 'grey disabled' : 'button-primary';
$step['icon'] = $addon_installed ? 'step-complete.svg' : 'step-3.svg';
break;
}
return $step;
}
/**
* Retrieve the license level of the plugin.
*
* @since 1.9.8.6
*
* @return string The plugin license level ('lite' or 'pro').
*/
protected function get_license_level(): string {
$plugin_license_level = 'lite';
// Check if premium features are available.
if ( function_exists( 'wpconsent' ) ) {
$wpconsent = wpconsent();
if ( isset( $wpconsent->license ) && method_exists( $wpconsent->license, 'is_active' ) ) {
$plugin_license_level = $wpconsent->license->is_active() ? 'pro' : 'lite';
}
}
return $plugin_license_level;
}
/**
* Whether the plugin is finished setup or not.
*
* @since 1.9.8.6
*/
protected function is_plugin_finished_setup(): bool {
if ( ! $this->is_plugin_configured() ) {
return false;
}
return $this->get_license_level() === 'pro';
}
/**
* Set the source of the plugin installation.
*
* @since 1.9.8
* @deprecated 1.9.8.6
*
* @param string $plugin_basename The basename of the plugin.
*/
public function privacy_compliance_activated( string $plugin_basename ): void {
$this->plugin_activated( $plugin_basename );
}
/**
* Whether a plugin is configured or not.
*
* @since 1.9.7.3
*
* @return bool True if plugin is configured properly.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function is_plugin_configured(): bool {
if ( ! $this->is_plugin_activated() ) {
return false;
}
// Check if WPConsent has been configured with basic settings.
// The plugin is considered configured if the consent banner is enabled.
if ( function_exists( 'wpconsent' ) ) {
$wpconsent = wpconsent();
if ( isset( $wpconsent->settings ) ) {
$enable_consent_banner = $wpconsent->settings->get_option( 'enable_consent_banner', 0 );
return ! empty( $enable_consent_banner );
}
}
return false;
}
/**
* Whether a plugin is active or not.
*
* @since 1.9.7.3
*
* @return bool True if plugin is active.
*/
protected function is_plugin_activated(): bool {
return (
function_exists( 'wpconsent' ) &&
(
is_plugin_active( $this->config['lite_plugin'] ) ||
is_plugin_active( $this->config['pro_plugin'] )
)
);
}
/**
* Whether a plugin is available (class/function exists).
*
* @since 1.9.7.3
*
* @return bool True if plugin is available.
*/
protected function is_plugin_available(): bool {
return function_exists( 'wpconsent' );
}
/**
* Whether pro version is active.
*
* @since 1.9.7.3
*
* @return bool True if pro version is active.
* @noinspection PhpUndefinedFunctionInspection
*/
protected function is_pro_active(): bool {
if ( ! function_exists( 'wpconsent' ) ) {
return false;
}
$wpconsent = wpconsent();
return isset( $wpconsent->license ) && method_exists( $wpconsent->license, 'is_active' ) && $wpconsent->license->is_active();
}
/**
* Get the heading for the install step.
*
* @since 1.9.7.3
*
* @return string Install step heading.
*/
protected function get_install_heading(): string {
return esc_html__( 'Install & Activate WPConsent', 'wpforms-lite' );
}
/**
* Get the description for the install step.
*
* @since 1.9.7.3
*
* @return string Install step description.
*/
protected function get_install_description(): string {
return esc_html__( 'Install WPConsent from the WordPress.org plugin repository.', 'wpforms-lite' );
}
/**
* Get the plugin title.
*
* @since 1.9.7.3
*
* @return string Plugin title.
*/
protected function get_plugin_title(): string {
return esc_html__( 'WPConsent', 'wpforms-lite' );
}
/**
* Get the install button text.
*
* @since 1.9.7.3
*
* @return string Install button text.
*/
protected function get_install_button_text(): string {
return esc_html__( 'Install WPConsent', 'wpforms-lite' );
}
/**
* Get the text when a plugin is installed and activated.
*
* @since 1.9.7.3
*
* @return string Installed & activated text.
*/
protected function get_installed_activated_text(): string {
return esc_html__( 'WPConsent Installed & Activated', 'wpforms-lite' );
}
/**
* Get the activate button text.
*
* @since 1.9.7.3
*
* @return string Activate button text.
*/
protected function get_activate_text(): string {
return esc_html__( 'Activate WPConsent', 'wpforms-lite' );
}
/**
* Get the heading for the setup step.
*
* @since 1.9.7.3
*
* @return string Setup step heading.
*/
protected function get_setup_heading(): string {
return esc_html__( 'Set Up WPConsent', 'wpforms-lite' );
}
/**
* Get the description for the setup step.
*
* @since 1.9.7.3
*
* @return string Setup step description.
*/
protected function get_setup_description(): string {
return esc_html__( 'WPConsent has an intuitive setup wizard to guide you through the cookie consent configuration process.', 'wpforms-lite' );
}
/**
* Get the setup button text.
*
* @since 1.9.7.3
*
* @return string Setup button text.
*/
protected function get_setup_button_text(): string {
return esc_html__( 'Run Setup Wizard', 'wpforms-lite' );
}
/**
* Get the text when setup is completed.
*
* @since 1.9.7.3
*
* @return string Setup completed text.
*/
protected function get_setup_completed_text(): string {
return esc_html__( 'Setup Complete', 'wpforms-lite' );
}
/**
* Get the text when a pro-version is installed and activated.
*
* @since 1.9.7.3
*
* @return string Pro installed and activated text.
*/
protected function get_pro_installed_activated_text(): string {
return esc_html__( 'WPConsent Pro Installed & Activated', 'wpforms-lite' );
}
}
@@ -0,0 +1,562 @@
<?php
namespace WPForms\Admin\Pages;
/**
* SMTP Sub-page.
*
* @since 1.5.7
*/
class SMTP {
/**
* Admin menu page slug.
*
* @since 1.5.7
*
* @var string
*/
const SLUG = 'wpforms-smtp';
/**
* Configuration.
*
* @since 1.5.7
*
* @var array
*/
private $config = [
'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_url' => 'admin.php?page=wp-mail-smtp',
'smtp_wizard_url' => 'admin.php?page=wp-mail-smtp-setup-wizard',
];
/**
* Runtime data used for generating page HTML.
*
* @since 1.5.7
*
* @var array
*/
private $output_data = [];
/**
* Constructor.
*
* @since 1.5.7
*/
public function __construct() {
if ( ! wpforms_current_user_can() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.5.7
*/
public function hooks() {
if ( wp_doing_ajax() ) {
add_action( 'wp_ajax_wpforms_smtp_page_check_plugin_status', [ $this, 'ajax_check_plugin_status' ] );
add_action( 'wpforms_plugin_activated', [ $this, 'smtp_activated' ] );
}
// Check what page we are on.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
// Only load if we are actually on the SMTP page.
if ( $page !== self::SLUG ) {
return;
}
add_action( 'admin_init', [ $this, 'redirect_to_smtp_settings' ] );
add_filter( 'wpforms_admin_header', '__return_false' );
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
// Hook for addons.
do_action( 'wpforms_admin_pages_smtp_hooks' );
}
/**
* Enqueue JS and CSS files.
*
* @since 1.5.7
*/
public function enqueue_assets() {
$min = wpforms_get_min_suffix();
// Lity.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
wp_enqueue_script(
'wpforms-admin-page-smtp',
WPFORMS_PLUGIN_URL . "assets/js/admin/pages/smtp{$min}.js",
[ 'jquery' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-admin-page-smtp',
'wpforms_pluginlanding',
$this->get_js_strings()
);
}
/**
* Set wp_mail_smtp_source option to 'wpforms' on WP Mail SMTP plugin activation.
*
* @since 1.8.7
*
* @param string $plugin_basename Plugin basename.
*/
public function smtp_activated( $plugin_basename ) {
if ( $plugin_basename !== $this->config['lite_plugin'] ) {
return;
}
// If user came from some certain page to install WP Mail SMTP, we can get the source and write it instead of default one.
$source = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : 'wpforms'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
update_option( 'wp_mail_smtp_source', $source );
}
/**
* JS Strings.
*
* @since 1.5.7
*
* @return array Array of strings.
*/
protected function get_js_strings() {
$error_could_not_install = sprintf(
wp_kses( /* translators: %s - Lite plugin download URL. */
__( 'Could not install the plugin automatically. Please <a href="%s">download</a> it and install it manually.', 'wpforms-lite' ),
[
'a' => [
'href' => true,
],
]
),
esc_url( $this->config['lite_download_url'] )
);
$error_could_not_activate = sprintf(
wp_kses( /* translators: %s - Lite plugin download URL. */
__( 'Could not activate the plugin. Please activate it on the <a href="%s">Plugins page</a>.', 'wpforms-lite' ),
[
'a' => [
'href' => true,
],
]
),
esc_url( admin_url( 'plugins.php' ) )
);
return [
'installing' => esc_html__( 'Installing...', 'wpforms-lite' ),
'activating' => esc_html__( 'Activating...', 'wpforms-lite' ),
'activated' => esc_html__( 'WP Mail SMTP Installed & Activated', 'wpforms-lite' ),
'install_now' => esc_html__( 'Install Now', 'wpforms-lite' ),
'activate_now' => esc_html__( 'Activate Now', 'wpforms-lite' ),
'download_now' => esc_html__( 'Download Now', 'wpforms-lite' ),
'plugins_page' => esc_html__( 'Go to Plugins page', 'wpforms-lite' ),
'error_could_not_install' => $error_could_not_install,
'error_could_not_activate' => $error_could_not_activate,
'manual_install_url' => $this->config['lite_download_url'],
'manual_activate_url' => admin_url( 'plugins.php' ),
'smtp_settings' => esc_html__( 'Go to SMTP settings', 'wpforms-lite' ),
'smtp_wizard' => esc_html__( 'Open Setup Wizard', 'wpforms-lite' ),
'smtp_settings_url' => esc_url( $this->config['smtp_settings_url'] ),
'smtp_wizard_url' => esc_url( $this->config['smtp_wizard_url'] ),
];
}
/**
* Generate and output page HTML.
*
* @since 1.5.7
*/
public function output() {
echo '<div id="wpforms-admin-smtp" class="wrap wpforms-admin-wrap wpforms-admin-plugin-landing">';
$this->output_section_heading();
$this->output_section_screenshot();
$this->output_section_step_install();
$this->output_section_step_setup();
echo '</div>';
}
/**
* Generate and output heading section HTML.
*
* @since 1.5.7
*/
protected function output_section_heading() {
// Heading section.
printf(
'<section class="top">
<img class="img-top" src="%1$s" srcset="%2$s 2x" alt="%3$s"/>
<h1>%4$s</h1>
<p>%5$s</p>
</section>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp.png' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/wpforms-wpmailsmtp@2x.png' ),
esc_attr__( 'WPForms ♥ WP Mail SMTP', 'wpforms-lite' ),
esc_html__( 'Making Email Deliverability Easy for WordPress', 'wpforms-lite' ),
esc_html__( 'WP Mail SMTP fixes deliverability problems with your WordPress emails and form notifications. It\'s built by the same folks behind WPForms.', 'wpforms-lite' )
);
}
/**
* Generate and output screenshot section HTML.
*
* @since 1.5.7
*/
protected function output_section_screenshot() {
// Screenshot section.
printf(
'<section class="screenshot">
<div class="cont">
<img src="%1$s" alt="%2$s"/>
<a href="%3$s" class="hover" data-lity></a>
</div>
<ul>
<li>%4$s</li>
<li>%5$s</li>
<li>%6$s</li>
<li>%7$s</li>
</ul>
</section>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/screenshot-tnail.png?ver=' . WPFORMS_VERSION ),
esc_attr__( 'WP Mail SMTP screenshot', 'wpforms-lite' ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/smtp/screenshot-full.png?ver=' . WPFORMS_VERSION ),
esc_html__( 'Improves email deliverability in WordPress.', 'wpforms-lite' ),
esc_html__( 'Used by 4+ million websites.', 'wpforms-lite' ),
esc_html__( 'Free mailers: SendLayer, SMTP.com, Brevo, Google Workspace / Gmail, Mailgun, Postmark, SendGrid.', 'wpforms-lite' ),
esc_html__( 'Pro mailers: Amazon SES, Microsoft 365 / Outlook.com, Zoho Mail.', 'wpforms-lite' )
);
}
/**
* Generate and output step 'Install' section HTML.
*
* @since 1.5.7
*/
protected function output_section_step_install() {
$step = $this->get_data_step_install();
if ( empty( $step ) ) {
return;
}
$button_format = '<button class="button %3$s" data-plugin="%1$s" data-action="%4$s" data-source="%5$s">%2$s</button>';
$button_allowed_html = [
'button' => [
'class' => true,
'data-plugin' => true,
'data-action' => true,
'data-source' => true,
],
];
if (
! $this->output_data['plugin_installed'] &&
! $this->output_data['pro_plugin_installed'] &&
! wpforms_can_install( 'plugin' )
) {
$button_format = '<a class="link" href="%1$s" target="_blank" rel="nofollow noopener">%2$s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
$button_allowed_html = [
'a' => [
'class' => true,
'href' => true,
'target' => true,
'rel' => true,
],
'span' => [
'class' => true,
'aria-hidden' => true,
],
];
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$source = isset( $_GET['source'] ) && $_GET['source'] === 'woocommerce' ? 'wpforms-woocommerce' : 'wpforms';
$button = sprintf( $button_format, esc_attr( $step['plugin'] ), esc_html( $step['button_text'] ), esc_attr( $step['button_class'] ), esc_attr( $step['button_action'] ), esc_attr( $source ) );
printf(
'<section class="step step-install">
<aside class="num">
<img src="%1$s" alt="%2$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%3$s</h2>
<p>%4$s</p>
%5$s
</div>
</section>',
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 1', 'wpforms-lite' ),
esc_html( $step['heading'] ),
esc_html( $step['description'] ),
wp_kses( $button, $button_allowed_html )
);
}
/**
* Generate and output step 'Setup' section HTML.
*
* @since 1.5.7
*/
protected function output_section_step_setup() {
$step = $this->get_data_step_setup();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-setup %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 2', 'wpforms-lite' ),
esc_html__( 'Set Up WP Mail SMTP', 'wpforms-lite' ),
esc_html__( 'Select and configure your mailer.', 'wpforms-lite' ),
esc_attr( $step['button_class'] ),
esc_url( admin_url( $this->config['smtp_wizard_url'] ) ),
esc_html( $step['button_text'] )
);
}
/**
* Step 'Install' data.
*
* @since 1.5.7
*
* @return array Step data.
*/
protected function get_data_step_install() {
$step = [];
$step['heading'] = esc_html__( 'Install and Activate WP Mail SMTP', 'wpforms-lite' );
$step['description'] = esc_html__( 'Install WP Mail SMTP from the WordPress.org plugin repository.', 'wpforms-lite' );
$this->output_data['all_plugins'] = get_plugins();
$this->output_data['plugin_installed'] = array_key_exists( $this->config['lite_plugin'], $this->output_data['all_plugins'] );
$this->output_data['pro_plugin_installed'] = array_key_exists( $this->config['pro_plugin'], $this->output_data['all_plugins'] );
$this->output_data['plugin_activated'] = false;
$this->output_data['plugin_setup'] = false;
if ( ! $this->output_data['plugin_installed'] && ! $this->output_data['pro_plugin_installed'] ) {
$step['icon'] = 'step-1.svg';
$step['button_text'] = esc_html__( 'Install WP Mail SMTP', 'wpforms-lite' );
$step['button_class'] = 'button-primary';
$step['button_action'] = 'install';
$step['plugin'] = $this->config['lite_download_url'];
if ( ! wpforms_can_install( 'plugin' ) ) {
$step['heading'] = esc_html__( 'WP Mail SMTP', 'wpforms-lite' );
$step['description'] = '';
$step['button_text'] = esc_html__( 'WP Mail SMTP on WordPress.org', 'wpforms-lite' );
$step['plugin'] = $this->config['lite_wporg_url'];
}
} else {
$this->output_data['plugin_activated'] = $this->is_smtp_activated();
$this->output_data['plugin_setup'] = $this->is_smtp_configured();
$step['icon'] = $this->output_data['plugin_activated'] ? 'step-complete.svg' : 'step-1.svg';
$step['button_text'] = $this->output_data['plugin_activated'] ? esc_html__( 'WP Mail SMTP Installed & Activated', 'wpforms-lite' ) : esc_html__( 'Activate WP Mail SMTP', 'wpforms-lite' );
$step['button_class'] = $this->output_data['plugin_activated'] ? 'grey disabled' : 'button-primary';
$step['button_action'] = $this->output_data['plugin_activated'] ? '' : 'activate';
$step['plugin'] = $this->output_data['pro_plugin_installed'] ? $this->config['pro_plugin'] : $this->config['lite_plugin'];
}
return $step;
}
/**
* Step 'Setup' data.
*
* @since 1.5.7
*
* @return array Step data.
*/
protected function get_data_step_setup() {
$step = [
'icon' => 'step-2.svg',
];
if ( $this->output_data['plugin_activated'] ) {
$step['section_class'] = '';
$step['button_class'] = 'button-primary';
$step['button_text'] = esc_html__( 'Open Setup Wizard', 'wpforms-lite' );
} else {
$step['section_class'] = 'grey';
$step['button_class'] = 'grey disabled';
$step['button_text'] = esc_html__( 'Start Setup', 'wpforms-lite' );
}
if ( $this->output_data['plugin_setup'] ) {
$step['icon'] = 'step-complete.svg';
$step['button_text'] = esc_html__( 'Go to SMTP settings', 'wpforms-lite' );
}
return $step;
}
/**
* Ajax endpoint. Check plugin setup status.
* Used to properly init step 'Setup' section after completing step 'Install'.
*
* @since 1.5.7
*/
public function ajax_check_plugin_status() {
// Security checks.
if (
! check_ajax_referer( 'wpforms-admin', 'nonce', false ) ||
! wpforms_current_user_can()
) {
wp_send_json_error(
[
'error' => esc_html__( 'You do not have permission.', 'wpforms-lite' ),
]
);
}
$result = [];
if ( ! $this->is_smtp_activated() ) {
wp_send_json_error(
[
'error' => esc_html__( 'Plugin unavailable.', 'wpforms-lite' ),
]
);
}
$result['setup_status'] = (int) $this->is_smtp_configured();
$result['license_level'] = wp_mail_smtp()->get_license_type();
// Prevent redirect to the WP Mail SMTP Setup Wizard on the fresh installs.
// We need this workaround since WP Mail SMTP doesn't check whether the mailer is already configured when redirecting to the Setup Wizard on the first run.
if ( $result['setup_status'] > 0 ) {
update_option( 'wp_mail_smtp_activation_prevent_redirect', true );
}
wp_send_json_success( $result );
}
/**
* Get $phpmailer instance.
*
* @since 1.5.7
* @since 1.6.1.2 Conditionally returns $phpmailer v5 or v6.
* @since 1.8.7 Use always $phpmailer v6.
*
* @return \PHPMailer|\PHPMailer\PHPMailer\PHPMailer Instance of PHPMailer.
*/
protected function get_phpmailer() {
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;
}
/**
* Whether WP Mail SMTP plugin configured or not.
*
* @since 1.5.7
*
* @return bool True if some mailer is selected and configured properly.
*/
protected function is_smtp_configured() {
if ( ! $this->is_smtp_activated() ) {
return false;
}
$phpmailer = $this->get_phpmailer();
$mailer = \WPMailSMTP\Options::init()->get( 'mail', 'mailer' );
return ! empty( $mailer ) &&
$mailer !== 'mail' &&
wp_mail_smtp()->get_providers()->get_mailer( $mailer, $phpmailer )->is_mailer_complete();
}
/**
* Whether WP Mail SMTP plugin active or not.
*
* @since 1.5.7
*
* @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'] ) );
}
/**
* Redirect to SMTP settings page.
*
* @since 1.5.7
*/
public function redirect_to_smtp_settings() {
// Redirect to SMTP plugin if it is activated.
if ( $this->is_smtp_configured() ) {
wp_safe_redirect( admin_url( $this->config['smtp_settings_url'] ) );
exit;
}
}
}
@@ -0,0 +1,492 @@
<?php
namespace WPForms\Admin\Pages;
/**
* Sugar Calendar Subpage.
*
* @since 1.9.8.6
*/
class SugarCalendar extends Page {
/**
* Admin menu page slug.
*
* @since 1.9.8.6
*
* @var string
*/
public const SLUG = 'wpforms-sugar-calendar';
/**
* Configuration.
*
* @since 1.9.8.6
*
* @var array
*/
protected $config = [
'lite_plugin' => 'sugar-calendar-lite/sugar-calendar-lite.php',
'lite_wporg_url' => 'https://wordpress.org/plugins/sugar-calendar-lite/',
'lite_download_url' => 'https://downloads.wordpress.org/plugin/sugar-calendar-lite.zip',
'pro_plugin' => 'sugar-calendar/sugar-calendar.php',
'sugar-calendar_addon' => 'sugar-calendar/sugar-calendar.php',
'sugar-calendar_addon_page' => 'https://sugarcalendar.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=sugar-calendar-page',
'sugar-calendar_onboarding' => 'post-new.php?post_type=sc_event',
];
/**
* Hooks.
*
* @since 1.9.8.6
*/
public function hooks(): void {
if ( wp_doing_ajax() ) {
add_filter( 'default_option_sugar_calendar_prevent_redirect', '__return_true' );
}
parent::hooks();
}
/**
* Get the plugin name for use in IDs, CSS classes, and config keys.
*
* @since 1.9.8.6
*
* @return string Plugin name.
*/
protected static function get_plugin_name(): string {
return 'sugar-calendar';
}
/**
* Get heading title text.
*
* @since 1.9.8.6
*
* @return string Heading title.
*/
protected function get_heading_title(): string {
return esc_html__( 'Taking Bookings? Put Them on a Calendar', 'wpforms-lite' );
}
/**
* Get heading alt text for logo.
*
* @since 1.9.8.6
*
* @return string Heading alt text.
*/
protected function get_heading_alt_text(): string {
return esc_attr__( 'WPForms ♥ Sugar Calendar', 'wpforms-lite' );
}
/**
* Get heading description strings.
*
* @since 1.9.8.6
*
* @return array Array of description strings.
*/
protected function get_heading_strings(): array {
return [
esc_html__( 'WPForms collects the "yes." Sugar Calendar shows the "when and where."', 'wpforms-lite' ),
esc_html__( 'Together, they turn bookings into events your visitors can browse, sync, and show up for.', 'wpforms-lite' ),
esc_html__( 'Simple, elegant, and built for your workflow.', 'wpforms-lite' ),
];
}
/**
* Get screenshot features list.
*
* @since 1.9.8.6
*
* @return array Array of feature strings.
*/
protected function get_screenshot_features(): array {
return [
esc_html__( 'Display events on beautiful calendars visitors can browse and filter.', 'wpforms-lite' ),
esc_html__( 'Sell tickets with Stripe or WooCommerce integration.', 'wpforms-lite' ),
esc_html__( 'Visitors can add events to Google, Apple, or Outlook calendars with one click.', 'wpforms-lite' ),
esc_html__( 'Set up recurring events: daily, weekly, monthly, or custom patterns.', 'wpforms-lite' ),
];
}
/**
* Get screenshot alt text.
*
* @since 1.9.8.6
*
* @return string Alt text for screenshot image.
*/
protected function get_screenshot_alt_text(): string {
return esc_attr__( 'Sugar Calendar screenshot', 'wpforms-lite' );
}
/**
* Generate and output step 'Result' section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_result(): void {
$step = $this->get_data_step_result();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-result %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 3', 'wpforms-lite' ),
esc_html__( 'Display Events on Your Site', 'wpforms-lite' ),
esc_html__( 'Use the Calendar block or shortcode [sc_events_calendar] to embed events anywhere on your site.', 'wpforms-lite' ),
esc_attr( $step['button_class'] ),
esc_url( $step['button_url'] ),
esc_html( $step['button_text'] )
);
}
/**
* Generate and output footer section HTML.
*
* @since 1.9.8.6
*/
protected function output_section_footer(): void {
printf(
'<section class="bottom">
<p>%s</p>
</section>',
esc_html__( 'From the same team trusted by over 6 million sites.', 'wpforms-lite' )
);
}
/**
* Step 'Result' data.
*
* @since 1.9.8.6
*
* @return array Step data.
*/
protected function get_data_step_result(): array {
$step = $this->get_default_step_data();
$plugin_license_level = $this->get_plugin_license_level();
if ( $plugin_license_level === 'lite' ) {
$this->apply_lite_step_data( $step );
} elseif ( $plugin_license_level === 'pro' ) {
$this->apply_pro_step_data( $step );
}
return $step;
}
/**
* Whether the plugin is finished setup or not.
*
* @since 1.9.8.6
*/
protected function is_plugin_finished_setup(): bool {
if ( ! $this->is_plugin_configured() ) {
return false;
}
return $this->get_plugin_license_level() === 'pro';
}
/**
* Get default step data.
*
* @since 1.9.8.6
*
* @return array Default step data.
*/
private function get_default_step_data(): array {
return [
'icon' => 'step-3.svg',
'section_class' => $this->output_data['plugin_setup'] ? '' : 'grey',
'button_text' => esc_html__( 'Learn More', 'wpforms-lite' ),
'button_class' => 'grey disabled',
'button_url' => '',
];
}
/**
* Get plugin license level.
*
* @since 1.9.8.6
*
* @return string License level ('lite', 'pro') or false if not activated.
*/
private function get_plugin_license_level(): string {
if ( ! function_exists( 'sugar_calendar' ) ) {
return 'lite';
}
$sugar_calendar = sugar_calendar();
return $sugar_calendar->__get( 'is_pro' ) ? 'pro' : 'lite';
}
/**
* Apply lite version step data.
*
* @since 1.9.8.6
*
* @param array $step Step data array (passed by reference).
*/
private function apply_lite_step_data( array &$step ): void {
$step['button_url'] = $this->config['sugar-calendar_addon_page'];
$step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled';
}
/**
* Apply pro version step data.
*
* @since 1.9.8.6
*
* @param array $step Step data array (passed by reference).
*/
private function apply_pro_step_data( array &$step ): void {
$addon_installed = array_key_exists( $this->config['sugar-calendar_addon'], $this->output_data['all_plugins'] );
$configured = $this->is_plugin_configured();
$step['button_text'] = $addon_installed && $configured
? esc_html__( 'Sugar Calendar Pro Installed & Activated', 'wpforms-lite' )
: esc_html__( 'Install Now', 'wpforms-lite' );
$step['button_class'] = $this->output_data['plugin_setup'] || ! $configured ? 'grey disabled' : 'button-primary';
$step['icon'] = $addon_installed && $configured ? 'step-complete.svg' : 'step-3.svg';
}
/**
* Whether a plugin is configured or not.
*
* @since 1.9.8.6
*
* @return bool True if plugin is configured properly.
*/
protected function is_plugin_configured(): bool {
if ( ! $this->is_plugin_activated() ) {
return false;
}
$events = get_posts(
[
'post_type' => 'sc_event',
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
]
);
return ! empty( $events );
}
/**
* Whether a plugin is active or not.
*
* @since 1.9.8.6
*
* @return bool True if the plugin is active.
*/
protected function is_plugin_activated(): bool {
return (
( function_exists( 'sugar_calendar' ) || class_exists( 'Sugar_Calendar\Plugin' ) ) &&
(
is_plugin_active( $this->config['lite_plugin'] ) ||
is_plugin_active( $this->config['pro_plugin'] )
)
);
}
/**
* Whether a plugin is available (class/function exists).
*
* @since 1.9.8.6
*
* @return bool True if plugin is available.
*/
protected function is_plugin_available(): bool {
return class_exists( 'Sugar_Calendar\Plugin' ) || function_exists( 'sugar_calendar' );
}
/**
* Whether pro version is active.
*
* @since 1.9.8.6
*
* @return bool True if pro version is active.
*/
protected function is_pro_active(): bool {
if ( ! function_exists( 'sugar_calendar' ) ) {
return false;
}
return sugar_calendar()->is_pro();
}
/**
* Get the heading for the install step.
*
* @since 1.9.8.6
*
* @return string Install step heading.
*/
protected function get_install_heading(): string {
return esc_html__( 'Install and Activate Sugar Calendar', 'wpforms-lite' );
}
/**
* Get the description for the install step.
*
* @since 1.9.8.6
*
* @return string Install step description.
*/
protected function get_install_description(): string {
return esc_html__( 'Bring your forms to life. Install Sugar Calendar and start creating events.', 'wpforms-lite' );
}
/**
* Get the plugin title.
*
* @since 1.9.8.6
*
* @return string Plugin title.
*/
protected function get_plugin_title(): string {
return esc_html__( 'Sugar Calendar', 'wpforms-lite' );
}
/**
* Get the install button text.
*
* @since 1.9.8.6
*
* @return string Install button text.
*/
protected function get_install_button_text(): string {
return esc_html__( 'Install Sugar Calendar', 'wpforms-lite' );
}
/**
* Get the text when a plugin is installed and activated.
*
* @since 1.9.8.6
*
* @return string Installed & activated text.
*/
protected function get_installed_activated_text(): string {
return esc_html__( 'Sugar Calendar Installed & Activated', 'wpforms-lite' );
}
/**
* Get the activate button text.
*
* @since 1.9.8.6
*
* @return string Activate button text.
*/
protected function get_activate_text(): string {
return esc_html__( 'Activate Sugar Calendar', 'wpforms-lite' );
}
/**
* Get the heading for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step heading.
*/
protected function get_setup_heading(): string {
return esc_html__( 'Create Your First Event', 'wpforms-lite' );
}
/**
* Get the description for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step description.
*/
protected function get_setup_description(): string {
return esc_html__( 'Add your first booking or class to your calendar in seconds. Clean, simple, and built right into WordPress.', 'wpforms-lite' );
}
/**
* Get the setup button text.
*
* @since 1.9.8.6
*
* @return string Setup button text.
*/
protected function get_setup_button_text(): string {
return esc_html__( 'Add First Event', 'wpforms-lite' );
}
/**
* Get the text when setup is completed.
*
* @since 1.9.8.6
*
* @return string Setup completed text.
*/
protected function get_setup_completed_text(): string {
return esc_html__( 'Event Created', 'wpforms-lite' );
}
/**
* Get the text when a pro-version is installed and activated.
*
* @since 1.9.8.6
*
* @return string Pro installed and activated text.
*/
protected function get_pro_installed_activated_text(): string {
return esc_html__( 'Sugar Calendar Pro Installed & Activated', 'wpforms-lite' );
}
}
@@ -0,0 +1,149 @@
<?php
namespace WPForms\Admin\Pages;
use WPForms\Admin\Traits\FormTemplates;
/**
* Main Templates page class.
*
* @since 1.7.7
*/
class Templates {
use FormTemplates;
/**
* Page slug.
*
* @since 1.7.7
*
* @var string
*/
const SLUG = 'wpforms-templates';
/**
* Initialize class.
*
* @since 1.7.7
*/
public function init() {
if (
! wpforms_is_admin_page( 'templates' ) &&
! wpforms_is_admin_ajax()
) {
return;
}
$this->addons_obj = wpforms()->obj( 'addons' );
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.7.7
*/
private function hooks() {
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueues' ] );
}
/**
* Enqueue assets.
*
* @since 1.7.7
*/
public function enqueues() {
$min = wpforms_get_min_suffix();
wp_enqueue_style(
'wpforms-form-templates',
WPFORMS_PLUGIN_URL . "assets/css/admin/admin-form-templates{$min}.css",
[],
WPFORMS_VERSION
);
wp_enqueue_script(
'wpforms-admin-form-templates',
WPFORMS_PLUGIN_URL . "assets/js/admin/pages/form-templates{$min}.js",
[ 'underscore', 'wp-util' ],
WPFORMS_VERSION,
true
);
wp_enqueue_style(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.css',
[],
'4.2.6'
);
wp_enqueue_script(
'tooltipster',
WPFORMS_PLUGIN_URL . 'assets/lib/jquery.tooltipster/jquery.tooltipster.min.js',
[ 'jquery', 'wpforms-admin-form-templates' ],
'4.2.6',
true
);
wp_localize_script(
'wpforms-admin-form-templates',
'wpforms_admin_form_templates',
[
'nonce' => wp_create_nonce( 'wpforms-builder' ),
'openAIFormUrl' => admin_url( 'admin.php?page=wpforms-builder&view=setup&ai-form' ),
]
);
}
/**
* Build the output for the Form Templates admin page.
*
* @since 1.7.7
*/
public function output() {
?>
<div id="wpforms-form-templates" class="wrap wpforms-admin-wrap">
<h1 class="page-title"><?php esc_html_e( 'Form Templates', 'wpforms-lite' ); ?></h1>
<div class="wpforms-form-setup-content" >
<div class="wpforms-setup-title">
<?php esc_html_e( 'Get a Head Start With Our Pre-Made Form Templates', 'wpforms-lite' ); ?>
</div>
<p class="wpforms-setup-desc secondary-text">
<?php
printf(
wp_kses( /* translators: %1$s - create template doc link; %2$s - Contact us page link. */
__( 'Choose a template to speed up the process of creating your form. You can also start with a <a href="#" class="wpforms-trigger-blank">blank form</a> or <a href="%1$s" target="_blank" rel="noopener noreferrer">create your own</a>. <br>Have a suggestion for a new template? <a href="%2$s" target="_blank" rel="noopener noreferrer">Wed love to hear it</a>!', 'wpforms-lite' ),
[
'strong' => [],
'br' => [],
'a' => [
'href' => [],
'class' => [],
'target' => [],
'rel' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/how-to-create-a-custom-form-template/', 'Form Templates Subpage', 'Create Your Own Template' ) ),
esc_url( wpforms_utm_link( 'https://wpforms.com/form-template-suggestion/', 'Form Templates Subpage', 'Form Template Suggestion' ) )
);
?>
</p>
<?php $this->output_templates_content(); ?>
</div>
</div>
<?php
}
}
@@ -0,0 +1,469 @@
<?php
namespace WPForms\Admin\Pages;
use Uncanny_Automator\Automator_Load;
use Uncanny_Automator_Pro\Automator_Pro_Load;
/**
* Uncanny Automator Subpage.
*
* @since 1.9.8.6
*/
class UncannyAutomator extends Page {
/**
* Admin menu page slug.
*
* @since 1.9.8.6
*
* @var string
*/
public const SLUG = 'wpforms-uncanny-automator';
/**
* Configuration.
*
* @since 1.9.8.6
*
* @var array
*/
protected $config = [
'lite_plugin' => 'uncanny-automator/uncanny-automator.php',
'lite_wporg_url' => 'https://wordpress.org/plugins/uncanny-automator/',
'lite_download_url' => 'https://downloads.wordpress.org/plugin/uncanny-automator.zip',
'pro_plugin' => 'uncanny-automator-pro/uncanny-automator-pro.php',
'uncanny-automator_addon' => 'uncanny-automator-pro/uncanny-automator-pro.php',
'uncanny-automator_addon_page' => 'https://automatorplugin.com/?utm_source=wpformsplugin&utm_medium=link&utm_campaign=uncanny-automator-page',
'uncanny-automator_onboarding' => 'post-new.php?post_type=uo-recipe',
];
/**
* Get the plugin name for use in IDs, CSS classes, and config keys.
*
* @since 1.9.8.6
*
* @return string Plugin name.
*/
protected static function get_plugin_name(): string {
return 'uncanny-automator';
}
/**
* Get heading title text.
*
* @since 1.9.8.6
*
* @return string Heading title.
*/
protected function get_heading_title(): string {
return esc_html__( 'Let Your Site Handle the Busywork.', 'wpforms-lite' );
}
/**
* Get heading alt text for logo.
*
* @since 1.9.8.6
*
* @return string Heading alt text.
*/
protected function get_heading_alt_text(): string {
return esc_attr__( 'WPForms ♥ Uncanny Automator', 'wpforms-lite' );
}
/**
* Get heading description strings.
*
* @since 1.9.8.6
*
* @return array Array of description strings.
*/
protected function get_heading_strings(): array {
return [
esc_html__( 'Automate tasks, save time, and keep everything running smoothly. Uncanny Automator connects your favorite tools so your site works smarter. No code. No stress.', 'wpforms-lite' ),
];
}
/**
* Get screenshot features list.
*
* @since 1.9.8.6
*
* @return array Array of feature strings.
*/
protected function get_screenshot_features(): array {
return [
'Connect 200+ plugins and apps automatically: social media, memberships, courses, WooCommerce, CRMs, team chat, and much more.',
'Create users, assign access, and enroll in courses with no manual work.',
'Build multi-step workflows with delays and conditional logic, no code required.',
'Unlimited automations with no per-task fees.',
];
}
/**
* Get screenshot alt text.
*
* @since 1.9.8.6
*
* @return string Alt text for screenshot image.
*/
protected function get_screenshot_alt_text(): string {
return esc_attr__( 'Uncanny Automator screenshot', 'wpforms-lite' );
}
/**
* Generate and output step 'Result' section HTML.
*
* @since 1.9.8.6
*
* @noinspection HtmlUnknownTarget
*/
protected function output_section_step_result(): void {
$step = $this->get_data_step_result();
if ( empty( $step ) ) {
return;
}
printf(
'<section class="step step-result %1$s">
<aside class="num">
<img src="%2$s" alt="%3$s" />
<i class="loader hidden"></i>
</aside>
<div>
<h2>%4$s</h2>
<p>%5$s</p>
<button class="button %6$s" data-url="%7$s">%8$s</button>
</div>
</section>',
esc_attr( $step['section_class'] ),
esc_url( WPFORMS_PLUGIN_URL . 'assets/images/' . $step['icon'] ),
esc_attr__( 'Step 3', 'wpforms-lite' ),
esc_html__( 'Save and Test Your Automation', 'wpforms-lite' ),
esc_html__( 'Click Save Recipe, run a test, and watch your workflow run on its own, no code needed.', 'wpforms-lite' ),
esc_attr( $step['button_class'] ),
esc_url( $step['button_url'] ),
esc_html( $step['button_text'] )
);
}
/**
* Step 'Result' data.
*
* @since 1.9.8.6
*
* @return array Step data.
*/
protected function get_data_step_result(): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$step = [];
$step['icon'] = 'step-3.svg';
$step['section_class'] = $this->output_data['plugin_setup'] ? '' : 'grey';
$step['button_text'] = esc_html__( 'Learn More', 'wpforms-lite' );
$step['button_class'] = 'grey disabled';
$step['button_url'] = '';
$plugin_license_level = $this->get_license_level();
switch ( $plugin_license_level ) {
case 'lite':
$step['button_url'] = $this->config['uncanny-automator_addon_page'];
$step['button_class'] = $this->output_data['plugin_setup'] ? 'button-primary' : 'grey disabled';
break;
case 'pro':
$addon_installed = array_key_exists( $this->config['uncanny-automator_addon'], $this->output_data['all_plugins'] );
$step['button_text'] =
$addon_installed
? esc_html__( 'Uncanny Automator Pro Installed & Activated', 'wpforms-lite' )
: esc_html__( 'Install Now', 'wpforms-lite' );
$step['button_class'] = $this->output_data['plugin_setup'] ? 'grey disabled' : 'button-primary';
$step['icon'] = $addon_installed ? 'step-complete.svg' : 'step-3.svg';
break;
}
return $step;
}
/**
* Retrieve the license level of the plugin.
*
* @since 1.9.8.6
*
* @return string The plugin license level ('lite' or 'pro').
*/
protected function get_license_level(): string {
$plugin_license_level = 'lite';
if ( isset( $this->output_data['plugin_activated'] ) ) {
// Check if premium features are available.
if ( defined( 'AUTOMATOR_PRO_PLUGIN_VERSION' ) || class_exists( Automator_Pro_Load::class ) ) {
$plugin_license_level = 'pro';
}
}
return $plugin_license_level;
}
/**
* Whether the plugin is finished setup or not.
*
* @since 1.9.8.6
*/
protected function is_plugin_finished_setup(): bool {
if ( ! $this->is_plugin_configured() ) {
return false;
}
return $this->get_license_level() === 'pro';
}
/**
* Get the heading for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step heading.
*/
protected function get_setup_heading(): string {
return esc_html__( 'Create Your First Automation (Recipe)', 'wpforms-lite' );
}
/**
* Get the description for the setup step.
*
* @since 1.9.8.6
*
* @return string Setup step description.
*/
protected function get_setup_description(): string {
return esc_html__( 'Open the Automator menu, click Add New, choose your trigger (e.g. form submission), and define your action (e.g. send email, update CRM).', 'wpforms-lite' );
}
/**
* Whether a plugin is configured or not.
*
* @since 1.9.8.6
*
* @return bool True if plugin is configured properly.
*/
protected function is_plugin_configured(): bool {
if ( ! $this->is_plugin_activated() ) {
return false;
}
// Check if Uncanny Automator has been configured with basic settings.
// The plugin is considered configured if there are recipes created.
global $wpdb;
// Check for Uncanny Automator posts (recipes).
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$recipes = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status != %s",
'uo-recipe',
'trash'
)
);
if ( (int) $recipes > 0 ) {
return true;
}
// Check for basic Automator settings.
$automator_settings = get_option( 'uap_automator_settings' );
return ! empty( $automator_settings );
}
/**
* Whether a plugin is active or not.
*
* @since 1.9.8.6
*
* @return bool True if the plugin is active.
*/
protected function is_plugin_activated(): bool {
return (
( defined( 'AUTOMATOR_PLUGIN_VERSION' ) || class_exists( Automator_Load::class ) ) &&
(
is_plugin_active( $this->config['lite_plugin'] ) ||
is_plugin_active( $this->config['pro_plugin'] )
)
);
}
/**
* Whether a plugin is available (class/function exists).
*
* @since 1.9.8.6
*
* @return bool True if a plugin is available.
*/
protected function is_plugin_available(): bool {
return function_exists( 'Automator' ) || defined( 'AUTOMATOR_VERSION' );
}
/**
* Whether a pro-version is active.
*
* @since 1.9.8.6
*
* @return bool True if a pro-version is active.
*/
protected function is_pro_active(): bool {
return class_exists( 'Uncanny_Automator_Pro\Plugin' ) || defined( 'AUTOMATOR_PRO_VERSION' );
}
/**
* Get the heading for the installation step.
*
* @since 1.9.8.6
*
* @return string Install step heading.
*/
protected function get_install_heading(): string {
return esc_html__( 'Install and Activate Uncanny Automator', 'wpforms-lite' );
}
/**
* Get the description for the installation step.
*
* @since 1.9.8.6
*
* @return string Install step description.
*/
protected function get_install_description(): string {
return esc_html__( 'Connect Automator and start building automations that save hours every week.', 'wpforms-lite' );
}
/**
* Get the plugin title.
*
* @since 1.9.8.6
*
* @return string Plugin title.
*/
protected function get_plugin_title(): string {
return esc_html__( 'Uncanny Automator', 'wpforms-lite' );
}
/**
* Get the installation button text.
*
* @since 1.9.8.6
*
* @return string Install button text.
*/
protected function get_install_button_text(): string {
return esc_html__( 'Install Uncanny Automator', 'wpforms-lite' );
}
/**
* Get the text when a plugin is installed and activated.
*
* @since 1.9.8.6
*
* @return string Installed & activated text.
*/
protected function get_installed_activated_text(): string {
return esc_html__( 'Uncanny Automator Installed & Activated', 'wpforms-lite' );
}
/**
* Get the activate button text.
*
* @since 1.9.8.6
*
* @return string Activate button text.
*/
protected function get_activate_text(): string {
return esc_html__( 'Activate Uncanny Automator', 'wpforms-lite' );
}
/**
* Get the setup button text.
*
* @since 1.9.8.6
*
* @return string Setup button text.
*/
protected function get_setup_button_text(): string {
return esc_html__( 'Create Your First Recipe', 'wpforms-lite' );
}
/**
* Get the text when setup is completed.
*
* @since 1.9.8.6
*
* @return string Setup completed text.
*/
protected function get_setup_completed_text(): string {
return esc_html__( 'Recipe Created', 'wpforms-lite' );
}
/**
* Get the text when a pro-version is installed and activated.
*
* @since 1.9.8.6
*
* @return string Pro installed and activated text.
*/
protected function get_pro_installed_activated_text(): string {
return esc_html__( 'Uncanny Automator Pro Installed & Activated', 'wpforms-lite' );
}
/**
* Set the source of the plugin installation.
*
* @since 1.9.8.6
*
* @param string $plugin_basename The basename of the plugin.
*/
public function plugin_activated( string $plugin_basename ): void {
if ( $plugin_basename !== $this->config['lite_plugin'] ) {
return;
}
$source = wpforms()->is_pro() ? 'WPForms' : 'WPForms Lite';
/**
* Rewrite the get_plugin_name() default value.
*
* Use `uncannyautomator` instead of `uncanny-automator`.
* This is necessary for maintaining consistency with the integration and the plugin itself.
*
* See: src/Integrations/UncannyAutomator/UncannyAutomator.php update_source() method.
*/
update_option( 'uncannyautomator_source', $source, false );
update_option( 'uncannyautomator_date', time(), false );
}
}
@@ -0,0 +1,299 @@
<?php
namespace WPForms\Admin\Payments;
use WPForms\Admin\Payments\Views\Coupons\Education;
use WPForms\Admin\Payments\Views\Overview\BulkActions;
use WPForms\Admin\Payments\Views\Single;
use WPForms\Admin\Payments\Views\Overview\Page;
use WPForms\Admin\Payments\Views\Overview\Coupon;
use WPForms\Admin\Payments\Views\Overview\Filters;
use WPForms\Admin\Payments\Views\Overview\Search;
/**
* Payments class.
*
* @since 1.8.2
*/
class Payments {
/**
* Payments page slug.
*
* @since 1.8.2
*
* @var string
*/
const SLUG = 'wpforms-payments';
/**
* Available views (pages).
*
* @since 1.8.2
*
* @var array
*/
private $views = [];
/**
* The current page slug.
*
* @since 1.8.2
*
* @var string
*/
private $active_view_slug;
/**
* The current page view.
*
* @since 1.8.2
*
* @var null|\WPForms\Admin\Payments\Views\PaymentsViewsInterface
*/
private $view;
/**
* Initialize class.
*
* @since 1.8.2
*/
public function init() {
if ( ! wpforms_is_admin_page( 'payments' ) ) {
return;
}
$this->update_request_uri();
( new ScreenOptions() )->init();
( new Coupon() )->init();
( new Filters() )->init();
( new Search() )->init();
( new BulkActions() )->init();
$this->hooks();
}
/**
* Initialize the active view.
*
* @since 1.8.2
*/
public function init_view() {
$view_ids = array_keys( $this->get_views() );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$this->active_view_slug = isset( $_GET['view'] ) ? sanitize_key( $_GET['view'] ) : 'payments';
// If the user tries to load an invalid view - fallback to the first available.
if ( ! in_array( $this->active_view_slug, $view_ids, true ) ) {
$this->active_view_slug = reset( $view_ids );
}
if ( ! isset( $this->views[ $this->active_view_slug ] ) ) {
return;
}
$this->view = $this->views[ $this->active_view_slug ];
$this->view->init();
}
/**
* Get available views.
*
* @since 1.8.2
*
* @return array
*/
private function get_views() {
if ( ! empty( $this->views ) ) {
return $this->views;
}
$views = [
'coupons' => new Education(),
];
/**
* Allow to extend payment views.
*
* @since 1.8.2
*
* @param array $views Array of views where key is slug.
*/
$this->views = (array) apply_filters( 'wpforms_admin_payments_payments_get_views', $views );
$this->views['payments'] = new Page();
$this->views['payment'] = new Single();
// Payments view should be the first one.
$this->views = array_merge( [ 'payments' => $this->views['payments'] ], $this->views );
$this->views = array_filter(
$this->views,
static function ( $view ) {
return $view->current_user_can();
}
);
return $this->views;
}
/**
* Register hooks.
*
* @since 1.8.2
*/
private function hooks() {
add_action( 'wpforms_admin_page', [ $this, 'output' ] );
add_action( 'current_screen', [ $this, 'init_view' ] );
add_filter( 'wpforms_db_payments_payment_add_secondary_where_conditions_args', [ $this, 'modify_secondary_where_conditions_args' ] );
}
/**
* Output the page.
*
* @since 1.8.2
*/
public function output() {
if ( empty( $this->view ) ) {
return;
}
?>
<div id="wpforms-payments" class="wrap wpforms-admin-wrap wpforms-payments-wrap wpforms-payments-wrap-<?php echo esc_attr( $this->active_view_slug ); ?>">
<h1 class="page-title">
<?php esc_html_e( 'Payments', 'wpforms-lite' ); ?>
<?php $this->view->heading(); ?>
</h1>
<?php if ( ! empty( $this->view->get_tab_label() ) ) : ?>
<div class="wpforms-tabs-wrapper">
<?php $this->display_tabs(); ?>
</div>
<?php endif; ?>
<div class="wpforms-admin-content wpforms-admin-settings">
<?php $this->view->display(); ?>
</div>
</div>
<?php
}
/**
* Display tabs.
*
* @since 1.8.2.2
*/
private function display_tabs() {
$views = $this->get_views();
// Remove views that should not be displayed.
$views = array_filter(
$views,
static function ( $view ) {
return ! empty( $view->get_tab_label() );
}
);
// If there is only one view - no need to display tabs.
if ( count( $views ) === 1 ) {
return;
}
?>
<nav class="nav-tab-wrapper">
<?php foreach ( $views as $slug => $view ) : ?>
<a href="<?php echo esc_url( $this->get_tab_url( $slug ) ); ?>" class="nav-tab <?php echo $slug === $this->active_view_slug ? 'nav-tab-active' : ''; ?>">
<?php echo esc_html( $view->get_tab_label() ); ?>
</a>
<?php endforeach; ?>
</nav>
<?php
}
/**
* Get tab URL.
*
* @since 1.8.2.2
*
* @param string $tab Tab slug.
*
* @return string
*/
private function get_tab_url( $tab ) {
return add_query_arg(
[
'page' => self::SLUG,
'view' => $tab,
],
admin_url( 'admin.php' )
);
}
/**
* Modify arguments of secondary where clauses.
*
* @since 1.8.2
*
* @param array $args Query arguments.
*
* @return array
*/
public function modify_secondary_where_conditions_args( $args ) {
// Set a current mode.
if ( ! isset( $args['mode'] ) ) {
$args['mode'] = Page::get_mode();
}
return $args;
}
/**
* Update view param in request URI.
*
* Backward compatibility for old URLs.
*
* @since 1.8.4
*/
private function update_request_uri() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! isset( $_GET['view'], $_SERVER['REQUEST_URI'] ) ) {
return;
}
$old_new = [
'single' => 'payment',
'overview' => 'payments',
];
if (
! array_key_exists( $_GET['view'], $old_new )
|| in_array( $_GET['view'], $old_new, true )
) {
return;
}
wp_safe_redirect(
str_replace(
'view=' . $_GET['view'],
'view=' . $old_new[ $_GET['view'] ],
esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
)
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
exit;
}
}
@@ -0,0 +1,187 @@
<?php
namespace WPForms\Admin\Payments;
use WP_Screen;
/**
* Payments screen options.
*
* @since 1.8.2
*/
class ScreenOptions {
/**
* Screen id.
*
* @since 1.8.2
*/
const SCREEN_ID = 'wpforms_page_wpforms-payments';
/**
* Screen option name.
*
* @since 1.8.2
*/
const PER_PAGE = 'wpforms_payments_per_page';
/**
* Screen option name.
*
* @since 1.8.2
*/
const SINGLE = 'wpforms_payments_single';
/**
* Initialize.
*
* @since 1.8.2
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.2
*/
private function hooks() {
// Setup screen options - this needs to run early.
add_action( 'load-wpforms_page_wpforms-payments', [ $this, 'screen_options' ] );
add_filter( 'screen_settings', [ $this, 'single_screen_settings' ], 10, 2 );
add_filter( 'set-screen-option', [ $this, 'screen_options_set' ], 10, 3 );
add_filter( 'set_screen_option_wpforms_payments_per_page', [ $this, 'screen_options_set' ], 10, 3 );
add_filter( 'set_screen_option_wpforms_payments_single', [ $this, 'screen_options_set' ], 10, 3 );
}
/**
* Add per-page screen option to the Payments table.
*
* @since 1.8.2
*/
public function screen_options() {
$screen = get_current_screen();
if ( ! isset( $screen->id ) || $screen->id !== self::SCREEN_ID ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['view'] ) && $_GET['view'] !== 'payments' ) {
return;
}
/**
* Filter the number of payments per page default value.
*
* Notice, the filter will be applied to default value in Screen Options only and still will be able to provide other value.
* If you want to change the number of payments per page, use the `wpforms_payments_per_page` filter.
*
* @since 1.8.2
*
* @param int $per_page Number of payments per page.
*/
$per_page = (int) apply_filters( 'wpforms_admin_payments_screen_options_per_page_default', 20 );
add_screen_option(
'per_page',
[
'label' => esc_html__( 'Number of payments per page:', 'wpforms-lite' ),
'option' => self::PER_PAGE,
'default' => $per_page,
]
);
}
/**
* Returns the screen options markup for the payment single page.
*
* @since 1.8.2
*
* @param string $status The current screen settings.
* @param WP_Screen $args WP_Screen object.
*
* @return string
*/
public function single_screen_settings( $status, $args ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $args->id !== self::SCREEN_ID || empty( $_GET['view'] ) || $_GET['view'] !== 'payment' ) {
return $status;
}
$screen_options = self::get_single_page_options();
$advanced_options = [
'advanced' => __( 'Advanced details', 'wpforms-lite' ),
'log' => __( 'Log', 'wpforms-lite' ),
];
$output = '<fieldset class="metabox-prefs">';
$output .= '<legend>' . esc_html__( 'Additional information', 'wpforms-lite' ) . '</legend>';
$output .= '<div>';
foreach ( $advanced_options as $key => $label ) {
$output .= sprintf(
'<input name="%1$s" type="checkbox" id="%1$s" value="true" %2$s /><label for="%1$s">%3$s</label>',
esc_attr( $key ),
! empty( $screen_options[ $key ] ) ? 'checked="checked"' : '',
esc_html( $label )
);
}
$output .= '</div></fieldset>';
$output .= '<p class="submit">';
$output .= '<input type="hidden" name="wp_screen_options[option]" value="wpforms_payments_single">';
$output .= '<input type="hidden" name="wp_screen_options[value]" value="true">';
$output .= '<input type="submit" name="screen-options-apply" id="screen-options-apply" class="button button-primary" value="' . esc_html__( 'Apply', 'wpforms-lite' ) . '">';
$output .= wp_nonce_field( 'screen-options-nonce', 'screenoptionnonce', false, false );
$output .= '</p>';
return $output;
}
/**
* Get single page screen options.
*
* @since 1.8.2
*
* @return false|mixed
*/
public static function get_single_page_options() {
return get_user_option( self::SINGLE );
}
/**
* Payments table per-page screen option value.
*
* @since 1.8.2
*
* @param mixed $status The value to save instead of the option value.
* @param string $option Screen option name.
* @param mixed $value Screen option value.
*
* @return mixed
*/
public function screen_options_set( $status, $option, $value ) {
if ( $option === self::PER_PAGE ) {
return $value;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( $option === self::SINGLE ) {
return [
'advanced' => isset( $_POST['advanced'] ) && (bool) $_POST['advanced'],
'log' => isset( $_POST['log'] ) && (bool) $_POST['log'],
];
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
return $status;
}
}
@@ -0,0 +1,189 @@
<?php
namespace WPForms\Admin\Payments\Views\Coupons;
use WPForms\Admin\Payments\Views\Overview\Helpers;
use WPForms\Admin\Payments\Views\PaymentsViewsInterface;
/**
* Payments Coupons Education class.
*
* @since 1.8.2.2
*/
class Education implements PaymentsViewsInterface {
/**
* Coupons addon data.
*
* @since 1.8.2.2
*
* @var array
*/
private $addon;
/**
* Initialize class.
*
* @since 1.8.2.2
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.8.2.2
*/
private function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
}
/**
* Get the page label.
*
* @since 1.8.2.2
*
* @return string
*/
public function get_tab_label() {
return __( 'Coupons', 'wpforms-lite' );
}
/**
* Enqueue scripts.
*
* @since 1.8.2.2
*/
public function enqueue_scripts() {
// Lity - lightbox for images.
wp_enqueue_style(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.css',
null,
'3.0.0'
);
wp_enqueue_script(
'wpforms-lity',
WPFORMS_PLUGIN_URL . 'assets/lib/lity/lity.min.js',
[ 'jquery' ],
'3.0.0',
true
);
}
/**
* Check if the current user has the capability to view the page.
*
* @since 1.8.2.2
*
* @return bool
*/
public function current_user_can() {
if ( ! wpforms_current_user_can() ) {
return false;
}
$this->addon = wpforms()->obj( 'addons' )->get_addon( 'coupons' );
if (
empty( $this->addon ) ||
empty( $this->addon['status'] ) ||
empty( $this->addon['action'] )
) {
return false;
}
return true;
}
/**
* Page heading content.
*
* @since 1.8.2.2
*/
public function heading() {
Helpers::get_default_heading();
}
/**
* Page content.
*
* @since 1.8.2.2
*/
public function display() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render( 'education/admin/page', $this->template_data(), true );
}
/**
* Get the template data.
*
* @since 1.8.6
*
* @return array
*/
private function template_data(): array {
$images_url = WPFORMS_PLUGIN_URL . 'assets/images/coupons-education/';
$utm_medium = 'Payments - Coupons';
$utm_content = 'Coupons Addon';
$upgrade_link = $this->addon['action'] === 'upgrade'
? sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */
' <strong><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a></strong>',
esc_url( wpforms_admin_upgrade_link( $utm_medium, $utm_content ) ),
esc_html__( 'Upgrade to WPForms Pro', 'wpforms-lite' )
)
: '';
$params = [
'features' => [
__( 'Custom Coupon Codes', 'wpforms-lite' ),
__( 'Percentage or Fixed Discounts', 'wpforms-lite' ),
__( 'Start and End Dates', 'wpforms-lite' ),
__( 'Maximum Usage Limit', 'wpforms-lite' ),
__( 'Once Per Email Address Limit', 'wpforms-lite' ),
__( 'Usage Statistics', 'wpforms-lite' ),
],
'images' => [
[
'url' => $images_url . 'coupons-addon-thumbnail-01.png',
'url2x' => $images_url . 'coupons-addon-screenshot-01.png',
'title' => __( 'Coupons Overview', 'wpforms-lite' ),
],
[
'url' => $images_url . 'coupons-addon-thumbnail-02.png',
'url2x' => $images_url . 'coupons-addon-screenshot-02.png',
'title' => __( 'Coupon Settings', 'wpforms-lite' ),
],
],
'utm_medium' => $utm_medium,
'utm_content' => $utm_content,
'upgrade_link' => $upgrade_link,
'heading_description' => '<p>' . sprintf( /* translators: %1$s - WPForms.com Upgrade page URL. */
esc_html__( 'With the Coupons addon, you can offer customers discounts using custom coupon codes. Create your own percentage or fixed rate discount, then add the Coupon field to any payment form. When a customer enters your unique code, theyll receive the specified discount. You can also add limits to restrict when coupons are available and how often they can be used. The Coupons addon requires a license level of Pro or higher.%s', 'wpforms-lite' ),
wp_kses(
$upgrade_link,
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
'strong' => [],
]
)
) . '</p>',
'features_description' => __( 'Easy to Use, Yet Powerful', 'wpforms-lite' ),
];
return array_merge( $params, $this->addon );
}
}
@@ -0,0 +1,557 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use DateTimeImmutable;
// phpcs:ignore WPForms.PHP.UseStatement.UnusedUseStatement
use wpdb;
use WPForms\Db\Payments\ValueValidator;
use WPForms\Admin\Helpers\Chart as ChartHelper;
use WPForms\Admin\Helpers\Datepicker;
/**
* "Payments" overview page inside the admin, which lists all payments.
* This page will be accessible via "WPForms" → "Payments".
*
* When requested data is sent via Ajax, this class is responsible for exchanging datasets.
*
* @since 1.8.2
*/
class Ajax {
/**
* Database table name.
*
* @since 1.8.2
*
* @var string
*/
private $table_name;
/**
* Temporary storage for the stat cards.
*
* @since 1.8.4
*
* @var array
*/
private $stat_cards;
/**
* Hooks.
*
* @since 1.8.2
*/
public function hooks() {
add_action( 'wp_ajax_wpforms_payments_overview_refresh_chart_dataset_data', [ $this, 'get_chart_dataset_data' ] );
add_action( 'wp_ajax_wpforms_payments_overview_save_chart_preference_settings', [ $this, 'save_chart_preference_settings' ] );
add_filter( 'wpforms_db_payments_payment_add_secondary_where_conditions_args', [ $this, 'modify_secondary_where_conditions_args' ] );
}
/**
* Generate and return the data for our dataset data.
*
* @since 1.8.2
*/
public function get_chart_dataset_data() {
// Run a security check.
check_ajax_referer( 'wpforms_payments_overview_nonce' );
// Check for permissions.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
$report = ! empty( $_POST['report'] ) ? sanitize_text_field( wp_unslash( $_POST['report'] ) ) : null;
$dates = ! empty( $_POST['dates'] ) ? sanitize_text_field( wp_unslash( $_POST['dates'] ) ) : null;
$fallback = [
'data' => [],
'reports' => [],
];
// If the report type or dates for the timespan are missing, leave early.
if ( ! $report || ! $dates ) {
wp_send_json_error( $fallback );
}
// Validates and creates date objects of given timespan string.
$timespans = Datepicker::process_string_timespan( $dates );
// If the timespan is not validated, leave early.
if ( ! $timespans ) {
wp_send_json_error( $fallback );
}
// Extract start and end timespans in local (site) and UTC timezones.
list( $start_date, $end_date, $utc_start_date, $utc_end_date ) = $timespans;
// Payment table name.
$this->table_name = wpforms()->obj( 'payment' )->table_name;
// Get the stat cards.
$this->stat_cards = Chart::stat_cards();
// Get the payments in the given timespan.
$results = $this->get_payments_in_timespan( $utc_start_date, $utc_end_date, $report );
// In case the database's results were empty, leave early.
if ( $report === Chart::ACTIVE_REPORT && empty( $results ) ) {
wp_send_json_error( $fallback );
}
// Process the results and return the data.
// The first element of the array is the total number of entries, the second is the data.
list( , $data ) = ChartHelper::process_chart_dataset_data( $results, $start_date, $end_date );
// Sends the JSON response back to the Ajax request, indicating success.
wp_send_json_success(
[
'data' => $data,
'reports' => $this->get_payments_summary_in_timespan( $start_date, $end_date ),
]
);
}
/**
* Save the user's preferred graph style and color scheme.
*
* @since 1.8.2
*/
public function save_chart_preference_settings() {
// Run a security check.
check_ajax_referer( 'wpforms_payments_overview_nonce' );
// Check for permissions.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'wpforms-lite' ) );
}
$graph_style = isset( $_POST['graphStyle'] ) ? absint( $_POST['graphStyle'] ) : 2; // Line.
update_user_meta( get_current_user_id(), 'wpforms_dash_widget_graph_style', $graph_style );
exit();
}
/**
* Retrieve and create payment entries from the database within the specified time frame (timespan).
*
* @global wpdb $wpdb Instantiation of the wpdb class.
*
* @since 1.8.2
*
* @param DateTimeImmutable $start_date Start date for the timespan preferably in UTC.
* @param DateTimeImmutable $end_date End date for the timespan preferably in UTC.
* @param string $report Payment summary stat card name. i.e. "total_payments".
*
* @return array
*/
private function get_payments_in_timespan( $start_date, $end_date, $report ) {
// Ensure given timespan dates are in UTC timezone.
list( $utc_start_date, $utc_end_date ) = Datepicker::process_timespan_mysql( [ $start_date, $end_date ] );
// If the time period is not a date object, leave early.
if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) {
return [];
}
// Get the database instance.
global $wpdb;
// SELECT clause to construct the SQL statement.
$column_clause = $this->get_stats_column_clause( $report );
// JOIN clause to construct the SQL statement for metadata.
$join_by_meta = $this->add_join_by_meta( $report );
// WHERE clauses for items query statement.
$where_clause = $this->get_stats_where_clause( $report );
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_results(
$wpdb->prepare(
"SELECT date_created_gmt AS day, $column_clause AS count FROM $this->table_name AS p {$join_by_meta}
WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s GROUP BY day ORDER BY day ASC",
[
$utc_start_date->format( Datepicker::DATETIME_FORMAT ),
$utc_end_date->format( Datepicker::DATETIME_FORMAT ),
]
),
ARRAY_A
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Fetch and generate payment summary reports from the database.
*
* @global wpdb $wpdb Instantiation of the wpdb class.
*
* @since 1.8.2
*
* @param DateTimeImmutable $start_date Start date for the timespan preferably in UTC.
* @param DateTimeImmutable $end_date End date for the timespan preferably in UTC.
*
* @return array
*/
private function get_payments_summary_in_timespan( $start_date, $end_date ) {
// Ensure given timespan dates are in UTC timezone.
list( $utc_start_date, $utc_end_date ) = Datepicker::process_timespan_mysql( [ $start_date, $end_date ] );
// If the time period is not a date object, leave early.
if ( ! ( $start_date instanceof DateTimeImmutable ) || ! ( $end_date instanceof DateTimeImmutable ) ) {
return [];
}
// Get the database instance.
global $wpdb;
list( $clause, $query ) = $this->prepare_sql_summary_reports( $utc_start_date, $utc_end_date );
$group_by = Chart::ACTIVE_REPORT;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$results = $wpdb->get_row(
"SELECT $clause FROM (SELECT $query) AS results GROUP BY $group_by",
ARRAY_A
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $this->maybe_format_amounts( $results );
}
/**
* Generate SQL statements to create a derived (virtual) table for the report stat cards.
*
* @global wpdb $wpdb Instantiation of the wpdb class.
*
* @since 1.8.2
*
* @param DateTimeImmutable $start_date Start date for the timespan.
* @param DateTimeImmutable $end_date End date for the timespan.
*
* @return array
*/
private function prepare_sql_summary_reports( $start_date, $end_date ) {
// In case there are no report stat cards defined, leave early.
if ( empty( $this->stat_cards ) ) {
return [ '', '' ];
}
global $wpdb;
$clause = []; // SELECT clause.
$query = []; // Query statement for the derived table.
// Validates and creates date objects for the previous time spans.
$prev_timespans = Datepicker::get_prev_timespan_dates( $start_date, $end_date );
// If the timespan is not validated, leave early.
if ( ! $prev_timespans ) {
return [ '', '' ];
}
list( $prev_start_date, $prev_end_date ) = $prev_timespans;
// Get the default number of decimals for the payment currency.
$current_currency = wpforms_get_currency();
$currency_decimals = wpforms_get_currency_decimals( $current_currency );
// Loop through the reports and create the SQL statements.
foreach ( $this->stat_cards as $report => $attributes ) {
// Skip stat card, if it's not supposed to be displayed or disabled (upsell).
if (
( isset( $attributes['condition'] ) && ! $attributes['condition'] )
|| in_array( 'disabled', $attributes['button_classes'], true )
) {
continue;
}
// Determine whether the number of rows has to be counted.
$has_count = isset( $attributes['has_count'] ) && $attributes['has_count'];
// SELECT clause to construct the SQL statement.
$column_clause = $this->get_stats_column_clause( $report, $has_count );
// JOIN clause to construct the SQL statement for metadata.
$join_by_meta = $this->add_join_by_meta( $report );
// WHERE clauses for items query statement.
$where_clause = $this->get_stats_where_clause( $report );
// Get the current and previous values for the report.
$current_value = "TRUNCATE($report,$currency_decimals)";
$prev_value = "TRUNCATE({$report}_prev,$currency_decimals)";
// Add the current and previous reports to the SELECT clause.
$clause[] = $report;
$clause[] = "ROUND( ( ( $current_value - $prev_value ) / $current_value ) * 100 ) AS {$report}_delta";
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.MissingReplacements
$query[] = $wpdb->prepare(
"(
SELECT $column_clause
FROM $this->table_name AS p
{$join_by_meta}
WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s
) AS $report,
(
SELECT $column_clause
FROM $this->table_name AS p
{$join_by_meta}
WHERE 1=1 $where_clause AND date_created_gmt BETWEEN %s AND %s
) AS {$report}_prev",
[
$start_date->format( Datepicker::DATETIME_FORMAT ),
$end_date->format( Datepicker::DATETIME_FORMAT ),
$prev_start_date->format( Datepicker::DATETIME_FORMAT ),
$prev_end_date->format( Datepicker::DATETIME_FORMAT ),
]
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.MissingReplacements
}
return [
implode( ',', $clause ),
implode( ',', $query ),
];
}
/**
* Helper method to build where clause used to construct the SQL statement.
*
* @since 1.8.2
*
* @param string $report Payment summary stat card name. i.e. "total_payments".
*
* @return string
*/
private function get_stats_where_clause( $report ) {
// Get the default WHERE clause from the Payments database class.
$clause = wpforms()->obj( 'payment' )->add_secondary_where_conditions();
// If the report doesn't have any additional funnel arguments, leave early.
if ( ! isset( $this->stat_cards[ $report ]['funnel'] ) ) {
return $clause;
}
// Get the where arguments for the report.
$where_args = (array) $this->stat_cards[ $report ]['funnel'];
// If the where arguments are empty, leave early.
if ( empty( $where_args ) ) {
return $clause;
}
return $this->prepare_sql_where_clause( $where_args, $clause );
}
/**
* Prepare SQL where clause for the given funnel arguments.
*
* @since 1.8.4
*
* @param array $where_args Array of where arguments.
* @param string $clause SQL where clause.
*
* @return string
*/
private function prepare_sql_where_clause( $where_args, $clause ) {
$allowed_funnels = [ 'in', 'not_in' ];
$filtered_where_args = array_filter(
$where_args,
static function ( $key ) use ( $allowed_funnels ) {
return in_array( $key, $allowed_funnels, true );
},
ARRAY_FILTER_USE_KEY
);
// Leave early if the filtered where arguments are empty.
if ( empty( $filtered_where_args ) ) {
return $clause;
}
// Loop through the where arguments and add them to the clause.
foreach ( $filtered_where_args as $operator => $columns ) {
foreach ( $columns as $column => $values ) {
if ( ! is_array( $values ) ) {
continue;
}
// Skip if the value is not valid.
$valid_values = array_filter(
$values,
static function ( $item ) use ( $column ) {
return ValueValidator::is_valid( $item, $column );
}
);
$placeholders = wpforms_wpdb_prepare_in( $valid_values );
$clause .= $operator === 'in' ? " AND {$column} IN ({$placeholders})" : " AND {$column} NOT IN ({$placeholders})";
}
}
return $clause;
}
/**
* Helper method to build column clause used to construct the SQL statement.
*
* @since 1.8.2
*
* @param string $report Stats card chart type (name). i.e. "total_payments".
* @param bool $with_count Whether to concatenate the count to the clause.
*
* @return string
*/
private function get_stats_column_clause( $report, $with_count = false ) {
// Default column clause.
// Count the number of rows as fast as possible.
$default = 'COUNT(*)';
// If the report has a meta key, then count the number of unique rows for the meta table.
if ( isset( $this->stat_cards[ $report ]['meta_key'] ) ) {
$default = 'COUNT(pm.id)';
}
/**
* Filters the column clauses for the stat cards.
*
* @since 1.8.2
*
* @param array $clauses Array of column clauses.
*/
$clauses = (array) apply_filters(
'wpforms_admin_payments_views_overview_ajax_stats_column_clauses',
[
'total_payments' => "FORMAT({$default},0)",
'total_sales' => 'IFNULL(SUM(total_amount),0)',
'total_refunded' => 'IFNULL(SUM(pm.meta_value),0)',
'total_subscription' => 'IFNULL(SUM(total_amount),0)',
'total_renewal_subscription' => 'IFNULL(SUM(total_amount),0)',
'total_coupons' => "FORMAT({$default},0)",
]
);
$clause = isset( $clauses[ $report ] ) ? $clauses[ $report ] : $default;
// Several stat cards might include the count of payment records.
if ( $with_count ) {
$clause = "CONCAT({$clause}, ' (', {$default}, ')')";
}
return $clause;
}
/**
* Add join by meta table.
*
* @since 1.8.4
*
* @param string $report Stats card chart type (name). i.e. "total_payments".
*
* @return string
*/
private function add_join_by_meta( $report ) {
// Leave early if the meta key is empty.
if ( ! isset( $this->stat_cards[ $report ]['meta_key'] ) ) {
return '';
}
// Retrieve the global database instance.
global $wpdb;
// Retrieve the meta table name.
$meta_table_name = wpforms()->obj( 'payment_meta' )->table_name;
return $wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"LEFT JOIN {$meta_table_name} AS pm ON p.id = pm.payment_id AND pm.meta_key = %s",
$this->stat_cards[ $report ]['meta_key']
);
}
/**
* Modify arguments of secondary where clauses.
*
* @since 1.8.2
*
* @param array $args Query arguments.
*
* @return array
*/
public function modify_secondary_where_conditions_args( $args ) {
// Set a current mode.
if ( ! isset( $args['mode'] ) ) {
$args['mode'] = Page::get_mode();
}
return $args;
}
/**
* Maybe format the amounts for the given stat cards.
*
* @since 1.8.4
*
* @param array $results Query results.
*
* @return array
*/
private function maybe_format_amounts( $results ) {
// If the input is empty, leave early.
if ( empty( $results ) ) {
return [];
}
foreach ( $results as $key => $value ) {
// If the given stat card doesn't have a button class, leave early.
// If the given stat card doesn't have a button class of "is-amount," leave early.
if ( ! isset( $this->stat_cards[ $key ]['button_classes'] ) || ! in_array( 'is-amount', $this->stat_cards[ $key ]['button_classes'], true ) ) {
continue;
}
// Split the input by space to look for the count.
$input_arr = (array) explode( ' ', $value );
// If the given stat card doesn't have a count, leave early.
if ( empty( $this->stat_cards[ $key ]['has_count'] ) || ! isset( $input_arr[1] ) ) {
// Format the given amount and split the input by space.
$results[ $key ] = wpforms_format_amount( $value, true );
continue;
}
// The fields are stored as a `decimal` in the DB, and appears here as the string.
// But all strings values, passed to wpforms_format_amount() are sanitized.
// There is no need to sanitize it, as it is already a regular numeric string.
$amount = wpforms_format_amount( (float) ( $input_arr[0] ?? $value ), true );
// Format the amount with the concatenation of count in parentheses.
// Example: 2185.52000000 (79).
$results[ $key ] = sprintf(
'%s <span>%s</span>',
esc_html( $amount ),
esc_html( $input_arr[1] ) // 1: Would be count of the records.
);
}
return $results;
}
}
@@ -0,0 +1,220 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use WPForms\Admin\Notice;
/**
* Bulk actions on the Payments Overview page.
*
* @since 1.8.2
*/
class BulkActions {
/**
* Allowed actions.
*
* @since 1.8.2
*
* @const array
*/
const ALLOWED_ACTIONS = [
'trash',
'restore',
'delete',
];
/**
* Payments ids.
*
* @since 1.8.2
*
* @var array
*/
private $ids;
/**
* Current action.
*
* @since 1.8.2
*
* @var string
*/
private $action;
/**
* Init.
*
* @since 1.8.2
*/
public function init() {
$this->process();
}
/**
* Get the current action selected from the bulk actions dropdown.
*
* @since 1.8.2
*
* @return string|false The action name or False if no action was selected
*/
private function current_action() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1' ) {
return sanitize_key( $_REQUEST['action'] );
}
if ( isset( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1' ) {
return sanitize_key( $_REQUEST['action2'] );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return false;
}
/**
* Process bulk actions.
*
* @since 1.8.2
*/
private function process() {
if ( empty( $_GET['_wpnonce'] ) || empty( $_GET['payment_id'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bulk-wpforms_page_wpforms-payments' ) ) {
wp_die( esc_html__( 'Your session expired. Please reload the page.', 'wpforms-lite' ) );
}
$this->ids = array_map( 'absint', (array) $_GET['payment_id'] );
$this->action = $this->current_action();
if ( empty( $this->ids ) || ! $this->action || ! $this->is_allowed_action( $this->action ) ) {
return;
}
$this->process_action();
}
/**
* Process a bulk action.
*
* @since 1.8.2
*/
private function process_action() {
$method = "process_action_{$this->action}";
// Check that we have a method for this action.
if ( ! method_exists( $this, $method ) ) {
return;
}
$processed = 0;
foreach ( $this->ids as $id ) {
$processed = $this->$method( $id ) ? $processed + 1 : $processed;
}
if ( ! $processed ) {
return;
}
$this->display_bulk_action_message( $processed );
}
/**
* Trash the payment.
*
* @since 1.8.2
*
* @param int $id Payment ID to trash.
*
* @return bool
*/
private function process_action_trash( $id ) {
return wpforms()->obj( 'payment' )->update( $id, [ 'is_published' => 0 ] );
}
/**
* Restore the payment.
*
* @since 1.8.2
*
* @param int $id Payment ID to restore from trash.
*
* @return bool
*/
private function process_action_restore( $id ) {
return wpforms()->obj( 'payment' )->update( $id, [ 'is_published' => 1 ] );
}
/**
* Delete the payment.
*
* @since 1.8.2
*
* @param int $id Payment ID to delete.
*
* @return bool
*/
private function process_action_delete( $id ) {
return wpforms()->obj( 'payment' )->delete( $id );
}
/**
* Display a bulk action message.
*
* @since 1.8.2
*
* @param int $count Count of processed payment IDs.
*/
private function display_bulk_action_message( $count ) {
switch ( $this->action ) {
case 'delete':
/* translators: %d - number of deleted payments. */
$message = sprintf( _n( '%d payment was successfully permanently deleted.', '%d payments were successfully permanently deleted.', $count, 'wpforms-lite' ), number_format_i18n( $count ) );
break;
case 'restore':
/* translators: %d - number of restored payments. */
$message = sprintf( _n( '%d payment was successfully restored.', '%d payments were successfully restored.', $count, 'wpforms-lite' ), number_format_i18n( $count ) );
break;
case 'trash':
/* translators: %d - number of trashed payments. */
$message = sprintf( _n( '%d payment was successfully moved to the Trash.', '%d payments were successfully moved to the Trash.', $count, 'wpforms-lite' ), number_format_i18n( $count ) );
break;
default:
$message = '';
}
if ( empty( $message ) ) {
return;
}
Notice::success( $message );
}
/**
* Determine whether the action is allowed.
*
* @since 1.8.2
*
* @param string $action Action name.
*
* @return bool
*/
private function is_allowed_action( $action ) {
return in_array( $action, self::ALLOWED_ACTIONS, true );
}
}
@@ -0,0 +1,336 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use WPForms\Admin\Helpers\Datepicker;
/**
* Payment Overview Chart class.
*
* @since 1.8.2
*/
class Chart {
/**
* Default payments summary report stat card.
*
* @since 1.8.2
*/
const ACTIVE_REPORT = 'total_payments';
/**
* Whether the chart should be displayed.
*
* @since 1.8.2
*
* @return bool
*/
private function allow_load() {
$disallowed_views = [
's', // Search.
'type', // Payment type.
'status', // Payment status.
'gateway', // Payment gateway.
'subscription_status', // Subscription status.
'form_id', // Form ID.
'coupon_id', // Coupon ID.
];
// Avoid displaying the chart when filtering of payment records is performed.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
return array_reduce(
array_keys( $_GET ),
static function ( $carry, $key ) use ( $disallowed_views ) {
if ( ! $carry ) {
return false;
}
return ! in_array( $key, $disallowed_views, true ) || empty( $_GET[ $key ] );
},
true
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* Display the chart.
*
* @since 1.8.2
*/
public function display() {
// If the chart should not be displayed, leave early.
if ( ! $this->allow_load() ) {
return;
}
// Output HTML elements on the page.
$this->output_top_bar();
$this->output_test_mode_banner();
$this->output_chart();
}
/**
* Handles output of the overview page top-bar.
*
* Includes:
* 1. Heading.
* 2. Datepicker filter.
* 3. Chart theme customization settings.
*
* @since 1.8.2
*/
private function output_top_bar() {
list( $choices, $chosen_filter, $value ) = Datepicker::process_datepicker_choices();
?>
<div class="wpforms-overview-top-bar">
<div class="wpforms-overview-top-bar-heading">
<h2><?php esc_html_e( 'Payments Summary', 'wpforms-lite' ); ?></h2>
</div>
<div class="wpforms-overview-top-bar-filters">
<?php
// Output "Mode Toggle" template.
( new ModeToggle() )->display();
// Output "Datepicker" form template.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/components/datepicker',
[
'id' => 'payments',
'action' => Page::get_url(),
'chosen_filter' => $chosen_filter,
'choices' => $choices,
'value' => $value,
'hidden_fields' => [ 'statcard' ],
],
true
);
?>
<div class="wpforms-overview-chart-settings">
<?php
// Output "Settings" template.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/dashboard/widget/settings',
array_merge( $this->get_chart_settings(), [ 'enabled' => true ] ),
true
);
?>
</div>
</div>
</div>
<?php
}
/**
* Display a banner when viewing test data.
*
* @since 1.8.2
*
* @return void
*/
private function output_test_mode_banner() {
// Determine if we are viewing test data.
if ( Page::get_mode() !== 'test' ) {
return;
}
?>
<div class="wpforms-payments-viewing-test-mode">
<p>
<?php esc_html_e( 'Viewing Test Data', 'wpforms-lite' ); ?>
</p>
</div>
<?php
}
/**
* Handles output of the overview page chart (graph).
*
* @since 1.8.2
*/
private function output_chart() {
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="wpforms-payments-overview-stats">';
echo wpforms_render(
'admin/components/chart',
[
'id' => 'payments',
'notice' => [
'heading' => esc_html__( 'No payments for selected period', 'wpforms-lite' ),
'description' => esc_html__( 'Please select a different period or check back later.', 'wpforms-lite' ),
],
],
true
);
echo wpforms_render(
'admin/payments/reports',
$this->get_reports_template_args(),
true
);
echo '</div>';
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Get the users preferences for displaying of the graph.
*
* @since 1.8.2
*
* @return array
*/
public function get_chart_settings() {
$graph_style = get_user_meta( get_current_user_id(), 'wpforms_dash_widget_graph_style', true );
return [
'graph_style' => $graph_style ? absint( $graph_style ) : 2, // Line.
];
}
/**
* Get the stat cards for the payment summary report.
*
* Note that "funnel" is used to filter the payments, and can take the following values:
* - in: payments that match the given criteria.
* - not_in: payments that do not match the given criteria.
*
* @since 1.8.2
*
* @return array
*/
public static function stat_cards() {
return [
'total_payments' => [
'label' => esc_html__( 'Total Payments', 'wpforms-lite' ),
'button_classes' => [
'total-payments',
],
],
'total_sales' => [
'label' => esc_html__( 'Total Sales', 'wpforms-lite' ),
'funnel' => [
'not_in' => [
'status' => [ 'failed' ],
'subscription_status' => [ 'failed' ],
],
],
'button_classes' => [
'total-sales',
'is-amount',
],
],
'total_refunded' => [
'label' => esc_html__( 'Total Refunded', 'wpforms-lite' ),
'has_count' => true,
'meta_key' => 'refunded_amount', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'button_classes' => [
'total-refunded',
'is-amount',
],
],
'total_subscription' => [
'label' => esc_html__( 'New Subscriptions', 'wpforms-lite' ),
'condition' => wpforms()->obj( 'payment_queries' )->has_subscription(),
'has_count' => true,
'funnel' => [
'in' => [
'type' => [ 'subscription' ],
],
'not_in' => [
'subscription_status' => [ 'failed' ],
],
],
'button_classes' => [
'total-subscription',
'is-amount',
],
],
'total_renewal_subscription' => [
'label' => esc_html__( 'Subscription Renewals', 'wpforms-lite' ),
'condition' => wpforms()->obj( 'payment_queries' )->has_subscription(),
'has_count' => true,
'funnel' => [
'in' => [
'type' => [ 'renewal' ],
],
'not_in' => [
'subscription_status' => [ 'failed' ],
],
],
'button_classes' => [
'total-renewal-subscription',
'is-amount',
],
],
'total_coupons' => [
'label' => esc_html__( 'Coupons Redeemed', 'wpforms-lite' ),
'meta_key' => 'coupon_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'funnel' => [
'not_in' => [
'status' => [ 'failed' ],
'subscription_status' => [ 'failed' ],
],
],
'button_classes' => [
'total-coupons',
],
],
];
}
/**
* Retrieves the arguments for the reports template.
*
* @since 1.8.8
*
* @return array
*/
private function get_reports_template_args(): array {
// Retrieve the stat cards.
$stat_cards = self::stat_cards();
// Set default arguments.
$args = [
'current' => self::ACTIVE_REPORT,
'statcards' => $stat_cards,
];
// Check if the statcard is set in the URL.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( empty( $_GET['statcard'] ) ) {
return $args;
}
// Sanitize and retrieve the tab value from the URL.
$active_report = sanitize_text_field( wp_unslash( $_GET['statcard'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// If the statcard is not valid, return default arguments.
if ( ! isset( $stat_cards[ $active_report ] ) ) {
return $args;
}
// If the statcard is not going to be displayed, return default arguments.
if ( isset( $stat_cards[ $active_report ]['condition'] ) && ! $stat_cards[ $active_report ]['condition'] ) {
return $args;
}
// Set the current statcard.
$args['current'] = $active_report;
return $args;
}
}
@@ -0,0 +1,170 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use WPForms\Admin\Payments\Payments;
/**
* Generic functionality for interacting with the Coupons data.
*
* @since 1.8.4
*/
class Coupon {
/**
* Initialize the Coupon class.
*
* @since 1.8.4
*/
public function init() {
$this->hooks();
}
/**
* Attach hooks for filtering payments by coupon ID.
*
* @since 1.8.4
*/
private function hooks() {
// This filter has been added for backward compatibility with older versions of the Coupons addon.
add_filter( 'wpforms_admin_payments_views_overview_table_get_columns', [ $this, 'remove_legacy_coupon_column' ], 99, 1 );
// Bail early if the current page is not the Payments page
// or if no coupon ID is given in the URL.
if ( ! self::is_coupon() ) {
return;
}
add_filter( 'wpforms_db_payments_payment_get_payments_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 );
add_filter( 'wpforms_db_payments_queries_count_all_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 );
add_filter( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_after_where', [ $this, 'filter_by_coupon_id' ], 10, 2 );
add_filter( 'wpforms_admin_payments_views_overview_search_inner_join_query', [ $this, 'join_search_by_coupon_id' ], 10, 2 );
}
/**
* Remove the legacy coupon column from the Payments page.
*
* This function has been added for backward compatibility with older versions of the Coupons addon.
* The legacy coupon column is no longer used by the Coupons addon.
*
* @since 1.8.4
*
* @param array $columns List of columns to be displayed on the Payments page.
*
* @return array
*/
public function remove_legacy_coupon_column( $columns ) {
// Bail early if the Coupons addon is not active.
if ( ! $this->is_addon_active() ) {
return $columns;
}
// Remove the legacy coupon column from the Payments page.
unset( $columns['coupon_id'] );
return $columns;
}
/**
* Retrieve payment entries based on a given coupon ID.
*
* @since 1.8.4
*
* @param string $after_where SQL query after the WHERE clause.
* @param array $args Query arguments.
*
* @return string
*/
public function filter_by_coupon_id( $after_where, $args ) {
// Check if the query is for the Payments Overview table.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( empty( $args['table_query'] ) ) {
return $after_where;
}
// Retrieve the coupon ID from the URL.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
$coupon_id = absint( $_GET['coupon_id'] );
global $wpdb;
$table_name = wpforms()->obj( 'payment_meta' )->table_name;
// Prepare and return the modified SQL query.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->prepare(
" AND EXISTS (
SELECT 1 FROM {$table_name} AS pm_coupon
WHERE pm_coupon.payment_id = p.id AND pm_coupon.meta_key = 'coupon_id' AND pm_coupon.meta_value = %d
)",
$coupon_id
);
}
/**
* Further filter down the search results by coupon ID.
*
* @since 1.8.4
*
* @param string $query The SQL JOIN clause.
* @param int $n The number of the JOIN clause.
*
* @return string
*/
public function join_search_by_coupon_id( $query, $n ) {
// Retrieve the coupon ID from the URL.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
$coupon_id = absint( $_GET['coupon_id'] );
// Retrieve the global database instance.
global $wpdb;
$n = absint( $n );
$table_name = wpforms()->obj( 'payment_meta' )->table_name;
// Build the derived query using a prepared statement.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$derived_query = $wpdb->prepare(
"RIGHT JOIN (
SELECT payment_id, meta_key, meta_value FROM {$table_name}
WHERE meta_key = 'coupon_id' AND meta_value = %d
) AS pm_coupon{$n} ON p.id = pm_coupon{$n}.payment_id",
$coupon_id
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// Combine the original query and the derived query.
return "$query $derived_query";
}
/**
* Determine if the overview page is being viewed, and coupon ID is given.
*
* @since 1.8.4
*
* @return bool
*/
public static function is_coupon() {
// Check if the URL parameters contain a coupon ID and if the current page is the Payments page.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return ! empty( $_GET['coupon_id'] ) && ! empty( $_GET['page'] ) && $_GET['page'] === Payments::SLUG;
}
/**
* Determine whether the addon is activated.
*
* @since 1.8.4
*
* @return bool
*/
private function is_addon_active() {
return function_exists( 'wpforms_coupons' );
}
}
@@ -0,0 +1,175 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
/**
* Class for extending SQL queries for filtering payments by multicheckbox fields.
*
* @since 1.8.4
*/
class Filters {
/**
* Initialize the Filters class.
*
* @since 1.8.4
*/
public function init() {
$this->hooks();
}
/**
* Attach hooks for filtering payments by multicheckbox fields.
*
* @since 1.8.4
*/
private function hooks() {
add_filter( 'wpforms_db_payments_payment_get_payments_query_after_where', [ $this, 'add_renewals_by_subscription_id' ], 10, 2 );
add_filter( 'wpforms_db_payments_queries_count_all_query_after_where', [ $this, 'count_renewals_by_subscription_id' ], 10, 2 );
add_filter( 'wpforms_db_payments_queries_count_if_exists_after_where', [ $this, 'exists_renewals_by_subscription_id' ], 10, 2 );
}
/**
* Add renewals to the query.
*
* @since 1.8.4
*
* @param string $after_where SQL query.
* @param array $args Query arguments.
*
* @return string
*/
public function add_renewals_by_subscription_id( $after_where, $args ) {
$query = $this->query_renewals_by_subscription_id( $args );
if ( empty( $query ) ) {
return $after_where; // Return early if $query is empty.
}
return "{$after_where} UNION {$query}";
}
/**
* Add renewals to the count query.
*
* @since 1.8.4
*
* @param string $after_where SQL query.
* @param array $args Query arguments.
*
* @return string
*/
public function count_renewals_by_subscription_id( $after_where, $args ) {
$query = $this->query_renewals_by_subscription_id( $args, 'COUNT(*)' );
if ( empty( $query ) ) {
return $after_where; // Return early if $query is empty.
}
return "{$after_where} UNION ALL {$query}";
}
/**
* Add renewals to the exists query.
*
* @since 1.8.4
*
* @param string $after_where SQL query.
* @param array $args Query arguments.
*
* @return string
*/
public function exists_renewals_by_subscription_id( $after_where, $args ) {
$query = $this->query_renewals_by_subscription_id( $args, '1' );
if ( empty( $query ) ) {
return $after_where; // Return early if $query is empty.
}
return "{$after_where} UNION ALL {$query}";
}
/**
* Query renewals by subscription ID.
*
* @since 1.8.4
*
* @param array $args Query arguments.
* @param string $selector SQL selector.
*
* @return string
*/
private function query_renewals_by_subscription_id( $args, $selector = 'p.*' ) {
// Check if essential arguments are missing.
if ( empty( $args['table_query'] ) || empty( $args['subscription_status'] ) ) {
return '';
}
// Check if the query type is not 'renewal'.
if ( ! empty( $args['type'] ) && ! in_array( 'renewal', explode( '|', $args['type'] ), true ) ) {
return '';
}
$payment_handle = wpforms()->obj( 'payment' );
$subscription_statuses = explode( '|', $args['subscription_status'] );
$placeholders = wpforms_wpdb_prepare_in( $subscription_statuses );
// This is needed to avoid the count_all method from adding the WHERE clause for the other types.
$args['type'] = 'renewal';
// Remove the subscription_status argument from the query.
// The primary reason for this is that the subscription_status has to be checked in the subquery.
unset( $args['subscription_status'] );
// Prepare the query.
$query[] = "SELECT {$selector} FROM {$payment_handle->table_name} as p";
/**
* Append custom query parts before the WHERE clause.
*
* This hook allows external code to extend the SQL query by adding custom conditions
* immediately before the WHERE clause.
*
* @since 1.8.4
*
* @param string $where Before the WHERE clause in the database query.
* @param array $args Query arguments.
*
* @return string
*/
$query[] = apply_filters( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_before_where', '', $args );
// Add the WHERE clause.
$query[] = 'WHERE 1=1';
$query[] = $payment_handle->add_columns_where_conditions( $args );
$query[] = $payment_handle->add_secondary_where_conditions( $args );
$query[] = "AND EXISTS (
SELECT 1 FROM {$payment_handle->table_name} as subquery_p
WHERE subquery_p.subscription_id = p.subscription_id
AND subquery_p.subscription_status IN ({$placeholders})
)";
/**
* Append custom query parts after the WHERE clause.
*
* This hook allows external code to extend the SQL query by adding custom conditions
* immediately after the WHERE clause.
*
* @since 1.8.4
*
* @param string $where After the WHERE clause in the database query.
* @param array $args Query arguments.
*
* @return string
*/
$query[] = apply_filters( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_after_where', '', $args );
return implode( ' ', $query );
}
}
@@ -0,0 +1,119 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use WPForms\Db\Payments\ValueValidator;
/**
* Helper methods for the Overview page.
*
* @since 1.8.2
*/
class Helpers {
/**
* Get subscription description.
*
* @since 1.8.2
*
* @param string $payment_id Payment id.
* @param string $amount Payment amount.
*
* @return string
*/
public static function get_subscription_description( $payment_id, $amount ) {
// Get the subscription period for the payment.
$period = wpforms()->obj( 'payment_meta' )->get_single( $payment_id, 'subscription_period' );
$intervals = ValueValidator::get_allowed_subscription_intervals();
// If the subscription period is not set or not allowed, return the amount only.
if ( ! isset( $intervals[ $period ] ) ) {
return $amount;
}
// Use "/" as a separator between the amount and the subscription period.
return $amount . ' / ' . $intervals[ $period ];
}
/**
* Return a placeholder text "N/A" when there is no actual data to display.
*
* @since 1.8.2
*
* @param string $with_wrapper Wrap the text within a span tag for styling purposes. Default: true.
*
* @return string
*/
public static function get_placeholder_na_text( $with_wrapper = true ) {
$text = __( 'N/A', 'wpforms-lite' );
// Check if the text should be wrapped within a span tag.
if ( $with_wrapper ) {
return sprintf( '<span class="payment-placeholder-text-none">%s</span>', $text );
}
return $text;
}
/**
* Get the default heading for the Payments pages.
*
* @since 1.8.2.2
*
* @param string $help_link Help link.
*/
public static function get_default_heading( $help_link = '' ) {
if ( ! $help_link ) {
$help_link = 'https://wpforms.com/docs/viewing-and-managing-payments/';
}
echo '<span class="wpforms-payments-overview-help">';
printf(
'<a href="%s" target="_blank"><i class="fa fa-question-circle-o"></i>%s</a>',
esc_url(
wpforms_utm_link(
$help_link,
'Payments Dashboard',
'Manage Payments Documentation'
)
),
esc_html__( 'Help', 'wpforms-lite' )
);
echo '</span>';
}
/**
* Look for at least one payment in test mode.
*
* @since 1.9.0
*
* @return bool
*/
public static function is_test_payment_exists(): bool {
$published = wpforms()->obj( 'payment' )->get_payments(
[
'mode' => 'test',
'number' => 1,
]
);
if ( $published ) {
return true;
}
// Check for trashed payments.
return ! empty(
wpforms()->obj( 'payment' )->get_payments(
[
'mode' => 'test',
'number' => 1,
'is_published' => 0,
]
)
);
}
}
@@ -0,0 +1,43 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
/**
* Payments Overview Mode Toggle class.
*
* @since 1.8.2
*/
class ModeToggle {
/**
* Determine if the toggle should be displayed and render it.
*
* @since 1.8.2
*/
public function display() {
// Bail early if no payments are found in test mode.
if ( ! Helpers::is_test_payment_exists() ) {
return;
}
$this->render();
}
/**
* Display the toggle button.
*
* @since 1.8.2
*/
private function render() {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/payments/mode-toggle',
[
'mode' => Page::get_mode(),
],
true
);
}
}
@@ -0,0 +1,510 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
use WPForms\Admin\Helpers\Datepicker;
use WPForms\Db\Payments\ValueValidator;
use WPForms\Admin\Payments\Payments;
use WPForms\Admin\Payments\Views\PaymentsViewsInterface;
use WPForms\Integrations\Stripe\Helpers as StripeHelpers;
use WPForms\Integrations\Square\Helpers as SquareHelpers;
/**
* Payments Overview Page class.
*
* @since 1.8.2
*/
class Page implements PaymentsViewsInterface {
/**
* Payments table.
*
* @since 1.8.2
*
* @var Table
*/
private $table;
/**
* Payments chart.
*
* @since 1.8.2
*
* @var Chart
*/
private $chart;
/**
* Initialize class.
*
* @since 1.8.2
*/
public function init() {
if ( ! $this->has_any_mode_payment() ) {
return;
}
$this->chart = new Chart();
$this->table = new Table();
$this->table->prepare_items();
$this->clean_request_uri();
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.8.2
*/
private function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Get the tab label.
*
* @since 1.8.2.2
*
* @return string
*/
public function get_tab_label() {
return __( 'Overview', 'wpforms-lite' );
}
/**
* Enqueue scripts and styles.
*
* @since 1.8.2
*/
public function enqueue_assets() {
$min = wpforms_get_min_suffix();
wp_enqueue_style(
'wpforms-flatpickr',
WPFORMS_PLUGIN_URL . 'assets/lib/flatpickr/flatpickr.min.css',
[],
'4.6.9'
);
wp_enqueue_script(
'wpforms-flatpickr',
WPFORMS_PLUGIN_URL . 'assets/lib/flatpickr/flatpickr.min.js',
[ 'jquery' ],
'4.6.9',
true
);
wp_enqueue_style(
'wpforms-multiselect-checkboxes',
WPFORMS_PLUGIN_URL . 'assets/lib/wpforms-multiselect/wpforms-multiselect-checkboxes.min.css',
[],
'1.0.0'
);
wp_enqueue_script(
'wpforms-multiselect-checkboxes',
WPFORMS_PLUGIN_URL . 'assets/lib/wpforms-multiselect/wpforms-multiselect-checkboxes.min.js',
[],
'1.0.0',
true
);
wp_enqueue_script(
'wpforms-chart',
WPFORMS_PLUGIN_URL . 'assets/lib/chart.min.js',
[ 'moment' ],
'4.5.1',
true
);
wp_enqueue_script(
'wpforms-chart-adapter-moment',
WPFORMS_PLUGIN_URL . 'assets/lib/chartjs-adapter-moment.min.js',
[ 'moment', 'wpforms-chart' ],
'1.0.1',
true
);
wp_enqueue_script(
'wpforms-admin-payments-overview',
WPFORMS_PLUGIN_URL . "assets/js/admin/payments/overview{$min}.js",
[ 'jquery', 'wpforms-flatpickr', 'wpforms-chart' ],
WPFORMS_VERSION,
true
);
$admin_l10n = [
'settings' => $this->chart->get_chart_settings(),
'locale' => sanitize_key( wpforms_get_language_code() ),
'nonce' => wp_create_nonce( 'wpforms_payments_overview_nonce' ),
'date_format' => sanitize_text_field( Datepicker::get_wp_date_format_for_momentjs() ),
'delimiter' => Datepicker::TIMESPAN_DELIMITER,
'report' => Chart::ACTIVE_REPORT,
'currency' => sanitize_text_field( wpforms_get_currency() ),
'decimals' => absint( wpforms_get_currency_decimals( wpforms_get_currency() ) ),
'i18n' => [
'label' => esc_html__( 'Payments', 'wpforms-lite' ),
'delete_button' => esc_html__( 'Delete', 'wpforms-lite' ),
'subscription_delete_confirm' => $this->get_subscription_delete_confirmation_message(),
'no_dataset' => [
'total_payments' => esc_html__( 'No payments for selected period', 'wpforms-lite' ),
'total_sales' => esc_html__( 'No sales for selected period', 'wpforms-lite' ),
'total_refunded' => esc_html__( 'No refunds for selected period', 'wpforms-lite' ),
'total_subscription' => esc_html__( 'No new subscriptions for selected period', 'wpforms-lite' ),
'total_renewal_subscription' => esc_html__( 'No subscription renewals for the selected period', 'wpforms-lite' ),
'total_coupons' => esc_html__( 'No coupons applied during the selected period', 'wpforms-lite' ),
],
],
'page_uri' => $this->get_current_uri(),
];
wp_localize_script(
'wpforms-admin-payments-overview', // Script handle the data will be attached to.
'wpforms_admin_payments_overview', // Name for the JavaScript object.
$admin_l10n
);
}
/**
* Retrieve a Payment Overview URI.
*
* @since 1.8.2
*
* @return string
*/
private function get_current_uri() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$query = $_GET;
unset( $query['mode'], $query['paged'] );
return add_query_arg( $query, self::get_url() );
}
/**
* Determine whether the current user has the capability to view the page.
*
* @since 1.8.2
*
* @return bool
*/
public function current_user_can() {
return wpforms_current_user_can();
}
/**
* Page heading.
*
* @since 1.8.2
*/
public function heading() {
Helpers::get_default_heading();
}
/**
* Page content.
*
* @since 1.8.2
*/
public function display() {
// If there are no payments at all, display an empty state.
if ( ! $this->has_any_mode_payment() ) {
$this->display_empty_state();
return;
}
// Display the page content, including the chart and the table.
$this->chart->display();
$this->table->display();
}
/**
* Get the URL of the page.
*
* @since 1.8.2
*
* @return string
*/
public static function get_url() {
static $url;
if ( $url ) {
return $url;
}
$url = add_query_arg(
[
'page' => Payments::SLUG,
],
admin_url( 'admin.php' )
);
return $url;
}
/**
* Get payment mode.
*
* Use only for logged-in users. Returns mode from user meta data or from the $_GET['mode'] parameter.
*
* @since 1.8.2
*
* @return string
*/
public static function get_mode(): string {
static $mode;
if ( ! self::is_valid_context_for_mode() ) {
return 'live';
}
if ( $mode ) {
return $mode;
}
$mode = self::get_mode_from_request();
$user_id = get_current_user_id();
$meta_key = 'wpforms-payments-mode';
if ( self::is_mode_valid_and_nonce_verified( $mode ) ) {
update_user_meta( $user_id, $meta_key, $mode );
return $mode;
}
$mode = (string) get_user_meta( $user_id, $meta_key, true );
if ( empty( $mode ) || ! Helpers::is_test_payment_exists() ) {
$mode = 'live';
}
return $mode;
}
/**
* Check if the context is valid for payment mode.
*
* @since 1.9.5
*
* @return bool
*/
private static function is_valid_context_for_mode(): bool {
return wpforms_is_admin_ajax() || wpforms_is_admin_page( 'payments' ) || wpforms_is_admin_page( 'entries' );
}
/**
* Retrieve the payment mode from the request.
*
* @since 1.9.5
*
* @return string
*/
private static function get_mode_from_request(): string {
// Nonce is checked in the `is_mode_valid_and_nonce_verified` method.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['mode'] ) ? sanitize_key( $_GET['mode'] ) : '';
}
/**
* Determine if the mode is valid and the nonce is verified.
*
* @since 1.9.5
*
* @param string $mode Payment mode to validate.
*
* @return bool
*/
private static function is_mode_valid_and_nonce_verified( string $mode ): bool {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return ValueValidator::is_valid( $mode, 'mode' ) &&
isset( $_GET['_wpnonce'] ) &&
wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_payments_overview_nonce' );
}
/**
* Display one of the empty states.
*
* @since 1.8.2
*/
private function display_empty_state() {
// If a payment gateway is configured, output no payments state.
if ( $this->is_gateway_configured() ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/empty-states/payments/no-payments',
[
'cta_url' => add_query_arg(
[
'page' => 'wpforms-overview',
],
'admin.php'
),
],
true
);
return;
}
// Otherwise, output get started state.
$is_upgraded = StripeHelpers::is_allowed_license_type();
$message = __( "First you need to set up a payment gateway. We've partnered with <strong>Stripe and Square</strong> to bring easy payment forms to everyone.&nbsp;", 'wpforms-lite' );
$message .= $is_upgraded
? sprintf( /* translators: %s - WPForms Addons admin page URL. */
__( 'Other payment gateways such as <strong>PayPal</strong> and <strong>Authorize.Net</strong> can be installed from the <a href="%s">Addons screen</a>.', 'wpforms-lite' ),
esc_url(
add_query_arg(
[
'page' => 'wpforms-addons',
],
admin_url( 'admin.php' )
)
)
)
: sprintf( /* translators: %s - WPForms.com Upgrade page URL. */
__( "If you'd like to use another payment gateway, please consider <a href='%s'>upgrading to WPForms Pro</a>.", 'wpforms-lite' ),
esc_url( wpforms_admin_upgrade_link( 'Payments Dashboard', 'Splash - Upgrade to Pro Text' ) )
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/empty-states/payments/get-started',
[
'message' => $message,
'version' => $is_upgraded ? 'pro' : 'lite',
'cta_url' => add_query_arg(
[
'page' => 'wpforms-settings',
'view' => 'payments',
],
admin_url( 'admin.php' )
),
],
true
);
}
/**
* Determine whether Stripe or Square payment gateway is configured.
*
* @since 1.8.2
*
* @return bool
*/
private function is_gateway_configured(): bool {
/**
* Allow to modify a status whether Stripe or Square payment gateway is configured.
*
* @since 1.8.2
*
* @param bool $is_configured True if Stripe or Square payment gateway is configured.
*/
return (bool) apply_filters( 'wpforms_admin_payments_views_overview_page_gateway_is_configured', StripeHelpers::has_stripe_keys() || SquareHelpers::is_square_configured() );
}
/**
* Determine whether there are payments of any modes.
*
* @since 1.8.2
*
* @return bool
*/
private function has_any_mode_payment() {
static $has_any_mode_payment;
if ( $has_any_mode_payment !== null ) {
return $has_any_mode_payment;
}
$has_any_mode_payment = count(
wpforms()->obj( 'payment' )->get_payments(
[
'mode' => 'any',
'number' => 1,
]
)
) > 0;
// Check on trashed payments.
if ( ! $has_any_mode_payment ) {
$has_any_mode_payment = count(
wpforms()->obj( 'payment' )->get_payments(
[
'mode' => 'any',
'number' => 1,
'is_published' => 0,
]
)
) > 0;
}
return $has_any_mode_payment;
}
/**
* To avoid recursively, remove the previous variables from the REQUEST_URI.
*
* @since 1.8.2
*/
private function clean_request_uri() {
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
$_SERVER['REQUEST_URI'] = remove_query_arg( [ '_wpnonce', '_wp_http_referer', 'action', 'action2', 'payment_id' ], wp_unslash( $_SERVER['REQUEST_URI'] ) );
if ( empty( $_GET['s'] ) ) {
$_SERVER['REQUEST_URI'] = remove_query_arg( [ 'search_where', 'search_mode', 's' ], wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
}
}
/**
* Get the subscription delete confirmation message.
* The returned message is used in the JavaScript file and shown in a "Heads up!" modal.
*
* @since 1.8.4
*
* @return string
*/
private function get_subscription_delete_confirmation_message() {
$help_link = wpforms_utm_link(
'https://wpforms.com/docs/viewing-and-managing-payments/#deleting-parent-subscription',
'Delete Payment',
'Learn More'
);
return sprintf(
wp_kses( /* translators: WPForms.com docs page URL. */
__( 'Deleting one or more selected payments may prevent processing of future subscription renewals. Payment filtering may also be affected. <a href="%1$s" rel="noopener" target="_blank">Learn More</a>', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
]
),
esc_url( $help_link )
);
}
}
@@ -0,0 +1,378 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview;
/**
* Search related methods for Payment and Payment Meta.
*
* @since 1.8.2
*/
class Search {
/**
* Credit card meta key.
*
* @since 1.8.2
*
* @var string
*/
const CREDIT_CARD = 'credit_card_last4';
/**
* Customer email meta key.
*
* @since 1.8.2
*
* @var string
*/
const EMAIL = 'customer_email';
/**
* Payment title column name.
*
* @since 1.8.2
*
* @var string
*/
const TITLE = 'title';
/**
* Transaction ID column name.
*
* @since 1.8.2
*
* @var string
*/
const TRANSACTION_ID = 'transaction_id';
/**
* Subscription ID column name.
*
* @since 1.8.2
*
* @var string
*/
const SUBSCRIPTION_ID = 'subscription_id';
/**
* Any column indicator key.
*
* @since 1.8.2
*
* @var string
*/
const ANY = 'any';
/**
* Equals mode.
*
* @since 1.8.2
*
* @var string
*/
const MODE_EQUALS = 'equals';
/**
* Starts with mode.
*
* @since 1.8.2
*
* @var string
*/
const MODE_STARTS = 'starts';
/**
* Contains mode.
*
* @since 1.8.2
*
* @var string
*/
const MODE_CONTAINS = 'contains';
/**
* Init.
*
* @since 1.8.2
*/
public function init() {
if ( ! self::is_search() ) {
return;
}
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.2
*/
private function hooks() {
add_filter( 'wpforms_db_payments_queries_count_all_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 );
add_filter( 'wpforms_db_payments_payment_get_payments_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 );
add_filter( 'wpforms_admin_payments_views_overview_filters_renewals_by_subscription_id_query_before_where', [ $this, 'add_search_where_conditions' ], 10, 2 );
}
/**
* Check if search query.
*
* @since 1.8.2
*
* @return bool
*/
public static function is_search() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return ! empty( $_GET['s'] );
}
/**
* Add search where conditions.
*
* @since 1.8.2
*
* @param string $where Query where string.
* @param array $args Query arguments.
*
* @return string
*/
public function add_search_where_conditions( $where, $args ) {
if ( empty( $args['search'] ) ) {
return $where;
}
if ( ! empty( $args['search_conditions']['search_mode'] ) && $args['search_conditions']['search_mode'] === self::MODE_CONTAINS ) {
$to_search = explode( ' ', $args['search'] );
} else {
$to_search = [ $args['search'] ];
}
$query = [];
foreach ( $to_search as $counter => $single ) {
$query[] = $this->add_single_search_condition( $single, $args, $counter );
}
return implode( ' ', $query );
}
/**
* Add single search condition.
*
* @since 1.8.2
*
* @param string $word Single searched part.
* @param array $args Query arguments.
* @param int $n Word counter.
*
* @return string
*/
private function add_single_search_condition( $word, $args, $n ) {
if ( empty( $word ) ) {
return '';
}
$mode = $this->prepare_mode( $args );
$where = $this->prepare_where( $args );
list( $operator, $word ) = $this->prepare_operator_and_word( $word, $mode );
$column = $this->prepare_column( $where );
if ( in_array( $column, [ self::EMAIL, self::CREDIT_CARD ], true ) ) {
return $this->select_from_meta_table( $column, $operator, $word, $n );
}
if ( $column === self::ANY ) {
return $this->select_from_any( $operator, $word, $n );
}
$payment_table = wpforms()->obj( 'payment' )->table_name;
$query = "SELECT id FROM {$payment_table}
WHERE {$payment_table}.{$column} {$operator} {$word}";
return $this->wrap_in_inner_join( $query, $n );
}
/**
* Prepare search mode part.
*
* @since 1.8.2
*
* @param array $args Query arguments.
*
* @return string Mode part for search.
*/
private function prepare_mode( $args ) {
return isset( $args['search_conditions']['search_mode'] ) ? $args['search_conditions']['search_mode'] : self::MODE_EQUALS;
}
/**
* Prepare search where part.
*
* @since 1.8.2
*
* @param array $args Query arguments.
*
* @return string Where part for search.
*/
private function prepare_where( $args ) {
return isset( $args['search_conditions']['search_where'] ) ? $args['search_conditions']['search_where'] : self::TITLE;
}
/**
* Prepare operator and word parts.
*
* @since 1.8.2
*
* @param string $word Single word.
* @param string $mode Search mode.
*
* @return array Array with operator and word parts for search.
*/
private function prepare_operator_and_word( $word, $mode ) {
global $wpdb;
if ( $mode === self::MODE_CONTAINS ) {
return [
'LIKE',
$wpdb->prepare( '%s', '%' . $wpdb->esc_like( $word ) . '%' ),
];
}
if ( $mode === self::MODE_STARTS ) {
return [
'LIKE',
$wpdb->prepare( '%s', $wpdb->esc_like( $word ) . '%' ),
];
}
return [
'=',
$wpdb->prepare( '%s', $word ),
];
}
/**
* Prepare column to search in.
*
* @since 1.8.2
*
* @param string $where Search where.
*
* @return string Column to search in.
*/
private function prepare_column( $where ) {
if ( in_array( $where, [ self::TRANSACTION_ID, self::SUBSCRIPTION_ID, self::EMAIL, self::CREDIT_CARD, self::ANY ], true ) ) {
return $where;
}
return self::TITLE;
}
/**
* Prepare select part to select from payments meta table.
*
* @since 1.8.2
*
* @param string $meta_key Meta key.
* @param string $operator Comparison operator.
* @param string $word Word to search.
* @param int $n Word count.
*
* @return string
* @noinspection CallableParameterUseCaseInTypeContextInspection
*/
private function select_from_meta_table( $meta_key, $operator, $word, $n ) {
global $wpdb;
$payment_table = wpforms()->obj( 'payment' )->table_name;
$meta_table = wpforms()->obj( 'payment_meta' )->table_name;
$meta_key = $wpdb->prepare( '%s', $meta_key );
$query = "SELECT id FROM $payment_table
WHERE id IN (
SELECT DISTINCT payment_id FROM $meta_table
WHERE meta_value $operator $word
AND meta_key = $meta_key
)";
return $this->wrap_in_inner_join( $query, $n );
}
/**
* Prepare select part to select from places from both tables.
*
* @since 1.8.2
*
* @param string $operator Comparison operator.
* @param string $word Word to search.
* @param int $n Word count.
*
* @return string
*/
private function select_from_any( $operator, $word, $n ) {
$payment_table = wpforms()->obj( 'payment' )->table_name;
$meta_table = wpforms()->obj( 'payment_meta' )->table_name;
$query = sprintf(
"SELECT id FROM {$payment_table}
WHERE (
{$payment_table}.%s {$operator} {$word}
OR {$payment_table}.%s {$operator} {$word}
OR {$payment_table}.%s {$operator} {$word}
OR id IN (
SELECT DISTINCT payment_id
FROM {$meta_table}
WHERE meta_value {$operator} {$word}
AND meta_key IN ( '%s', '%s' )
)
)",
self::TITLE,
self::TRANSACTION_ID,
self::SUBSCRIPTION_ID,
self::CREDIT_CARD,
self::EMAIL
);
return $this->wrap_in_inner_join( $query, $n );
}
/**
* Wrap the query in INNER JOIN part.
*
* @since 1.8.2
*
* @param string $query Partial query.
* @param int $n Word count.
*
* @return string
*/
private function wrap_in_inner_join( $query, $n ) {
/**
* Filter to modify the inner join query.
*
* @since 1.8.4
*
* @param string $query Partial query.
* @param int $n The number of the JOIN clause.
*/
return apply_filters(
'wpforms_admin_payments_views_overview_search_inner_join_query',
sprintf( 'INNER JOIN ( %1$s ) AS p%2$d ON p.id = p%2$d.id', $query, $n ),
$n
);
}
}
@@ -0,0 +1,272 @@
<?php
namespace WPForms\Admin\Payments\Views\Overview\Traits;
use WPForms\Admin\Payments\Views\Overview\Coupon;
use WPForms\Admin\Payments\Views\Overview\Search;
use WPForms\Db\Payments\ValueValidator;
/**
* This file is part of the Table class and contains methods responsible for
* displaying notices on the Payments Overview page.
*
* @since 1.8.4
*/
trait ResetNotices {
/**
* Show reset filter box.
*
* @since 1.8.4
*/
private function show_reset_filter() {
$applied_filters = [
$this->get_search_reset_filter(),
$this->get_status_reset_filter(),
$this->get_coupon_reset_filter(),
$this->get_form_reset_filter(),
$this->get_type_reset_filter(),
$this->get_gateway_reset_filter(),
$this->get_subscription_status_reset_filter(),
];
$applied_filters = array_filter( $applied_filters );
// Let's not show the reset filter notice if there are no applied filters.
if ( empty( $applied_filters ) ) {
return;
}
// Output the reset filter notice.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_render(
'admin/payments/reset-filter-notice',
[
'total' => $this->get_valid_status_count_from_request(),
'applied_filters' => $applied_filters,
],
true
);
}
/**
* Show search reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_search_reset_filter() {
// Do not show the reset filter notice on the search results page.
if ( ! Search::is_search() ) {
return [];
}
$search_where = $this->get_search_where( $this->get_search_where_key() );
$search_mode = $this->get_search_mode( $this->get_search_mode_key() );
return [
'reset_url' => remove_query_arg( [ 's', 'search_where', 'search_mode', 'paged' ] ),
'results' => sprintf(
' %s <em>%s</em> %s "<em>%s</em>"',
__( 'where', 'wpforms-lite' ),
esc_html( $search_where ),
esc_html( $search_mode ),
// It's important to escape the search term here for security.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
esc_html( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' )
),
];
}
/**
* Show status reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_status_reset_filter() {
// Do not show the reset filter notice on the status results page.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $this->get_valid_status_from_request() ) || $this->is_trash_view() ) {
return [];
}
$statuses = ValueValidator::get_allowed_one_time_statuses();
// Leave early if the status is not found.
if ( ! isset( $statuses[ $this->get_valid_status_from_request() ] ) ) {
return [];
}
return [
'reset_url' => remove_query_arg( [ 'status' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
__( 'with the status', 'wpforms-lite' ),
$statuses[ $this->get_valid_status_from_request() ]
),
];
}
/**
* Show coupon reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_coupon_reset_filter() {
// Do not show the reset filter notice on the coupon results page.
if ( ! Coupon::is_coupon() ) {
return [];
}
// Get the payment meta with the specified coupon ID.
$payment_meta = wpforms()->obj( 'payment_meta' )->get_all_by_meta(
'coupon_id',
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
absint( $_GET['coupon_id'] )
);
// If the coupon info is empty, exit the function.
if ( empty( $payment_meta['coupon_info'] ) ) {
return [];
}
return [
'reset_url' => remove_query_arg( [ 'coupon_id', 'paged' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
__( 'with the coupon', 'wpforms-lite' ),
$this->get_coupon_name_by_info( $payment_meta['coupon_info']->value )
),
];
}
/**
* Show form reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_form_reset_filter() {
// Do not show the reset filter notice on the form results page.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $_GET['form_id'] ) ) {
return [];
}
// Retrieve the form with the specified ID.
$form = wpforms()->obj( 'form' )->get( absint( $_GET['form_id'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// If the form is not found or not published, exit the function.
if ( ! $form || $form->post_status !== 'publish' ) {
return [];
}
return [
'reset_url' => remove_query_arg( [ 'form_id', 'paged' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
__( 'with the form titled', 'wpforms-lite' ),
! empty( $form->post_title ) ? $form->post_title : $form->post_name
),
];
}
/**
* Show type reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_type_reset_filter() {
// Do not show the reset filter notice on the type results page.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $_GET['type'] ) ) {
return [];
}
$allowed_types = ValueValidator::get_allowed_types();
$type = explode( '|', sanitize_text_field( wp_unslash( $_GET['type'] ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return [
'reset_url' => remove_query_arg( [ 'type', 'paged' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
_n( 'with the type', 'with the types', count( $type ), 'wpforms-lite' ),
implode( ', ', array_intersect_key( $allowed_types, array_flip( $type ) ) )
),
];
}
/**
* Show gateway reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_gateway_reset_filter() {
// Do not show the reset filter notice on the gateway results page.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $_GET['gateway'] ) ) {
return [];
}
$allowed_gateways = ValueValidator::get_allowed_gateways();
$gateway = explode( '|', sanitize_text_field( wp_unslash( $_GET['gateway'] ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return [
'reset_url' => remove_query_arg( [ 'gateway', 'paged' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
_n( 'with the gateway', 'with the gateways', count( $gateway ), 'wpforms-lite' ),
implode( ', ', array_intersect_key( $allowed_gateways, array_flip( $gateway ) ) )
),
];
}
/**
* Show subscription status reset filter.
*
* @since 1.8.4
*
* @return array
*/
private function get_subscription_status_reset_filter() {
// Do not show the reset filter notice on the subscription status results page.
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $_GET['subscription_status'] ) ) {
return [];
}
$allowed_subscription_statuses = ValueValidator::get_allowed_subscription_statuses();
$subscription_status = explode( '|', sanitize_text_field( wp_unslash( $_GET['subscription_status'] ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return [
'reset_url' => remove_query_arg( [ 'subscription_status', 'paged' ] ),
'results' => sprintf(
' %s "<em>%s</em>"',
_n( 'with the subscription status', 'with the subscription statuses', count( $subscription_status ), 'wpforms-lite' ),
implode( ', ', array_intersect_key( $allowed_subscription_statuses, array_flip( $subscription_status ) ) )
),
];
}
}
@@ -0,0 +1,43 @@
<?php
namespace WPForms\Admin\Payments\Views;
interface PaymentsViewsInterface {
/**
* Initialize class.
*
* @since 1.8.2
*/
public function init();
/**
* Check if the current user has the capability to view the page.
*
* @since 1.8.2
*
* @return bool
*/
public function current_user_can();
/**
* Page heading content.
*
* @since 1.8.2
*/
public function heading();
/**
* Page content.
*
* @since 1.8.2
*/
public function display();
/**
* Get the Tab label.
*
* @since 1.8.2.2
*/
public function get_tab_label();
}
@@ -0,0 +1,496 @@
<?php
namespace WPForms\Admin;
use WP_Post;
/**
* Form Revisions.
*
* @since 1.7.3
*/
class Revisions {
/**
* Current Form Builder panel view.
*
* @since 1.7.3
*
* @var string
*/
private $view = 'revisions';
/**
* Current Form ID.
*
* @since 1.7.3
*
* @var int|false
*/
private $form_id = false;
/**
* Current Form.
*
* @since 1.7.3
*
* @var WP_Post|null
*/
private $form;
/**
* Current Form Revision ID.
*
* @since 1.7.3
*
* @var int|false
*/
private $revision_id = false;
/**
* Current Form Revision.
*
* @since 1.7.3
*
* @var WP_Post|null
*/
private $revision;
/**
* Whether revisions panel was already viewed by the user at least once.
*
* @since 1.7.3
*
* @var bool
*/
private $viewed;
/**
* Initialize the class if preconditions are met.
*
* @since 1.7.3
*
* @return void
*/
public function init() {
if ( ! $this->allow_load() ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST['view'] ) ) {
$this->view = sanitize_key( $_REQUEST['view'] );
}
if ( isset( $_REQUEST['revision_id'] ) ) {
$this->revision_id = absint( $_REQUEST['revision_id'] );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( ! $this->can_access_form() ) {
return;
}
if ( $this->revision_id && wp_revisions_enabled( $this->form ) ) {
$this->revision = wp_get_post_revision( $this->revision_id );
// Bail if we don't have a valid revision.
if (
! $this->revision instanceof WP_Post ||
$this->revision->post_parent !== $this->form_id ||
$this->revision->ID !== $this->revision_id
) {
return;
}
}
$this->hooks();
}
/**
* Whether it is allowed to load under certain conditions.
*
* - numeric, non-zero form ID provided,
* - the form with this ID exists and was successfully fetched,
* - we're in the Form Builder or processing an ajax request.
*
* @since 1.7.3
*
* @return bool
*/
private function allow_load() {
if ( ! ( wpforms_is_admin_page( 'builder' ) || wp_doing_ajax() ) ) {
return false;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$id = wp_doing_ajax() && isset( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : false;
$id = isset( $_REQUEST['form_id'] ) && ! is_array( $_REQUEST['form_id'] ) ? absint( $_REQUEST['form_id'] ) : $id;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$this->form_id = $id;
$form_handler = wpforms()->obj( 'form' );
if ( ! $form_handler ) {
return false;
}
$this->form = $form_handler->get( $this->form_id );
return $this->form_id && $this->form instanceof WP_Post;
}
/**
* Hook into WordPress lifecycle.
*
* @since 1.7.3
*/
private function hooks() {
// Restore a revision. The `admin_init` action has already fired, `current_screen` fires before headers are sent.
add_action( 'current_screen', [ $this, 'process_restore' ] );
// Refresh a rendered list of revisions on the frontend.
add_action( 'wp_ajax_wpforms_get_form_revisions', [ $this, 'fetch_revisions_list' ] );
// Mark Revisions panel as viewed when viewed for the first time. Hides the error badge.
add_action( 'wp_ajax_wpforms_mark_panel_viewed', [ $this, 'mark_panel_viewed' ] );
// Back-compat for forms created with revisions disabled.
add_action( 'wpforms_builder_init', [ $this, 'maybe_create_initial_revision' ] );
// Pass localized strings to frontend.
add_filter( 'wpforms_builder_strings', [ $this, 'get_localized_strings' ], 10, 2 );
}
/**
* Get current revision, if available.
*
* @since 1.7.3
*
* @return WP_Post|null
*/
public function get_revision() {
return $this->revision;
}
/**
* Get formatted date or time.
*
* @since 1.7.3
*
* @param string $datetime UTC datetime from the post object.
* @param string $part What to return - date or time, defaults to date.
*
* @return string
*/
public function get_formatted_datetime( $datetime, $part = 'date' ) {
if ( $part === 'time' ) {
return wpforms_time_format( $datetime, '', true );
}
// M j format needs to keep one-line date.
return wpforms_date_format( $datetime, 'M j', true );
}
/**
* Get admin (Form Builder) base URL with additional query args.
*
* @since 1.7.3
*
* @param array $query_args Additional query args to append to the base URL.
*
* @return string
*/
public function get_url( $query_args = [] ) {
$defaults = [
'page' => 'wpforms-builder',
'view' => $this->view,
'form_id' => $this->form_id,
];
return add_query_arg(
wp_parse_args( $query_args, $defaults ),
admin_url( 'admin.php' )
);
}
/**
* Determine if Revisions panel was previously viewed by current user.
*
* @since 1.7.3
*
* @return bool
*/
public function panel_viewed() {
if ( $this->viewed === null ) {
$this->viewed = (bool) get_user_meta( get_current_user_id(), 'wpforms_revisions_disabled_notice_dismissed', true );
}
return $this->viewed;
}
/**
* Mark Revisions panel as viewed by current user.
*
* @since 1.7.3
*/
public function mark_panel_viewed() {
// Run a security check.
check_ajax_referer( 'wpforms-builder', 'nonce' );
if ( ! $this->panel_viewed() ) {
$this->viewed = update_user_meta( get_current_user_id(), 'wpforms_revisions_disabled_notice_dismissed', true );
}
wp_send_json_success( [ 'updated' => $this->viewed ] );
}
/**
* Get a rendered list of all revisions.
*
* @since 1.7.3
*
* @return string
*/
public function render_revisions_list() {
return wpforms_render(
'builder/revisions/list',
$this->prepare_template_render_arguments(),
true
);
}
/**
* Prepare all arguments for the template to be rendered.
*
* Note: All data is escaped in the template.
*
* @since 1.7.3
*
* @return array
*/
private function prepare_template_render_arguments() {
$args = [
'active_class' => $this->revision ? '' : ' active',
'current_version_url' => $this->get_url(),
'author_id' => $this->form->post_author,
'revisions' => [],
'show_avatars' => get_option( 'show_avatars' ),
];
$revisions = wp_get_post_revisions( $this->form_id );
if ( empty( $revisions ) ) {
return $args;
}
// WordPress always orders entries by `post_date` column, which contains a date and time in site's timezone configured in settings.
// This setting is per site, not per user, and it's not expected to be changed. However, if it was changed for whatever reason,
// the order of revisions will be incorrect. This is definitely an edge case, but we can prevent this from ever happening
// by sorting the results using `post_date_gmt` or `post_modified_gmt`, which contains UTC date and never changes.
uasort(
$revisions,
static function ( $a, $b ) {
return strtotime( $a->post_modified_gmt ) > strtotime( $b->post_modified_gmt ) ? -1 : 1;
}
);
// The first revision is always identical to the current version and should not be displayed in the list.
$current_revision = array_shift( $revisions );
// Display the author of current version instead of a form author.
$args['author_id'] = $current_revision->post_author;
foreach ( $revisions as $revision ) {
$time_diff = sprintf( /* translators: %s - relative time difference, e.g. "5 minutes", "12 days". */
__( '%s ago', 'wpforms-lite' ),
human_time_diff( strtotime( $revision->post_modified_gmt . ' +0000' ) )
);
$date_time = sprintf( /* translators: %1$s - date, %2$s - time when item was created, e.g. "Oct 22 at 11:11am". */
__( '%1$s at %2$s', 'wpforms-lite' ),
$this->get_formatted_datetime( $revision->post_modified_gmt ),
$this->get_formatted_datetime( $revision->post_modified_gmt, 'time' )
);
$args['revisions'][] = [
'active_class' => $this->revision && $this->revision->ID === $revision->ID ? ' active' : '',
'url' => $this->get_url(
[
'revision_id' => $revision->ID,
]
),
'author_id' => $revision->post_author,
'time_diff' => $time_diff,
'date_time' => $date_time,
];
}
return $args;
}
/**
* Fetch a list of revisions via ajax.
*
* @since 1.7.3
*/
public function fetch_revisions_list() {
// Run a security check.
check_ajax_referer( 'wpforms-builder', 'nonce' );
wp_send_json_success(
[
'html' => $this->render_revisions_list(),
]
);
}
/**
* Restore the revision (if needed) and reload the Form Builder.
*
* @since 1.7.3
*
* @return void
*/
public function process_restore() {
$is_restore_request = isset( $_GET['action'] ) && $_GET['action'] === 'restore_revision';
// Bail early.
if (
! $is_restore_request ||
! $this->form_id ||
! $this->form ||
! $this->revision_id ||
! $this->revision ||
! check_admin_referer( 'restore_revision', 'wpforms_nonce' )
) {
return;
}
if ( ! $this->can_access_form() ) {
wp_die( esc_html__( 'You do not have permission to restore revisions for this form.', 'wpforms-lite' ) );
}
if (
! $this->revision instanceof WP_Post ||
$this->revision->post_parent !== $this->form_id ||
$this->revision->ID !== $this->revision_id
) {
wp_die( esc_html__( 'Invalid revision. The revision does not belong to this form.', 'wpforms-lite' ) );
}
$restored_id = wp_restore_post_revision( $this->revision );
if ( $restored_id ) {
wp_safe_redirect(
wpforms()->obj( 'revisions' )->get_url(
[
'form_id' => $restored_id,
]
)
);
exit;
}
}
/**
* Create initial revision for existing form.
*
* When a new form is created with revisions enabled, WordPress immediately creates first revision which is identical to the form. But when
* a form was created with revisions disabled, this initial revision does not exist. Revisions are saved after post update, so modifying
* a form that have no initial revision will update the post first, then a revision of this updated post will be saved. The version of
* the form that existed before this update is now gone. To avoid losing this pre-revisions state, we create this initial revision
* when the Form Builder loads, if needed.
*
* @since 1.7.3
*
* @return void
*/
public function maybe_create_initial_revision() {
// On new form creation there's no revisions yet, bail. Also, when revisions are disabled.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['newform'] ) || ! wp_revisions_enabled( $this->form ) ) {
return;
}
$revisions = wp_get_post_revisions(
$this->form_id,
[
'fields' => 'ids',
'numberposts' => 1,
]
);
if ( $revisions ) {
return;
}
$initial_revision_id = wp_save_post_revision( $this->form_id );
$initial_revision = wp_get_post_revision( $initial_revision_id );
// Initial revision should belong to the author of the original form.
if ( $initial_revision->post_author !== $this->form->post_author ) {
wp_update_post(
[
'ID' => $initial_revision_id,
'post_author' => $this->form->post_author,
]
);
}
}
/**
* Pass localized strings to frontend.
*
* @since 1.7.3
*
* @param array $strings All strings that will be passed to frontend.
* @param WP_Post $form Current form object.
*
* @return array
*/
public function get_localized_strings( $strings, $form ) {
$strings['revision_update_confirm'] = esc_html__( 'Youre about to save a form revision. Continuing will make this the current version.', 'wpforms-lite' );
return $strings;
}
/**
* Check if the current user has permission to access the form and its revisions.
*
* @since 1.9.5
*
* @return bool
*/
private function can_access_form(): bool {
if ( ! wpforms_current_user_can( 'view_form_single', $this->form_id ) ) {
return false;
}
if ( ! wpforms_current_user_can( 'edit_form_single', $this->form_id ) ) {
return false;
}
return true;
}
}
@@ -0,0 +1,197 @@
<?php
namespace WPForms\Admin\Settings\Captcha;
/**
* Base captcha settings class.
*
* @since 1.8.0
*/
abstract class Captcha {
/**
* Saved CAPTCHA settings.
*
* @since 1.8.0
*
* @var array
*/
protected $settings;
/**
* List of required static properties.
*
* @since 1.8.0
*
* @var array
*/
private $required_static_properties = [
'api_var',
'slug',
'url',
];
/**
* Initialize class.
*
* @since 1.8.0
*/
public function init() {
$this->settings = wp_parse_args( wpforms_get_captcha_settings(), [ 'provider' => 'none' ] );
foreach ( $this->required_static_properties as $property ) {
if ( empty( static::${$property} ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error(
sprintf(
'The $%s static property is required for a %s class',
esc_html( $property ),
__CLASS__
),
E_USER_ERROR
);
}
}
}
/**
* Array of captcha settings fields.
*
* @since 1.8.0
*
* @return array[]
*/
abstract public function get_settings_fields();
/**
* Get API request url for the captcha preview.
*
* @since 1.8.0
*
* @return string
*/
public function get_api_url() {
$url = static::$url;
if ( ! empty( $url ) ) {
$url = add_query_arg( $this->get_api_url_query_arg(), $url );
}
/**
* Filter API URL.
*
* @since 1.6.4
*
* @param string $url API URL.
* @param array $settings Captcha settings array.
*/
return apply_filters( 'wpforms_admin_settings_captcha_get_api_url', $url, $this->settings );
}
/**
* Enqueue assets for the CAPTCHA settings page.
*
* @since 1.8.0
*/
public function enqueues() {
/**
* Allow/disallow to enquire captcha settings.
*
* @since 1.6.4
*
* @param boolean $allow True/false. Default: false.
*/
$disable_enqueues = apply_filters( 'wpforms_admin_settings_captcha_enqueues_disable', false );
if ( $disable_enqueues || ! $this->is_captcha_preview_ready() ) {
return;
}
$api_url = $this->get_api_url();
$provider_name = $this->settings['provider'];
$handle = "wpforms-settings-{$provider_name}";
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_script( $handle, $api_url, [ 'jquery' ], null, true );
wp_add_inline_script( $handle, $this->get_inline_script() );
}
/**
* Inline script for initialize captcha JS code.
*
* @since 1.8.0
*
* @return string
*/
protected function get_inline_script() {
return /** @lang JavaScript */
'var wpformsSettingsCaptchaLoad = function() {
jQuery( ".wpforms-captcha" ).each( function( index, el ) {
var widgetID = ' . static::$api_var . '.render( el );
jQuery( el ).attr( "data-captcha-id", widgetID );
} );
jQuery( document ).trigger( "wpformsSettingsCaptchaLoaded" );
};';
}
/**
* Check if CAPTCHA config is ready to display a preview.
*
* @since 1.8.0
*
* @return bool
*/
public function is_captcha_preview_ready() {
return (
( $this->settings['provider'] === static::$slug || ( $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v2' ) ) &&
! empty( $this->settings['site_key'] ) &&
! empty( $this->settings['secret_key'] )
);
}
/**
* Retrieve query arguments for the CAPTCHA API URL.
*
* @since 1.8.0
*
* @return array
*/
protected function get_api_url_query_arg() {
/**
* Modify captcha api url parameters.
*
* @since 1.8.0
*
* @param array $params Array of parameters.
* @param array $params Saved CAPTCHA settings.
*/
return (array) apply_filters(
'wpforms_admin_settings_captcha_get_api_url_query_arg',
[
'onload' => 'wpformsSettingsCaptchaLoad',
'render' => 'explicit',
],
$this->settings
);
}
/**
* Heading description.
*
* @since 1.8.0
*
* @return string
*/
public function get_field_desc() {
$content = wpforms_render( 'admin/settings/' . static::$slug . '-description' );
return wpforms_render( 'admin/settings/specific-note', [ 'content' => $content ], true );
}
}
@@ -0,0 +1,75 @@
<?php
namespace WPForms\Admin\Settings\Captcha;
/**
* HCaptcha settings class.
*
* @since 1.8.0
*/
class HCaptcha extends Captcha {
/**
* Captcha variable used for JS invoking.
*
* @since 1.8.0
*
* @var string
*/
protected static $api_var = 'hcaptcha';
/**
* Get captcha key name.
*
* @since 1.8.0
*
* @var string
*/
protected static $slug = 'hcaptcha';
/**
* The hCaptcha Javascript URL-resource.
*
* @since 1.8.0
*
* @var string
*/
protected static $url = 'https://hcaptcha.com/1/api.js';
/**
* Array of captcha settings fields.
*
* @since 1.8.0
*
* @return array[]
*/
public function get_settings_fields() {
return [
'hcaptcha-heading' => [
'id' => 'hcaptcha-heading',
'content' => $this->get_field_desc(),
'type' => 'content',
'no_label' => true,
'class' => [ 'section-heading', 'specific-note' ],
],
'hcaptcha-site-key' => [
'id' => 'hcaptcha-site-key',
'name' => esc_html__( 'Site Key', 'wpforms-lite' ),
'type' => 'text',
],
'hcaptcha-secret-key' => [
'id' => 'hcaptcha-secret-key',
'name' => esc_html__( 'Secret Key', 'wpforms-lite' ),
'type' => 'text',
],
'hcaptcha-fail-msg' => [
'id' => 'hcaptcha-fail-msg',
'name' => esc_html__( 'Fail Message', 'wpforms-lite' ),
'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ),
'type' => 'text',
'default' => esc_html__( 'hCaptcha verification failed, please try again later.', 'wpforms-lite' ),
],
];
}
}
@@ -0,0 +1,372 @@
<?php
namespace WPForms\Admin\Settings\Captcha;
use WPForms\Admin\Notice;
/**
* CAPTCHA setting page.
*
* @since 1.8.0
*/
class Page {
/**
* Slug identifier for admin page view.
*
* @since 1.8.0
*
* @var string
*/
const VIEW = 'captcha';
/**
* Saved CAPTCHA settings.
*
* @since 1.8.0
*
* @var array
*/
private $settings;
/**
* All available captcha types.
*
* @since 1.8.0
*
* @var array
*/
private $captchas;
/**
* Initialize class.
*
* @since 1.8.0
*/
public function init() {
// Only load if we are actually on the settings page.
if ( ! wpforms_is_admin_page( 'settings' ) ) {
return;
}
// Listen the previous reCAPTCHA page and safely redirect from it.
if ( wpforms_is_admin_page( 'settings', 'recaptcha' ) ) {
wp_safe_redirect( add_query_arg( 'view', self::VIEW, admin_url( 'admin.php?page=wpforms-settings' ) ) );
exit;
}
$this->init_settings();
$this->hooks();
}
/**
* Init CAPTCHA settings.
*
* @since 1.8.0
*/
public function init_settings() {
$this->settings = wp_parse_args( wpforms_get_captcha_settings(), [ 'provider' => 'none' ] );
/**
* Filter available captcha for the settings page.
*
* @since 1.8.0
*
* @param array $captcha Array where key is captcha name and value is captcha class instance.
* @param array $settings Array of settings.
*/
$this->captchas = apply_filters(
'wpforms_admin_settings_captcha_page_init_settings_available_captcha',
[
'hcaptcha' => new HCaptcha(),
'recaptcha' => new ReCaptcha(),
'turnstile' => new Turnstile(),
],
$this->settings
);
foreach ( $this->captchas as $captcha ) {
$captcha->init();
}
}
/**
* Hooks.
*
* @since 1.8.0
*/
public function hooks() {
add_filter( 'wpforms_settings_tabs', [ $this, 'register_settings_tabs' ], 5, 1 );
add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 5, 1 );
add_action( 'wpforms_settings_updated', [ $this, 'updated' ] );
add_action( 'wpforms_settings_enqueue', [ $this, 'enqueues' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'apply_noconflict' ], 9999 );
}
/**
* Register CAPTCHA settings tab.
*
* @since 1.8.0
*
* @param array $tabs Admin area tabs list.
*
* @return array
*/
public function register_settings_tabs( $tabs ) {
$captcha = [
self::VIEW => [
'name' => esc_html__( 'CAPTCHA', 'wpforms-lite' ),
'form' => true,
'submit' => esc_html__( 'Save Settings', 'wpforms-lite' ),
],
];
return wpforms_array_insert( $tabs, $captcha, 'email' );
}
/**
* Register CAPTCHA settings fields.
*
* @since 1.8.0
*
* @param array $settings Admin area settings list.
*
* @return array
*/
public function register_settings_fields( $settings ) {
$settings[ self::VIEW ] = [
self::VIEW . '-heading' => [
'id' => self::VIEW . '-heading',
'content' => '<h4>' . esc_html__( 'CAPTCHA', 'wpforms-lite' ) . '</h4><p>' . esc_html__( 'A CAPTCHA is an anti-spam technique which helps to protect your website from spam and abuse while letting real people pass through with ease.', 'wpforms-lite' ) . '</p>',
'type' => 'content',
'no_label' => true,
'class' => [ 'wpforms-setting-captcha-heading', 'section-heading' ],
],
self::VIEW . '-provider' => [
'id' => self::VIEW . '-provider',
'type' => 'radio',
'default' => 'none',
'options' => [
'hcaptcha' => 'hCaptcha',
'recaptcha' => 'reCAPTCHA',
'turnstile' => 'Turnstile',
'none' => esc_html__( 'None', 'wpforms-lite' ),
],
'desc' => sprintf(
wp_kses( /* translators: %s - WPForms.com CAPTCHA comparison page URL. */
__( 'Not sure which service is right for you? <a href="%s" target="_blank" rel="noopener noreferrer">Check out our comparison</a> for more details.', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'target' => [],
'rel' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/setup-captcha-wpforms/', 'Settings - Captcha', 'Captcha Comparison Documentation' ) )
),
],
];
// Add settings fields for each of available captcha types.
foreach ( $this->captchas as $captcha ) {
$settings[ self::VIEW ] = array_merge( $settings[ self::VIEW ], $captcha->get_settings_fields() );
}
$settings[ self::VIEW ] = array_merge(
$settings[ self::VIEW ],
[
'recaptcha-noconflict' => [
'id' => 'recaptcha-noconflict',
'name' => esc_html__( 'No-Conflict Mode', 'wpforms-lite' ),
'desc' => esc_html__( 'Forcefully remove other CAPTCHA occurrences in order to prevent conflicts. Only enable this option if your site is having compatibility issues or instructed by support.', 'wpforms-lite' ),
'type' => 'toggle',
'status' => true,
],
self::VIEW . '-preview' =>
[
'id' => self::VIEW . '-preview',
'name' => esc_html__( 'Preview', 'wpforms-lite' ),
'content' => '<p class="desc wpforms-captcha-preview-desc">' . esc_html__( 'Please save settings to generate a preview of your CAPTCHA here.', 'wpforms-lite' ) . '</p>',
'type' => 'content',
'class' => [ 'wpforms-hidden' ],
],
]
);
if (
$this->settings['provider'] === 'hcaptcha' ||
$this->settings['provider'] === 'turnstile' ||
( $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v2' )
) {
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Modify captcha settings data.
*
* @since 1.6.4
*
* @param array $data Array of settings.
*/
$data = apply_filters(
'wpforms_admin_pages_settings_captcha_data',
[
'sitekey' => $this->settings['site_key'],
'theme' => $this->settings['theme'],
]
);
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
// Prepare HTML for CAPTCHA preview.
$placeholder_description = $settings[ self::VIEW ][ self::VIEW . '-preview' ]['content'];
$captcha_description = esc_html__( 'This CAPTCHA is generated using your site and secret keys. If an error is displayed, please double-check your keys.', 'wpforms-lite' );
$captcha_preview = sprintf(
'<div class="wpforms-captcha-container" style="pointer-events:none!important;cursor:default!important;">
<div %s></div>
<input type="text" name="wpforms-captcha-hidden" class="wpforms-recaptcha-hidden" style="position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;">
</div>',
wpforms_html_attributes( '', [ 'wpforms-captcha', 'wpforms-captcha-' . $this->settings['provider'] ], $data )
);
$settings[ self::VIEW ][ self::VIEW . '-preview' ]['content'] = sprintf(
'<div class="wpforms-captcha-preview">
%1$s <p class="desc">%2$s</p>
</div>
<div class="wpforms-captcha-placeholder wpforms-hidden">%3$s</div>',
$captcha_preview,
$captcha_description,
$placeholder_description
);
$settings[ self::VIEW ][ self::VIEW . '-preview' ]['class'] = [];
}
return $settings;
}
/**
* Re-init CAPTCHA settings when plugin settings were updated.
*
* @since 1.8.0
*/
public function updated() {
$this->init_settings();
$this->notice();
}
/**
* Display notice about the CAPTCHA preview.
*
* @since 1.8.0
*/
private function notice() {
if ( ! wpforms_is_admin_page( 'settings', self::VIEW ) || ! $this->is_captcha_preview_ready() ) {
return;
}
Notice::info( esc_html__( 'A preview of your CAPTCHA is displayed below. Please view to verify the CAPTCHA settings are correct.', 'wpforms-lite' ) );
}
/**
* Check if CAPTCHA config is ready to display a preview.
*
* @since 1.8.0
*
* @return bool
*/
private function is_captcha_preview_ready() {
$current_captcha = $this->get_current_captcha();
if ( ! $current_captcha ) {
return false;
}
return $current_captcha->is_captcha_preview_ready();
}
/**
* Enqueue assets for the CAPTCHA settings page.
*
* @since 1.8.0
*/
public function enqueues() {
$current_captcha = $this->get_current_captcha();
if ( ! $current_captcha ) {
return;
}
$current_captcha->enqueues();
}
/**
* Get current active captcha object.
*
* @since 1.8.0
*
* @return object|string
*/
private function get_current_captcha() {
return ! empty( $this->captchas[ $this->settings['provider'] ] ) ? $this->captchas[ $this->settings['provider'] ] : '';
}
/**
* Use the CAPTCHA no-conflict mode.
*
* When enabled in the WPForms settings, forcefully remove all other
* CAPTCHA enqueues to prevent conflicts. Filter can be used to target
* specific pages, etc.
*
* @since 1.6.4
*/
public function apply_noconflict() {
if (
! wpforms_is_admin_page( 'settings', self::VIEW ) ||
empty( wpforms_setting( 'recaptcha-noconflict' ) ) ||
/**
* Allow/disallow applying non-conflict mode for captcha scripts.
*
* @since 1.6.4
*
* @param boolean $allow True/false. Default: true.
*/
! apply_filters( 'wpforms_admin_settings_captcha_apply_noconflict', true ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
) {
return;
}
$scripts = wp_scripts();
$urls = [ 'google.com/recaptcha', 'gstatic.com/recaptcha', 'hcaptcha.com/1', 'challenges.cloudflare.com/turnstile' ];
foreach ( $scripts->queue as $handle ) {
// Skip the WPForms JavaScript assets.
if (
! isset( $scripts->registered[ $handle ] ) ||
false !== strpos( $scripts->registered[ $handle ]->handle, 'wpforms' )
) {
return;
}
foreach ( $urls as $url ) {
if ( false !== strpos( $scripts->registered[ $handle ]->src, $url ) ) {
wp_dequeue_script( $handle );
wp_deregister_script( $handle );
break;
}
}
}
}
}
@@ -0,0 +1,100 @@
<?php
namespace WPForms\Admin\Settings\Captcha;
/**
* ReCaptcha settings class.
*
* @since 1.8.0
*/
class ReCaptcha extends Captcha {
/**
* Captcha variable used for JS invoking.
*
* @since 1.8.0
*
* @var string
*/
protected static $api_var = 'grecaptcha';
/**
* Get captcha key name.
*
* @since 1.8.0
*
* @var string
*/
protected static $slug = 'recaptcha';
/**
* The ReCAPTCHA Javascript URL-resource.
*
* @since 1.8.0
*
* @var string
*/
protected static $url = 'https://www.google.com/recaptcha/api.js';
/**
* Array of captcha settings fields.
*
* @since 1.8.0
*
* @return array[]
*/
public function get_settings_fields() {
return [
'recaptcha-heading' => [
'id' => 'recaptcha-heading',
'content' => $this->get_field_desc(),
'type' => 'content',
'no_label' => true,
'class' => [ 'wpforms-setting-recaptcha', 'section-heading', 'specific-note' ],
],
'recaptcha-type' => [
'id' => 'recaptcha-type',
'name' => esc_html__( 'Type', 'wpforms-lite' ),
'type' => 'radio',
'default' => 'v2',
'options' => [
'v2' => esc_html__( 'Checkbox reCAPTCHA v2', 'wpforms-lite' ),
'invisible' => esc_html__( 'Invisible reCAPTCHA v2', 'wpforms-lite' ),
'v3' => esc_html__( 'reCAPTCHA v3', 'wpforms-lite' ),
],
'class' => [ 'wpforms-setting-recaptcha' ],
],
'recaptcha-site-key' => [
'id' => 'recaptcha-site-key',
'name' => esc_html__( 'Site Key', 'wpforms-lite' ),
'type' => 'text',
],
'recaptcha-secret-key' => [
'id' => 'recaptcha-secret-key',
'name' => esc_html__( 'Secret Key', 'wpforms-lite' ),
'type' => 'text',
],
'recaptcha-fail-msg' => [
'id' => 'recaptcha-fail-msg',
'name' => esc_html__( 'Fail Message', 'wpforms-lite' ),
'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ),
'type' => 'text',
'default' => esc_html__( 'Google reCAPTCHA verification failed, please try again later.', 'wpforms-lite' ),
],
'recaptcha-v3-threshold' => [
'id' => 'recaptcha-v3-threshold',
'name' => esc_html__( 'Score Threshold', 'wpforms-lite' ),
'desc' => esc_html__( 'reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). If the score is less than or equal to this threshold, the form submission will be blocked and the message above will be displayed.', 'wpforms-lite' ),
'type' => 'number',
'attr' => [
'step' => '0.1',
'min' => '0.0',
'max' => '1.0',
],
'default' => esc_html__( '0.4', 'wpforms-lite' ),
'class' => $this->settings['provider'] === 'recaptcha' && $this->settings['recaptcha_type'] === 'v3' ? [ 'wpforms-setting-recaptcha' ] : [ 'wpforms-setting-recaptcha', 'wpforms-hidden' ],
],
];
}
}
@@ -0,0 +1,106 @@
<?php
namespace WPForms\Admin\Settings\Captcha;
/**
* Cloudflare Turnstile settings class.
*
* @since 1.8.0
*/
class Turnstile extends Captcha {
/**
* Captcha variable used for JS invoking.
*
* @since 1.8.0
*
* @var string
*/
protected static $api_var = 'turnstile';
/**
* Captcha key name.
*
* @since 1.8.0
*
* @var string
*/
protected static $slug = 'turnstile';
/**
* The Turnstile Javascript URL-resource.
*
* @since 1.8.0
*
* @var string
*/
protected static $url = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
/**
* Inline script for captcha initialization JS code.
*
* @since 1.8.0
*
* @return string
*/
protected function get_inline_script() {
return /** @lang JavaScript */
'const wpformsCaptcha = jQuery( ".wpforms-captcha" );
if ( wpformsCaptcha.length > 0 ) {
var widgetID = ' . static::$api_var . '.render( ".wpforms-captcha", {
"refresh-expired": "auto"
} );
wpformsCaptcha.attr( "data-captcha-id", widgetID);
jQuery( document ).trigger( "wpformsSettingsCaptchaLoaded" );
}';
}
/**
* Array of captcha settings fields.
*
* @since 1.8.0
*
* @return array[]
*/
public function get_settings_fields() {
return [
'turnstile-heading' => [
'id' => 'turnstile-heading',
'content' => $this->get_field_desc(),
'type' => 'content',
'no_label' => true,
'class' => [ 'section-heading', 'specific-note' ],
],
'turnstile-site-key' => [
'id' => 'turnstile-site-key',
'name' => esc_html__( 'Site Key', 'wpforms-lite' ),
'type' => 'text',
],
'turnstile-secret-key' => [
'id' => 'turnstile-secret-key',
'name' => esc_html__( 'Secret Key', 'wpforms-lite' ),
'type' => 'text',
],
'turnstile-fail-msg' => [
'id' => 'turnstile-fail-msg',
'name' => esc_html__( 'Fail Message', 'wpforms-lite' ),
'desc' => esc_html__( 'Displays to users who fail the verification process.', 'wpforms-lite' ),
'type' => 'text',
'default' => esc_html__( 'Cloudflare Turnstile verification failed, please try again later.', 'wpforms-lite' ),
],
'turnstile-theme' => [
'id' => 'turnstile-theme',
'name' => esc_html__( 'Type', 'wpforms-lite' ),
'type' => 'select',
'default' => 'auto',
'options' => [
'auto' => esc_html__( 'Auto', 'wpforms-lite' ),
'light' => esc_html__( 'Light', 'wpforms-lite' ),
'dark' => esc_html__( 'Dark', 'wpforms-lite' ),
],
],
];
}
}
@@ -0,0 +1,669 @@
<?php
namespace WPForms\Admin\Settings;
use WPForms\Emails\Helpers;
use WPForms\Emails\Notifications;
use WPForms\Admin\Education\Helpers as EducationHelpers;
/**
* Email setting page.
* Settings will be accessible via “WPForms” → “Settings” → “Email”.
*
* @since 1.8.5
*/
class Email {
/**
* Content is plain text type.
*
* @since 1.8.5
*
* @var bool
*/
private $plain_text;
/**
* Temporary storage for the style overrides preferences.
*
* @since 1.8.6
*
* @var array
*/
private $style_overrides;
/**
* Determines if the user has the education modal.
* If true, the value will be used to add the Education modal class to the setting controls.
* This is only available in the free version.
*
* @since 1.8.6
*
* @var string
*/
private $has_education;
/**
* Determines if the user has the legacy template.
* If true, the value will be used to add the Legacy template class to the setting controls.
*
* @since 1.8.6
*
* @var string
*/
private $has_legacy_template;
/**
* Initialize class.
*
* @since 1.8.5
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.5
*/
private function hooks() {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_filter( 'wpforms_update_settings', [ $this, 'maybe_update_settings' ] );
add_filter( 'wpforms_settings_tabs', [ $this, 'register_settings_tabs' ], 5 );
add_filter( 'wpforms_settings_defaults', [ $this, 'register_settings_fields' ], 5 );
}
/**
* Enqueue scripts and styles.
* Static resources are enqueued only on the "Email" settings page.
*
* @since 1.8.5
*/
public function enqueue_assets() {
// Leave if the current page is not the "Email" settings page.
if ( ! $this->is_settings_page() ) {
return;
}
$min = wpforms_get_min_suffix();
wp_enqueue_script(
'wpforms-contrast-checker',
WPFORMS_PLUGIN_URL . "assets/js/admin/share/contrast-checker{$min}.js",
[],
WPFORMS_VERSION,
true
);
wp_enqueue_script(
'wpforms-xor',
WPFORMS_PLUGIN_URL . "assets/js/admin/share/xor{$min}.js",
[],
WPFORMS_VERSION,
true
);
wp_enqueue_script(
'wpforms-admin-email-settings',
WPFORMS_PLUGIN_URL . "assets/js/admin/email/settings{$min}.js",
[ 'jquery', 'wpforms-admin', 'wp-escape-html', 'wp-url', 'choicesjs', 'wpforms-contrast-checker', 'wpforms-xor' ],
WPFORMS_VERSION,
true
);
wp_localize_script(
'wpforms-admin-email-settings',
'wpforms_admin_email_settings',
[
'contrast_fail' => esc_html__( 'This color combination may be hard to read. Try increasing the contrast between the body and text colors.', 'wpforms-lite' ),
]
);
}
/**
* Maybe update settings.
*
* @since 1.8.5
*
* @param array $settings Admin area settings list.
*
* @return array
*/
public function maybe_update_settings( $settings ) {
// Leave if the current page is not the "Email" settings page.
if ( ! $this->is_settings_page() ) {
return $settings;
}
// Remove the appearance mode switcher from the settings array.
unset( $settings['email-appearance'] );
// Backup the Pro version background color setting to the free version.
// This is needed to keep the background color when the Pro version is deactivated.
if ( wpforms()->is_pro() && ! Helpers::is_legacy_html_template() ) {
$settings['email-background-color'] = sanitize_hex_color( $settings['email-color-scheme']['email_background_color'] );
$settings['email-background-color-dark'] = sanitize_hex_color( $settings['email-color-scheme-dark']['email_background_color_dark'] );
return $settings;
}
// Backup the free version background color setting to the Pro version.
// This is needed to keep the background color when the Pro version is activated.
$settings['email-color-scheme']['email_background_color'] = sanitize_hex_color( $settings['email-background-color'] );
$settings['email-color-scheme-dark']['email_background_color_dark'] = sanitize_hex_color( $settings['email-background-color-dark'] );
return $settings;
}
/**
* Register "Email" settings tab.
*
* @since 1.8.5
*
* @param array $tabs Admin area tabs list.
*
* @return array
*/
public function register_settings_tabs( $tabs ) {
$payments = [
'email' => [
'form' => true,
'name' => esc_html__( 'Email', 'wpforms-lite' ),
'submit' => esc_html__( 'Save Settings', 'wpforms-lite' ),
],
];
return wpforms_array_insert( $tabs, $payments, 'general' );
}
/**
* Register "Email" settings fields.
*
* @since 1.8.5
*
* @param array $settings Admin area settings list.
*
* @return array
*/
public function register_settings_fields( $settings ) {
$this->plain_text = Helpers::is_plain_text_template();
$this->has_education = ! wpforms()->is_pro() ? 'education-modal' : '';
$this->style_overrides = Helpers::get_current_template_style_overrides();
$this->has_legacy_template = Helpers::is_legacy_html_template() ? 'legacy-template' : '';
$preview_link = $this->get_current_template_preview_link();
// Add Email settings.
$settings['email'] = [
'email-heading' => [
'id' => 'email-heading',
'content' => $this->get_heading_content(),
'type' => 'content',
'no_label' => true,
'class' => [ 'section-heading', 'no-desc' ],
],
'email-template' => [
'id' => 'email-template',
'name' => esc_html__( 'Template', 'wpforms-lite' ),
'class' => [ 'wpforms-email-template', 'wpforms-card-image-group' ],
'type' => 'email_template',
'default' => Notifications::DEFAULT_TEMPLATE,
'options' => Helpers::get_email_template_choices(),
'value' => Helpers::get_current_template_name(),
],
// The reason that we're using the 'content' type is to avoid saving the value in the option storage.
// The value is used only for the appearance mode switcher, and merely acts as a switch between dark and light mode controls.
'email-appearance' => [
'id' => 'email-appearance',
'name' => esc_html__( 'Appearance', 'wpforms-lite' ),
'desc' => esc_html__( 'Modern email clients support viewing emails in light and dark modes. You can upload a header image and customize the style for each appearance mode independently to ensure an optimal reading experience.', 'wpforms-lite' ),
'type' => 'radio',
'class' => [ 'wpforms-setting-row-radio', 'hide-for-template-none', 'email-appearance-mode-toggle', $this->has_legacy_template ],
'default' => 'light',
'is_hidden' => $this->plain_text,
'options' => [
'light' => esc_html__( 'Light', 'wpforms-lite' ),
'dark' => esc_html__( 'Dark', 'wpforms-lite' ),
],
],
'email-preview' => [
'id' => 'email-preview',
'type' => 'content',
'is_hidden' => empty( $preview_link ),
'content' => $preview_link,
],
'sending-heading' => [
'id' => 'sending-heading',
'content' => '<h4>' . esc_html__( 'Sending', 'wpforms-lite' ) . '</h4>',
'type' => 'content',
'no_label' => true,
'class' => [ 'section-heading', 'no-desc' ],
],
'email-async' => [
'id' => 'email-async',
'name' => esc_html__( 'Optimize Email Sending', 'wpforms-lite' ),
'desc' => sprintf(
wp_kses( /* translators: %1$s - WPForms.com Email settings documentation URL. */
__( 'Send emails asynchronously, which can make processing faster but may delay email delivery by a minute or two. <a href="%1$s" target="_blank" rel="noopener noreferrer" class="wpforms-learn-more">Learn More</a>', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'target' => [],
'rel' => [],
'class' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/a-complete-guide-to-wpforms-settings/#email', 'Settings - Email', 'Optimize Email Sending Documentation' ) )
),
'type' => 'toggle',
'status' => true,
],
'email-carbon-copy' => [
'id' => 'email-carbon-copy',
'name' => esc_html__( 'Carbon Copy', 'wpforms-lite' ),
'desc' => esc_html__( 'Enable the ability to CC: email addresses in the form notification settings.', 'wpforms-lite' ),
'type' => 'toggle',
'status' => true,
],
];
// Add the style controls.
$settings['email'] = $this->add_appearance_controls( $settings['email'] );
// Maybe add the Legacy template notice.
$settings['email'] = $this->maybe_add_legacy_notice( $settings['email'] );
return $settings;
}
/**
* Maybe add the legacy template notice.
*
* @since 1.8.5
*
* @param array $settings Email settings.
*
* @return array
*/
private function maybe_add_legacy_notice( $settings ) {
if ( ! $this->is_settings_page() || ! Helpers::is_legacy_html_template() ) {
return $settings;
}
$content = '<div class="notice-info"><p>';
$content .= sprintf(
wp_kses( /* translators: %1$s - WPForms.com Email settings legacy template documentation URL. */
__( 'Some style settings are not available when using the Legacy template. <a href="%1$s" target="_blank" rel="noopener noreferrer">Learn More</a>', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'target' => [],
'rel' => [],
],
]
),
esc_url( wpforms_utm_link( 'https://wpforms.com/docs/customizing-form-notification-emails/#legacy-template', 'Settings - Email', 'Legacy Template' ) )
);
$content .= '</p></div>';
// Add the background color control after the header image.
return wpforms_array_insert(
$settings,
[
'email-legacy-notice' => [
'id' => 'email-legacy-notice',
'content' => $content,
'type' => 'content',
'class' => 'wpforms-email-legacy-notice',
],
],
'email-template'
);
}
/**
* Add appearance controls.
* This will include controls for both Light and Dark modes.
*
* @since 1.8.6
*
* @param array $settings Email settings.
*
* @return array
*/
private function add_appearance_controls( $settings ) {
// Education modal arguments.
$education_args = [ 'action' => 'upgrade' ];
// New settings for the Light mode.
$new_setting = [
'email-header-image' => [
'id' => 'email-header-image',
'name' => esc_html__( 'Header Image', 'wpforms-lite' ),
'desc' => esc_html__( 'Upload or choose a logo to be displayed at the top of email notifications.', 'wpforms-lite' ),
'class' => [ 'wpforms-email-header-image', 'hide-for-template-none', 'has-preview-changes', 'email-light-mode', $this->get_external_header_image_class() ],
'type' => 'image',
'is_hidden' => $this->plain_text,
'show_remove' => true,
],
'email-header-image-size' => [
'id' => 'email-header-image-size',
'no_label' => true,
'type' => 'select',
'class' => [ 'wpforms-email-header-image-size', 'has-preview-changes', 'email-light-mode' ],
'is_hidden' => true,
'choicesjs' => false,
'default' => 'medium',
'options' => [
'small' => esc_html__( 'Small', 'wpforms-lite' ),
'medium' => esc_html__( 'Medium', 'wpforms-lite' ),
'large' => esc_html__( 'Large', 'wpforms-lite' ),
],
],
'email-color-scheme' => [
'id' => 'email-color-scheme',
'name' => esc_html__( 'Color Scheme', 'wpforms-lite' ),
'class' => [ 'email-color-scheme', 'hide-for-template-none', 'has-preview-changes', 'email-light-mode', $this->has_education, $this->has_legacy_template ],
'type' => 'color_scheme',
'is_hidden' => $this->plain_text,
'education_badge' => $this->get_pro_education_badge(),
'data_attributes' => $this->has_education ? array_merge( [ 'name' => esc_html__( 'Color Scheme', 'wpforms-lite' ) ], $education_args ) : [],
'colors' => $this->get_color_scheme_controls(),
],
'email-typography' => [
'id' => 'email-typography',
'name' => esc_html__( 'Typography', 'wpforms-lite' ),
'desc' => esc_html__( 'Choose the style thats applied to all text in email notifications.', 'wpforms-lite' ),
'class' => [ 'hide-for-template-none', 'has-preview-changes', 'email-typography', 'email-light-mode', $this->has_education, $this->has_legacy_template ],
'education_badge' => $this->get_pro_education_badge(),
'data_attributes' => $this->has_education ? array_merge( [ 'name' => esc_html__( 'Typography', 'wpforms-lite' ) ], $education_args ) : [],
'type' => 'select',
'is_hidden' => $this->plain_text,
'choicesjs' => true,
'default' => 'sans-serif',
'options' => [
'sans-serif' => esc_html__( 'Sans Serif', 'wpforms-lite' ),
'serif' => esc_html__( 'Serif', 'wpforms-lite' ),
],
],
];
// Add background color control if the Pro version is not active or Legacy template is selected.
$new_setting = $this->maybe_add_background_color_control( $new_setting );
return wpforms_array_insert(
$settings,
$this->add_appearance_dark_mode_controls( $new_setting ),
'email-appearance'
);
}
/**
* Add appearance dark mode controls.
*
* This function will duplicate the default "Light" color
* controls to create corresponding controls for dark mode.
*
* @since 1.8.6
*
* @param array $settings Email settings.
*
* @return array
*/
private function add_appearance_dark_mode_controls( $settings ) {
// Duplicate and modify each item for dark mode.
foreach ( $settings as $key => $item ) {
// Duplicate the item with '-dark' added to the key.
$dark_key = "{$key}-dark";
$settings[ $dark_key ] = $item;
// Modify the 'name' within the duplicated item.
if ( isset( $settings[ $dark_key ]['id'] ) ) {
$settings[ $dark_key ]['id'] .= '-dark';
$classes = &$settings[ $dark_key ]['class'];
$classes[] = 'email-dark-mode';
$classes[] = 'wpforms-hide';
// Remove classes related to light mode.
$classes = array_filter(
$classes,
static function ( $class_name ) {
return $class_name !== 'email-light-mode' && $class_name !== 'has-external-image-url';
}
);
}
// Override the description for the header image control.
if ( $key === 'email-header-image' ) {
$settings[ $dark_key ]['desc'] = esc_html__( 'Upload or choose a logo to be displayed at the top of email notifications. Light mode image will be used if not set.', 'wpforms-lite' );
}
// Override the background color control attributes.
if ( $key === 'email-background-color' ) {
$settings[ $dark_key ]['default'] = sanitize_hex_color( $this->style_overrides['email_background_color_dark'] );
$settings[ $dark_key ]['data']['fallback-color'] = sanitize_hex_color( $this->style_overrides['email_background_color_dark'] );
}
// Override the color scheme control attributes.
if ( $key === 'email-color-scheme' ) {
$settings[ $dark_key ]['colors'] = $this->get_color_scheme_controls( true );
}
}
return $settings;
}
/**
* Get Email settings heading content.
*
* @since 1.8.5
*
* @return string
*/
private function get_heading_content() {
return wpforms_render( 'admin/settings/email-heading' );
}
/**
* Get Email settings education badge.
* This is only available in the free version.
*
* @since 1.8.6
*
* @return string
*/
private function get_pro_education_badge() {
// Leave early if the user has the Lite version.
if ( empty( $this->has_education ) ) {
return '';
}
// Output the education badge.
return EducationHelpers::get_badge( 'Pro' );
}
/**
* Generate color scheme controls for the color picker.
*
* @since 1.8.6
*
* @param bool $is_dark_mode Whether the color scheme is for dark mode.
*
* @return array
*/
private function get_color_scheme_controls( $is_dark_mode = false ) {
// Append '_dark' to keys if it's for dark mode.
$is_dark_mode_suffix = $is_dark_mode ? '_dark' : '';
// Data attributes to disable extensions from appearing in the input field.
$color_scheme_data = [
'1p-ignore' => 'true', // 1Password ignore.
'lp-ignore' => 'true', // LastPass ignore.
];
$colors = [];
$controls = [
"email_background_color{$is_dark_mode_suffix}" => esc_html__( 'Background', 'wpforms-lite' ),
"email_body_color{$is_dark_mode_suffix}" => esc_html__( 'Body', 'wpforms-lite' ),
"email_text_color{$is_dark_mode_suffix}" => esc_html__( 'Text', 'wpforms-lite' ),
"email_links_color{$is_dark_mode_suffix}" => esc_html__( 'Links', 'wpforms-lite' ),
];
foreach ( $controls as $key => $label ) {
// Construct the color controls array.
$colors[ $key ] = [
'name' => $label,
'data' => array_merge(
[
'fallback-color' => $this->style_overrides[ $key ],
],
$color_scheme_data
),
];
}
return $colors;
}
/**
* Get current email template hyperlink.
*
* @since 1.8.5
*
* @return string
*/
private function get_current_template_preview_link() {
// Leave if the user has the legacy template is set or the user doesn't have the capability.
if ( ! wpforms_current_user_can() || Helpers::is_legacy_html_template() ) {
return '';
}
$template_name = Helpers::get_current_template_name();
$current_template = Notifications::get_available_templates( $template_name );
// Return empty string if the current template is not found.
// Leave early if the preview link is empty.
if ( ! isset( $current_template['path'] ) || ! class_exists( $current_template['path'] ) || empty( $current_template['preview'] ) ) {
return '';
}
return sprintf(
wp_kses( /* translators: %1$s - Email template preview URL. */
__( '<a href="%1$s" class="wpforms-btn-preview" target="_blank" rel="noopener">Preview Email Template</a>', 'wpforms-lite' ),
[
'a' => [
'class' => true,
'href' => true,
'target' => true,
'rel' => true,
],
]
),
esc_url( $current_template['preview'] )
);
}
/**
* Maybe add the background color control to the email settings.
* This is only available in the free version.
*
* @since 1.8.5
*
* @param array $settings Email settings.
*
* @return array
*/
private function maybe_add_background_color_control( $settings ) {
// Leave as is if the Pro version is active and no legacy template available.
if ( ! Helpers::is_legacy_html_template() && wpforms()->is_pro() ) {
return $settings;
}
// Add the background color control after the header image.
return wpforms_array_insert(
$settings,
[
'email-background-color' => [
'id' => 'email-background-color',
'name' => esc_html__( 'Background Color', 'wpforms-lite' ),
'desc' => esc_html__( 'Customize the background color of the email template.', 'wpforms-lite' ),
'class' => [ 'email-background-color', 'has-preview-changes', 'email-light-mode' ],
'type' => 'color',
'is_hidden' => $this->plain_text,
'default' => '#e9eaec',
'data' => [
'fallback-color' => $this->style_overrides['email_background_color'],
'1p-ignore' => 'true', // 1Password ignore.
'lp-ignore' => 'true', // LastPass ignore.
],
],
],
'email-color-scheme',
'before'
);
}
/**
* Gets the class for the header image control.
*
* This is used to determine if the header image is external.
* Legacy header image control was allowing external URLs.
*
* Note that this evaluation is only available for the "Light" mode,
* as the "Dark" mode is a new feature and doesn't have the legacy header image control.
*
* @since 1.8.5
*
* @return string
*/
private function get_external_header_image_class() {
$header_image_url = wpforms_setting( 'email-header-image', '' );
// If the header image URL is empty, return an empty string.
if ( empty( $header_image_url ) ) {
return '';
}
$site_url = home_url(); // Get the current site's URL.
// Get the hosts of the site URL and the header image URL.
$site_url_host = wp_parse_url( $site_url, PHP_URL_HOST );
$header_image_url_host = wp_parse_url( $header_image_url, PHP_URL_HOST );
// Check if the header image URL host is different from the site URL host.
if ( $header_image_url_host && $site_url_host && $header_image_url_host !== $site_url_host ) {
return 'has-external-image-url';
}
return ''; // If none of the conditions match, return an empty string.
}
/**
* Determine if the current page is the "Email" settings page.
*
* @since 1.8.5
*
* @return bool
*/
private function is_settings_page() {
return wpforms_is_admin_page( 'settings', 'email' );
}
}
@@ -0,0 +1,159 @@
<?php
namespace WPForms\Admin\Settings;
use WPForms\Helpers\Transient;
/**
* Modern Markup setting element.
*
* @since 1.8.1
*/
class ModernMarkup {
/**
* Initialize class.
*
* @since 1.8.1
*/
public function init() {
$this->hooks();
}
/**
* Hooks.
*
* @since 1.8.1
*/
public function hooks() {
add_action( 'wpforms_create_form', [ $this, 'clear_transient' ] );
add_action( 'wpforms_save_form', [ $this, 'clear_transient' ] );
add_action( 'wpforms_delete_form', [ $this, 'clear_transient' ] );
add_action( 'wpforms_form_handler_update_status', [ $this, 'clear_transient' ] );
// Only continue if we are actually on the settings page.
if ( ! wpforms_is_admin_page( 'settings' ) ) {
return;
}
add_filter( 'wpforms_settings_defaults', [ $this, 'register_field' ] );
}
/**
* Register setting field.
*
* @since 1.8.1
*
* @param array|mixed $settings Settings data.
*
* @return array
* @noinspection HtmlUnknownTarget
* @noinspection NullPointerExceptionInspection
*/
public function register_field( $settings ): array {
/**
* Allows to show/hide the Modern Markup setting field on the Settings page.
*
* @since 1.8.1
*
* @param mixed $is_hidden Whether the setting must be hidden.
*/
$is_hidden = apply_filters(
'wpforms_admin_settings_modern_markup_register_field_is_hidden',
wpforms_setting( 'modern-markup-hide-setting' )
);
if ( ! empty( $is_hidden ) ) {
return $settings;
}
$settings = (array) $settings;
$modern_markup = [
'id' => 'modern-markup',
'name' => esc_html__( 'Use Modern Markup', 'wpforms-lite' ),
'desc' => sprintf(
wp_kses( /* translators: %s - WPForms.com form markup setting URL. */
__( 'Check this option to use modern markup, which has increased accessibility and allows you to easily customize your forms in the block editor. <a href="%s" target="_blank" rel="noopener noreferrer">Read our form markup documentation</a> to learn more.', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'target' => [],
'rel' => [],
'class' => [],
],
]
),
wpforms_utm_link( 'https://wpforms.com/docs/styling-your-forms/', 'settings-license', 'Form Markup Documentation' )
),
'type' => 'toggle',
'status' => true,
];
$is_disabled_transient = Transient::get( 'modern_markup_setting_disabled' );
// Transient doesn't set or expired.
if ( $is_disabled_transient === false ) {
$forms = wpforms()->obj( 'form' )->get( '', [ 'post_status' => 'publish' ] );
$is_disabled_transient = ( ! empty( $forms ) && wpforms_has_field_type( 'credit-card', $forms, true ) ) ? '1' : '0';
// Re-check all the forms for the CC field once per day.
Transient::set( 'modern_markup_setting_disabled', $is_disabled_transient, DAY_IN_SECONDS );
}
/**
* Allows enabling/disabling the Modern Markup setting field on the Settings page.
*
* @since 1.8.1
*
* @param mixed $is_disabled Whether the Modern Markup setting must be disabled.
*/
$is_disabled = (bool) apply_filters(
'wpforms_admin_settings_modern_markup_register_field_is_disabled',
! empty( $is_disabled_transient )
);
$current_value = wpforms_setting( 'modern-markup' );
// In the case, when it is disabled because of the legacy CC field, add the corresponding description.
if ( $is_disabled && ! empty( $is_disabled_transient ) && empty( $current_value ) ) {
$modern_markup['disabled'] = true;
$modern_markup['disabled_desc'] = sprintf(
wp_kses( /* translators: %s - WPForms Stripe addon URL. */
__( '<strong>You cannot use modern markup because youre using the deprecated Credit Card field.</strong> If youd like to use modern markup, replace your credit card field with a payment gateway like <a href="%s" target="_blank" rel="noopener noreferrer">Stripe</a>.', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'target' => [],
'rel' => [],
],
'strong' => [],
]
),
'https://wpforms.com/docs/how-to-install-and-use-the-stripe-addon-with-wpforms'
);
}
$modern_markup = [
'modern-markup' => $modern_markup,
];
$settings['general'] = wpforms_list_insert_after( $settings['general'], 'disable-css', $modern_markup );
return $settings;
}
/**
* Clear transient in the case when the form is created/saved/deleted.
* So, next time when the user opens the Settings page,
* the Modern Markup setting will check for the legacy Credit Card field in all the forms again.
*
* @since 1.8.1
*/
public function clear_transient() {
Transient::delete( 'modern_markup_setting_disabled' );
}
}

Some files were not shown because too many files have changed in this diff Show More