namespace WPForms\Admin\Notifications;
* WPForms version when the Event Driven feature has been introduced.
const FEATURE_INTRODUCED = '1.7.5';
* Expected date format for notifications.
const DATE_FORMAT = 'Y-m-d H:i:s';
'utm_source' => 'WordPress',
'utm_medium' => 'Event Notification',
* Common targets for date logic.
* - upgraded (upgraded to a latest version)
* - X.X.X.X (upgraded to a specific version)
* - pro (activated/installed)
* - lite (activated/installed)
const DATE_LOGIC = [ 'upgraded', 'activated', 'forms_first_created' ];
private $timestamps = [];
if ( ! $this->allow_load() ) {
* Indicate if this is allowed to load.
private function allow_load() {
return wpforms()->obj( 'notifications' )->has_access() || wp_doing_cron();
private function hooks() {
add_filter( 'wpforms_admin_notifications_update_data', [ $this, 'update_events' ] );
* Add Event Driven notifications before saving them in database.
* @param array $data Notification data.
public function update_events( $data ) {
* Allow developers to turn on debug mode: store all notifications and then show all of them.
* @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.
( $is_processed && $is_conditional_ok && $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;
// Ignore if a condition is not passed conditional checks.
if ( ! $is_conditional_ok ) {
$timestamp = $this->get_timestamp_by_date_logic(
$this->prepare_date_logic( $notification )
if ( empty( $timestamp ) ) {
// Probably, notification should be visible after some time.
$offset = empty( $notification['offset'] ) ? 0 : absint( $notification['offset'] );
// Set a start date when 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 ) ) {
// Remove unnecessary values, mark notification as active, and save it.
unset( $notification['date_logic'], $notification['offset'], $notification['condition'] );
$updated[ $slug ] = $notification;
$data['events'] = $updated;
* Prepare and retrieve date logic.
* @param array $notification Notification data.
private function prepare_date_logic( $notification ) {
$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.
* @param array $args Date logic.
private function get_timestamp_by_date_logic( $args ) {
foreach ( $args as $target ) {
if ( ! empty( $this->timestamps[ $target ] ) ) {
return $this->timestamps[ $target ];
$timestamp = call_user_func(
$this->get_timestamp_callback( $target ),
if ( ! empty( $timestamp ) ) {
$this->timestamps[ $target ] = $timestamp;
* Retrieve a callback that determines needed timestamp.
* @param string $target Date logic target.
private function get_timestamp_callback( $target ) {
// As $target should be a part of name for callback method,
// this regular expression allow lowercase characters, numbers, and underscore.
$target = strtolower( preg_replace( '/[^a-z0-9_]/', '', $target ) );
$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 callback is callable, return it. Otherwise, return fallback.
return is_callable( $callback ) ? $callback : '__return_zero';
* Retrieve a timestamp when WPForms was upgraded.
* @param string $version WPForms version.
* @return int|false Unix timestamp. False on failure.
private function get_timestamp_upgraded( $version ) {
if ( $version === 'upgraded' ) {
$version = WPFORMS_VERSION;
$timestamp = wpforms_get_upgraded_timestamp( $version );
if ( $timestamp === 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.
* @return int|false Unix timestamp. False on failure.
private function get_timestamp_activated() {
return wpforms_get_activated_timestamp();
* Retrieve a timestamp when Lite was first installed.
* @return int|false Unix timestamp. False on failure.
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.
* @return int|false Unix timestamp. False on failure.
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.
* @return int|false Unix timestamp. False on failure.
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.
private function get_entry_count() {
if ( is_int( $count ) ) {
$entry_handler = wpforms()->obj( 'entry' );
$entry_meta_handler = wpforms()->obj( 'entry_meta' );
if ( ! $entry_handler || ! $entry_meta_handler ) {
$query = "SELECT COUNT( $entry_handler->primary_key )
FROM $entry_handler->table_name
WHERE $entry_handler->primary_key
FROM $entry_meta_handler->table_name
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
$count = (int) $wpdb->get_var( $query );
* @param int $posts_per_page Number of form to return.
private function get_forms( $posts_per_page ) {
$forms = wpforms()->obj( 'form' )->get(
'posts_per_page' => (int) $posts_per_page,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
return ! empty( $forms ) ? (array) $forms : [];
* Determine if the user has at least 1 form.
private function has_form() {
return ! empty( $this->get_forms( 1 ) );
* Determine if it is a new user.
private function is_new_user() {
// Check if this is an update or first install.
return ! get_option( 'wpforms_version_upgraded_from' );
* Determine if it's an English site.
private function is_english_site() {
if ( is_bool( $result ) ) {
[ $this, 'language_to_iso' ],
[ get_locale(), get_user_locale() ]
$result = count( $locales ) === 1 && $locales[0] === 'en';
* Convert language to ISO.
* @param string $lang Language value.
private function language_to_iso( $lang ) {
return $lang === '' ? $lang : explode( '_', $lang )[0];
* Retrieve a modified URL query string.
* @param array $args An associative array of query variables.
* @param string $url A URL to act upon.
private function add_query_arg( $args, $url ) {
array_merge( $this->get_utm_params(), array_map( 'rawurlencode', $args ) ),
* Retrieve UTM parameters for Event Driven notifications links.
private function get_utm_params() {