first commit

This commit is contained in:
Ryan Ariana
2024-05-06 11:04:37 +07:00
commit aee061ddba
7322 changed files with 2918816 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Entities;
use Elementor\Core\Base\Base_Object;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* The Form_Snapshot is a snapshot of the form as it saved in the document data, on each submission creation it updates the snapshot to the current state of the form,
* As a consequence the queries are quicker (filters, export, etc.) and in case the form itself removed from the document, the Form_Snapshot
* remains and allows the user export and filter submissions as before.
*/
class Form_Snapshot extends Base_Object implements \JsonSerializable {
/**
* @var string
*/
public $id;
/**
* @var int
*/
public $post_id;
/**
* @var string
*/
public $name;
/**
* @var array {
* @type string $id
* @type string $type
* @type string $label
* }
*/
public $fields = [];
/**
* @param $post_id
* @param $form_id
*
* @return string
*/
public static function generate_key( $post_id, $form_id ) {
return "{$post_id}_{$form_id}";
}
/**
* @return string
*/
public function get_key() {
return static::generate_key( $this->post_id, $this->id );
}
/**
* @return string
*/
public function get_label() {
return "{$this->name} ($this->id)";
}
/**
* Implement for the JsonSerializable method, will trigger when trying to json_encode this object.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return [
'id' => $this->id,
'name' => $this->name,
'fields' => $this->fields,
];
}
/**
* Form constructor.
*
* @param $post_id
* @param $data
*/
public function __construct( $post_id, $data ) {
$this->post_id = (int) $post_id;
$this->id = $data['id'];
$this->name = $data['name'];
$this->fields = $data['fields'];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Utils\Collection;
class Migration extends Base_Object {
const OPTION_DB_VERSION = 'elementor_submissions_db_version';
// This version must be updated when new migration created.
const CURRENT_DB_VERSION = 5;
private static $migrations = [
1 => Migrations\Initial::class,
// It jumps from version 1 to 4 because some users already migrated the DB when the migrations system worked with the Elementor Pro version
// when the int value of the version "3.2.0" was 3.
4 => Migrations\Referer_Extra::class,
5 => Migrations\Fix_Indexes::class,
];
/**
* Checks if there is a need to run migrations.
*/
public static function install() {
$installed_version = intval( get_option( self::OPTION_DB_VERSION ) );
// Up to date. Nothing to do.
if ( static::CURRENT_DB_VERSION <= $installed_version ) {
return;
}
global $wpdb;
( new Collection( static::$migrations ) )
->filter( function ( $_, $version ) use ( $installed_version ) {
// Filter all the migrations that already done.
return $version > $installed_version;
} )
->map( function ( $migration_class_name, $version ) use ( $wpdb ) {
/** @var Migrations\Base_Migration $migration */
$migration = new $migration_class_name( $wpdb );
$migration->run();
// In case some migration failed it updates version every migration.
update_option( static::OPTION_DB_VERSION, $version );
} );
update_option( static::OPTION_DB_VERSION, self::CURRENT_DB_VERSION );
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Migrations;
use Elementor\Core\Base\Base_Object;
use ElementorPro\Modules\Forms\Submissions\Database\Query;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Migration extends Base_Object {
/*
* Ref: wp-admin/includes/schema.php::wp_get_db_schema
*
* Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
* As of 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which
* used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
*/
const MAX_INDEX_LENGTH = 191;
/**
* @var \wpdb
*/
protected $wpdb;
/**
* @var Query
*/
protected $query;
/**
* Base_Migration constructor.
*
* @param \wpdb $wpdb
*/
public function __construct( \wpdb $wpdb ) {
$this->wpdb = $wpdb;
$this->query = Query::get_instance();
}
/**
* Run migration.
*
* @return void
*/
abstract public function run();
}

View File

@@ -0,0 +1,101 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Migrations;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Fix_Indexes extends Base_Migration {
/**
* In the previous migrations some databases had problems with the indexes.
* this migration checks if user's tables are filled with required indexes, if not it creates them.
*/
public function run() {
$indexes = $this->get_indexes();
foreach ( $indexes as $table => $table_indexes ) {
$existing_indexes = $this->get_existed_indexes_of( $table );
// Protect from database errors (for example: table do not exists somehow).
if ( null === $existing_indexes ) {
continue;
}
$indexes_query = $table_indexes->except( $existing_indexes )->implode( ',' );
$this->wpdb->query( "ALTER TABLE `{$table}` {$indexes_query};" ); // phpcs:ignore
}
}
/**
* Get the user exited indexes
*
* @param $table_name
*
* @return array|null
*/
private function get_existed_indexes_of( $table_name ) {
$result = $this->wpdb->get_results( "SHOW INDEX FROM {$table_name};", ARRAY_A ); // phpcs:ignore
if ( false === $result ) {
return null;
}
return ( new Collection( $result ) )
->map( function ( $row ) {
if ( ! isset( $row['Key_name'] ) ) {
return null;
}
return $row['Key_name'];
} )
->filter()
->values();
}
/**
* Get all the database indexes.
*
* @return Collection[]
*/
private function get_indexes() {
$max_index_length = static::MAX_INDEX_LENGTH;
return [
$this->query->get_table_submissions() => new Collection( [
'main_meta_id_index' => 'ADD INDEX `main_meta_id_index` (`main_meta_id`)',
'hash_id_unique_index' => "ADD UNIQUE INDEX `hash_id_unique_index` (`hash_id` ({$max_index_length}))",
'hash_id_index' => "ADD INDEX `hash_id_index` (`hash_id` ({$max_index_length}))",
'type_index' => "ADD INDEX `type_index` (`type` ({$max_index_length}))",
'post_id_index' => 'ADD INDEX `post_id_index` (`post_id`)',
'element_id_index' => "ADD INDEX `element_id_index` (`element_id` ({$max_index_length}))",
'campaign_id_index' => 'ADD INDEX `campaign_id_index` (`campaign_id`)',
'user_id_index' => 'ADD INDEX `user_id_index` (`user_id`)',
'user_ip_index' => 'ADD INDEX `user_ip_index` (`user_ip`)',
'status_index' => 'ADD INDEX `status_index` (`status`)',
'is_read_index' => 'ADD INDEX `is_read_index` (`is_read`)',
'created_at_gmt_index' => 'ADD INDEX `created_at_gmt_index` (`created_at_gmt`)',
'updated_at_gmt_index' => 'ADD INDEX `updated_at_gmt_index` (`updated_at_gmt`)',
'created_at_index' => 'ADD INDEX `created_at_index` (`created_at`)',
'updated_at_index' => 'ADD INDEX `updated_at_index` (`updated_at`)',
'referer_index' => "ADD INDEX `referer_index` (`referer` ({$max_index_length}))",
'referer_title_index' => "ADD INDEX `referer_title_index` (`referer_title` ({$max_index_length}))",
] ),
$this->query->get_table_submissions_values() => new Collection( [
'submission_id_index' => 'ADD INDEX `submission_id_index` (`submission_id`)',
'key_index' => "ADD INDEX `key_index` (`key` ({$max_index_length}))",
] ),
$this->query->get_table_form_actions_log() => new Collection( [
'submission_id_index' => 'ADD INDEX `submission_id_index` (`submission_id`)',
'action_name_index' => "ADD INDEX `action_name_index` (`action_name` ({$max_index_length}))",
'status_index' => 'ADD INDEX `status_index` (`status`)',
'created_at_gmt_index' => 'ADD INDEX `created_at_gmt_index` (`created_at_gmt`)',
'updated_at_gmt_index' => 'ADD INDEX `updated_at_gmt_index` (`updated_at_gmt`)',
'created_at_index' => 'ADD INDEX `created_at_index` (`created_at`)',
'updated_at_index' => 'ADD INDEX `updated_at_index` (`updated_at`)',
] ),
];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Migrations;
class Initial extends Base_Migration {
public function run() {
$this->create_tables();
$this->add_indexes();
}
private function create_tables() {
$charset_collate = $this->wpdb->get_charset_collate();
$e_submission_table = "CREATE TABLE `{$this->query->get_table_submissions()}` (
id bigint(20) unsigned auto_increment primary key,
type varchar(60) null,
hash_id varchar(60) not null,
main_meta_id bigint(20) unsigned not null comment 'Id of main field. to represent the main meta field',
post_id bigint(20) unsigned not null,
referer varchar(500) not null,
element_id varchar(20) not null,
form_name varchar(60) not null,
campaign_id bigint(20) unsigned not null,
user_id bigint(20) unsigned null,
user_ip varchar(46) not null,
user_agent text not null,
actions_count INT DEFAULT 0,
actions_succeeded_count INT DEFAULT 0,
status varchar(20) not null,
is_read tinyint(1) default 0 not null,
meta text null,
created_at_gmt datetime not null,
updated_at_gmt datetime not null,
created_at datetime not null,
updated_at datetime not null
) {$charset_collate};";
$e_submission_values_table = "CREATE TABLE `{$this->query->get_table_submissions_values()}` (
id bigint(20) unsigned auto_increment primary key,
submission_id bigint(20) unsigned not null default 0,
`key` varchar(60) null,
value longtext null
) {$charset_collate};";
$e_submission_actions_log_table = "CREATE TABLE `{$this->query->get_table_form_actions_log()}` (
id bigint(20) unsigned auto_increment primary key,
submission_id bigint(20) unsigned not null,
action_name varchar(60) not null,
action_label varchar(60) null,
status varchar(20) not null,
log text null,
created_at_gmt datetime not null,
updated_at_gmt datetime not null,
created_at datetime not null,
updated_at datetime not null
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $e_submission_table . $e_submission_values_table . $e_submission_actions_log_table );
}
private function add_indexes() {
// phpcs:disable
$this->wpdb->query( "ALTER TABLE `{$this->query->get_table_submissions()}`
ADD INDEX `main_meta_id_index` (`main_meta_id`),
ADD UNIQUE INDEX `hash_id_unique_index` (`hash_id`),
ADD INDEX `hash_id_index` (`hash_id`),
ADD INDEX `type_index` (`type`),
ADD INDEX `post_id_index` (`post_id`),
ADD INDEX `element_id_index` (`element_id`),
ADD INDEX `campaign_id_index` (`campaign_id`),
ADD INDEX `user_id_index` (`user_id`),
ADD INDEX `user_ip_index` (`user_ip`),
ADD INDEX `status_index` (`status`),
ADD INDEX `is_read_index` (`is_read`),
ADD INDEX `created_at_gmt_index` (`created_at_gmt`),
ADD INDEX `updated_at_gmt_index` (`updated_at_gmt`),
ADD INDEX `created_at_index` (`created_at`),
ADD INDEX `updated_at_index` (`updated_at`)
" );
$this->wpdb->query( "ALTER TABLE `{$this->query->get_table_submissions_values()}`
ADD INDEX `submission_id_index` (`submission_id`),
ADD INDEX `key_index` (`key`)
" );
$this->wpdb->query( "ALTER TABLE `{$this->query->get_table_form_actions_log()}`
ADD INDEX `submission_id_index` (`submission_id`),
ADD INDEX `action_name_index` (`action_name`),
ADD INDEX `status_index` (`status`),
ADD INDEX `created_at_gmt_index` (`created_at_gmt`),
ADD INDEX `updated_at_gmt_index` (`updated_at_gmt`),
ADD INDEX `created_at_index` (`created_at`),
ADD INDEX `updated_at_index` (`updated_at`)
" );
// phpcs:enable
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Migrations;
class Referer_Extra extends Base_Migration {
public function run() {
$max_index_length = static::MAX_INDEX_LENGTH;
// phpcs:disable
$this->wpdb->query("
ALTER TABLE `{$this->query->get_table_submissions()}`
ADD COLUMN `referer_title` varchar(300) null AFTER `referer`;
");
$this->wpdb->query("
ALTER TABLE `{$this->query->get_table_submissions()}`
ADD INDEX `referer_index` (`referer`({$max_index_length})),
ADD INDEX `referer_title_index` (`referer_title`({$max_index_length}));
");
// phpcs:enable
}
}

View File

@@ -0,0 +1,904 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database;
use Elementor\Core\Base\Base_Object;
use ElementorPro\Plugin;
use Elementor\Core\Utils\Collection;
use Exception;
use ElementorPro\Modules\Forms\Classes\Action_Base;
use ElementorPro\Modules\Forms\Submissions\Database\Repositories\Form_Snapshot_Repository;
class Query extends Base_Object {
const STATUS_NEW = 'new';
const STATUS_TRASH = 'trash';
const ACTIONS_LOG_STATUS_SUCCESS = 'success';
const ACTIONS_LOG_STATUS_FAILED = 'failed';
const TYPE_SUBMISSION = 'submission';
/**
* @var Query
*/
private static $instance = null;
const E_SUBMISSIONS_ACTIONS_LOG = 'e_submissions_actions_log';
const E_SUBMISSIONS_VALUES = 'e_submissions_values';
const E_SUBMISSIONS = 'e_submissions';
/**
* @var \wpdb
*/
private $wpdb;
/**
* @var string
*/
private $table_submissions;
/**
* @var string
*/
private $table_submissions_values;
/**
* @var string
*/
private $table_submissions_actions_log;
public static function get_instance() {
if ( ! self::$instance ) {
self::$instance = new Query();
}
return self::$instance;
}
public function get_submissions( $args = [] ) {
$args = wp_parse_args( $args, [
'page' => 1,
'per_page' => 10,
'filters' => [],
'order' => [],
'with_meta' => false,
'with_form_fields' => false,
] );
$page = $args['page'];
$per_page = $args['per_page'];
$filters = $args['filters'];
$order = $args['order'];
$with_meta = $args['with_meta'];
$with_form_fields = $args['with_form_fields'];
$result = [
'data' => [],
'meta' => [],
];
$where_sql = $this->apply_filter( $filters );
$order_sql = '';
$total = (int) $this->wpdb->get_var("SELECT COUNT(*) FROM `{$this->get_table_submissions()}` t_submissions {$where_sql}" );// phpcs:ignore
$last_page = 0 < $total && 0 < $per_page ? (int) ceil( $total / $per_page ) : 1;
if ( $page > $last_page ) {
$page = $last_page;
}
$offset = (int) ceil( ( $page - 1 ) * $per_page );
$result['meta']['pagination'] = [
'current_page' => $page,
'per_page' => $per_page,
'total' => $total,
'last_page' => $last_page,
];
$this->handle_order( $order, $order_sql );
$per_page = (int) $per_page;
$q = "SELECT t_submissions.* FROM `{$this->get_table_submissions()}` t_submissions {$where_sql} {$order_sql} LIMIT {$per_page} OFFSET {$offset}";
$submissions = $this->wpdb->get_results( $q );// phpcs:ignore
$data = new Collection( [] );
foreach ( $submissions as $current_submission ) {
$data[] = $this->get_submission_body( $current_submission, $with_form_fields );
}
$submissions_meta = $this
->get_submissions_meta( $data, ! $with_meta )
->group_by( 'submission_id' );
$result['data'] = $data
->map( function ( $submission ) use ( $submissions_meta ) {
$current_submission_meta = $submissions_meta->get( $submission['id'], [] );
foreach ( $current_submission_meta as $meta ) {
$extract_meta = $this->extract( $meta, [ 'id', 'key', 'value' ], [ 'id:int' ] );
if ( $meta->id === $submission['main_meta_id'] ) {
$submission['main'] = $extract_meta;
}
$submission['values'][] = $extract_meta;
}
return $submission;
} )
->all();
return $result;
}
/**
* Get count by status.
*
* @param $filter
*
* @return Collection
*/
public function count_submissions_by_status( $filter = [] ) {
// Should not filter by status.
unset( $filter['status'] );
$where_sql = $this->apply_filter( $filter );
$trash_status = '"' . static::STATUS_TRASH . '"';
$sql = "
SELECT
SUM(IF(`status` != {$trash_status}, 1, 0)) AS `all`,
SUM(IF(`status` = {$trash_status}, 1, 0)) AS `trash`,
SUM(IF(is_read = 1 AND `status` != {$trash_status}, 1, 0)) AS `read`,
SUM(IF(is_read = 0 AND `status` != {$trash_status}, 1, 0)) AS `unread`
FROM {$this->get_table_submissions()} AS `t_submissions` {$where_sql};
";
$sql_result = $this->wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore
$result = new Collection( [
'all' => 0,
'trash' => 0,
'read' => 0,
'unread' => 0,
] );
if ( ! $sql_result ) {
return $result;
}
return $result->map( function ( $count, $key ) use ( $sql_result ) {
if ( ! isset( $sql_result[ $key ] ) ) {
return $count;
}
return intval( $sql_result[ $key ] );
} );
}
public function get_submissions_by_email( $email, $include_submission_values = false ) {
$user = get_user_by( 'email', $email );
$user_filter = '';
if ( $user ) {
$user_filter = $this->wpdb->prepare( 't_submissions.user_id = %s OR', $user->ID );
}
$query = "
SELECT DISTINCT(t_submissions.id), t_submissions.*
FROM `{$this->get_table_submissions()}` t_submissions
INNER JOIN {$this->get_table_submissions_values()} t_submissions_meta
ON t_submissions.id = t_submissions_meta.submission_id
WHERE
{$user_filter}
t_submissions_meta.value = %s
;
";
$data = $this->wpdb->get_results(
$this->wpdb->prepare( $query, $email ) // phpcs:ignore
);
if ( ! $data ) {
return new Collection( [] );
}
$data = new Collection( $data );
if ( $include_submission_values ) {
$submissions_meta = $this->get_submissions_meta( $data )
->group_by( 'submission_id' );
$data->map( function ( $submission ) use ( $submissions_meta ) {
$submission->values = $submissions_meta->get( $submission->id, [] );
return $submission;
} );
}
return $data;
}
/**
* @param int $delete_timestamp
*
* @return array
*/
public function get_trashed_submission_ids_to_delete( $delete_timestamp ) {
$date = gmdate( 'Y-m-d H:i:s', $delete_timestamp );
$sql = "
SELECT s.id FROM `{$this->get_table_submissions()}` s
WHERE s.status = %s AND s.updated_at_gmt < %s;
";
return $this->wpdb->get_col(
$this->wpdb->prepare( $sql, static::STATUS_TRASH, $date ) // phpcs:ignore
);
}
public function get_submission( $id ) {
$result = [
'data' => [],
'meta' => [],
];
$q = "SELECT * FROM `{$this->get_table_submissions()}` WHERE id=%d";
$submission = $this->wpdb->get_row( $this->wpdb->prepare( $q, [ $id ] ) );// phpcs:ignore
if ( ! $submission ) {
return null;
}
$result['data'] = $this->get_submission_body( $submission, true );
$current_submission_meta = $this->get_submissions_meta( $submission, false )->all();
foreach ( $current_submission_meta as $meta ) {
$extract_meta = $this->extract( $meta, [ 'id', 'key', 'value' ], [ 'id:int' ] );
if ( $meta->id === $result['data']['main_meta_id'] ) {
$result['data']['main'] = $extract_meta;
}
$result['data']['values'][] = $extract_meta;
}
$result['data']['form_actions_log'] = ( new Collection( $this->get_submissions_actions_log( $id ) ) )
->map( function ( $value ) {
return $this->extract(
$value,
[ 'id', 'action_name', 'action_label', 'status', 'log', 'created_at', 'updated_at' ],
[ 'id:int', 'name', 'label' ]
);
} )
->all();
return $result;
}
public function get_referrers( $search = '', $value = '' ) {
$where = '';
if ( $search ) {
$search = '%' . $this->wpdb->esc_like( $search ) . '%';
$where = $this->wpdb->prepare( ' AND s.referer_title LIKE %s', $search );
}
if ( $value ) {
$where = $this->wpdb->prepare( ' AND s.referer = %s', $value ); // phpcs:ignore
}
$where = 'WHERE 1=1' . $where;
$query = "
SELECT DISTINCT (s.referer), s.referer_title
FROM {$this->get_table_submissions()} s
{$where};
";
$result = $this->wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore
if ( ! $result ) {
return new Collection( [] );
}
return new Collection( $result );
}
/**
* @param $submissions
* @param false $only_main
*
* @return Collection
*/
public function get_submissions_meta( $submissions, $only_main = false ) {
if ( ! ( $submissions instanceof Collection ) ) {
$submissions = new Collection(
is_array( $submissions ) ? $submissions : [ $submissions ]
);
}
if ( $submissions->is_empty() ) {
return new Collection( [] );
}
if ( $only_main ) {
$column = 'id';
$ids = $submissions->pluck( 'main_meta_id' );
} else {
$column = 'submission_id';
$ids = $submissions->pluck( 'id' );
}
$placeholders = $ids->map( function () {
return '%d';
} )->implode( ',' );
$q = "SELECT * FROM `{$this->get_table_submissions_values()}` WHERE `{$column}` IN ({$placeholders})";
$result = $this->wpdb->get_results( $this->wpdb->prepare( $q, $ids->all() ) ); // phpcs:ignore
if ( ! $result ) {
return new Collection( [] );
}
return new Collection( $result );
}
/**
* @param $post_id
* @param $element_id
*
* @return Collection
*/
public function get_submissions_value_keys( $post_id, $element_id ) {
$sql = "
SELECT DISTINCT(wesv.`key`)
FROM {$this->get_table_submissions_values()} wesv
INNER JOIN {$this->get_table_submissions()} wes
ON wes.id = wesv.submission_id
WHERE wes.post_id = %s AND wes.element_id = %s;
";
$result = $this->wpdb->get_col( $this->wpdb->prepare( $sql, $post_id, $element_id ) ); // phpcs:ignore
$form = Form_Snapshot_Repository::instance()->find( $post_id, $element_id );
if ( $form ) {
$ordered_keys = array_map( function( $field ) {
return $field['id'];
}, $form->fields );
$result = array_merge( array_intersect( $ordered_keys, $result ), array_diff( $result, $ordered_keys ) );
}
if ( ! $result ) {
return new Collection( [] );
}
return new Collection( $result );
}
/**
* @param $submission_id
*
* @return array|null
*/
public function get_submissions_actions_log( $submission_id ) {
$query = "SELECT * FROM `{$this->get_table_form_actions_log()}` WHERE `submission_id` = %d;";
return $this->wpdb->get_results(
$this->wpdb->prepare( $query, (int) $submission_id ), // phpcs:ignore
ARRAY_A
);
}
/**
* Add submission.
*
* @param array $submission_data
* @param array $fields_data
*
* @return int id or 0
*/
public function add_submission( array $submission_data, array $fields_data ) {
global $wpdb;
$result = 0;
$submission_data = $this->get_new_submission_initial_data( $submission_data );
try {
$wpdb->query( 'START TRANSACTION' );
$submission_success = $wpdb->insert( $this->get_table_submissions(), $submission_data );
if ( ! $submission_success ) {
throw new Exception( $wpdb->last_error );
}
$submission_id = $wpdb->insert_id;
// Meta's keys/values.
$main_meta_id = 0;
$first_submissions_meta_id = 0;
foreach ( $fields_data as $field ) {
$wpdb->insert( $this->get_table_submissions_values(), [
'submission_id' => $submission_id,
'key' => $field['id'],
'value' => $field['value'],
] );
if ( ! $first_submissions_meta_id ) {
$first_submissions_meta_id = $wpdb->insert_id;
}
if ( 0 === $main_meta_id && 'email' === $field['type'] ) {
$main_meta_id = $wpdb->insert_id;
}
}
// If `$main_meta_id` not determined.
if ( ! $main_meta_id ) {
$main_meta_id = $first_submissions_meta_id;
}
// Update main_meta_id.
$wpdb->update( $this->get_table_submissions(),
[
'main_meta_id' => $main_meta_id,
],
[
'id' => $submission_id,
]
);
$wpdb->query( 'COMMIT' );
$result = $submission_id;
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
Plugin::elementor()->logger->get_logger()->error( 'Save submission failed, db error: ' . $e->getMessage() );
}
return $result;
}
/**
* @param $submission_id
* @param array $data
* @param array $values
*
* @return bool|int affected rows
*/
public function update_submission( $submission_id, array $data, $values = [] ) {
if ( $values ) {
foreach ( $values as $key => $value ) {
$this->wpdb->update(
$this->get_table_submissions_values(),
[ 'value' => $value ],
[
'submission_id' => $submission_id,
'key' => $key,
]
);
}
}
$data['updated_at_gmt'] = current_time( 'mysql', true );
$data['updated_at'] = get_date_from_gmt( $data['updated_at_gmt'] );
return $this->wpdb->update(
$this->get_table_submissions(),
$data,
[ 'id' => $submission_id ]
);
}
/**
* Move single submission to trash
*
* @param $id
*
* @return bool|int number of affected rows or false if failed
*/
public function move_to_trash_submission( $id ) {
return $this->update_submission(
$id,
[ 'status' => static::STATUS_TRASH ]
);
}
/**
* Delete a single submission.
*
* @param $id
*
* @return bool|int number of affected rows or false if failed
*/
public function delete_submission( $id ) {
$this->wpdb->delete(
$this->get_table_submissions_values(),
[ 'submission_id' => $id ]
);
$this->wpdb->delete(
$this->get_table_form_actions_log(),
[ 'submission_id' => $id ]
);
return $this->wpdb->delete(
$this->get_table_submissions(),
[ 'id' => $id ]
);
}
/**
* Restore a single submission.
*
* @param $id
*
* @return bool|int number of affected rows or false if failed
*/
public function restore( $id ) {
return $this->wpdb->update(
$this->get_table_submissions(),
[ 'status' => self::STATUS_NEW ],
[ 'id' => $id ]
);
}
/**
* @param $submission_id
* @param Action_Base $action Should be class based on ActionBase (do not type hint to support third party plugins)
* @param $status
* @param null $log_message
*
* @return bool|int
*/
public function add_action_log( $submission_id, $action, $status, $log_message = null ) {
$current_datetime_gmt = current_time( 'mysql', true );
$current_datetime = get_date_from_gmt( $current_datetime_gmt );
return $this->wpdb->insert(
$this->get_table_form_actions_log(),
[
'submission_id' => $submission_id,
'action_name' => $action->get_name(),
'action_label' => $action->get_label(),
'status' => $status,
'log' => $log_message,
'created_at_gmt' => $current_datetime_gmt,
'updated_at_gmt' => $current_datetime_gmt,
'created_at' => $current_datetime,
'updated_at' => $current_datetime,
]
);
}
public function get_last_error() {
$this->wpdb->last_error;
}
public function get_table_submissions() {
return $this->table_submissions;
}
public function get_table_submissions_values() {
return $this->table_submissions_values;
}
public function get_table_form_actions_log() {
return $this->table_submissions_actions_log;
}
/**
* Extract data from `$target` by selected `$keys`.
* `$as` used to convert extracted data with different keys.
* `$as` support converting to types. using ':' after the key name.
*
* Example: `$target = [ 'someId' => '5' ];`
* `$keys = [ 'someId' ]`
* `$as = [ 'id:int' ]`
* Output will be `[ 'id' => 5 ];`
*
* @param array|\stdClass $target
* @param array $keys
* @param array $as
*
* @return array
*/
private function extract( $target, $keys, $as = [] ) {
$result = [];
$as_types = [];
$as_count = 0;
if ( is_object( $target ) ) {
$target = (array) $target;
}
foreach ( $as as $key => $as_item ) {
$exploded = explode( ':', $as_item );
if ( count( $exploded ) > 1 ) {
$as_types [] = $exploded[1];
$as[ $key ] = $exploded[0];
}
}
foreach ( $keys as $key ) {
if ( isset( $target[ $key ] ) ) {
if ( isset( $as[ $as_count ] ) ) {
$value = $target[ $key ];
if ( isset( $as_types[ $as_count ] ) ) {
settype( $value, $as_types[ $as_count ] );
}
$result[ $as[ $as_count ] ] = $value;
} else {
$result[ $key ] = $target[ $key ];
}
}
++$as_count;
}
return $result;
}
private function handle_and_for_where_query( &$target_where_query ) {
if ( $target_where_query ) {
$target_where_query .= ' AND ';
}
}
private function filter_after_and_before( $filters, &$target_where_query ) {
// Filters 'after' & 'before' known in advance, and could handled systematically.
if ( isset( $filters['after'] ) || isset( $filters['before'] ) ) {
$this->handle_and_for_where_query( $target_where_query );
// TODO: This logic applies only for 'date' format not 'date-time' format.
$after = '0000-01-01 00:00:00';
$before = '9999-12-31 23:59:59';
if ( isset( $filters['after'] ) ) {
$after = $filters['after']['value'] . ' 00:00:00';
}
if ( isset( $filters['before'] ) ) {
$before = $filters['before']['value'] . ' 23:59:59';
}
$after = get_gmt_from_date( $after );
$before = get_gmt_from_date( $before );
$target_where_query .= $this->wpdb->prepare( 'created_at_gmt BETWEEN %s and %s', [ $after, $before ] );
}
}
private function filter_status( $filters, &$target_where_query ) {
if ( isset( $filters['status'] ) ) {
$this->handle_and_for_where_query( $target_where_query );
switch ( $filters['status']['value'] ) {
case 'all':
$target_where_query .= 'status != \'' . self::STATUS_TRASH . '\'';
break;
case 'unread':
$target_where_query .= 'status = \'' . self::STATUS_NEW . '\' AND is_read = 0';
break;
case 'read':
$target_where_query .= 'status = \'' . self::STATUS_NEW . '\' AND is_read > 0';
break;
case 'trash':
$target_where_query .= 'status = \'' . self::STATUS_TRASH . '\'';
break;
}
}
}
private function filter_ids( $filters, &$target_where_query ) {
if ( ! isset( $filters['ids'] ) || empty( $filters['ids'] ) ) {
return;
}
$this->handle_and_for_where_query( $target_where_query );
$ids_collection = new Collection( $filters['ids']['value'] );
$placeholder = $ids_collection->map( function () {
return '%d';
} )->implode( ', ' );
$target_where_query .= $this->wpdb->prepare( "`id` IN ({$placeholder})", $ids_collection->all() ); // phpcs:ignore
}
private function filter_search( $filters, &$target_where_query ) {
if ( isset( $filters['search'] ) ) {
$this->handle_and_for_where_query( $target_where_query );
$like = '%' . $this->wpdb->esc_like( $filters['search']['value'] ) . '%';
$meta_table_name = $this->get_table_submissions_values();
$search = $this->wpdb->prepare( 'LIKE %s OR t_submissions.id LIKE %s', [ $like, $like ] );
$target_where_query .= "(
(
SELECT GROUP_CONCAT({$meta_table_name}.value)
FROM `{$meta_table_name}`
WHERE {$meta_table_name}.submission_id = t_submissions.id
GROUP BY {$meta_table_name}.submission_id
) {$search}
)";
}
}
/**
* Filter bu element_id and post_id
*
* @param $filters
* @param $target_where_query
*/
private function filter_by_form( $filters, &$target_where_query ) {
if ( ! isset( $filters['form']['value'] ) ) {
return;
}
$this->handle_and_for_where_query( $target_where_query );
list( $post_id, $form_id ) = explode( '_', $filters['form']['value'] );
$target_where_query .= $this->wpdb->prepare(
'post_id = %d AND element_id = %s',
$post_id,
$form_id
);
}
/**
* @param $filters
* @param $target_where_query
*/
private function filter_by_referer( $filters, &$target_where_query ) {
if ( ! isset( $filters['referer']['value'] ) ) {
return;
}
$this->handle_and_for_where_query( $target_where_query );
$target_where_query .= $this->wpdb->prepare(
'referer = %s',
$filters['referer']['value']
);
}
private function handle_order( $order, &$target_order_query ) {
if ( ! empty( $order ) ) {
$order['by'] = esc_sql( $order['by'] );
$order['order'] = strtoupper( $order['order'] );
if ( ! in_array( $order['order'], [ 'ASC', 'DESC' ], true ) ) {
$order['order'] = 'ASC';
}
$target_order_query = 'ORDER BY ' . $order['by'] . ' ' . $order['order'];
}
}
/**
* @param \stdClass $submission
* @param bool $with_form_fields
*
* @return array
*/
private function get_submission_body( $submission, $with_form_fields = false ) {
$id = (int) $submission->id;
$result = [
'id' => $id,
];
$result['post'] = $this->extract( $submission, [ 'post_id' ], [ 'id:int' ] );
$result['form'] = $this->extract( $submission, [ 'form_name', 'element_id' ], [ 'name' ] );
$edit_post_id = $submission->post_id;
// TODO: Should be removed if there is an ability to edit "global widgets"
$meta = json_decode( $submission->meta ?? '', true );
if ( isset( $meta['edit_post_id'] ) ) {
$edit_post_id = $meta['edit_post_id'];
}
$document = Plugin::elementor()->documents->get( $edit_post_id );
if ( $document ) {
$result['post']['edit_url'] = $document->get_edit_url();
}
if ( $with_form_fields ) {
$form = Form_Snapshot_Repository::instance()
->find( $submission->post_id, $submission->element_id );
if ( $form ) {
$result['form']['fields'] = $form->fields;
}
}
$user = get_user_by( 'id', $submission->user_id );
$result['actions_count'] = (int) $submission->actions_count;
$result['actions_succeeded_count'] = (int) $submission->actions_succeeded_count;
$result['referer'] = $submission->referer;
$result['referer_title'] = $submission->referer_title;
$result['element_id'] = $submission->element_id;
$result['main_meta_id'] = $submission->main_meta_id;
$result['user_id'] = $submission->user_id;
$result['user_agent'] = $submission->user_agent;
$result['user_ip'] = $submission->user_ip;
$result['user_name'] = $user ? $user->display_name : null;
$result['created_at_gmt'] = $submission->created_at_gmt;
$result['updated_at_gmt'] = $submission->updated_at_gmt;
// Return the the dates according to WP current selected timezone.
$result['created_at'] = get_date_from_gmt( $submission->created_at_gmt );
$result['updated_at'] = get_date_from_gmt( $submission->updated_at_gmt );
$result['status'] = $submission->status;
$result['is_read'] = (bool) $submission->is_read;
return $result;
}
private function get_new_submission_initial_data( array $submission_data ) {
$current_datetime_gmt = current_time( 'mysql', true );
$current_datetime = get_date_from_gmt( $current_datetime_gmt );
$submission_data['hash_id'] = wp_generate_uuid4();
$submission_data = array_merge( [
'created_at_gmt' => $current_datetime_gmt,
'updated_at_gmt' => $current_datetime_gmt,
'created_at' => $current_datetime,
'updated_at' => $current_datetime,
'type' => self::TYPE_SUBMISSION,
'status' => self::STATUS_NEW,
], $submission_data );
return $submission_data;
}
private function apply_filter( array $filter ) {
$where_sql = '';
$this->filter_after_and_before( $filter, $where_sql );
$this->filter_status( $filter, $where_sql );
$this->filter_search( $filter, $where_sql );
$this->filter_by_form( $filter, $where_sql );
$this->filter_ids( $filter, $where_sql );
$this->filter_by_referer( $filter, $where_sql );
if ( ! $where_sql ) {
return '';
}
return 'WHERE ' . $where_sql;
}
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->table_submissions = $wpdb->prefix . self::E_SUBMISSIONS;
$this->table_submissions_values = $wpdb->prefix . self::E_SUBMISSIONS_VALUES;
$this->table_submissions_actions_log = $wpdb->prefix . self::E_SUBMISSIONS_ACTIONS_LOG;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace ElementorPro\Modules\Forms\Submissions\Database\Repositories;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\Utils\Collection;
use ElementorPro\Modules\Forms\Submissions\Database\Entities\Form_Snapshot;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Form_Snapshot_Repository extends Base_Object {
// There are two underscore prefix to avoid duplicate the meta when the post will be published.
const POST_META_KEY = '__elementor_forms_snapshot';
/**
* @var static
*/
private static $instance = null;
/**
* @var Collection
*/
private $cache;
/**
* @return static
*/
public static function instance() {
if ( null === static::$instance ) {
static::$instance = new static();
}
return static::$instance;
}
/**
* Get specific form.
*
* @param $post_id
* @param $form_id
* @param bool $from_cache
*
* @return Form_Snapshot|null
*/
public function find( $post_id, $form_id, $from_cache = true ) {
$key = Form_Snapshot::generate_key( $post_id, $form_id );
if ( $from_cache && $this->cache->get( $key, false ) ) {
return $this->cache->get( $key, false );
}
return $this->save_in_cache(
$this->get_post_forms( $post_id )
)->get( $key );
}
/**
* Get all the forms.
*
* @return Collection
*/
public function all() {
global $wpdb;
$result = $wpdb->get_results(
$wpdb->prepare(
"SELECT pm.meta_value, pm.post_id FROM {$wpdb->postmeta} pm WHERE pm.meta_key = %s",
static::POST_META_KEY
)
);
if ( ! $result ) {
return new Collection( [] );
}
foreach ( $result as $post_forms ) {
$this->save_in_cache(
$this->parse_meta( $post_forms->meta_value, $post_forms->post_id )
);
}
return $this->cache;
}
/**
* @param $post_id
* @param $form_id
* @param $data
*
* @return Form_Snapshot
*/
public function create_or_update( $post_id, $form_id, $data ) {
$forms = $this->get_post_forms( $post_id )
->filter( function ( Form_Snapshot $form ) use ( $form_id ) {
return $form->id !== $form_id;
} );
$form = new Form_Snapshot( $post_id, $data + [ 'id' => $form_id ] );
$forms[] = $form;
update_post_meta(
$post_id,
self::POST_META_KEY,
// Use `wp_slash` in order to avoid the unslashing during the `update_post_meta`
wp_slash( wp_json_encode( $forms->values() ) )
);
$this->save_in_cache( $forms );
return $form;
}
public function clear_cache() {
$this->cache = new Collection( [] );
}
/**
* @param $post_id
*
* @return Collection
*/
private function get_post_forms( $post_id ) {
$meta_value = get_post_meta( $post_id, self::POST_META_KEY, true );
if ( ! $meta_value ) {
return new Collection( [] );
}
return $this->parse_meta( $meta_value, $post_id );
}
/**
* Receive a meta value and transform it to an array of Form objects.
*
* @param $meta_value
* @param $post_id
*
* @return Collection
*/
private function parse_meta( $meta_value, $post_id ) {
return ( new Collection( json_decode( $meta_value, true ) ) )
->map( function ( $item ) use ( $post_id ) {
return new Form_Snapshot( $post_id, $item );
} );
}
/**
* @param $forms
*
* @return Collection
*/
private function save_in_cache( Collection $forms ) {
/** @var Form_Snapshot $form */
foreach ( $forms as $form ) {
$this->cache[ $form->get_key() ] = $form;
}
return $this->cache;
}
/**
* Forms_Repository constructor.
*/
public function __construct() {
$this->cache = new Collection( [] );
}
}