Completed
Branch master (cfeae0)
by
unknown
44:56 queued 35:56
created
core/data_migration_scripts/EE_Data_Migration_Script_Base.core.php 1 patch
Indentation   +876 added lines, -876 removed lines patch added patch discarded remove patch
@@ -15,880 +15,880 @@
 block discarded – undo
15 15
 abstract class EE_Data_Migration_Script_Base extends EE_Data_Migration_Class_Base
16 16
 {
17 17
 
18
-    /**
19
-     * Set by client code to indicate this DMS is being ran as part of a proper migration,
20
-     * instead of being used to merely setup (or verify) the database structure.
21
-     * Defaults to TRUE, so client code that's NOT using this DMS as part of a proper migration
22
-     * should call EE_Data_Migration_Script_Base::set_migrating( FALSE )
23
-     *
24
-     * @var boolean
25
-     */
26
-    protected $_migrating = true;
27
-
28
-    /**
29
-     * numerically-indexed array where each value is EE_Data_Migration_Script_Stage object
30
-     *
31
-     * @var EE_Data_Migration_Script_Stage[] $migration_functions
32
-     */
33
-    protected $_migration_stages = array();
34
-
35
-    /**
36
-     * Indicates we've already ran the schema changes that needed to happen BEFORE the data migration
37
-     *
38
-     * @var boolean
39
-     */
40
-    protected $_schema_changes_before_migration_ran = null;
41
-
42
-    /**
43
-     * Indicates we've already ran the schema changes that needed to happen AFTER the data migration
44
-     *
45
-     * @var boolean
46
-     */
47
-    protected $_schema_changes_after_migration_ran = null;
48
-
49
-    /**
50
-     * String which describes what's currently happening in this migration
51
-     *
52
-     * @var string
53
-     */
54
-    protected $_feedback_message;
55
-
56
-    /**
57
-     * Indicates the script's priority. Like wp's add_action and add_filter, lower numbers
58
-     * correspond to earlier execution
59
-     *
60
-     * @var int
61
-     */
62
-    protected $_priority = 5;
63
-
64
-    /**
65
-     * Multi-dimensional array that defines the mapping from OLD table Primary Keys
66
-     * to NEW table Primary Keys.
67
-     * Top-level array keys are OLD table names (minus the "wp_" part),
68
-     * 2nd-level array keys are NEW table names (again, minus the "wp_" part),
69
-     * 3rd-level array keys are the OLD table primary keys
70
-     * and 3rd-level array values are the NEW table primary keys
71
-     *
72
-     * @var array
73
-     */
74
-    protected $_mappings = array();
75
-
76
-
77
-    /**
78
-     * Returns whether or not this data migration script can operate on the given version of the database.
79
-     * Eg, if this migration script can migrate from 3.1.26 or higher (but not anything after 4.0.0), and
80
-     * it's passed a string like '3.1.38B', it should return true.
81
-     * If this DMS is to migrate data from an EE3 addon, you will probably want to use
82
-     * EventEspresso\core\services\database\TableAnalysis::tableExists() to check for old EE3 tables, and
83
-     * EE_Data_Migration_Manager::get_migration_ran() to check that core was already
84
-     * migrated from EE3 to EE4 (ie, this DMS probably relies on some migration data generated
85
-     * during the Core 4.1.0 DMS. If core didn't run that DMS, you probably don't want
86
-     * to run this DMS).
87
-     * If this DMS migrates data from a previous version of this EE4 addon, just
88
-     * comparing $current_database_state_of[ $this->slug() ] will probably suffice.
89
-     * If this DMS should never migrate data, because it's only used to define the initial
90
-     * database state, just return FALSE (and core's activation process will take care
91
-     * of calling its schema_changes_before_migration() and
92
-     * schema_changes_after_migration() for you. )
93
-     *
94
-     * @param array $current_database_state_of keys are EE plugin slugs (eg 'Core', 'Calendar', 'Mailchimp', etc)
95
-     * @return boolean
96
-     */
97
-    abstract public function can_migrate_from_version($current_database_state_of);
98
-
99
-
100
-    /**
101
-     * Performs database schema changes that need to occur BEFORE the data is migrated.
102
-     * Eg, if we were going to change user passwords from plaintext to encoded versions
103
-     * during this migration, this would probably add a new column called something like
104
-     * "encoded_password".
105
-     *
106
-     * @return boolean of success
107
-     */
108
-    abstract public function schema_changes_before_migration();
109
-
110
-
111
-    /**
112
-     * Performs the database schema changes that need to occur AFTER the data has been migrated.
113
-     * Usually this will mean we'll be removing old columns. Eg, if we were changing passwords
114
-     * from plaintext to encoded versions, and we had added a column called "encoded_password",
115
-     * this function would probably remove the old column "password" (which still holds the plaintext password)
116
-     * and possibly rename "encoded_password" to "password"
117
-     *
118
-     * @return boolean of success
119
-     */
120
-    abstract public function schema_changes_after_migration();
121
-
122
-
123
-    /**
124
-     * All children of this must call parent::__construct()
125
-     * at the end of their constructor or suffer the consequences!
126
-     *
127
-     * @param TableManager  $table_manager
128
-     * @param TableAnalysis $table_analysis
129
-     */
130
-    public function __construct(TableManager $table_manager = null, TableAnalysis $table_analysis = null)
131
-    {
132
-        $this->_migration_stages = (array) apply_filters(
133
-            'FHEE__' . get_class($this) . '__construct__migration_stages',
134
-            $this->_migration_stages
135
-        );
136
-        foreach ($this->_migration_stages as $migration_stage) {
137
-            if ($migration_stage instanceof EE_Data_Migration_Script_Stage) {
138
-                $migration_stage->_construct_finalize($this);
139
-            }
140
-        }
141
-        parent::__construct($table_manager, $table_analysis);
142
-    }
143
-
144
-
145
-    /**
146
-     * Place to add hooks and filters for tweaking the migrations page, in order
147
-     * to customize it
148
-     */
149
-    public function migration_page_hooks()
150
-    {
151
-        // by default none are added because we normally like the default look of the migration page
152
-    }
153
-
154
-
155
-    /**
156
-     * Sets the mapping from old table primary keys to new table primary keys.
157
-     * This mapping is automatically persisted as a property on the migration
158
-     *
159
-     * @param string     $old_table with wpdb prefix (wp_). Eg: wp_events_detail
160
-     * @param int|string $old_pk    old primary key. Eg events_detail.id's value
161
-     * @param string     $new_table with wpdb prefix (wp_). Eg: wp_posts
162
-     * @param int|string $new_pk    eg posts.ID
163
-     * @return void
164
-     */
165
-    public function set_mapping($old_table, $old_pk, $new_table, $new_pk)
166
-    {
167
-        // make sure it has the needed keys
168
-        if (! isset($this->_mappings[ $old_table ]) || ! isset($this->_mappings[ $old_table ][ $new_table ])) {
169
-            $this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
170
-        }
171
-        $this->_mappings[ $old_table ][ $new_table ][ $old_pk ] = $new_pk;
172
-    }
173
-
174
-
175
-    /**
176
-     * Gets the new primary key, if provided with the OLD table and the primary key
177
-     * of an item in the old table, and the new table
178
-     *
179
-     * @param string     $old_table with wpdb prefix (wp_). Eg: wp_events_detail
180
-     * @param int|string $old_pk    old primary key. Eg events_detail.id's value
181
-     * @param string     $new_table with wpdb prefix (wp_). Eg: wp_posts
182
-     * @return mixed the primary key on the new table
183
-     */
184
-    public function get_mapping_new_pk($old_table, $old_pk, $new_table)
185
-    {
186
-        if (! isset($this->_mappings[ $old_table ]) ||
187
-            ! isset($this->_mappings[ $old_table ][ $new_table ])) {
188
-            // try fetching the option
189
-            $this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
190
-        }
191
-        return isset($this->_mappings[ $old_table ][ $new_table ][ $old_pk ])
192
-            ? $this->_mappings[ $old_table ][ $new_table ][ $old_pk ] : null;
193
-    }
194
-
195
-
196
-    /**
197
-     * Gets the old primary key, if provided with the OLD table,
198
-     * and the new table and the primary key of an item in the new table
199
-     *
200
-     * @param string $old_table with wpdb prefix (wp_). Eg: wp_events_detail
201
-     * @param string $new_table with wpdb prefix (wp_). Eg: wp_posts
202
-     * @param mixed  $new_pk
203
-     * @return mixed
204
-     */
205
-    public function get_mapping_old_pk($old_table, $new_table, $new_pk)
206
-    {
207
-        if (! isset($this->_mappings[ $old_table ]) ||
208
-            ! isset($this->_mappings[ $old_table ][ $new_table ])) {
209
-            // try fetching the option
210
-            $this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
211
-        }
212
-        if (isset($this->_mappings[ $old_table ][ $new_table ])) {
213
-            $new_pk_to_old_pk = array_flip($this->_mappings[ $old_table ][ $new_table ]);
214
-            if (isset($new_pk_to_old_pk[ $new_pk ])) {
215
-                return $new_pk_to_old_pk[ $new_pk ];
216
-            }
217
-        }
218
-        return null;
219
-    }
220
-
221
-
222
-    /**
223
-     * Gets the mapping array option specified by the table names
224
-     *
225
-     * @param string $old_table_name
226
-     * @param string $new_table_name
227
-     * @return array
228
-     */
229
-    protected function _get_mapping_option($old_table_name, $new_table_name)
230
-    {
231
-        $option = get_option($this->_get_mapping_option_name($old_table_name, $new_table_name), array());
232
-        return $option;
233
-    }
234
-
235
-
236
-    /**
237
-     * Updates the mapping option specified by the table names with the array provided
238
-     *
239
-     * @param string $old_table_name
240
-     * @param string $new_table_name
241
-     * @param array  $mapping_array
242
-     * @return boolean success of updating option
243
-     */
244
-    protected function _set_mapping_option($old_table_name, $new_table_name, $mapping_array)
245
-    {
246
-        $success = update_option($this->_get_mapping_option_name($old_table_name, $new_table_name), $mapping_array, false);
247
-        return $success;
248
-    }
249
-
250
-
251
-    /**
252
-     * Gets the option name for this script to map from $old_table_name to $new_table_name
253
-     *
254
-     * @param string $old_table_name
255
-     * @param string $new_table_name
256
-     * @return string
257
-     */
258
-    protected function _get_mapping_option_name($old_table_name, $new_table_name)
259
-    {
260
-        global $wpdb;
261
-        $old_table_name_sans_wp = str_replace($wpdb->prefix, "", $old_table_name);
262
-        $new_table_name_sans_wp = str_replace($wpdb->prefix, "", $new_table_name);
263
-        $migrates_to = EE_Data_Migration_Manager::instance()->script_migrates_to_version(get_class($this));
264
-        return substr(
265
-            EE_Data_Migration_Manager::data_migration_script_mapping_option_prefix . $migrates_to ['slug'] . '_' . $migrates_to['version'] . '_' . $old_table_name_sans_wp . '_' . $new_table_name_sans_wp,
266
-            0,
267
-            64
268
-        );
269
-    }
270
-
271
-
272
-    /**
273
-     * Counts all the records that will be migrated during this data migration.
274
-     * For example, if we were changing old user passwords from plaintext to encoded versions,
275
-     * this would be a count of all users who have passwords. If we were going to also split
276
-     * attendee records into transactions, registrations, and attendee records, this would include
277
-     * the count of all attendees currently in existence in the DB (ie, users + attendees).
278
-     * If you can't determine how many records there are to migrate, just provide a guess: this
279
-     * number will only be used in calculating the percent complete. If you estimate there to be
280
-     * 100 records to migrate, and it turns out there's 120, we'll just show the migration as being at
281
-     * 99% until the function "migration_step" returns EE_Data_Migration_Script_Base::status_complete.
282
-     *
283
-     * @return int
284
-     */
285
-    protected function _count_records_to_migrate()
286
-    {
287
-        $count = 0;
288
-        foreach ($this->stages() as $stage) {
289
-            $count += $stage->count_records_to_migrate();
290
-        }
291
-        return $count;
292
-    }
293
-
294
-
295
-    /**
296
-     * Returns the number of records updated so far. Usually this is easiest to do
297
-     * by just setting a transient and updating it after each migration_step
298
-     *
299
-     * @return int
300
-     */
301
-    public function count_records_migrated()
302
-    {
303
-        $count = 0;
304
-        foreach ($this->stages() as $stage) {
305
-            $count += $stage->count_records_migrated();
306
-        }
307
-        $this->_records_migrated = $count;
308
-        return $count;
309
-    }
310
-
311
-
312
-    /**
313
-     * @param int $num_records_to_migrate_limit
314
-     * @return int
315
-     * @throws EE_Error
316
-     * @throws Exception
317
-     */
318
-    public function migration_step($num_records_to_migrate_limit)
319
-    {
320
-        // reset the feedback message
321
-        $this->_feedback_message = '';
322
-        // if we haven't yet done the 1st schema changes, do them now. buffer any output
323
-        $this->_maybe_do_schema_changes(true);
324
-
325
-        $num_records_actually_migrated = 0;
326
-        $records_migrated_per_stage = array();
327
-        // setup the 'stage' variable, which should hold the last run stage of the migration  (or none at all if nothing runs)
328
-        $stage = null;
329
-        // get the next stage that isn't complete
330
-        foreach ($this->stages() as $stage) {
331
-            if ($stage->get_status() == EE_Data_Migration_Manager::status_continue) {
332
-                try {
333
-                    $records_migrated_during_stage = $stage->migration_step(
334
-                        $num_records_to_migrate_limit - $num_records_actually_migrated
335
-                    );
336
-                    $num_records_actually_migrated += $records_migrated_during_stage;
337
-                    $records_migrated_per_stage[ $stage->pretty_name() ] = $records_migrated_during_stage;
338
-                } catch (Exception $e) {
339
-                    // yes if we catch an exception here, we consider that migration stage borked.
340
-                    $stage->set_status(EE_Data_Migration_Manager::status_fatal_error);
341
-                    $this->set_status(EE_Data_Migration_Manager::status_fatal_error);
342
-                    $stage->add_error($e->getMessage() . ". Stack-trace:" . $e->getTraceAsString());
343
-                    throw $e;
344
-                }
345
-                // check that the migration stage didn't mark itself as having a fatal error
346
-                if ($stage->is_broken()) {
347
-                    $this->set_broken();
348
-                    throw new EE_Error($stage->get_last_error());
349
-                }
350
-            }
351
-            // once we've migrated all the number we intended to (possibly from different stages), stop migrating
352
-            // or if we had a fatal error
353
-            // or if the current script stopped early- its not done, but it's done all it thinks we should do on this step
354
-            if ($num_records_actually_migrated >= $num_records_to_migrate_limit
355
-                || $stage->is_broken()
356
-                || $stage->has_more_to_do()
357
-            ) {
358
-                break;
359
-            }
360
-        }
361
-        // check if we're all done this data migration...
362
-        // which is indicated by being done early AND the last stage claims to be done
363
-        if ($stage == null) {
364
-            // this migration script apparently has NO stages... which is super weird, but whatever
365
-            $this->set_completed();
366
-            $this->_maybe_do_schema_changes(false);
367
-        } elseif ($num_records_actually_migrated < $num_records_to_migrate_limit && ! $stage->has_more_to_do()) {
368
-            // apparently we're done, because we couldn't migrate the number we intended to
369
-            $this->set_completed();
370
-            $this->_update_feedback_message(array_reverse($records_migrated_per_stage));
371
-            // do schema changes for after the migration now
372
-            // first double-check we haven't already done this
373
-            $this->_maybe_do_schema_changes(false);
374
-        } else {
375
-            // update feedback message, keeping in mind that we show them with the most recent at the top
376
-            $this->_update_feedback_message(array_reverse($records_migrated_per_stage));
377
-        }
378
-        return $num_records_actually_migrated;
379
-    }
380
-
381
-
382
-    /**
383
-     * Updates the feedback message according to what was done during this migration stage.
384
-     *
385
-     * @param array $records_migrated_per_stage KEYS are pretty names for each stage; values are the count of records
386
-     *                                          migrated from that stage
387
-     * @return void
388
-     */
389
-    private function _update_feedback_message($records_migrated_per_stage)
390
-    {
391
-        $feedback_message_array = array();
392
-        foreach ($records_migrated_per_stage as $migration_stage_name => $num_records_migrated) {
393
-            $feedback_message_array[] = sprintf(
394
-                __("Migrated %d records successfully during %s", "event_espresso"),
395
-                $num_records_migrated,
396
-                $migration_stage_name
397
-            );
398
-        }
399
-        $this->_feedback_message .= implode("<br>", $feedback_message_array);
400
-    }
401
-
402
-
403
-    /**
404
-     * Calls either schema_changes_before_migration() (if $before==true) or schema_changes_after_migration
405
-     * (if $before==false). Buffers their outputs and stores them on the class.
406
-     *
407
-     * @param boolean $before
408
-     * @throws Exception
409
-     * @return void
410
-     */
411
-    private function _maybe_do_schema_changes($before = true)
412
-    {
413
-        // so this property will be either _schema_changes_after_migration_ran or _schema_changes_before_migration_ran
414
-        $property_name = '_schema_changes_' . ($before ? 'before' : 'after') . '_migration_ran';
415
-        if (! $this->{$property_name}) {
416
-            try {
417
-                ob_start();
418
-                if ($before) {
419
-                    $this->schema_changes_before_migration();
420
-                } else {
421
-                    $this->schema_changes_after_migration();
422
-                }
423
-                $output = ob_get_contents();
424
-                ob_end_clean();
425
-            } catch (Exception $e) {
426
-                $this->set_status(EE_Data_Migration_Manager::status_fatal_error);
427
-                throw $e;
428
-            }
429
-            // record that we've done these schema changes
430
-            $this->{$property_name} = true;
431
-            // if there were any warnings etc, record them as non-fatal errors
432
-            if ($output) {
433
-                // there were some warnings
434
-                $this->_errors[] = $output;
435
-            }
436
-        }
437
-    }
438
-
439
-
440
-    /**
441
-     * Wrapper for EEH_Activation::create_table. However, takes into account the request type when
442
-     * deciding what to pass for its 4th arg, $drop_pre_existing_tables. Using this function, instead
443
-     * of _table_should_exist_previously, indicates that this table should be new to the EE version being migrated to
444
-     * or
445
-     * activated currently. If this is a brand new activation or a migration, and we're indicating this table should
446
-     * not
447
-     * previously exist, then we want to set $drop_pre_existing_tables to TRUE (ie, we shouldn't discover that this
448
-     * table exists in the DB in EEH_Activation::create_table- if it DOES exist, something's wrong and the old table
449
-     * should be nuked.
450
-     *
451
-     * Just for a bit of context, the migration script's db_schema_changes_* methods
452
-     * are called basically in 3 cases: on brand new activation of EE4 (ie no previous version of EE existed and the
453
-     * plugin is being activated and we want to add all the brand new tables), upon reactivation of EE4 (it was
454
-     * deactivated and then reactivated, in which case we want to just verify the DB structure is ok) that table should
455
-     * be dropped), and during a migration when we're moving the DB to the state of the migration script
456
-     *
457
-     * @param string $table_name
458
-     * @param string $table_definition_sql
459
-     * @param string $engine_string
460
-     */
461
-    protected function _table_is_new_in_this_version(
462
-        $table_name,
463
-        $table_definition_sql,
464
-        $engine_string = 'ENGINE=InnoDB '
465
-    ) {
466
-        $this->_create_table_and_catch_errors(
467
-            $table_name,
468
-            $table_definition_sql,
469
-            $engine_string,
470
-            $this->_pre_existing_table_should_be_dropped(true)
471
-        );
472
-    }
473
-
474
-    /**
475
-     * Like _table_is_new_in_this_version and _table_should_exist_previously, this function verifies the given table
476
-     * exists. But we understand that this table has CHANGED in this version since the previous version. So it's not
477
-     * completely new, but it's different. So we need to treat it like a new table in terms of verifying it's schema is
478
-     * correct on activations, migrations, upgrades; but if it exists when it shouldn't, we need to be as lenient as
479
-     * _table_should_exist_previously.
480
-     * 8656]{Assumes only this plugin could have added this table (ie, if its a new activation of this plugin, the
481
-     * table shouldn't exist).
482
-     *
483
-     * @param string $table_name
484
-     * @param string $table_definition_sql
485
-     * @param string $engine_string
486
-     */
487
-    protected function _table_is_changed_in_this_version(
488
-        $table_name,
489
-        $table_definition_sql,
490
-        $engine_string = 'ENGINE=MyISAM'
491
-    ) {
492
-        $this->_create_table_and_catch_errors(
493
-            $table_name,
494
-            $table_definition_sql,
495
-            $engine_string,
496
-            $this->_pre_existing_table_should_be_dropped(false)
497
-        );
498
-    }
499
-
500
-
501
-    /**
502
-     * _old_table_exists
503
-     * returns TRUE if the requested table exists in the current database
504
-     *
505
-     * @param string $table_name
506
-     * @return boolean
507
-     */
508
-    protected function _old_table_exists($table_name)
509
-    {
510
-        return $this->_get_table_analysis()->tableExists($table_name);
511
-    }
512
-
513
-
514
-    /**
515
-     * _delete_table_if_empty
516
-     * returns TRUE if the requested table was empty and successfully empty
517
-     *
518
-     * @param string $table_name
519
-     * @return boolean
520
-     */
521
-    protected function _delete_table_if_empty($table_name)
522
-    {
523
-        return EEH_Activation::delete_db_table_if_empty($table_name);
524
-    }
525
-
526
-
527
-    /**
528
-     * It is preferred to use _table_has_not_changed_since_previous or _table_is_changed_in_this_version
529
-     * as these are significantly more efficient or explicit.
530
-     * Please see description of _table_is_new_in_this_version. This function will only set
531
-     * EEH_Activation::create_table's $drop_pre_existing_tables to TRUE if it's a brand
532
-     * new activation. ie, a more accurate name for this method would be "_table_added_previously_by_this_plugin"
533
-     * because the table will be cleared out if this is a new activation (ie, if its a new activation, it actually
534
-     * should exist previously). Otherwise, we'll always set $drop_pre_existing_tables to FALSE because the table
535
-     * should have existed. Note, if the table is being MODIFIED in this version being activated or migrated to, then
536
-     * you want _table_is_changed_in_this_version NOT this one. We don't check this table's structure during migrations
537
-     * because apparently it hasn't changed since the previous one, right?
538
-     *
539
-     * @param string $table_name
540
-     * @param string $table_definition_sql
541
-     * @param string $engine_string
542
-     */
543
-    protected function _table_should_exist_previously(
544
-        $table_name,
545
-        $table_definition_sql,
546
-        $engine_string = 'ENGINE=MyISAM'
547
-    ) {
548
-        $this->_create_table_and_catch_errors(
549
-            $table_name,
550
-            $table_definition_sql,
551
-            $engine_string,
552
-            $this->_pre_existing_table_should_be_dropped(false)
553
-        );
554
-    }
555
-
556
-    /**
557
-     * Exactly the same as _table_should_exist_previously(), except if this migration script is currently doing
558
-     * a migration, we skip checking this table's structure in the database and just assume it's correct.
559
-     * So this is useful only to improve efficiency when doing migrations (not a big deal for single site installs,
560
-     * but important for multisite where migrations can take a very long time otherwise).
561
-     * If the table is known to have changed since previous version, use _table_is_changed_in_this_version().
562
-     * Assumes only this plugin could have added this table (ie, if its a new activation of this plugin, the table
563
-     * shouldn't exist).
564
-     *
565
-     * @param string $table_name
566
-     * @param string $table_definition_sql
567
-     * @param string $engine_string
568
-     */
569
-    protected function _table_has_not_changed_since_previous(
570
-        $table_name,
571
-        $table_definition_sql,
572
-        $engine_string = 'ENGINE=MyISAM'
573
-    ) {
574
-        if ($this->_currently_migrating()) {
575
-            // if we're doing a migration, and this table apparently already exists, then we don't need do anything right?
576
-            return;
577
-        }
578
-        $this->_create_table_and_catch_errors(
579
-            $table_name,
580
-            $table_definition_sql,
581
-            $engine_string,
582
-            $this->_pre_existing_table_should_be_dropped(false)
583
-        );
584
-    }
585
-
586
-    /**
587
-     * Returns whether or not this migration script is being used as part of an actual migration
588
-     *
589
-     * @return boolean
590
-     */
591
-    protected function _currently_migrating()
592
-    {
593
-        // we want to know if we are currently performing a migration. We could just believe what was set on the _migrating property, but let's double-check (ie the script should apply and we should be in MM)
594
-        return $this->_migrating &&
595
-               $this->can_migrate_from_version(
596
-                   EE_Data_Migration_Manager::instance()->ensure_current_database_state_is_set()
597
-               ) &&
598
-               EE_Maintenance_Mode::instance()->real_level() == EE_Maintenance_Mode::level_2_complete_maintenance;
599
-    }
600
-
601
-    /**
602
-     * Determines if a table should be dropped, based on whether it's reported to be new in $table_is_new,
603
-     * and the plugin's request type.
604
-     * Assumes only this plugin could have added the table (ie, if its a new activation of this plugin, the table
605
-     * shouldn't exist no matter what).
606
-     *
607
-     * @param boolean $table_is_new
608
-     * @return boolean
609
-     */
610
-    protected function _pre_existing_table_should_be_dropped($table_is_new)
611
-    {
612
-        if ($table_is_new) {
613
-            if ($this->_get_req_type_for_plugin_corresponding_to_this_dms() == EE_System::req_type_new_activation
614
-                || $this->_currently_migrating()
615
-            ) {
616
-                return true;
617
-            } else {
618
-                return false;
619
-            }
620
-        } else {
621
-            if (in_array(
622
-                $this->_get_req_type_for_plugin_corresponding_to_this_dms(),
623
-                array(EE_System::req_type_new_activation)
624
-            )) {
625
-                return true;
626
-            } else {
627
-                return false;
628
-            }
629
-        }
630
-    }
631
-
632
-    /**
633
-     * Just wraps EEH_Activation::create_table, but catches any errors it may throw and adds them as errors on the DMS
634
-     *
635
-     * @param string  $table_name
636
-     * @param string  $table_definition_sql
637
-     * @param string  $engine_string
638
-     * @param boolean $drop_pre_existing_tables
639
-     */
640
-    private function _create_table_and_catch_errors(
641
-        $table_name,
642
-        $table_definition_sql,
643
-        $engine_string = 'ENGINE=MyISAM',
644
-        $drop_pre_existing_tables = false
645
-    ) {
646
-        try {
647
-            EEH_Activation::create_table($table_name, $table_definition_sql, $engine_string, $drop_pre_existing_tables);
648
-        } catch (EE_Error $e) {
649
-            $message = $e->getMessage() . '<br>Stack Trace:' . $e->getTraceAsString();
650
-            $this->add_error($message);
651
-            $this->_feedback_message .= $message;
652
-        }
653
-    }
654
-
655
-
656
-    /**
657
-     * Gets the request type for the plugin (core or addon) that corresponds to this DMS
658
-     *
659
-     * @return int one of EE_System::_req_type_* constants
660
-     * @throws EE_Error
661
-     */
662
-    private function _get_req_type_for_plugin_corresponding_to_this_dms()
663
-    {
664
-        if ($this->slug() == 'Core') {
665
-            return EE_System::instance()->detect_req_type();
666
-        } else {// it must be for an addon
667
-            $addon_name = $this->slug();
668
-            if (EE_Registry::instance()->get_addon_by_name($addon_name)) {
669
-                return EE_Registry::instance()->get_addon_by_name($addon_name)->detect_req_type();
670
-            } else {
671
-                throw new EE_Error(
672
-                    sprintf(
673
-                        __(
674
-                            "The DMS slug '%s' should correspond to the addon's name, which should also be '%s', but no such addon was registered. These are the registered addons' names: %s",
675
-                            "event_espresso"
676
-                        ),
677
-                        $this->slug(),
678
-                        $addon_name,
679
-                        implode(",", array_keys(EE_Registry::instance()->get_addons_by_name()))
680
-                    )
681
-                );
682
-            }
683
-        }
684
-    }
685
-
686
-
687
-    /**
688
-     * returns an array of strings describing errors by all the script's stages
689
-     *
690
-     * @return array
691
-     */
692
-    public function get_errors()
693
-    {
694
-        $all_errors = $this->_errors;
695
-        if (! is_array($all_errors)) {
696
-            $all_errors = array();
697
-        }
698
-        foreach ($this->stages() as $stage) {
699
-            $all_errors = array_merge($stage->get_errors(), $all_errors);
700
-        }
701
-        return $all_errors;
702
-    }
703
-
704
-
705
-    /**
706
-     * Indicates whether or not this migration script should continue
707
-     *
708
-     * @return boolean
709
-     */
710
-    public function can_continue()
711
-    {
712
-        return in_array(
713
-            $this->get_status(),
714
-            EE_Data_Migration_Manager::instance()->stati_that_indicate_to_continue_single_migration_script
715
-        );
716
-    }
717
-
718
-
719
-    /**
720
-     * Gets all the data migration stages associated with this script. Note:
721
-     * addons can filter this list to add their own stages, and because the list is
722
-     * numerically-indexed, they can insert their stage wherever they like and it will
723
-     * get ordered by the indexes
724
-     *
725
-     * @return EE_Data_Migration_Script_Stage[]
726
-     */
727
-    protected function stages()
728
-    {
729
-        $stages = apply_filters('FHEE__' . get_class($this) . '__stages', $this->_migration_stages);
730
-        ksort($stages);
731
-        return $stages;
732
-    }
733
-
734
-
735
-    /**
736
-     * Gets a string which should describe what's going on currently with this migration, which
737
-     * can be displayed to the user
738
-     *
739
-     * @return string
740
-     */
741
-    public function get_feedback_message()
742
-    {
743
-        return $this->_feedback_message;
744
-    }
745
-
746
-
747
-    /**
748
-     * A lot like "__sleep()" magic method in purpose, this is meant for persisting this class'
749
-     * properties to the DB. However, we don't want to use __sleep() because its quite
750
-     * possible that this class is defined when it goes to sleep, but NOT available when it
751
-     * awakes (eg, this class is part of an addon that is deactivated at some point).
752
-     */
753
-    public function properties_as_array()
754
-    {
755
-        $properties = parent::properties_as_array();
756
-        $properties['_migration_stages'] = array();
757
-        foreach ($this->_migration_stages as $migration_stage_priority => $migration_stage_class) {
758
-            $properties['_migration_stages'][ $migration_stage_priority ] = $migration_stage_class->properties_as_array(
759
-            );
760
-        }
761
-        unset($properties['_mappings']);
762
-
763
-        foreach ($this->_mappings as $old_table_name => $mapping_to_new_table) {
764
-            foreach ($mapping_to_new_table as $new_table_name => $mapping) {
765
-                $this->_set_mapping_option($old_table_name, $new_table_name, $mapping);
766
-            }
767
-        }
768
-        return $properties;
769
-    }
770
-
771
-
772
-    /**
773
-     * Sets all of the properties of this script stage to match what's in the array, which is assumed
774
-     * to have been made from the properties_as_array() function.
775
-     *
776
-     * @param array $array_of_properties like what's produced from properties_as_array() method
777
-     * @return void
778
-     */
779
-    public function instantiate_from_array_of_properties($array_of_properties)
780
-    {
781
-        $stages_properties_arrays = $array_of_properties['_migration_stages'];
782
-        unset($array_of_properties['_migration_stages']);
783
-        unset($array_of_properties['class']);
784
-        foreach ($array_of_properties as $property_name => $property_value) {
785
-            $this->{$property_name} = $property_value;
786
-        }
787
-        // _migration_stages are already instantiated, but have only default data
788
-        foreach ($this->_migration_stages as $stage) {
789
-            $stage_data = $this->_find_migration_stage_data_with_classname(
790
-                get_class($stage),
791
-                $stages_properties_arrays
792
-            );
793
-            // SO, if we found the stage data that was saved, use it. Otherwise, I guess the stage is new? (maybe added by
794
-            // an addon? Unlikely... not sure why it wouldn't exist, but if it doesn't just treat it like it was never started yet)
795
-            if ($stage_data) {
796
-                $stage->instantiate_from_array_of_properties($stage_data);
797
-            }
798
-        }
799
-    }
800
-
801
-
802
-    /**
803
-     * Gets the migration data from the array $migration_stage_data_arrays (which is an array of arrays, each of which
804
-     * is pretty well identical to EE_Data_Migration_Stage objects except all their properties are array indexes)
805
-     * for the given classname
806
-     *
807
-     * @param string $classname
808
-     * @param array  $migration_stage_data_arrays
809
-     * @return null
810
-     */
811
-    private function _find_migration_stage_data_with_classname($classname, $migration_stage_data_arrays)
812
-    {
813
-        foreach ($migration_stage_data_arrays as $migration_stage_data_array) {
814
-            if (isset($migration_stage_data_array['class']) && $migration_stage_data_array['class'] == $classname) {
815
-                return $migration_stage_data_array;
816
-            }
817
-        }
818
-        return null;
819
-    }
820
-
821
-
822
-    /**
823
-     * Returns the version that this script migrates to, based on the script's name.
824
-     * Cannot be overwritten because lots of code needs to know which version a script
825
-     * migrates to knowing only its name.
826
-     *
827
-     * @return array where the first key is the plugin's slug, the 2nd is the version of that plugin
828
-     * that will be updated to. Eg array('Core','4.1.0')
829
-     */
830
-    final public function migrates_to_version()
831
-    {
832
-        return EE_Data_Migration_Manager::instance()->script_migrates_to_version(get_class($this));
833
-    }
834
-
835
-
836
-    /**
837
-     * Gets this addon's slug as it would appear in the current_db_state wp option,
838
-     * and if this migration script is for an addon, it SHOULD match the addon's slug
839
-     * (and also the addon's classname, minus the 'EE_' prefix.). Eg, 'Calendar' for the EE_Calendar addon.
840
-     * Or 'Core' for core (non-addon).
841
-     *
842
-     * @return string
843
-     */
844
-    public function slug()
845
-    {
846
-        $migrates_to_version_info = $this->migrates_to_version();
847
-        // the slug is the first part of the array
848
-        return $migrates_to_version_info['slug'];
849
-    }
850
-
851
-
852
-    /**
853
-     * Returns the script's priority relative to DMSs from other addons. However, when
854
-     * two DMSs from the same addon/core apply, this is ignored (and instead the version that
855
-     * the script migrates to is used to determine which to run first). The default is 5, but all core DMSs
856
-     * normally have priority 10. (So if you want a DMS "A" to run before DMS "B", both of which are from addons,
857
-     * and both of which CAN run at the same time (ie, "B" doesn't depend on "A" to set
858
-     * the database up so it can run), then you can set "A" to priority 3 or something.
859
-     *
860
-     * @return int
861
-     */
862
-    public function priority()
863
-    {
864
-        return $this->_priority;
865
-    }
866
-
867
-
868
-    /**
869
-     * Sets whether or not this DMS is being ran as part of a migration, instead of
870
-     * just being used to setup (or verify) the current database structure matches
871
-     * what the latest DMS indicates it should be
872
-     *
873
-     * @param boolean $migrating
874
-     * @return void
875
-     */
876
-    public function set_migrating($migrating = true)
877
-    {
878
-        $this->_migrating = $migrating;
879
-    }
880
-
881
-    /**
882
-     * Marks that we think this migration class can continue to migrate
883
-     */
884
-    public function reattempt()
885
-    {
886
-        parent::reattempt();
887
-        // also, we want to reattempt any stages that were marked as borked
888
-        foreach ($this->stages() as $stage) {
889
-            if ($stage->is_broken()) {
890
-                $stage->reattempt();
891
-            }
892
-        }
893
-    }
18
+	/**
19
+	 * Set by client code to indicate this DMS is being ran as part of a proper migration,
20
+	 * instead of being used to merely setup (or verify) the database structure.
21
+	 * Defaults to TRUE, so client code that's NOT using this DMS as part of a proper migration
22
+	 * should call EE_Data_Migration_Script_Base::set_migrating( FALSE )
23
+	 *
24
+	 * @var boolean
25
+	 */
26
+	protected $_migrating = true;
27
+
28
+	/**
29
+	 * numerically-indexed array where each value is EE_Data_Migration_Script_Stage object
30
+	 *
31
+	 * @var EE_Data_Migration_Script_Stage[] $migration_functions
32
+	 */
33
+	protected $_migration_stages = array();
34
+
35
+	/**
36
+	 * Indicates we've already ran the schema changes that needed to happen BEFORE the data migration
37
+	 *
38
+	 * @var boolean
39
+	 */
40
+	protected $_schema_changes_before_migration_ran = null;
41
+
42
+	/**
43
+	 * Indicates we've already ran the schema changes that needed to happen AFTER the data migration
44
+	 *
45
+	 * @var boolean
46
+	 */
47
+	protected $_schema_changes_after_migration_ran = null;
48
+
49
+	/**
50
+	 * String which describes what's currently happening in this migration
51
+	 *
52
+	 * @var string
53
+	 */
54
+	protected $_feedback_message;
55
+
56
+	/**
57
+	 * Indicates the script's priority. Like wp's add_action and add_filter, lower numbers
58
+	 * correspond to earlier execution
59
+	 *
60
+	 * @var int
61
+	 */
62
+	protected $_priority = 5;
63
+
64
+	/**
65
+	 * Multi-dimensional array that defines the mapping from OLD table Primary Keys
66
+	 * to NEW table Primary Keys.
67
+	 * Top-level array keys are OLD table names (minus the "wp_" part),
68
+	 * 2nd-level array keys are NEW table names (again, minus the "wp_" part),
69
+	 * 3rd-level array keys are the OLD table primary keys
70
+	 * and 3rd-level array values are the NEW table primary keys
71
+	 *
72
+	 * @var array
73
+	 */
74
+	protected $_mappings = array();
75
+
76
+
77
+	/**
78
+	 * Returns whether or not this data migration script can operate on the given version of the database.
79
+	 * Eg, if this migration script can migrate from 3.1.26 or higher (but not anything after 4.0.0), and
80
+	 * it's passed a string like '3.1.38B', it should return true.
81
+	 * If this DMS is to migrate data from an EE3 addon, you will probably want to use
82
+	 * EventEspresso\core\services\database\TableAnalysis::tableExists() to check for old EE3 tables, and
83
+	 * EE_Data_Migration_Manager::get_migration_ran() to check that core was already
84
+	 * migrated from EE3 to EE4 (ie, this DMS probably relies on some migration data generated
85
+	 * during the Core 4.1.0 DMS. If core didn't run that DMS, you probably don't want
86
+	 * to run this DMS).
87
+	 * If this DMS migrates data from a previous version of this EE4 addon, just
88
+	 * comparing $current_database_state_of[ $this->slug() ] will probably suffice.
89
+	 * If this DMS should never migrate data, because it's only used to define the initial
90
+	 * database state, just return FALSE (and core's activation process will take care
91
+	 * of calling its schema_changes_before_migration() and
92
+	 * schema_changes_after_migration() for you. )
93
+	 *
94
+	 * @param array $current_database_state_of keys are EE plugin slugs (eg 'Core', 'Calendar', 'Mailchimp', etc)
95
+	 * @return boolean
96
+	 */
97
+	abstract public function can_migrate_from_version($current_database_state_of);
98
+
99
+
100
+	/**
101
+	 * Performs database schema changes that need to occur BEFORE the data is migrated.
102
+	 * Eg, if we were going to change user passwords from plaintext to encoded versions
103
+	 * during this migration, this would probably add a new column called something like
104
+	 * "encoded_password".
105
+	 *
106
+	 * @return boolean of success
107
+	 */
108
+	abstract public function schema_changes_before_migration();
109
+
110
+
111
+	/**
112
+	 * Performs the database schema changes that need to occur AFTER the data has been migrated.
113
+	 * Usually this will mean we'll be removing old columns. Eg, if we were changing passwords
114
+	 * from plaintext to encoded versions, and we had added a column called "encoded_password",
115
+	 * this function would probably remove the old column "password" (which still holds the plaintext password)
116
+	 * and possibly rename "encoded_password" to "password"
117
+	 *
118
+	 * @return boolean of success
119
+	 */
120
+	abstract public function schema_changes_after_migration();
121
+
122
+
123
+	/**
124
+	 * All children of this must call parent::__construct()
125
+	 * at the end of their constructor or suffer the consequences!
126
+	 *
127
+	 * @param TableManager  $table_manager
128
+	 * @param TableAnalysis $table_analysis
129
+	 */
130
+	public function __construct(TableManager $table_manager = null, TableAnalysis $table_analysis = null)
131
+	{
132
+		$this->_migration_stages = (array) apply_filters(
133
+			'FHEE__' . get_class($this) . '__construct__migration_stages',
134
+			$this->_migration_stages
135
+		);
136
+		foreach ($this->_migration_stages as $migration_stage) {
137
+			if ($migration_stage instanceof EE_Data_Migration_Script_Stage) {
138
+				$migration_stage->_construct_finalize($this);
139
+			}
140
+		}
141
+		parent::__construct($table_manager, $table_analysis);
142
+	}
143
+
144
+
145
+	/**
146
+	 * Place to add hooks and filters for tweaking the migrations page, in order
147
+	 * to customize it
148
+	 */
149
+	public function migration_page_hooks()
150
+	{
151
+		// by default none are added because we normally like the default look of the migration page
152
+	}
153
+
154
+
155
+	/**
156
+	 * Sets the mapping from old table primary keys to new table primary keys.
157
+	 * This mapping is automatically persisted as a property on the migration
158
+	 *
159
+	 * @param string     $old_table with wpdb prefix (wp_). Eg: wp_events_detail
160
+	 * @param int|string $old_pk    old primary key. Eg events_detail.id's value
161
+	 * @param string     $new_table with wpdb prefix (wp_). Eg: wp_posts
162
+	 * @param int|string $new_pk    eg posts.ID
163
+	 * @return void
164
+	 */
165
+	public function set_mapping($old_table, $old_pk, $new_table, $new_pk)
166
+	{
167
+		// make sure it has the needed keys
168
+		if (! isset($this->_mappings[ $old_table ]) || ! isset($this->_mappings[ $old_table ][ $new_table ])) {
169
+			$this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
170
+		}
171
+		$this->_mappings[ $old_table ][ $new_table ][ $old_pk ] = $new_pk;
172
+	}
173
+
174
+
175
+	/**
176
+	 * Gets the new primary key, if provided with the OLD table and the primary key
177
+	 * of an item in the old table, and the new table
178
+	 *
179
+	 * @param string     $old_table with wpdb prefix (wp_). Eg: wp_events_detail
180
+	 * @param int|string $old_pk    old primary key. Eg events_detail.id's value
181
+	 * @param string     $new_table with wpdb prefix (wp_). Eg: wp_posts
182
+	 * @return mixed the primary key on the new table
183
+	 */
184
+	public function get_mapping_new_pk($old_table, $old_pk, $new_table)
185
+	{
186
+		if (! isset($this->_mappings[ $old_table ]) ||
187
+			! isset($this->_mappings[ $old_table ][ $new_table ])) {
188
+			// try fetching the option
189
+			$this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
190
+		}
191
+		return isset($this->_mappings[ $old_table ][ $new_table ][ $old_pk ])
192
+			? $this->_mappings[ $old_table ][ $new_table ][ $old_pk ] : null;
193
+	}
194
+
195
+
196
+	/**
197
+	 * Gets the old primary key, if provided with the OLD table,
198
+	 * and the new table and the primary key of an item in the new table
199
+	 *
200
+	 * @param string $old_table with wpdb prefix (wp_). Eg: wp_events_detail
201
+	 * @param string $new_table with wpdb prefix (wp_). Eg: wp_posts
202
+	 * @param mixed  $new_pk
203
+	 * @return mixed
204
+	 */
205
+	public function get_mapping_old_pk($old_table, $new_table, $new_pk)
206
+	{
207
+		if (! isset($this->_mappings[ $old_table ]) ||
208
+			! isset($this->_mappings[ $old_table ][ $new_table ])) {
209
+			// try fetching the option
210
+			$this->_mappings[ $old_table ][ $new_table ] = $this->_get_mapping_option($old_table, $new_table);
211
+		}
212
+		if (isset($this->_mappings[ $old_table ][ $new_table ])) {
213
+			$new_pk_to_old_pk = array_flip($this->_mappings[ $old_table ][ $new_table ]);
214
+			if (isset($new_pk_to_old_pk[ $new_pk ])) {
215
+				return $new_pk_to_old_pk[ $new_pk ];
216
+			}
217
+		}
218
+		return null;
219
+	}
220
+
221
+
222
+	/**
223
+	 * Gets the mapping array option specified by the table names
224
+	 *
225
+	 * @param string $old_table_name
226
+	 * @param string $new_table_name
227
+	 * @return array
228
+	 */
229
+	protected function _get_mapping_option($old_table_name, $new_table_name)
230
+	{
231
+		$option = get_option($this->_get_mapping_option_name($old_table_name, $new_table_name), array());
232
+		return $option;
233
+	}
234
+
235
+
236
+	/**
237
+	 * Updates the mapping option specified by the table names with the array provided
238
+	 *
239
+	 * @param string $old_table_name
240
+	 * @param string $new_table_name
241
+	 * @param array  $mapping_array
242
+	 * @return boolean success of updating option
243
+	 */
244
+	protected function _set_mapping_option($old_table_name, $new_table_name, $mapping_array)
245
+	{
246
+		$success = update_option($this->_get_mapping_option_name($old_table_name, $new_table_name), $mapping_array, false);
247
+		return $success;
248
+	}
249
+
250
+
251
+	/**
252
+	 * Gets the option name for this script to map from $old_table_name to $new_table_name
253
+	 *
254
+	 * @param string $old_table_name
255
+	 * @param string $new_table_name
256
+	 * @return string
257
+	 */
258
+	protected function _get_mapping_option_name($old_table_name, $new_table_name)
259
+	{
260
+		global $wpdb;
261
+		$old_table_name_sans_wp = str_replace($wpdb->prefix, "", $old_table_name);
262
+		$new_table_name_sans_wp = str_replace($wpdb->prefix, "", $new_table_name);
263
+		$migrates_to = EE_Data_Migration_Manager::instance()->script_migrates_to_version(get_class($this));
264
+		return substr(
265
+			EE_Data_Migration_Manager::data_migration_script_mapping_option_prefix . $migrates_to ['slug'] . '_' . $migrates_to['version'] . '_' . $old_table_name_sans_wp . '_' . $new_table_name_sans_wp,
266
+			0,
267
+			64
268
+		);
269
+	}
270
+
271
+
272
+	/**
273
+	 * Counts all the records that will be migrated during this data migration.
274
+	 * For example, if we were changing old user passwords from plaintext to encoded versions,
275
+	 * this would be a count of all users who have passwords. If we were going to also split
276
+	 * attendee records into transactions, registrations, and attendee records, this would include
277
+	 * the count of all attendees currently in existence in the DB (ie, users + attendees).
278
+	 * If you can't determine how many records there are to migrate, just provide a guess: this
279
+	 * number will only be used in calculating the percent complete. If you estimate there to be
280
+	 * 100 records to migrate, and it turns out there's 120, we'll just show the migration as being at
281
+	 * 99% until the function "migration_step" returns EE_Data_Migration_Script_Base::status_complete.
282
+	 *
283
+	 * @return int
284
+	 */
285
+	protected function _count_records_to_migrate()
286
+	{
287
+		$count = 0;
288
+		foreach ($this->stages() as $stage) {
289
+			$count += $stage->count_records_to_migrate();
290
+		}
291
+		return $count;
292
+	}
293
+
294
+
295
+	/**
296
+	 * Returns the number of records updated so far. Usually this is easiest to do
297
+	 * by just setting a transient and updating it after each migration_step
298
+	 *
299
+	 * @return int
300
+	 */
301
+	public function count_records_migrated()
302
+	{
303
+		$count = 0;
304
+		foreach ($this->stages() as $stage) {
305
+			$count += $stage->count_records_migrated();
306
+		}
307
+		$this->_records_migrated = $count;
308
+		return $count;
309
+	}
310
+
311
+
312
+	/**
313
+	 * @param int $num_records_to_migrate_limit
314
+	 * @return int
315
+	 * @throws EE_Error
316
+	 * @throws Exception
317
+	 */
318
+	public function migration_step($num_records_to_migrate_limit)
319
+	{
320
+		// reset the feedback message
321
+		$this->_feedback_message = '';
322
+		// if we haven't yet done the 1st schema changes, do them now. buffer any output
323
+		$this->_maybe_do_schema_changes(true);
324
+
325
+		$num_records_actually_migrated = 0;
326
+		$records_migrated_per_stage = array();
327
+		// setup the 'stage' variable, which should hold the last run stage of the migration  (or none at all if nothing runs)
328
+		$stage = null;
329
+		// get the next stage that isn't complete
330
+		foreach ($this->stages() as $stage) {
331
+			if ($stage->get_status() == EE_Data_Migration_Manager::status_continue) {
332
+				try {
333
+					$records_migrated_during_stage = $stage->migration_step(
334
+						$num_records_to_migrate_limit - $num_records_actually_migrated
335
+					);
336
+					$num_records_actually_migrated += $records_migrated_during_stage;
337
+					$records_migrated_per_stage[ $stage->pretty_name() ] = $records_migrated_during_stage;
338
+				} catch (Exception $e) {
339
+					// yes if we catch an exception here, we consider that migration stage borked.
340
+					$stage->set_status(EE_Data_Migration_Manager::status_fatal_error);
341
+					$this->set_status(EE_Data_Migration_Manager::status_fatal_error);
342
+					$stage->add_error($e->getMessage() . ". Stack-trace:" . $e->getTraceAsString());
343
+					throw $e;
344
+				}
345
+				// check that the migration stage didn't mark itself as having a fatal error
346
+				if ($stage->is_broken()) {
347
+					$this->set_broken();
348
+					throw new EE_Error($stage->get_last_error());
349
+				}
350
+			}
351
+			// once we've migrated all the number we intended to (possibly from different stages), stop migrating
352
+			// or if we had a fatal error
353
+			// or if the current script stopped early- its not done, but it's done all it thinks we should do on this step
354
+			if ($num_records_actually_migrated >= $num_records_to_migrate_limit
355
+				|| $stage->is_broken()
356
+				|| $stage->has_more_to_do()
357
+			) {
358
+				break;
359
+			}
360
+		}
361
+		// check if we're all done this data migration...
362
+		// which is indicated by being done early AND the last stage claims to be done
363
+		if ($stage == null) {
364
+			// this migration script apparently has NO stages... which is super weird, but whatever
365
+			$this->set_completed();
366
+			$this->_maybe_do_schema_changes(false);
367
+		} elseif ($num_records_actually_migrated < $num_records_to_migrate_limit && ! $stage->has_more_to_do()) {
368
+			// apparently we're done, because we couldn't migrate the number we intended to
369
+			$this->set_completed();
370
+			$this->_update_feedback_message(array_reverse($records_migrated_per_stage));
371
+			// do schema changes for after the migration now
372
+			// first double-check we haven't already done this
373
+			$this->_maybe_do_schema_changes(false);
374
+		} else {
375
+			// update feedback message, keeping in mind that we show them with the most recent at the top
376
+			$this->_update_feedback_message(array_reverse($records_migrated_per_stage));
377
+		}
378
+		return $num_records_actually_migrated;
379
+	}
380
+
381
+
382
+	/**
383
+	 * Updates the feedback message according to what was done during this migration stage.
384
+	 *
385
+	 * @param array $records_migrated_per_stage KEYS are pretty names for each stage; values are the count of records
386
+	 *                                          migrated from that stage
387
+	 * @return void
388
+	 */
389
+	private function _update_feedback_message($records_migrated_per_stage)
390
+	{
391
+		$feedback_message_array = array();
392
+		foreach ($records_migrated_per_stage as $migration_stage_name => $num_records_migrated) {
393
+			$feedback_message_array[] = sprintf(
394
+				__("Migrated %d records successfully during %s", "event_espresso"),
395
+				$num_records_migrated,
396
+				$migration_stage_name
397
+			);
398
+		}
399
+		$this->_feedback_message .= implode("<br>", $feedback_message_array);
400
+	}
401
+
402
+
403
+	/**
404
+	 * Calls either schema_changes_before_migration() (if $before==true) or schema_changes_after_migration
405
+	 * (if $before==false). Buffers their outputs and stores them on the class.
406
+	 *
407
+	 * @param boolean $before
408
+	 * @throws Exception
409
+	 * @return void
410
+	 */
411
+	private function _maybe_do_schema_changes($before = true)
412
+	{
413
+		// so this property will be either _schema_changes_after_migration_ran or _schema_changes_before_migration_ran
414
+		$property_name = '_schema_changes_' . ($before ? 'before' : 'after') . '_migration_ran';
415
+		if (! $this->{$property_name}) {
416
+			try {
417
+				ob_start();
418
+				if ($before) {
419
+					$this->schema_changes_before_migration();
420
+				} else {
421
+					$this->schema_changes_after_migration();
422
+				}
423
+				$output = ob_get_contents();
424
+				ob_end_clean();
425
+			} catch (Exception $e) {
426
+				$this->set_status(EE_Data_Migration_Manager::status_fatal_error);
427
+				throw $e;
428
+			}
429
+			// record that we've done these schema changes
430
+			$this->{$property_name} = true;
431
+			// if there were any warnings etc, record them as non-fatal errors
432
+			if ($output) {
433
+				// there were some warnings
434
+				$this->_errors[] = $output;
435
+			}
436
+		}
437
+	}
438
+
439
+
440
+	/**
441
+	 * Wrapper for EEH_Activation::create_table. However, takes into account the request type when
442
+	 * deciding what to pass for its 4th arg, $drop_pre_existing_tables. Using this function, instead
443
+	 * of _table_should_exist_previously, indicates that this table should be new to the EE version being migrated to
444
+	 * or
445
+	 * activated currently. If this is a brand new activation or a migration, and we're indicating this table should
446
+	 * not
447
+	 * previously exist, then we want to set $drop_pre_existing_tables to TRUE (ie, we shouldn't discover that this
448
+	 * table exists in the DB in EEH_Activation::create_table- if it DOES exist, something's wrong and the old table
449
+	 * should be nuked.
450
+	 *
451
+	 * Just for a bit of context, the migration script's db_schema_changes_* methods
452
+	 * are called basically in 3 cases: on brand new activation of EE4 (ie no previous version of EE existed and the
453
+	 * plugin is being activated and we want to add all the brand new tables), upon reactivation of EE4 (it was
454
+	 * deactivated and then reactivated, in which case we want to just verify the DB structure is ok) that table should
455
+	 * be dropped), and during a migration when we're moving the DB to the state of the migration script
456
+	 *
457
+	 * @param string $table_name
458
+	 * @param string $table_definition_sql
459
+	 * @param string $engine_string
460
+	 */
461
+	protected function _table_is_new_in_this_version(
462
+		$table_name,
463
+		$table_definition_sql,
464
+		$engine_string = 'ENGINE=InnoDB '
465
+	) {
466
+		$this->_create_table_and_catch_errors(
467
+			$table_name,
468
+			$table_definition_sql,
469
+			$engine_string,
470
+			$this->_pre_existing_table_should_be_dropped(true)
471
+		);
472
+	}
473
+
474
+	/**
475
+	 * Like _table_is_new_in_this_version and _table_should_exist_previously, this function verifies the given table
476
+	 * exists. But we understand that this table has CHANGED in this version since the previous version. So it's not
477
+	 * completely new, but it's different. So we need to treat it like a new table in terms of verifying it's schema is
478
+	 * correct on activations, migrations, upgrades; but if it exists when it shouldn't, we need to be as lenient as
479
+	 * _table_should_exist_previously.
480
+	 * 8656]{Assumes only this plugin could have added this table (ie, if its a new activation of this plugin, the
481
+	 * table shouldn't exist).
482
+	 *
483
+	 * @param string $table_name
484
+	 * @param string $table_definition_sql
485
+	 * @param string $engine_string
486
+	 */
487
+	protected function _table_is_changed_in_this_version(
488
+		$table_name,
489
+		$table_definition_sql,
490
+		$engine_string = 'ENGINE=MyISAM'
491
+	) {
492
+		$this->_create_table_and_catch_errors(
493
+			$table_name,
494
+			$table_definition_sql,
495
+			$engine_string,
496
+			$this->_pre_existing_table_should_be_dropped(false)
497
+		);
498
+	}
499
+
500
+
501
+	/**
502
+	 * _old_table_exists
503
+	 * returns TRUE if the requested table exists in the current database
504
+	 *
505
+	 * @param string $table_name
506
+	 * @return boolean
507
+	 */
508
+	protected function _old_table_exists($table_name)
509
+	{
510
+		return $this->_get_table_analysis()->tableExists($table_name);
511
+	}
512
+
513
+
514
+	/**
515
+	 * _delete_table_if_empty
516
+	 * returns TRUE if the requested table was empty and successfully empty
517
+	 *
518
+	 * @param string $table_name
519
+	 * @return boolean
520
+	 */
521
+	protected function _delete_table_if_empty($table_name)
522
+	{
523
+		return EEH_Activation::delete_db_table_if_empty($table_name);
524
+	}
525
+
526
+
527
+	/**
528
+	 * It is preferred to use _table_has_not_changed_since_previous or _table_is_changed_in_this_version
529
+	 * as these are significantly more efficient or explicit.
530
+	 * Please see description of _table_is_new_in_this_version. This function will only set
531
+	 * EEH_Activation::create_table's $drop_pre_existing_tables to TRUE if it's a brand
532
+	 * new activation. ie, a more accurate name for this method would be "_table_added_previously_by_this_plugin"
533
+	 * because the table will be cleared out if this is a new activation (ie, if its a new activation, it actually
534
+	 * should exist previously). Otherwise, we'll always set $drop_pre_existing_tables to FALSE because the table
535
+	 * should have existed. Note, if the table is being MODIFIED in this version being activated or migrated to, then
536
+	 * you want _table_is_changed_in_this_version NOT this one. We don't check this table's structure during migrations
537
+	 * because apparently it hasn't changed since the previous one, right?
538
+	 *
539
+	 * @param string $table_name
540
+	 * @param string $table_definition_sql
541
+	 * @param string $engine_string
542
+	 */
543
+	protected function _table_should_exist_previously(
544
+		$table_name,
545
+		$table_definition_sql,
546
+		$engine_string = 'ENGINE=MyISAM'
547
+	) {
548
+		$this->_create_table_and_catch_errors(
549
+			$table_name,
550
+			$table_definition_sql,
551
+			$engine_string,
552
+			$this->_pre_existing_table_should_be_dropped(false)
553
+		);
554
+	}
555
+
556
+	/**
557
+	 * Exactly the same as _table_should_exist_previously(), except if this migration script is currently doing
558
+	 * a migration, we skip checking this table's structure in the database and just assume it's correct.
559
+	 * So this is useful only to improve efficiency when doing migrations (not a big deal for single site installs,
560
+	 * but important for multisite where migrations can take a very long time otherwise).
561
+	 * If the table is known to have changed since previous version, use _table_is_changed_in_this_version().
562
+	 * Assumes only this plugin could have added this table (ie, if its a new activation of this plugin, the table
563
+	 * shouldn't exist).
564
+	 *
565
+	 * @param string $table_name
566
+	 * @param string $table_definition_sql
567
+	 * @param string $engine_string
568
+	 */
569
+	protected function _table_has_not_changed_since_previous(
570
+		$table_name,
571
+		$table_definition_sql,
572
+		$engine_string = 'ENGINE=MyISAM'
573
+	) {
574
+		if ($this->_currently_migrating()) {
575
+			// if we're doing a migration, and this table apparently already exists, then we don't need do anything right?
576
+			return;
577
+		}
578
+		$this->_create_table_and_catch_errors(
579
+			$table_name,
580
+			$table_definition_sql,
581
+			$engine_string,
582
+			$this->_pre_existing_table_should_be_dropped(false)
583
+		);
584
+	}
585
+
586
+	/**
587
+	 * Returns whether or not this migration script is being used as part of an actual migration
588
+	 *
589
+	 * @return boolean
590
+	 */
591
+	protected function _currently_migrating()
592
+	{
593
+		// we want to know if we are currently performing a migration. We could just believe what was set on the _migrating property, but let's double-check (ie the script should apply and we should be in MM)
594
+		return $this->_migrating &&
595
+			   $this->can_migrate_from_version(
596
+				   EE_Data_Migration_Manager::instance()->ensure_current_database_state_is_set()
597
+			   ) &&
598
+			   EE_Maintenance_Mode::instance()->real_level() == EE_Maintenance_Mode::level_2_complete_maintenance;
599
+	}
600
+
601
+	/**
602
+	 * Determines if a table should be dropped, based on whether it's reported to be new in $table_is_new,
603
+	 * and the plugin's request type.
604
+	 * Assumes only this plugin could have added the table (ie, if its a new activation of this plugin, the table
605
+	 * shouldn't exist no matter what).
606
+	 *
607
+	 * @param boolean $table_is_new
608
+	 * @return boolean
609
+	 */
610
+	protected function _pre_existing_table_should_be_dropped($table_is_new)
611
+	{
612
+		if ($table_is_new) {
613
+			if ($this->_get_req_type_for_plugin_corresponding_to_this_dms() == EE_System::req_type_new_activation
614
+				|| $this->_currently_migrating()
615
+			) {
616
+				return true;
617
+			} else {
618
+				return false;
619
+			}
620
+		} else {
621
+			if (in_array(
622
+				$this->_get_req_type_for_plugin_corresponding_to_this_dms(),
623
+				array(EE_System::req_type_new_activation)
624
+			)) {
625
+				return true;
626
+			} else {
627
+				return false;
628
+			}
629
+		}
630
+	}
631
+
632
+	/**
633
+	 * Just wraps EEH_Activation::create_table, but catches any errors it may throw and adds them as errors on the DMS
634
+	 *
635
+	 * @param string  $table_name
636
+	 * @param string  $table_definition_sql
637
+	 * @param string  $engine_string
638
+	 * @param boolean $drop_pre_existing_tables
639
+	 */
640
+	private function _create_table_and_catch_errors(
641
+		$table_name,
642
+		$table_definition_sql,
643
+		$engine_string = 'ENGINE=MyISAM',
644
+		$drop_pre_existing_tables = false
645
+	) {
646
+		try {
647
+			EEH_Activation::create_table($table_name, $table_definition_sql, $engine_string, $drop_pre_existing_tables);
648
+		} catch (EE_Error $e) {
649
+			$message = $e->getMessage() . '<br>Stack Trace:' . $e->getTraceAsString();
650
+			$this->add_error($message);
651
+			$this->_feedback_message .= $message;
652
+		}
653
+	}
654
+
655
+
656
+	/**
657
+	 * Gets the request type for the plugin (core or addon) that corresponds to this DMS
658
+	 *
659
+	 * @return int one of EE_System::_req_type_* constants
660
+	 * @throws EE_Error
661
+	 */
662
+	private function _get_req_type_for_plugin_corresponding_to_this_dms()
663
+	{
664
+		if ($this->slug() == 'Core') {
665
+			return EE_System::instance()->detect_req_type();
666
+		} else {// it must be for an addon
667
+			$addon_name = $this->slug();
668
+			if (EE_Registry::instance()->get_addon_by_name($addon_name)) {
669
+				return EE_Registry::instance()->get_addon_by_name($addon_name)->detect_req_type();
670
+			} else {
671
+				throw new EE_Error(
672
+					sprintf(
673
+						__(
674
+							"The DMS slug '%s' should correspond to the addon's name, which should also be '%s', but no such addon was registered. These are the registered addons' names: %s",
675
+							"event_espresso"
676
+						),
677
+						$this->slug(),
678
+						$addon_name,
679
+						implode(",", array_keys(EE_Registry::instance()->get_addons_by_name()))
680
+					)
681
+				);
682
+			}
683
+		}
684
+	}
685
+
686
+
687
+	/**
688
+	 * returns an array of strings describing errors by all the script's stages
689
+	 *
690
+	 * @return array
691
+	 */
692
+	public function get_errors()
693
+	{
694
+		$all_errors = $this->_errors;
695
+		if (! is_array($all_errors)) {
696
+			$all_errors = array();
697
+		}
698
+		foreach ($this->stages() as $stage) {
699
+			$all_errors = array_merge($stage->get_errors(), $all_errors);
700
+		}
701
+		return $all_errors;
702
+	}
703
+
704
+
705
+	/**
706
+	 * Indicates whether or not this migration script should continue
707
+	 *
708
+	 * @return boolean
709
+	 */
710
+	public function can_continue()
711
+	{
712
+		return in_array(
713
+			$this->get_status(),
714
+			EE_Data_Migration_Manager::instance()->stati_that_indicate_to_continue_single_migration_script
715
+		);
716
+	}
717
+
718
+
719
+	/**
720
+	 * Gets all the data migration stages associated with this script. Note:
721
+	 * addons can filter this list to add their own stages, and because the list is
722
+	 * numerically-indexed, they can insert their stage wherever they like and it will
723
+	 * get ordered by the indexes
724
+	 *
725
+	 * @return EE_Data_Migration_Script_Stage[]
726
+	 */
727
+	protected function stages()
728
+	{
729
+		$stages = apply_filters('FHEE__' . get_class($this) . '__stages', $this->_migration_stages);
730
+		ksort($stages);
731
+		return $stages;
732
+	}
733
+
734
+
735
+	/**
736
+	 * Gets a string which should describe what's going on currently with this migration, which
737
+	 * can be displayed to the user
738
+	 *
739
+	 * @return string
740
+	 */
741
+	public function get_feedback_message()
742
+	{
743
+		return $this->_feedback_message;
744
+	}
745
+
746
+
747
+	/**
748
+	 * A lot like "__sleep()" magic method in purpose, this is meant for persisting this class'
749
+	 * properties to the DB. However, we don't want to use __sleep() because its quite
750
+	 * possible that this class is defined when it goes to sleep, but NOT available when it
751
+	 * awakes (eg, this class is part of an addon that is deactivated at some point).
752
+	 */
753
+	public function properties_as_array()
754
+	{
755
+		$properties = parent::properties_as_array();
756
+		$properties['_migration_stages'] = array();
757
+		foreach ($this->_migration_stages as $migration_stage_priority => $migration_stage_class) {
758
+			$properties['_migration_stages'][ $migration_stage_priority ] = $migration_stage_class->properties_as_array(
759
+			);
760
+		}
761
+		unset($properties['_mappings']);
762
+
763
+		foreach ($this->_mappings as $old_table_name => $mapping_to_new_table) {
764
+			foreach ($mapping_to_new_table as $new_table_name => $mapping) {
765
+				$this->_set_mapping_option($old_table_name, $new_table_name, $mapping);
766
+			}
767
+		}
768
+		return $properties;
769
+	}
770
+
771
+
772
+	/**
773
+	 * Sets all of the properties of this script stage to match what's in the array, which is assumed
774
+	 * to have been made from the properties_as_array() function.
775
+	 *
776
+	 * @param array $array_of_properties like what's produced from properties_as_array() method
777
+	 * @return void
778
+	 */
779
+	public function instantiate_from_array_of_properties($array_of_properties)
780
+	{
781
+		$stages_properties_arrays = $array_of_properties['_migration_stages'];
782
+		unset($array_of_properties['_migration_stages']);
783
+		unset($array_of_properties['class']);
784
+		foreach ($array_of_properties as $property_name => $property_value) {
785
+			$this->{$property_name} = $property_value;
786
+		}
787
+		// _migration_stages are already instantiated, but have only default data
788
+		foreach ($this->_migration_stages as $stage) {
789
+			$stage_data = $this->_find_migration_stage_data_with_classname(
790
+				get_class($stage),
791
+				$stages_properties_arrays
792
+			);
793
+			// SO, if we found the stage data that was saved, use it. Otherwise, I guess the stage is new? (maybe added by
794
+			// an addon? Unlikely... not sure why it wouldn't exist, but if it doesn't just treat it like it was never started yet)
795
+			if ($stage_data) {
796
+				$stage->instantiate_from_array_of_properties($stage_data);
797
+			}
798
+		}
799
+	}
800
+
801
+
802
+	/**
803
+	 * Gets the migration data from the array $migration_stage_data_arrays (which is an array of arrays, each of which
804
+	 * is pretty well identical to EE_Data_Migration_Stage objects except all their properties are array indexes)
805
+	 * for the given classname
806
+	 *
807
+	 * @param string $classname
808
+	 * @param array  $migration_stage_data_arrays
809
+	 * @return null
810
+	 */
811
+	private function _find_migration_stage_data_with_classname($classname, $migration_stage_data_arrays)
812
+	{
813
+		foreach ($migration_stage_data_arrays as $migration_stage_data_array) {
814
+			if (isset($migration_stage_data_array['class']) && $migration_stage_data_array['class'] == $classname) {
815
+				return $migration_stage_data_array;
816
+			}
817
+		}
818
+		return null;
819
+	}
820
+
821
+
822
+	/**
823
+	 * Returns the version that this script migrates to, based on the script's name.
824
+	 * Cannot be overwritten because lots of code needs to know which version a script
825
+	 * migrates to knowing only its name.
826
+	 *
827
+	 * @return array where the first key is the plugin's slug, the 2nd is the version of that plugin
828
+	 * that will be updated to. Eg array('Core','4.1.0')
829
+	 */
830
+	final public function migrates_to_version()
831
+	{
832
+		return EE_Data_Migration_Manager::instance()->script_migrates_to_version(get_class($this));
833
+	}
834
+
835
+
836
+	/**
837
+	 * Gets this addon's slug as it would appear in the current_db_state wp option,
838
+	 * and if this migration script is for an addon, it SHOULD match the addon's slug
839
+	 * (and also the addon's classname, minus the 'EE_' prefix.). Eg, 'Calendar' for the EE_Calendar addon.
840
+	 * Or 'Core' for core (non-addon).
841
+	 *
842
+	 * @return string
843
+	 */
844
+	public function slug()
845
+	{
846
+		$migrates_to_version_info = $this->migrates_to_version();
847
+		// the slug is the first part of the array
848
+		return $migrates_to_version_info['slug'];
849
+	}
850
+
851
+
852
+	/**
853
+	 * Returns the script's priority relative to DMSs from other addons. However, when
854
+	 * two DMSs from the same addon/core apply, this is ignored (and instead the version that
855
+	 * the script migrates to is used to determine which to run first). The default is 5, but all core DMSs
856
+	 * normally have priority 10. (So if you want a DMS "A" to run before DMS "B", both of which are from addons,
857
+	 * and both of which CAN run at the same time (ie, "B" doesn't depend on "A" to set
858
+	 * the database up so it can run), then you can set "A" to priority 3 or something.
859
+	 *
860
+	 * @return int
861
+	 */
862
+	public function priority()
863
+	{
864
+		return $this->_priority;
865
+	}
866
+
867
+
868
+	/**
869
+	 * Sets whether or not this DMS is being ran as part of a migration, instead of
870
+	 * just being used to setup (or verify) the current database structure matches
871
+	 * what the latest DMS indicates it should be
872
+	 *
873
+	 * @param boolean $migrating
874
+	 * @return void
875
+	 */
876
+	public function set_migrating($migrating = true)
877
+	{
878
+		$this->_migrating = $migrating;
879
+	}
880
+
881
+	/**
882
+	 * Marks that we think this migration class can continue to migrate
883
+	 */
884
+	public function reattempt()
885
+	{
886
+		parent::reattempt();
887
+		// also, we want to reattempt any stages that were marked as borked
888
+		foreach ($this->stages() as $stage) {
889
+			if ($stage->is_broken()) {
890
+				$stage->reattempt();
891
+			}
892
+		}
893
+	}
894 894
 }
Please login to merge, or discard this patch.
core/libraries/rest_api/ModelDataTranslator.php 2 patches
Unused Use Statements   -4 removed lines patch added patch discarded remove patch
@@ -4,14 +4,10 @@
 block discarded – undo
4 4
 
5 5
 use DomainException;
6 6
 use EE_Boolean_Field;
7
-use EE_Capabilities;
8 7
 use EE_Datetime_Field;
9 8
 use EE_Error;
10 9
 use EE_Infinite_Integer_Field;
11
-use EE_Maybe_Serialized_Simple_HTML_Field;
12 10
 use EE_Model_Field_Base;
13
-use EE_Password_Field;
14
-use EE_Restriction_Generator_Base;
15 11
 use EE_Serialized_Text_Field;
16 12
 use EED_Core_Rest_Api;
17 13
 use EEM_Base;
Please login to merge, or discard this patch.
Indentation   +642 added lines, -642 removed lines patch added patch discarded remove patch
@@ -39,646 +39,646 @@
 block discarded – undo
39 39
 class ModelDataTranslator
40 40
 {
41 41
 
42
-    /**
43
-     * We used to use -1 for infinity in the rest api, but that's ambiguous for
44
-     * fields that COULD contain -1; so we use null
45
-     */
46
-    const EE_INF_IN_REST = null;
47
-
48
-
49
-    /**
50
-     * Prepares a possible array of input values from JSON for use by the models
51
-     *
52
-     * @param EE_Model_Field_Base $field_obj
53
-     * @param mixed               $original_value_maybe_array
54
-     * @param string              $requested_version
55
-     * @param string              $timezone_string treat values as being in this timezone
56
-     * @return mixed
57
-     * @throws RestException
58
-     */
59
-    public static function prepareFieldValuesFromJson(
60
-        $field_obj,
61
-        $original_value_maybe_array,
62
-        $requested_version,
63
-        $timezone_string = 'UTC'
64
-    ) {
65
-        if (is_array($original_value_maybe_array)
66
-            && ! $field_obj instanceof EE_Serialized_Text_Field
67
-        ) {
68
-            $new_value_maybe_array = array();
69
-            foreach ($original_value_maybe_array as $array_key => $array_item) {
70
-                $new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
71
-                    $field_obj,
72
-                    $array_item,
73
-                    $requested_version,
74
-                    $timezone_string
75
-                );
76
-            }
77
-        } else {
78
-            $new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
79
-                $field_obj,
80
-                $original_value_maybe_array,
81
-                $requested_version,
82
-                $timezone_string
83
-            );
84
-        }
85
-        return $new_value_maybe_array;
86
-    }
87
-
88
-
89
-    /**
90
-     * Prepares an array of field values FOR use in JSON/REST API
91
-     *
92
-     * @param EE_Model_Field_Base $field_obj
93
-     * @param mixed               $original_value_maybe_array
94
-     * @param string              $request_version (eg 4.8.36)
95
-     * @return array
96
-     */
97
-    public static function prepareFieldValuesForJson($field_obj, $original_value_maybe_array, $request_version)
98
-    {
99
-        if (is_array($original_value_maybe_array)) {
100
-            $new_value = array();
101
-            foreach ($original_value_maybe_array as $key => $value) {
102
-                $new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
103
-                    $field_obj,
104
-                    $value,
105
-                    $request_version
106
-                );
107
-            }
108
-        } else {
109
-            $new_value = ModelDataTranslator::prepareFieldValueForJson(
110
-                $field_obj,
111
-                $original_value_maybe_array,
112
-                $request_version
113
-            );
114
-        }
115
-        return $new_value;
116
-    }
117
-
118
-
119
-    /**
120
-     * Prepares incoming data from the json or $_REQUEST parameters for the models'
121
-     * "$query_params".
122
-     *
123
-     * @param EE_Model_Field_Base $field_obj
124
-     * @param mixed               $original_value
125
-     * @param string              $requested_version
126
-     * @param string              $timezone_string treat values as being in this timezone
127
-     * @return mixed
128
-     * @throws RestException
129
-     * @throws DomainException
130
-     * @throws EE_Error
131
-     */
132
-    public static function prepareFieldValueFromJson(
133
-        $field_obj,
134
-        $original_value,
135
-        $requested_version,
136
-        $timezone_string = 'UTC' // UTC
137
-    ) {
138
-        // check if they accidentally submitted an error value. If so throw an exception
139
-        if (is_array($original_value)
140
-            && isset($original_value['error_code'], $original_value['error_message'])) {
141
-            throw new RestException(
142
-                'rest_submitted_error_value',
143
-                sprintf(
144
-                    esc_html__(
145
-                        'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
146
-                        'event_espresso'
147
-                    ),
148
-                    $field_obj->get_name()
149
-                ),
150
-                array(
151
-                    'status' => 400,
152
-                )
153
-            );
154
-        }
155
-        // double-check for serialized PHP. We never accept serialized PHP. No way Jose.
156
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
157
-        $timezone_string = $timezone_string !== '' ? $timezone_string : get_option('timezone_string', '');
158
-        $new_value = null;
159
-        // walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
160
-        // way Jose.
161
-        ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
162
-        if ($field_obj instanceof EE_Infinite_Integer_Field
163
-            && in_array($original_value, array(null, ''), true)
164
-        ) {
165
-            $new_value = EE_INF;
166
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
167
-            $new_value = rest_parse_date(
168
-                self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
169
-            );
170
-            if ($new_value === false) {
171
-                throw new RestException(
172
-                    'invalid_format_for_timestamp',
173
-                    sprintf(
174
-                        esc_html__(
175
-                            'Timestamps received on a request as the value for Date and Time fields must be in %1$s/%2$s format.  The timestamp provided (%3$s) is not that format.',
176
-                            'event_espresso'
177
-                        ),
178
-                        'RFC3339',
179
-                        'ISO8601',
180
-                        $original_value
181
-                    ),
182
-                    array(
183
-                        'status' => 400,
184
-                    )
185
-                );
186
-            }
187
-        } elseif ($field_obj instanceof EE_Boolean_Field) {
188
-            // Interpreted the strings "false", "true", "on", "off" appropriately.
189
-            $new_value = filter_var($original_value, FILTER_VALIDATE_BOOLEAN);
190
-        } else {
191
-            $new_value = $original_value;
192
-        }
193
-        return $new_value;
194
-    }
195
-
196
-
197
-    /**
198
-     * This checks if the incoming timestamp has timezone information already on it and if it doesn't then adds timezone
199
-     * information via details obtained from the host site.
200
-     *
201
-     * @param string            $original_timestamp
202
-     * @param EE_Datetime_Field $datetime_field
203
-     * @param                   $timezone_string
204
-     * @return string
205
-     * @throws DomainException
206
-     */
207
-    private static function getTimestampWithTimezoneOffset(
208
-        $original_timestamp,
209
-        EE_Datetime_Field $datetime_field,
210
-        $timezone_string
211
-    ) {
212
-        // already have timezone information?
213
-        if (preg_match('/Z|(\+|\-)(\d{2}:\d{2})/', $original_timestamp)) {
214
-            // yes, we're ignoring the timezone.
215
-            return $original_timestamp;
216
-        }
217
-        // need to append timezone
218
-        list($offset_sign, $offset_secs) = self::parseTimezoneOffset(
219
-            $datetime_field->get_timezone_offset(
220
-                new \DateTimeZone($timezone_string),
221
-                $original_timestamp
222
-            )
223
-        );
224
-        $offset_string =
225
-            str_pad(
226
-                floor($offset_secs / HOUR_IN_SECONDS),
227
-                2,
228
-                '0',
229
-                STR_PAD_LEFT
230
-            )
231
-            . ':'
232
-            . str_pad(
233
-                ($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
234
-                2,
235
-                '0',
236
-                STR_PAD_LEFT
237
-            );
238
-        return $original_timestamp . $offset_sign . $offset_string;
239
-    }
240
-
241
-
242
-    /**
243
-     * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
244
-     * think that can happen). If $data is an array, recurses into its keys and values
245
-     *
246
-     * @param mixed $data
247
-     * @throws RestException
248
-     * @return void
249
-     */
250
-    public static function throwExceptionIfContainsSerializedData($data)
251
-    {
252
-        if (is_array($data)) {
253
-            foreach ($data as $key => $value) {
254
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
255
-                ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
256
-            }
257
-        } else {
258
-            if (is_serialized($data) || is_object($data)) {
259
-                throw new RestException(
260
-                    'serialized_data_submission_prohibited',
261
-                    esc_html__(
262
-                    // @codingStandardsIgnoreStart
263
-                        'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
264
-                        // @codingStandardsIgnoreEnd
265
-                        'event_espresso'
266
-                    )
267
-                );
268
-            }
269
-        }
270
-    }
271
-
272
-
273
-    /**
274
-     * determines what's going on with them timezone strings
275
-     *
276
-     * @param int $timezone_offset
277
-     * @return array
278
-     */
279
-    private static function parseTimezoneOffset($timezone_offset)
280
-    {
281
-        $first_char = substr((string) $timezone_offset, 0, 1);
282
-        if ($first_char === '+' || $first_char === '-') {
283
-            $offset_sign = $first_char;
284
-            $offset_secs = substr((string) $timezone_offset, 1);
285
-        } else {
286
-            $offset_sign = '+';
287
-            $offset_secs = $timezone_offset;
288
-        }
289
-        return array($offset_sign, $offset_secs);
290
-    }
291
-
292
-
293
-    /**
294
-     * Prepares a field's value for display in the API
295
-     *
296
-     * @param EE_Model_Field_Base $field_obj
297
-     * @param mixed               $original_value
298
-     * @param string              $requested_version
299
-     * @return mixed
300
-     */
301
-    public static function prepareFieldValueForJson($field_obj, $original_value, $requested_version)
302
-    {
303
-        if ($original_value === EE_INF) {
304
-            $new_value = ModelDataTranslator::EE_INF_IN_REST;
305
-        } elseif ($field_obj instanceof EE_Datetime_Field) {
306
-            if (is_string($original_value)) {
307
-                // did they submit a string of a unix timestamp?
308
-                if (is_numeric($original_value)) {
309
-                    $datetime_obj = new \DateTime();
310
-                    $datetime_obj->setTimestamp((int) $original_value);
311
-                } else {
312
-                    // first, check if its a MySQL timestamp in GMT
313
-                    $datetime_obj = \DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
314
-                }
315
-                if (! $datetime_obj instanceof \DateTime) {
316
-                    // so it's not a unix timestamp or a MySQL timestamp. Maybe its in the field's date/time format?
317
-                    $datetime_obj = $field_obj->prepare_for_set($original_value);
318
-                }
319
-                $original_value = $datetime_obj;
320
-            }
321
-            if ($original_value instanceof \DateTime) {
322
-                $new_value = $original_value->format('Y-m-d H:i:s');
323
-            } elseif (is_int($original_value) || is_float($original_value)) {
324
-                $new_value = date('Y-m-d H:i:s', $original_value);
325
-            } elseif ($original_value === null || $original_value === '') {
326
-                $new_value = null;
327
-            } else {
328
-                // so it's not a datetime object, unix timestamp (as string or int),
329
-                // MySQL timestamp, or even a string in the field object's format. So no idea what it is
330
-                throw new \EE_Error(
331
-                    sprintf(
332
-                        esc_html__(
333
-                        // @codingStandardsIgnoreStart
334
-                            'The value "%1$s" for the field "%2$s" on model "%3$s" could not be understood. It should be a PHP DateTime, unix timestamp, MySQL date, or string in the format "%4$s".',
335
-                            // @codingStandardsIgnoreEnd
336
-                            'event_espresso'
337
-                        ),
338
-                        $original_value,
339
-                        $field_obj->get_name(),
340
-                        $field_obj->get_model_name(),
341
-                        $field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
342
-                    )
343
-                );
344
-            }
345
-            if ($new_value !== null) {
346
-                $new_value = mysql_to_rfc3339($new_value);
347
-            }
348
-        } else {
349
-            $new_value = $original_value;
350
-        }
351
-        // are we about to send an object? just don't. We have no good way to represent it in JSON.
352
-        // can't just check using is_object() because that missed PHP incomplete objects
353
-        if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
354
-            $new_value = array(
355
-                'error_code'    => 'php_object_not_return',
356
-                'error_message' => esc_html__(
357
-                    'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
358
-                    'event_espresso'
359
-                ),
360
-            );
361
-        }
362
-        return apply_filters(
363
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
364
-            $new_value,
365
-            $field_obj,
366
-            $original_value,
367
-            $requested_version
368
-        );
369
-    }
370
-
371
-
372
-    /**
373
-     * Prepares condition-query-parameters (like what's in where and having) from
374
-     * the format expected in the API to use in the models
375
-     *
376
-     * @param array $inputted_query_params_of_this_type
377
-     * @param EEM_Base $model
378
-     * @param string $requested_version
379
-     * @param boolean $writing whether this data will be written to the DB, or if we're just building a query.
380
-     *                          If we're writing to the DB, we don't expect any operators, or any logic query
381
-     *                          parameters, and we also won't accept serialized data unless the current user has
382
-     *                          unfiltered_html.
383
-     * @return array
384
-     * @throws DomainException
385
-     * @throws EE_Error
386
-     * @throws RestException
387
-     * @throws InvalidDataTypeException
388
-     * @throws InvalidInterfaceException
389
-     * @throws InvalidArgumentException
390
-     */
391
-    public static function prepareConditionsQueryParamsForModels(
392
-        $inputted_query_params_of_this_type,
393
-        EEM_Base $model,
394
-        $requested_version,
395
-        $writing = false
396
-    ) {
397
-        $query_param_for_models = array();
398
-        $context = new RestIncomingQueryParamContext($model, $requested_version, $writing);
399
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
400
-            $query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
401
-            if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
402
-                $translated_value = $query_param_meta->determineConditionsQueryParameterValue();
403
-                if ((isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ]) && $query_param_meta->isGmtField())
404
-                    || $translated_value === null
405
-                ) {
406
-                    // they have already provided a non-gmt field, ignore the gmt one. That's what WP core
407
-                    // currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
408
-                    // OR we couldn't create a translated value from their input
409
-                    continue;
410
-                }
411
-                $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
412
-            } else {
413
-                $nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
414
-                if ($nested_query_params) {
415
-                    $query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
416
-                }
417
-            }
418
-        }
419
-        return $query_param_for_models;
420
-    }
421
-
422
-    /**
423
-     * Mostly checks if the last 4 characters are "_gmt", indicating its a
424
-     * gmt date field name
425
-     *
426
-     * @param string $field_name
427
-     * @return boolean
428
-     */
429
-    public static function isGmtDateFieldName($field_name)
430
-    {
431
-        return substr(
432
-            ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name),
433
-            -4,
434
-            4
435
-        ) === '_gmt';
436
-    }
437
-
438
-
439
-    /**
440
-     * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
441
-     *
442
-     * @param string $field_name
443
-     * @return string
444
-     */
445
-    public static function removeGmtFromFieldName($field_name)
446
-    {
447
-        if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
448
-            return $field_name;
449
-        }
450
-        $query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
451
-            $field_name
452
-        );
453
-        $query_param_sans_gmt_and_sans_stars = substr(
454
-            $query_param_sans_stars,
455
-            0,
456
-            strrpos(
457
-                $field_name,
458
-                '_gmt'
459
-            )
460
-        );
461
-        return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
462
-    }
463
-
464
-
465
-    /**
466
-     * Takes a field name from the REST API and prepares it for the model querying
467
-     *
468
-     * @param string $field_name
469
-     * @return string
470
-     */
471
-    public static function prepareFieldNameFromJson($field_name)
472
-    {
473
-        if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
474
-            return ModelDataTranslator::removeGmtFromFieldName($field_name);
475
-        }
476
-        return $field_name;
477
-    }
478
-
479
-
480
-    /**
481
-     * Takes array of field names from REST API and prepares for models
482
-     *
483
-     * @param array $field_names
484
-     * @return array of field names (possibly include model prefixes)
485
-     */
486
-    public static function prepareFieldNamesFromJson(array $field_names)
487
-    {
488
-        $new_array = array();
489
-        foreach ($field_names as $key => $field_name) {
490
-            $new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
491
-        }
492
-        return $new_array;
493
-    }
494
-
495
-
496
-    /**
497
-     * Takes array where array keys are field names (possibly with model path prefixes)
498
-     * from the REST API and prepares them for model querying
499
-     *
500
-     * @param array $field_names_as_keys
501
-     * @return array
502
-     */
503
-    public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys)
504
-    {
505
-        $new_array = array();
506
-        foreach ($field_names_as_keys as $field_name => $value) {
507
-            $new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
508
-        }
509
-        return $new_array;
510
-    }
511
-
512
-
513
-    /**
514
-     * Prepares an array of model query params for use in the REST API
515
-     *
516
-     * @param array    $model_query_params
517
-     * @param EEM_Base $model
518
-     * @param string   $requested_version  eg "4.8.36". If null is provided, defaults to the latest release of the EE4
519
-     *                                     REST API
520
-     * @return array which can be passed into the EE4 REST API when querying a model resource
521
-     * @throws EE_Error
522
-     */
523
-    public static function prepareQueryParamsForRestApi(
524
-        array $model_query_params,
525
-        EEM_Base $model,
526
-        $requested_version = null
527
-    ) {
528
-        if ($requested_version === null) {
529
-            $requested_version = EED_Core_Rest_Api::latest_rest_api_version();
530
-        }
531
-        $rest_query_params = $model_query_params;
532
-        if (isset($model_query_params[0])) {
533
-            $rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
534
-                $model_query_params[0],
535
-                $model,
536
-                $requested_version
537
-            );
538
-            unset($rest_query_params[0]);
539
-        }
540
-        if (isset($model_query_params['having'])) {
541
-            $rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
542
-                $model_query_params['having'],
543
-                $model,
544
-                $requested_version
545
-            );
546
-        }
547
-        return apply_filters(
548
-            'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
549
-            $rest_query_params,
550
-            $model_query_params,
551
-            $model,
552
-            $requested_version
553
-        );
554
-    }
555
-
556
-
557
-    /**
558
-     * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
559
-     *
560
-     * @param array    $inputted_query_params_of_this_type  eg like the "where" or "having" conditions query params
561
-     *                                                      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
562
-     * @param EEM_Base $model
563
-     * @param string   $requested_version                   eg "4.8.36"
564
-     * @return array ready for use in the rest api query params
565
-     * @throws EE_Error
566
-     * @throws ObjectDetectedException if somehow a PHP object were in the query params' values,
567
-     *                                                      (which would be really unusual)
568
-     */
569
-    public static function prepareConditionsQueryParamsForRestApi(
570
-        $inputted_query_params_of_this_type,
571
-        EEM_Base $model,
572
-        $requested_version
573
-    ) {
574
-        $query_param_for_models = array();
575
-        foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
576
-            $field = ModelDataTranslator::deduceFieldFromQueryParam(
577
-                ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
578
-                $model
579
-            );
580
-            if ($field instanceof EE_Model_Field_Base) {
581
-                // did they specify an operator?
582
-                if (is_array($query_param_value)) {
583
-                    $op = $query_param_value[0];
584
-                    $translated_value = array($op);
585
-                    if (isset($query_param_value[1])) {
586
-                        $value = $query_param_value[1];
587
-                        $translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
588
-                            $field,
589
-                            $value,
590
-                            $requested_version
591
-                        );
592
-                    }
593
-                } else {
594
-                    $translated_value = ModelDataTranslator::prepareFieldValueForJson(
595
-                        $field,
596
-                        $query_param_value,
597
-                        $requested_version
598
-                    );
599
-                }
600
-                $query_param_for_models[ $query_param_key ] = $translated_value;
601
-            } else {
602
-                // so it's not for a field, assume it's a logic query param key
603
-                $query_param_for_models[ $query_param_key ] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
604
-                    $query_param_value,
605
-                    $model,
606
-                    $requested_version
607
-                );
608
-            }
609
-        }
610
-        return $query_param_for_models;
611
-    }
612
-
613
-
614
-    /**
615
-     * @param $condition_query_param_key
616
-     * @return string
617
-     */
618
-    public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key)
619
-    {
620
-        $pos_of_star = strpos($condition_query_param_key, '*');
621
-        if ($pos_of_star === false) {
622
-            return $condition_query_param_key;
623
-        } else {
624
-            $condition_query_param_sans_star = substr($condition_query_param_key, 0, $pos_of_star);
625
-            return $condition_query_param_sans_star;
626
-        }
627
-    }
628
-
629
-
630
-    /**
631
-     * Takes the input parameter and finds the model field that it indicates.
632
-     *
633
-     * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
634
-     * @param EEM_Base $model
635
-     * @return EE_Model_Field_Base
636
-     * @throws EE_Error
637
-     */
638
-    public static function deduceFieldFromQueryParam($query_param_name, EEM_Base $model)
639
-    {
640
-        // ok, now proceed with deducing which part is the model's name, and which is the field's name
641
-        // which will help us find the database table and column
642
-        $query_param_parts = explode('.', $query_param_name);
643
-        if (empty($query_param_parts)) {
644
-            throw new EE_Error(
645
-                sprintf(
646
-                    __(
647
-                        '_extract_column_name is empty when trying to extract column and table name from %s',
648
-                        'event_espresso'
649
-                    ),
650
-                    $query_param_name
651
-                )
652
-            );
653
-        }
654
-        $number_of_parts = count($query_param_parts);
655
-        $last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
656
-        if ($number_of_parts === 1) {
657
-            $field_name = $last_query_param_part;
658
-        } else {// $number_of_parts >= 2
659
-            // the last part is the column name, and there are only 2parts. therefore...
660
-            $field_name = $last_query_param_part;
661
-            $model = \EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
662
-        }
663
-        try {
664
-            return $model->field_settings_for($field_name, false);
665
-        } catch (EE_Error $e) {
666
-            return null;
667
-        }
668
-    }
669
-
670
-
671
-    /**
672
-     * Returns true if $data can be easily represented in JSON.
673
-     * Basically, objects and resources can't be represented in JSON easily.
674
-     *
675
-     * @param mixed $data
676
-     * @return bool
677
-     */
678
-    protected static function isRepresentableInJson($data)
679
-    {
680
-        return is_scalar($data)
681
-               || is_array($data)
682
-               || is_null($data);
683
-    }
42
+	/**
43
+	 * We used to use -1 for infinity in the rest api, but that's ambiguous for
44
+	 * fields that COULD contain -1; so we use null
45
+	 */
46
+	const EE_INF_IN_REST = null;
47
+
48
+
49
+	/**
50
+	 * Prepares a possible array of input values from JSON for use by the models
51
+	 *
52
+	 * @param EE_Model_Field_Base $field_obj
53
+	 * @param mixed               $original_value_maybe_array
54
+	 * @param string              $requested_version
55
+	 * @param string              $timezone_string treat values as being in this timezone
56
+	 * @return mixed
57
+	 * @throws RestException
58
+	 */
59
+	public static function prepareFieldValuesFromJson(
60
+		$field_obj,
61
+		$original_value_maybe_array,
62
+		$requested_version,
63
+		$timezone_string = 'UTC'
64
+	) {
65
+		if (is_array($original_value_maybe_array)
66
+			&& ! $field_obj instanceof EE_Serialized_Text_Field
67
+		) {
68
+			$new_value_maybe_array = array();
69
+			foreach ($original_value_maybe_array as $array_key => $array_item) {
70
+				$new_value_maybe_array[ $array_key ] = ModelDataTranslator::prepareFieldValueFromJson(
71
+					$field_obj,
72
+					$array_item,
73
+					$requested_version,
74
+					$timezone_string
75
+				);
76
+			}
77
+		} else {
78
+			$new_value_maybe_array = ModelDataTranslator::prepareFieldValueFromJson(
79
+				$field_obj,
80
+				$original_value_maybe_array,
81
+				$requested_version,
82
+				$timezone_string
83
+			);
84
+		}
85
+		return $new_value_maybe_array;
86
+	}
87
+
88
+
89
+	/**
90
+	 * Prepares an array of field values FOR use in JSON/REST API
91
+	 *
92
+	 * @param EE_Model_Field_Base $field_obj
93
+	 * @param mixed               $original_value_maybe_array
94
+	 * @param string              $request_version (eg 4.8.36)
95
+	 * @return array
96
+	 */
97
+	public static function prepareFieldValuesForJson($field_obj, $original_value_maybe_array, $request_version)
98
+	{
99
+		if (is_array($original_value_maybe_array)) {
100
+			$new_value = array();
101
+			foreach ($original_value_maybe_array as $key => $value) {
102
+				$new_value[ $key ] = ModelDataTranslator::prepareFieldValuesForJson(
103
+					$field_obj,
104
+					$value,
105
+					$request_version
106
+				);
107
+			}
108
+		} else {
109
+			$new_value = ModelDataTranslator::prepareFieldValueForJson(
110
+				$field_obj,
111
+				$original_value_maybe_array,
112
+				$request_version
113
+			);
114
+		}
115
+		return $new_value;
116
+	}
117
+
118
+
119
+	/**
120
+	 * Prepares incoming data from the json or $_REQUEST parameters for the models'
121
+	 * "$query_params".
122
+	 *
123
+	 * @param EE_Model_Field_Base $field_obj
124
+	 * @param mixed               $original_value
125
+	 * @param string              $requested_version
126
+	 * @param string              $timezone_string treat values as being in this timezone
127
+	 * @return mixed
128
+	 * @throws RestException
129
+	 * @throws DomainException
130
+	 * @throws EE_Error
131
+	 */
132
+	public static function prepareFieldValueFromJson(
133
+		$field_obj,
134
+		$original_value,
135
+		$requested_version,
136
+		$timezone_string = 'UTC' // UTC
137
+	) {
138
+		// check if they accidentally submitted an error value. If so throw an exception
139
+		if (is_array($original_value)
140
+			&& isset($original_value['error_code'], $original_value['error_message'])) {
141
+			throw new RestException(
142
+				'rest_submitted_error_value',
143
+				sprintf(
144
+					esc_html__(
145
+						'You tried to submit a JSON error object as a value for %1$s. That\'s not allowed.',
146
+						'event_espresso'
147
+					),
148
+					$field_obj->get_name()
149
+				),
150
+				array(
151
+					'status' => 400,
152
+				)
153
+			);
154
+		}
155
+		// double-check for serialized PHP. We never accept serialized PHP. No way Jose.
156
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
157
+		$timezone_string = $timezone_string !== '' ? $timezone_string : get_option('timezone_string', '');
158
+		$new_value = null;
159
+		// walk through the submitted data and double-check for serialized PHP. We never accept serialized PHP. No
160
+		// way Jose.
161
+		ModelDataTranslator::throwExceptionIfContainsSerializedData($original_value);
162
+		if ($field_obj instanceof EE_Infinite_Integer_Field
163
+			&& in_array($original_value, array(null, ''), true)
164
+		) {
165
+			$new_value = EE_INF;
166
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
167
+			$new_value = rest_parse_date(
168
+				self::getTimestampWithTimezoneOffset($original_value, $field_obj, $timezone_string)
169
+			);
170
+			if ($new_value === false) {
171
+				throw new RestException(
172
+					'invalid_format_for_timestamp',
173
+					sprintf(
174
+						esc_html__(
175
+							'Timestamps received on a request as the value for Date and Time fields must be in %1$s/%2$s format.  The timestamp provided (%3$s) is not that format.',
176
+							'event_espresso'
177
+						),
178
+						'RFC3339',
179
+						'ISO8601',
180
+						$original_value
181
+					),
182
+					array(
183
+						'status' => 400,
184
+					)
185
+				);
186
+			}
187
+		} elseif ($field_obj instanceof EE_Boolean_Field) {
188
+			// Interpreted the strings "false", "true", "on", "off" appropriately.
189
+			$new_value = filter_var($original_value, FILTER_VALIDATE_BOOLEAN);
190
+		} else {
191
+			$new_value = $original_value;
192
+		}
193
+		return $new_value;
194
+	}
195
+
196
+
197
+	/**
198
+	 * This checks if the incoming timestamp has timezone information already on it and if it doesn't then adds timezone
199
+	 * information via details obtained from the host site.
200
+	 *
201
+	 * @param string            $original_timestamp
202
+	 * @param EE_Datetime_Field $datetime_field
203
+	 * @param                   $timezone_string
204
+	 * @return string
205
+	 * @throws DomainException
206
+	 */
207
+	private static function getTimestampWithTimezoneOffset(
208
+		$original_timestamp,
209
+		EE_Datetime_Field $datetime_field,
210
+		$timezone_string
211
+	) {
212
+		// already have timezone information?
213
+		if (preg_match('/Z|(\+|\-)(\d{2}:\d{2})/', $original_timestamp)) {
214
+			// yes, we're ignoring the timezone.
215
+			return $original_timestamp;
216
+		}
217
+		// need to append timezone
218
+		list($offset_sign, $offset_secs) = self::parseTimezoneOffset(
219
+			$datetime_field->get_timezone_offset(
220
+				new \DateTimeZone($timezone_string),
221
+				$original_timestamp
222
+			)
223
+		);
224
+		$offset_string =
225
+			str_pad(
226
+				floor($offset_secs / HOUR_IN_SECONDS),
227
+				2,
228
+				'0',
229
+				STR_PAD_LEFT
230
+			)
231
+			. ':'
232
+			. str_pad(
233
+				($offset_secs % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS,
234
+				2,
235
+				'0',
236
+				STR_PAD_LEFT
237
+			);
238
+		return $original_timestamp . $offset_sign . $offset_string;
239
+	}
240
+
241
+
242
+	/**
243
+	 * Throws an exception if $data is a serialized PHP string (or somehow an actually PHP object, although I don't
244
+	 * think that can happen). If $data is an array, recurses into its keys and values
245
+	 *
246
+	 * @param mixed $data
247
+	 * @throws RestException
248
+	 * @return void
249
+	 */
250
+	public static function throwExceptionIfContainsSerializedData($data)
251
+	{
252
+		if (is_array($data)) {
253
+			foreach ($data as $key => $value) {
254
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($key);
255
+				ModelDataTranslator::throwExceptionIfContainsSerializedData($value);
256
+			}
257
+		} else {
258
+			if (is_serialized($data) || is_object($data)) {
259
+				throw new RestException(
260
+					'serialized_data_submission_prohibited',
261
+					esc_html__(
262
+					// @codingStandardsIgnoreStart
263
+						'You tried to submit a string of serialized text. Serialized PHP is prohibited over the EE4 REST API.',
264
+						// @codingStandardsIgnoreEnd
265
+						'event_espresso'
266
+					)
267
+				);
268
+			}
269
+		}
270
+	}
271
+
272
+
273
+	/**
274
+	 * determines what's going on with them timezone strings
275
+	 *
276
+	 * @param int $timezone_offset
277
+	 * @return array
278
+	 */
279
+	private static function parseTimezoneOffset($timezone_offset)
280
+	{
281
+		$first_char = substr((string) $timezone_offset, 0, 1);
282
+		if ($first_char === '+' || $first_char === '-') {
283
+			$offset_sign = $first_char;
284
+			$offset_secs = substr((string) $timezone_offset, 1);
285
+		} else {
286
+			$offset_sign = '+';
287
+			$offset_secs = $timezone_offset;
288
+		}
289
+		return array($offset_sign, $offset_secs);
290
+	}
291
+
292
+
293
+	/**
294
+	 * Prepares a field's value for display in the API
295
+	 *
296
+	 * @param EE_Model_Field_Base $field_obj
297
+	 * @param mixed               $original_value
298
+	 * @param string              $requested_version
299
+	 * @return mixed
300
+	 */
301
+	public static function prepareFieldValueForJson($field_obj, $original_value, $requested_version)
302
+	{
303
+		if ($original_value === EE_INF) {
304
+			$new_value = ModelDataTranslator::EE_INF_IN_REST;
305
+		} elseif ($field_obj instanceof EE_Datetime_Field) {
306
+			if (is_string($original_value)) {
307
+				// did they submit a string of a unix timestamp?
308
+				if (is_numeric($original_value)) {
309
+					$datetime_obj = new \DateTime();
310
+					$datetime_obj->setTimestamp((int) $original_value);
311
+				} else {
312
+					// first, check if its a MySQL timestamp in GMT
313
+					$datetime_obj = \DateTime::createFromFormat('Y-m-d H:i:s', $original_value);
314
+				}
315
+				if (! $datetime_obj instanceof \DateTime) {
316
+					// so it's not a unix timestamp or a MySQL timestamp. Maybe its in the field's date/time format?
317
+					$datetime_obj = $field_obj->prepare_for_set($original_value);
318
+				}
319
+				$original_value = $datetime_obj;
320
+			}
321
+			if ($original_value instanceof \DateTime) {
322
+				$new_value = $original_value->format('Y-m-d H:i:s');
323
+			} elseif (is_int($original_value) || is_float($original_value)) {
324
+				$new_value = date('Y-m-d H:i:s', $original_value);
325
+			} elseif ($original_value === null || $original_value === '') {
326
+				$new_value = null;
327
+			} else {
328
+				// so it's not a datetime object, unix timestamp (as string or int),
329
+				// MySQL timestamp, or even a string in the field object's format. So no idea what it is
330
+				throw new \EE_Error(
331
+					sprintf(
332
+						esc_html__(
333
+						// @codingStandardsIgnoreStart
334
+							'The value "%1$s" for the field "%2$s" on model "%3$s" could not be understood. It should be a PHP DateTime, unix timestamp, MySQL date, or string in the format "%4$s".',
335
+							// @codingStandardsIgnoreEnd
336
+							'event_espresso'
337
+						),
338
+						$original_value,
339
+						$field_obj->get_name(),
340
+						$field_obj->get_model_name(),
341
+						$field_obj->get_time_format() . ' ' . $field_obj->get_time_format()
342
+					)
343
+				);
344
+			}
345
+			if ($new_value !== null) {
346
+				$new_value = mysql_to_rfc3339($new_value);
347
+			}
348
+		} else {
349
+			$new_value = $original_value;
350
+		}
351
+		// are we about to send an object? just don't. We have no good way to represent it in JSON.
352
+		// can't just check using is_object() because that missed PHP incomplete objects
353
+		if (! ModelDataTranslator::isRepresentableInJson($new_value)) {
354
+			$new_value = array(
355
+				'error_code'    => 'php_object_not_return',
356
+				'error_message' => esc_html__(
357
+					'The value of this field in the database is a PHP object, which can\'t be represented in JSON.',
358
+					'event_espresso'
359
+				),
360
+			);
361
+		}
362
+		return apply_filters(
363
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_field_for_rest_api',
364
+			$new_value,
365
+			$field_obj,
366
+			$original_value,
367
+			$requested_version
368
+		);
369
+	}
370
+
371
+
372
+	/**
373
+	 * Prepares condition-query-parameters (like what's in where and having) from
374
+	 * the format expected in the API to use in the models
375
+	 *
376
+	 * @param array $inputted_query_params_of_this_type
377
+	 * @param EEM_Base $model
378
+	 * @param string $requested_version
379
+	 * @param boolean $writing whether this data will be written to the DB, or if we're just building a query.
380
+	 *                          If we're writing to the DB, we don't expect any operators, or any logic query
381
+	 *                          parameters, and we also won't accept serialized data unless the current user has
382
+	 *                          unfiltered_html.
383
+	 * @return array
384
+	 * @throws DomainException
385
+	 * @throws EE_Error
386
+	 * @throws RestException
387
+	 * @throws InvalidDataTypeException
388
+	 * @throws InvalidInterfaceException
389
+	 * @throws InvalidArgumentException
390
+	 */
391
+	public static function prepareConditionsQueryParamsForModels(
392
+		$inputted_query_params_of_this_type,
393
+		EEM_Base $model,
394
+		$requested_version,
395
+		$writing = false
396
+	) {
397
+		$query_param_for_models = array();
398
+		$context = new RestIncomingQueryParamContext($model, $requested_version, $writing);
399
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
400
+			$query_param_meta = new RestIncomingQueryParamMetadata($query_param_key, $query_param_value, $context);
401
+			if ($query_param_meta->getField() instanceof EE_Model_Field_Base) {
402
+				$translated_value = $query_param_meta->determineConditionsQueryParameterValue();
403
+				if ((isset($query_param_for_models[ $query_param_meta->getQueryParamKey() ]) && $query_param_meta->isGmtField())
404
+					|| $translated_value === null
405
+				) {
406
+					// they have already provided a non-gmt field, ignore the gmt one. That's what WP core
407
+					// currently does (they might change it though). See https://core.trac.wordpress.org/ticket/39954
408
+					// OR we couldn't create a translated value from their input
409
+					continue;
410
+				}
411
+				$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $translated_value;
412
+			} else {
413
+				$nested_query_params = $query_param_meta->determineNestedConditionQueryParameters();
414
+				if ($nested_query_params) {
415
+					$query_param_for_models[ $query_param_meta->getQueryParamKey() ] = $nested_query_params;
416
+				}
417
+			}
418
+		}
419
+		return $query_param_for_models;
420
+	}
421
+
422
+	/**
423
+	 * Mostly checks if the last 4 characters are "_gmt", indicating its a
424
+	 * gmt date field name
425
+	 *
426
+	 * @param string $field_name
427
+	 * @return boolean
428
+	 */
429
+	public static function isGmtDateFieldName($field_name)
430
+	{
431
+		return substr(
432
+			ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($field_name),
433
+			-4,
434
+			4
435
+		) === '_gmt';
436
+	}
437
+
438
+
439
+	/**
440
+	 * Removes the last "_gmt" part of a field name (and if there is no "_gmt" at the end, leave it alone)
441
+	 *
442
+	 * @param string $field_name
443
+	 * @return string
444
+	 */
445
+	public static function removeGmtFromFieldName($field_name)
446
+	{
447
+		if (! ModelDataTranslator::isGmtDateFieldName($field_name)) {
448
+			return $field_name;
449
+		}
450
+		$query_param_sans_stars = ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey(
451
+			$field_name
452
+		);
453
+		$query_param_sans_gmt_and_sans_stars = substr(
454
+			$query_param_sans_stars,
455
+			0,
456
+			strrpos(
457
+				$field_name,
458
+				'_gmt'
459
+			)
460
+		);
461
+		return str_replace($query_param_sans_stars, $query_param_sans_gmt_and_sans_stars, $field_name);
462
+	}
463
+
464
+
465
+	/**
466
+	 * Takes a field name from the REST API and prepares it for the model querying
467
+	 *
468
+	 * @param string $field_name
469
+	 * @return string
470
+	 */
471
+	public static function prepareFieldNameFromJson($field_name)
472
+	{
473
+		if (ModelDataTranslator::isGmtDateFieldName($field_name)) {
474
+			return ModelDataTranslator::removeGmtFromFieldName($field_name);
475
+		}
476
+		return $field_name;
477
+	}
478
+
479
+
480
+	/**
481
+	 * Takes array of field names from REST API and prepares for models
482
+	 *
483
+	 * @param array $field_names
484
+	 * @return array of field names (possibly include model prefixes)
485
+	 */
486
+	public static function prepareFieldNamesFromJson(array $field_names)
487
+	{
488
+		$new_array = array();
489
+		foreach ($field_names as $key => $field_name) {
490
+			$new_array[ $key ] = ModelDataTranslator::prepareFieldNameFromJson($field_name);
491
+		}
492
+		return $new_array;
493
+	}
494
+
495
+
496
+	/**
497
+	 * Takes array where array keys are field names (possibly with model path prefixes)
498
+	 * from the REST API and prepares them for model querying
499
+	 *
500
+	 * @param array $field_names_as_keys
501
+	 * @return array
502
+	 */
503
+	public static function prepareFieldNamesInArrayKeysFromJson(array $field_names_as_keys)
504
+	{
505
+		$new_array = array();
506
+		foreach ($field_names_as_keys as $field_name => $value) {
507
+			$new_array[ ModelDataTranslator::prepareFieldNameFromJson($field_name) ] = $value;
508
+		}
509
+		return $new_array;
510
+	}
511
+
512
+
513
+	/**
514
+	 * Prepares an array of model query params for use in the REST API
515
+	 *
516
+	 * @param array    $model_query_params
517
+	 * @param EEM_Base $model
518
+	 * @param string   $requested_version  eg "4.8.36". If null is provided, defaults to the latest release of the EE4
519
+	 *                                     REST API
520
+	 * @return array which can be passed into the EE4 REST API when querying a model resource
521
+	 * @throws EE_Error
522
+	 */
523
+	public static function prepareQueryParamsForRestApi(
524
+		array $model_query_params,
525
+		EEM_Base $model,
526
+		$requested_version = null
527
+	) {
528
+		if ($requested_version === null) {
529
+			$requested_version = EED_Core_Rest_Api::latest_rest_api_version();
530
+		}
531
+		$rest_query_params = $model_query_params;
532
+		if (isset($model_query_params[0])) {
533
+			$rest_query_params['where'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
534
+				$model_query_params[0],
535
+				$model,
536
+				$requested_version
537
+			);
538
+			unset($rest_query_params[0]);
539
+		}
540
+		if (isset($model_query_params['having'])) {
541
+			$rest_query_params['having'] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
542
+				$model_query_params['having'],
543
+				$model,
544
+				$requested_version
545
+			);
546
+		}
547
+		return apply_filters(
548
+			'FHEE__EventEspresso\core\libraries\rest_api\Model_Data_Translator__prepare_query_params_for_rest_api',
549
+			$rest_query_params,
550
+			$model_query_params,
551
+			$model,
552
+			$requested_version
553
+		);
554
+	}
555
+
556
+
557
+	/**
558
+	 * Prepares all the sub-conditions query parameters (eg having or where conditions) for use in the rest api
559
+	 *
560
+	 * @param array    $inputted_query_params_of_this_type  eg like the "where" or "having" conditions query params
561
+	 *                                                      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
562
+	 * @param EEM_Base $model
563
+	 * @param string   $requested_version                   eg "4.8.36"
564
+	 * @return array ready for use in the rest api query params
565
+	 * @throws EE_Error
566
+	 * @throws ObjectDetectedException if somehow a PHP object were in the query params' values,
567
+	 *                                                      (which would be really unusual)
568
+	 */
569
+	public static function prepareConditionsQueryParamsForRestApi(
570
+		$inputted_query_params_of_this_type,
571
+		EEM_Base $model,
572
+		$requested_version
573
+	) {
574
+		$query_param_for_models = array();
575
+		foreach ($inputted_query_params_of_this_type as $query_param_key => $query_param_value) {
576
+			$field = ModelDataTranslator::deduceFieldFromQueryParam(
577
+				ModelDataTranslator::removeStarsAndAnythingAfterFromConditionQueryParamKey($query_param_key),
578
+				$model
579
+			);
580
+			if ($field instanceof EE_Model_Field_Base) {
581
+				// did they specify an operator?
582
+				if (is_array($query_param_value)) {
583
+					$op = $query_param_value[0];
584
+					$translated_value = array($op);
585
+					if (isset($query_param_value[1])) {
586
+						$value = $query_param_value[1];
587
+						$translated_value[1] = ModelDataTranslator::prepareFieldValuesForJson(
588
+							$field,
589
+							$value,
590
+							$requested_version
591
+						);
592
+					}
593
+				} else {
594
+					$translated_value = ModelDataTranslator::prepareFieldValueForJson(
595
+						$field,
596
+						$query_param_value,
597
+						$requested_version
598
+					);
599
+				}
600
+				$query_param_for_models[ $query_param_key ] = $translated_value;
601
+			} else {
602
+				// so it's not for a field, assume it's a logic query param key
603
+				$query_param_for_models[ $query_param_key ] = ModelDataTranslator::prepareConditionsQueryParamsForRestApi(
604
+					$query_param_value,
605
+					$model,
606
+					$requested_version
607
+				);
608
+			}
609
+		}
610
+		return $query_param_for_models;
611
+	}
612
+
613
+
614
+	/**
615
+	 * @param $condition_query_param_key
616
+	 * @return string
617
+	 */
618
+	public static function removeStarsAndAnythingAfterFromConditionQueryParamKey($condition_query_param_key)
619
+	{
620
+		$pos_of_star = strpos($condition_query_param_key, '*');
621
+		if ($pos_of_star === false) {
622
+			return $condition_query_param_key;
623
+		} else {
624
+			$condition_query_param_sans_star = substr($condition_query_param_key, 0, $pos_of_star);
625
+			return $condition_query_param_sans_star;
626
+		}
627
+	}
628
+
629
+
630
+	/**
631
+	 * Takes the input parameter and finds the model field that it indicates.
632
+	 *
633
+	 * @param string   $query_param_name like Registration.Transaction.TXN_ID, Event.Datetime.start_time, or REG_ID
634
+	 * @param EEM_Base $model
635
+	 * @return EE_Model_Field_Base
636
+	 * @throws EE_Error
637
+	 */
638
+	public static function deduceFieldFromQueryParam($query_param_name, EEM_Base $model)
639
+	{
640
+		// ok, now proceed with deducing which part is the model's name, and which is the field's name
641
+		// which will help us find the database table and column
642
+		$query_param_parts = explode('.', $query_param_name);
643
+		if (empty($query_param_parts)) {
644
+			throw new EE_Error(
645
+				sprintf(
646
+					__(
647
+						'_extract_column_name is empty when trying to extract column and table name from %s',
648
+						'event_espresso'
649
+					),
650
+					$query_param_name
651
+				)
652
+			);
653
+		}
654
+		$number_of_parts = count($query_param_parts);
655
+		$last_query_param_part = $query_param_parts[ count($query_param_parts) - 1 ];
656
+		if ($number_of_parts === 1) {
657
+			$field_name = $last_query_param_part;
658
+		} else {// $number_of_parts >= 2
659
+			// the last part is the column name, and there are only 2parts. therefore...
660
+			$field_name = $last_query_param_part;
661
+			$model = \EE_Registry::instance()->load_model($query_param_parts[ $number_of_parts - 2 ]);
662
+		}
663
+		try {
664
+			return $model->field_settings_for($field_name, false);
665
+		} catch (EE_Error $e) {
666
+			return null;
667
+		}
668
+	}
669
+
670
+
671
+	/**
672
+	 * Returns true if $data can be easily represented in JSON.
673
+	 * Basically, objects and resources can't be represented in JSON easily.
674
+	 *
675
+	 * @param mixed $data
676
+	 * @return bool
677
+	 */
678
+	protected static function isRepresentableInJson($data)
679
+	{
680
+		return is_scalar($data)
681
+			   || is_array($data)
682
+			   || is_null($data);
683
+	}
684 684
 }
Please login to merge, or discard this patch.
core/db_models/EEM_Datetime.model.php 1 patch
Indentation   +658 added lines, -658 removed lines patch added patch discarded remove patch
@@ -9,662 +9,662 @@
 block discarded – undo
9 9
 class EEM_Datetime extends EEM_Soft_Delete_Base
10 10
 {
11 11
 
12
-    /**
13
-     * @var EEM_Datetime $_instance
14
-     */
15
-    protected static $_instance;
16
-
17
-
18
-    /**
19
-     * private constructor to prevent direct creation
20
-     *
21
-     * @param string $timezone A string representing the timezone we want to set for returned Date Time Strings
22
-     *                         (and any incoming timezone data that gets saved).
23
-     *                         Note this just sends the timezone info to the date time model field objects.
24
-     *                         Default is NULL
25
-     *                         (and will be assumed using the set timezone in the 'timezone_string' wp option)
26
-     * @throws EE_Error
27
-     * @throws InvalidArgumentException
28
-     * @throws InvalidArgumentException
29
-     */
30
-    protected function __construct($timezone)
31
-    {
32
-        $this->singular_item           = esc_html__('Datetime', 'event_espresso');
33
-        $this->plural_item             = esc_html__('Datetimes', 'event_espresso');
34
-        $this->_tables                 = array(
35
-            'Datetime' => new EE_Primary_Table('esp_datetime', 'DTT_ID'),
36
-        );
37
-        $this->_fields                 = array(
38
-            'Datetime' => array(
39
-                'DTT_ID'          => new EE_Primary_Key_Int_Field(
40
-                    'DTT_ID',
41
-                    esc_html__('Datetime ID', 'event_espresso')
42
-                ),
43
-                'EVT_ID'          => new EE_Foreign_Key_Int_Field(
44
-                    'EVT_ID',
45
-                    esc_html__('Event ID', 'event_espresso'),
46
-                    false,
47
-                    0,
48
-                    'Event'
49
-                ),
50
-                'DTT_name'        => new EE_Plain_Text_Field(
51
-                    'DTT_name',
52
-                    esc_html__('Datetime Name', 'event_espresso'),
53
-                    false,
54
-                    ''
55
-                ),
56
-                'DTT_description' => new EE_Post_Content_Field(
57
-                    'DTT_description',
58
-                    esc_html__('Description for Datetime', 'event_espresso'),
59
-                    false,
60
-                    ''
61
-                ),
62
-                'DTT_EVT_start'   => new EE_Datetime_Field(
63
-                    'DTT_EVT_start',
64
-                    esc_html__('Start time/date of Event', 'event_espresso'),
65
-                    false,
66
-                    EE_Datetime_Field::now,
67
-                    $timezone
68
-                ),
69
-                'DTT_EVT_end'     => new EE_Datetime_Field(
70
-                    'DTT_EVT_end',
71
-                    esc_html__('End time/date of Event', 'event_espresso'),
72
-                    false,
73
-                    EE_Datetime_Field::now,
74
-                    $timezone
75
-                ),
76
-                'DTT_reg_limit'   => new EE_Infinite_Integer_Field(
77
-                    'DTT_reg_limit',
78
-                    esc_html__('Registration Limit for this time', 'event_espresso'),
79
-                    true,
80
-                    EE_INF
81
-                ),
82
-                'DTT_sold'        => new EE_Integer_Field(
83
-                    'DTT_sold',
84
-                    esc_html__('How many sales for this Datetime that have occurred', 'event_espresso'),
85
-                    true,
86
-                    0
87
-                ),
88
-                'DTT_reserved'    => new EE_Integer_Field(
89
-                    'DTT_reserved',
90
-                    esc_html__('Quantity of tickets reserved, but not yet fully purchased', 'event_espresso'),
91
-                    false,
92
-                    0
93
-                ),
94
-                'DTT_is_primary'  => new EE_Boolean_Field(
95
-                    'DTT_is_primary',
96
-                    esc_html__('Flag indicating datetime is primary one for event', 'event_espresso'),
97
-                    false,
98
-                    false
99
-                ),
100
-                'DTT_order'       => new EE_Integer_Field(
101
-                    'DTT_order',
102
-                    esc_html__('The order in which the Datetime is displayed', 'event_espresso'),
103
-                    false,
104
-                    0
105
-                ),
106
-                'DTT_parent'      => new EE_Integer_Field(
107
-                    'DTT_parent',
108
-                    esc_html__('Indicates what DTT_ID is the parent of this DTT_ID', 'event_espresso'),
109
-                    true,
110
-                    0
111
-                ),
112
-                'DTT_deleted'     => new EE_Trashed_Flag_Field(
113
-                    'DTT_deleted',
114
-                    esc_html__('Flag indicating datetime is archived', 'event_espresso'),
115
-                    false,
116
-                    false
117
-                ),
118
-            ),
119
-        );
120
-        $this->_model_relations        = array(
121
-            'Ticket'  => new EE_HABTM_Relation('Datetime_Ticket'),
122
-            'Event'   => new EE_Belongs_To_Relation(),
123
-            'Checkin' => new EE_Has_Many_Relation(),
124
-            'Datetime_Ticket' => new EE_Has_Many_Relation(),
125
-        );
126
-        $path_to_event_model = 'Event';
127
-        $this->model_chain_to_password = $path_to_event_model;
128
-        $this->_model_chain_to_wp_user = $path_to_event_model;
129
-        // this model is generally available for reading
130
-        $this->_cap_restriction_generators[ EEM_Base::caps_read ]       = new EE_Restriction_Generator_Event_Related_Public(
131
-            $path_to_event_model
132
-        );
133
-        $this->_cap_restriction_generators[ EEM_Base::caps_read_admin ] = new EE_Restriction_Generator_Event_Related_Protected(
134
-            $path_to_event_model
135
-        );
136
-        $this->_cap_restriction_generators[ EEM_Base::caps_edit ]       = new EE_Restriction_Generator_Event_Related_Protected(
137
-            $path_to_event_model
138
-        );
139
-        $this->_cap_restriction_generators[ EEM_Base::caps_delete ]     = new EE_Restriction_Generator_Event_Related_Protected(
140
-            $path_to_event_model,
141
-            EEM_Base::caps_edit
142
-        );
143
-        parent::__construct($timezone);
144
-    }
145
-
146
-
147
-    /**
148
-     * create new blank datetime
149
-     *
150
-     * @access public
151
-     * @return EE_Datetime[] array on success, FALSE on fail
152
-     * @throws EE_Error
153
-     */
154
-    public function create_new_blank_datetime()
155
-    {
156
-        // makes sure timezone is always set.
157
-        $timezone_string = $this->get_timezone();
158
-        $blank_datetime  = EE_Datetime::new_instance(
159
-            array(
160
-                'DTT_EVT_start' => $this->current_time_for_query('DTT_EVT_start', true) + MONTH_IN_SECONDS,
161
-                'DTT_EVT_end'   => $this->current_time_for_query('DTT_EVT_end', true) + MONTH_IN_SECONDS,
162
-                'DTT_order'     => 1,
163
-                'DTT_reg_limit' => EE_INF,
164
-            ),
165
-            $timezone_string
166
-        );
167
-        $blank_datetime->set_start_time(
168
-            $this->convert_datetime_for_query(
169
-                'DTT_EVT_start',
170
-                '8am',
171
-                'ga',
172
-                $timezone_string
173
-            )
174
-        );
175
-        $blank_datetime->set_end_time(
176
-            $this->convert_datetime_for_query(
177
-                'DTT_EVT_end',
178
-                '5pm',
179
-                'ga',
180
-                $timezone_string
181
-            )
182
-        );
183
-        return array($blank_datetime);
184
-    }
185
-
186
-
187
-    /**
188
-     * get event start date from db
189
-     *
190
-     * @access public
191
-     * @param  int $EVT_ID
192
-     * @return EE_Datetime[] array on success, FALSE on fail
193
-     * @throws EE_Error
194
-     */
195
-    public function get_all_event_dates($EVT_ID = 0)
196
-    {
197
-        if (! $EVT_ID) { // on add_new_event event_id gets set to 0
198
-            return $this->create_new_blank_datetime();
199
-        }
200
-        $results = $this->get_datetimes_for_event_ordered_by_DTT_order($EVT_ID);
201
-        if (empty($results)) {
202
-            return $this->create_new_blank_datetime();
203
-        }
204
-        return $results;
205
-    }
206
-
207
-
208
-    /**
209
-     * get all datetimes attached to an event ordered by the DTT_order field
210
-     *
211
-     * @public
212
-     * @param  int    $EVT_ID     event id
213
-     * @param boolean $include_expired
214
-     * @param boolean $include_deleted
215
-     * @param  int    $limit      If included then limit the count of results by
216
-     *                            the given number
217
-     * @return EE_Datetime[]
218
-     * @throws EE_Error
219
-     */
220
-    public function get_datetimes_for_event_ordered_by_DTT_order(
221
-        $EVT_ID,
222
-        $include_expired = true,
223
-        $include_deleted = true,
224
-        $limit = null
225
-    ) {
226
-        // sanitize EVT_ID
227
-        $EVT_ID         = absint($EVT_ID);
228
-        $old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
229
-        $this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
230
-        $where_params = array('Event.EVT_ID' => $EVT_ID);
231
-        $query_params = ! empty($limit)
232
-            ? array(
233
-                $where_params,
234
-                'limit'                    => $limit,
235
-                'order_by'                 => array('DTT_order' => 'ASC'),
236
-                'default_where_conditions' => 'none',
237
-            )
238
-            : array(
239
-                $where_params,
240
-                'order_by'                 => array('DTT_order' => 'ASC'),
241
-                'default_where_conditions' => 'none',
242
-            );
243
-        if (! $include_expired) {
244
-            $query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
245
-        }
246
-        if ($include_deleted) {
247
-            $query_params[0]['DTT_deleted'] = array('IN', array(true, false));
248
-        }
249
-        /** @var EE_Datetime[] $result */
250
-        $result = $this->get_all($query_params);
251
-        $this->assume_values_already_prepared_by_model_object($old_assumption);
252
-        return $result;
253
-    }
254
-
255
-
256
-    /**
257
-     * Gets the datetimes for the event (with the given limit), and orders them by "importance".
258
-     * By importance, we mean that the primary datetimes are most important (DEPRECATED FOR NOW),
259
-     * and then the earlier datetimes are the most important.
260
-     * Maybe we'll want this to take into account datetimes that haven't already passed, but we don't yet.
261
-     *
262
-     * @param int $EVT_ID
263
-     * @param int $limit
264
-     * @return EE_Datetime[]|EE_Base_Class[]
265
-     * @throws EE_Error
266
-     */
267
-    public function get_datetimes_for_event_ordered_by_importance($EVT_ID = 0, $limit = null)
268
-    {
269
-        return $this->get_all(
270
-            array(
271
-                array('Event.EVT_ID' => $EVT_ID),
272
-                'limit'                    => $limit,
273
-                'order_by'                 => array('DTT_EVT_start' => 'ASC'),
274
-                'default_where_conditions' => 'none',
275
-            )
276
-        );
277
-    }
278
-
279
-
280
-    /**
281
-     * @param int     $EVT_ID
282
-     * @param boolean $include_expired
283
-     * @param boolean $include_deleted
284
-     * @return EE_Datetime
285
-     * @throws EE_Error
286
-     */
287
-    public function get_oldest_datetime_for_event($EVT_ID, $include_expired = false, $include_deleted = false)
288
-    {
289
-        $results = $this->get_datetimes_for_event_ordered_by_start_time(
290
-            $EVT_ID,
291
-            $include_expired,
292
-            $include_deleted,
293
-            1
294
-        );
295
-        if ($results) {
296
-            return array_shift($results);
297
-        }
298
-        return null;
299
-    }
300
-
301
-
302
-    /**
303
-     * Gets the 'primary' datetime for an event.
304
-     *
305
-     * @param int  $EVT_ID
306
-     * @param bool $try_to_exclude_expired
307
-     * @param bool $try_to_exclude_deleted
308
-     * @return \EE_Datetime
309
-     * @throws EE_Error
310
-     */
311
-    public function get_primary_datetime_for_event(
312
-        $EVT_ID,
313
-        $try_to_exclude_expired = true,
314
-        $try_to_exclude_deleted = true
315
-    ) {
316
-        if ($try_to_exclude_expired) {
317
-            $non_expired = $this->get_oldest_datetime_for_event($EVT_ID, false, false);
318
-            if ($non_expired) {
319
-                return $non_expired;
320
-            }
321
-        }
322
-        if ($try_to_exclude_deleted) {
323
-            $expired_even = $this->get_oldest_datetime_for_event($EVT_ID, true);
324
-            if ($expired_even) {
325
-                return $expired_even;
326
-            }
327
-        }
328
-        return $this->get_oldest_datetime_for_event($EVT_ID, true, true);
329
-    }
330
-
331
-
332
-    /**
333
-     * Gets ALL the datetimes for an event (including trashed ones, for now), ordered
334
-     * only by start date
335
-     *
336
-     * @param int     $EVT_ID
337
-     * @param boolean $include_expired
338
-     * @param boolean $include_deleted
339
-     * @param int     $limit
340
-     * @return EE_Datetime[]
341
-     * @throws EE_Error
342
-     */
343
-    public function get_datetimes_for_event_ordered_by_start_time(
344
-        $EVT_ID,
345
-        $include_expired = true,
346
-        $include_deleted = true,
347
-        $limit = null
348
-    ) {
349
-        // sanitize EVT_ID
350
-        $EVT_ID         = absint($EVT_ID);
351
-        $old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
352
-        $this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
353
-        $query_params = array(array('Event.EVT_ID' => $EVT_ID), 'order_by' => array('DTT_EVT_start' => 'asc'));
354
-        if (! $include_expired) {
355
-            $query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
356
-        }
357
-        if ($include_deleted) {
358
-            $query_params[0]['DTT_deleted'] = array('IN', array(true, false));
359
-        }
360
-        if ($limit) {
361
-            $query_params['limit'] = $limit;
362
-        }
363
-        /** @var EE_Datetime[] $result */
364
-        $result = $this->get_all($query_params);
365
-        $this->assume_values_already_prepared_by_model_object($old_assumption);
366
-        return $result;
367
-    }
368
-
369
-
370
-    /**
371
-     * Gets ALL the datetimes for an ticket (including trashed ones, for now), ordered
372
-     * only by start date
373
-     *
374
-     * @param int     $TKT_ID
375
-     * @param boolean $include_expired
376
-     * @param boolean $include_deleted
377
-     * @param int     $limit
378
-     * @return EE_Datetime[]
379
-     * @throws EE_Error
380
-     */
381
-    public function get_datetimes_for_ticket_ordered_by_start_time(
382
-        $TKT_ID,
383
-        $include_expired = true,
384
-        $include_deleted = true,
385
-        $limit = null
386
-    ) {
387
-        // sanitize TKT_ID
388
-        $TKT_ID         = absint($TKT_ID);
389
-        $old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
390
-        $this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
391
-        $query_params = array(array('Ticket.TKT_ID' => $TKT_ID), 'order_by' => array('DTT_EVT_start' => 'asc'));
392
-        if (! $include_expired) {
393
-            $query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
394
-        }
395
-        if ($include_deleted) {
396
-            $query_params[0]['DTT_deleted'] = array('IN', array(true, false));
397
-        }
398
-        if ($limit) {
399
-            $query_params['limit'] = $limit;
400
-        }
401
-        /** @var EE_Datetime[] $result */
402
-        $result = $this->get_all($query_params);
403
-        $this->assume_values_already_prepared_by_model_object($old_assumption);
404
-        return $result;
405
-    }
406
-
407
-
408
-    /**
409
-     * Gets all the datetimes for a ticket (including trashed ones, for now), ordered by the DTT_order for the
410
-     * datetimes.
411
-     *
412
-     * @param  int      $TKT_ID          ID of ticket to retrieve the datetimes for
413
-     * @param  boolean  $include_expired whether to include expired datetimes or not
414
-     * @param  boolean  $include_deleted whether to include trashed datetimes or not.
415
-     * @param  int|null $limit           if null, no limit, if int then limit results by
416
-     *                                   that number
417
-     * @return EE_Datetime[]
418
-     * @throws EE_Error
419
-     */
420
-    public function get_datetimes_for_ticket_ordered_by_DTT_order(
421
-        $TKT_ID,
422
-        $include_expired = true,
423
-        $include_deleted = true,
424
-        $limit = null
425
-    ) {
426
-        // sanitize id.
427
-        $TKT_ID         = absint($TKT_ID);
428
-        $old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
429
-        $this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
430
-        $where_params = array('Ticket.TKT_ID' => $TKT_ID);
431
-        $query_params = array($where_params, 'order_by' => array('DTT_order' => 'ASC'));
432
-        if (! $include_expired) {
433
-            $query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
434
-        }
435
-        if ($include_deleted) {
436
-            $query_params[0]['DTT_deleted'] = array('IN', array(true, false));
437
-        }
438
-        if ($limit) {
439
-            $query_params['limit'] = $limit;
440
-        }
441
-        /** @var EE_Datetime[] $result */
442
-        $result = $this->get_all($query_params);
443
-        $this->assume_values_already_prepared_by_model_object($old_assumption);
444
-        return $result;
445
-    }
446
-
447
-
448
-    /**
449
-     * Gets the most important datetime for a particular event (ie, the primary event usually. But if for some WACK
450
-     * reason it doesn't exist, we consider the earliest event the most important)
451
-     *
452
-     * @param int $EVT_ID
453
-     * @return EE_Datetime
454
-     * @throws EE_Error
455
-     */
456
-    public function get_most_important_datetime_for_event($EVT_ID)
457
-    {
458
-        $results = $this->get_datetimes_for_event_ordered_by_importance($EVT_ID, 1);
459
-        if ($results) {
460
-            return array_shift($results);
461
-        }
462
-        return null;
463
-    }
464
-
465
-
466
-    /**
467
-     * This returns a wpdb->results        Array of all DTT month and years matching the incoming query params and
468
-     * grouped by month and year.
469
-     *
470
-     * @param  array  $where_params      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
471
-     * @param  string $evt_active_status A string representing the evt active status to filter the months by.
472
-     *                                   Can be:
473
-     *                                   - '' = no filter
474
-     *                                   - upcoming = Published events with at least one upcoming datetime.
475
-     *                                   - expired = Events with all datetimes expired.
476
-     *                                   - active = Events that are published and have at least one datetime that
477
-     *                                   starts before now and ends after now.
478
-     *                                   - inactive = Events that are either not published.
479
-     * @return EE_Base_Class[]
480
-     * @throws EE_Error
481
-     * @throws InvalidArgumentException
482
-     * @throws InvalidArgumentException
483
-     */
484
-    public function get_dtt_months_and_years($where_params, $evt_active_status = '')
485
-    {
486
-        $current_time_for_DTT_EVT_start = $this->current_time_for_query('DTT_EVT_start');
487
-        $current_time_for_DTT_EVT_end   = $this->current_time_for_query('DTT_EVT_end');
488
-        switch ($evt_active_status) {
489
-            case 'upcoming':
490
-                $where_params['Event.status'] = 'publish';
491
-                // if there are already query_params matching DTT_EVT_start then we need to modify that to add them.
492
-                if (isset($where_params['DTT_EVT_start'])) {
493
-                    $where_params['DTT_EVT_start*****'] = $where_params['DTT_EVT_start'];
494
-                }
495
-                $where_params['DTT_EVT_start'] = array('>', $current_time_for_DTT_EVT_start);
496
-                break;
497
-            case 'expired':
498
-                if (isset($where_params['Event.status'])) {
499
-                    unset($where_params['Event.status']);
500
-                }
501
-                // get events to exclude
502
-                $exclude_query[0] = array_merge(
503
-                    $where_params,
504
-                    array('DTT_EVT_end' => array('>', $current_time_for_DTT_EVT_end))
505
-                );
506
-                // first get all events that have datetimes where its not expired.
507
-                $event_ids = $this->_get_all_wpdb_results(
508
-                    $exclude_query,
509
-                    OBJECT_K,
510
-                    'Datetime.EVT_ID'
511
-                );
512
-                $event_ids = array_keys($event_ids);
513
-                if (isset($where_params['DTT_EVT_end'])) {
514
-                    $where_params['DTT_EVT_end****'] = $where_params['DTT_EVT_end'];
515
-                }
516
-                $where_params['DTT_EVT_end']  = array('<', $current_time_for_DTT_EVT_end);
517
-                $where_params['Event.EVT_ID'] = array('NOT IN', $event_ids);
518
-                break;
519
-            case 'active':
520
-                $where_params['Event.status'] = 'publish';
521
-                if (isset($where_params['DTT_EVT_start'])) {
522
-                    $where_params['Datetime.DTT_EVT_start******'] = $where_params['DTT_EVT_start'];
523
-                }
524
-                if (isset($where_params['Datetime.DTT_EVT_end'])) {
525
-                    $where_params['Datetime.DTT_EVT_end*****'] = $where_params['DTT_EVT_end'];
526
-                }
527
-                $where_params['DTT_EVT_start'] = array('<', $current_time_for_DTT_EVT_start);
528
-                $where_params['DTT_EVT_end']   = array('>', $current_time_for_DTT_EVT_end);
529
-                break;
530
-            case 'inactive':
531
-                if (isset($where_params['Event.status'])) {
532
-                    unset($where_params['Event.status']);
533
-                }
534
-                if (isset($where_params['OR'])) {
535
-                    $where_params['AND']['OR'] = $where_params['OR'];
536
-                }
537
-                if (isset($where_params['DTT_EVT_end'])) {
538
-                    $where_params['AND']['DTT_EVT_end****'] = $where_params['DTT_EVT_end'];
539
-                    unset($where_params['DTT_EVT_end']);
540
-                }
541
-                if (isset($where_params['DTT_EVT_start'])) {
542
-                    $where_params['AND']['DTT_EVT_start'] = $where_params['DTT_EVT_start'];
543
-                    unset($where_params['DTT_EVT_start']);
544
-                }
545
-                $where_params['AND']['Event.status'] = array('!=', 'publish');
546
-                break;
547
-        }
548
-        $query_params[0]          = $where_params;
549
-        $query_params['group_by'] = array('dtt_year', 'dtt_month');
550
-        $query_params['order_by'] = array('DTT_EVT_start' => 'DESC');
551
-        $query_interval           = EEH_DTT_Helper::get_sql_query_interval_for_offset(
552
-            $this->get_timezone(),
553
-            'DTT_EVT_start'
554
-        );
555
-        $columns_to_select        = array(
556
-            'dtt_year'      => array('YEAR(' . $query_interval . ')', '%s'),
557
-            'dtt_month'     => array('MONTHNAME(' . $query_interval . ')', '%s'),
558
-            'dtt_month_num' => array('MONTH(' . $query_interval . ')', '%s'),
559
-        );
560
-        return $this->_get_all_wpdb_results($query_params, OBJECT, $columns_to_select);
561
-    }
562
-
563
-
564
-    /**
565
-     * Updates the DTT_sold attribute on each datetime (based on the registrations
566
-     * for the tickets for each datetime)
567
-     *
568
-     * @param EE_Base_Class[]|EE_Datetime[] $datetimes
569
-     * @throws EE_Error
570
-     */
571
-    public function update_sold($datetimes)
572
-    {
573
-        EE_Error::doing_it_wrong(
574
-            __FUNCTION__,
575
-            esc_html__(
576
-                'Please use \EEM_Ticket::update_tickets_sold() instead which will in turn correctly update both the Ticket AND Datetime counts.',
577
-                'event_espresso'
578
-            ),
579
-            '4.9.32.rc.005'
580
-        );
581
-        foreach ($datetimes as $datetime) {
582
-            $datetime->update_sold();
583
-        }
584
-    }
585
-
586
-
587
-    /**
588
-     *    Gets the total number of tickets available at a particular datetime
589
-     *    (does NOT take into account the datetime's spaces available)
590
-     *
591
-     * @param int   $DTT_ID
592
-     * @param array $query_params
593
-     * @return int of tickets available. If sold out, return less than 1. If infinite, returns EE_INF,  IF there are NO
594
-     *             tickets attached to datetime then FALSE is returned.
595
-     */
596
-    public function sum_tickets_currently_available_at_datetime($DTT_ID, array $query_params = array())
597
-    {
598
-        $datetime = $this->get_one_by_ID($DTT_ID);
599
-        if ($datetime instanceof EE_Datetime) {
600
-            return $datetime->tickets_remaining($query_params);
601
-        }
602
-        return 0;
603
-    }
604
-
605
-
606
-    /**
607
-     * This returns an array of counts of datetimes in the database for each Datetime status that can be queried.
608
-     *
609
-     * @param  array $stati_to_include If included you can restrict the statuses we return counts for by including the
610
-     *                                 stati you want counts for as values in the array.  An empty array returns counts
611
-     *                                 for all valid stati.
612
-     * @param  array $query_params     If included can be used to refine the conditions for returning the count (i.e.
613
-     *                                 only for Datetimes connected to a specific event, or specific ticket.
614
-     * @return array  The value returned is an array indexed by Datetime Status and the values are the counts.  The
615
-     * @throws EE_Error
616
-     *                                 stati used as index keys are: EE_Datetime::active EE_Datetime::upcoming
617
-     *                                 EE_Datetime::expired
618
-     */
619
-    public function get_datetime_counts_by_status(array $stati_to_include = array(), array $query_params = array())
620
-    {
621
-        // only accept where conditions for this query.
622
-        $_where            = isset($query_params[0]) ? $query_params[0] : array();
623
-        $status_query_args = array(
624
-            EE_Datetime::active   => array_merge(
625
-                $_where,
626
-                array('DTT_EVT_start' => array('<', time()), 'DTT_EVT_end' => array('>', time()))
627
-            ),
628
-            EE_Datetime::upcoming => array_merge(
629
-                $_where,
630
-                array('DTT_EVT_start' => array('>', time()))
631
-            ),
632
-            EE_Datetime::expired  => array_merge(
633
-                $_where,
634
-                array('DTT_EVT_end' => array('<', time()))
635
-            ),
636
-        );
637
-        if (! empty($stati_to_include)) {
638
-            foreach (array_keys($status_query_args) as $status) {
639
-                if (! in_array($status, $stati_to_include, true)) {
640
-                    unset($status_query_args[ $status ]);
641
-                }
642
-            }
643
-        }
644
-        // loop through and query counts for each stati.
645
-        $status_query_results = array();
646
-        foreach ($status_query_args as $status => $status_where_conditions) {
647
-            $status_query_results[ $status ] = EEM_Datetime::count(
648
-                array($status_where_conditions),
649
-                'DTT_ID',
650
-                true
651
-            );
652
-        }
653
-        return $status_query_results;
654
-    }
655
-
656
-
657
-    /**
658
-     * Returns the specific count for a given Datetime status matching any given query_params.
659
-     *
660
-     * @param string $status Valid string representation for Datetime status requested. (Defaults to Active).
661
-     * @param array  $query_params
662
-     * @return int
663
-     * @throws EE_Error
664
-     */
665
-    public function get_datetime_count_for_status($status = EE_Datetime::active, array $query_params = array())
666
-    {
667
-        $count = $this->get_datetime_counts_by_status(array($status), $query_params);
668
-        return ! empty($count[ $status ]) ? $count[ $status ] : 0;
669
-    }
12
+	/**
13
+	 * @var EEM_Datetime $_instance
14
+	 */
15
+	protected static $_instance;
16
+
17
+
18
+	/**
19
+	 * private constructor to prevent direct creation
20
+	 *
21
+	 * @param string $timezone A string representing the timezone we want to set for returned Date Time Strings
22
+	 *                         (and any incoming timezone data that gets saved).
23
+	 *                         Note this just sends the timezone info to the date time model field objects.
24
+	 *                         Default is NULL
25
+	 *                         (and will be assumed using the set timezone in the 'timezone_string' wp option)
26
+	 * @throws EE_Error
27
+	 * @throws InvalidArgumentException
28
+	 * @throws InvalidArgumentException
29
+	 */
30
+	protected function __construct($timezone)
31
+	{
32
+		$this->singular_item           = esc_html__('Datetime', 'event_espresso');
33
+		$this->plural_item             = esc_html__('Datetimes', 'event_espresso');
34
+		$this->_tables                 = array(
35
+			'Datetime' => new EE_Primary_Table('esp_datetime', 'DTT_ID'),
36
+		);
37
+		$this->_fields                 = array(
38
+			'Datetime' => array(
39
+				'DTT_ID'          => new EE_Primary_Key_Int_Field(
40
+					'DTT_ID',
41
+					esc_html__('Datetime ID', 'event_espresso')
42
+				),
43
+				'EVT_ID'          => new EE_Foreign_Key_Int_Field(
44
+					'EVT_ID',
45
+					esc_html__('Event ID', 'event_espresso'),
46
+					false,
47
+					0,
48
+					'Event'
49
+				),
50
+				'DTT_name'        => new EE_Plain_Text_Field(
51
+					'DTT_name',
52
+					esc_html__('Datetime Name', 'event_espresso'),
53
+					false,
54
+					''
55
+				),
56
+				'DTT_description' => new EE_Post_Content_Field(
57
+					'DTT_description',
58
+					esc_html__('Description for Datetime', 'event_espresso'),
59
+					false,
60
+					''
61
+				),
62
+				'DTT_EVT_start'   => new EE_Datetime_Field(
63
+					'DTT_EVT_start',
64
+					esc_html__('Start time/date of Event', 'event_espresso'),
65
+					false,
66
+					EE_Datetime_Field::now,
67
+					$timezone
68
+				),
69
+				'DTT_EVT_end'     => new EE_Datetime_Field(
70
+					'DTT_EVT_end',
71
+					esc_html__('End time/date of Event', 'event_espresso'),
72
+					false,
73
+					EE_Datetime_Field::now,
74
+					$timezone
75
+				),
76
+				'DTT_reg_limit'   => new EE_Infinite_Integer_Field(
77
+					'DTT_reg_limit',
78
+					esc_html__('Registration Limit for this time', 'event_espresso'),
79
+					true,
80
+					EE_INF
81
+				),
82
+				'DTT_sold'        => new EE_Integer_Field(
83
+					'DTT_sold',
84
+					esc_html__('How many sales for this Datetime that have occurred', 'event_espresso'),
85
+					true,
86
+					0
87
+				),
88
+				'DTT_reserved'    => new EE_Integer_Field(
89
+					'DTT_reserved',
90
+					esc_html__('Quantity of tickets reserved, but not yet fully purchased', 'event_espresso'),
91
+					false,
92
+					0
93
+				),
94
+				'DTT_is_primary'  => new EE_Boolean_Field(
95
+					'DTT_is_primary',
96
+					esc_html__('Flag indicating datetime is primary one for event', 'event_espresso'),
97
+					false,
98
+					false
99
+				),
100
+				'DTT_order'       => new EE_Integer_Field(
101
+					'DTT_order',
102
+					esc_html__('The order in which the Datetime is displayed', 'event_espresso'),
103
+					false,
104
+					0
105
+				),
106
+				'DTT_parent'      => new EE_Integer_Field(
107
+					'DTT_parent',
108
+					esc_html__('Indicates what DTT_ID is the parent of this DTT_ID', 'event_espresso'),
109
+					true,
110
+					0
111
+				),
112
+				'DTT_deleted'     => new EE_Trashed_Flag_Field(
113
+					'DTT_deleted',
114
+					esc_html__('Flag indicating datetime is archived', 'event_espresso'),
115
+					false,
116
+					false
117
+				),
118
+			),
119
+		);
120
+		$this->_model_relations        = array(
121
+			'Ticket'  => new EE_HABTM_Relation('Datetime_Ticket'),
122
+			'Event'   => new EE_Belongs_To_Relation(),
123
+			'Checkin' => new EE_Has_Many_Relation(),
124
+			'Datetime_Ticket' => new EE_Has_Many_Relation(),
125
+		);
126
+		$path_to_event_model = 'Event';
127
+		$this->model_chain_to_password = $path_to_event_model;
128
+		$this->_model_chain_to_wp_user = $path_to_event_model;
129
+		// this model is generally available for reading
130
+		$this->_cap_restriction_generators[ EEM_Base::caps_read ]       = new EE_Restriction_Generator_Event_Related_Public(
131
+			$path_to_event_model
132
+		);
133
+		$this->_cap_restriction_generators[ EEM_Base::caps_read_admin ] = new EE_Restriction_Generator_Event_Related_Protected(
134
+			$path_to_event_model
135
+		);
136
+		$this->_cap_restriction_generators[ EEM_Base::caps_edit ]       = new EE_Restriction_Generator_Event_Related_Protected(
137
+			$path_to_event_model
138
+		);
139
+		$this->_cap_restriction_generators[ EEM_Base::caps_delete ]     = new EE_Restriction_Generator_Event_Related_Protected(
140
+			$path_to_event_model,
141
+			EEM_Base::caps_edit
142
+		);
143
+		parent::__construct($timezone);
144
+	}
145
+
146
+
147
+	/**
148
+	 * create new blank datetime
149
+	 *
150
+	 * @access public
151
+	 * @return EE_Datetime[] array on success, FALSE on fail
152
+	 * @throws EE_Error
153
+	 */
154
+	public function create_new_blank_datetime()
155
+	{
156
+		// makes sure timezone is always set.
157
+		$timezone_string = $this->get_timezone();
158
+		$blank_datetime  = EE_Datetime::new_instance(
159
+			array(
160
+				'DTT_EVT_start' => $this->current_time_for_query('DTT_EVT_start', true) + MONTH_IN_SECONDS,
161
+				'DTT_EVT_end'   => $this->current_time_for_query('DTT_EVT_end', true) + MONTH_IN_SECONDS,
162
+				'DTT_order'     => 1,
163
+				'DTT_reg_limit' => EE_INF,
164
+			),
165
+			$timezone_string
166
+		);
167
+		$blank_datetime->set_start_time(
168
+			$this->convert_datetime_for_query(
169
+				'DTT_EVT_start',
170
+				'8am',
171
+				'ga',
172
+				$timezone_string
173
+			)
174
+		);
175
+		$blank_datetime->set_end_time(
176
+			$this->convert_datetime_for_query(
177
+				'DTT_EVT_end',
178
+				'5pm',
179
+				'ga',
180
+				$timezone_string
181
+			)
182
+		);
183
+		return array($blank_datetime);
184
+	}
185
+
186
+
187
+	/**
188
+	 * get event start date from db
189
+	 *
190
+	 * @access public
191
+	 * @param  int $EVT_ID
192
+	 * @return EE_Datetime[] array on success, FALSE on fail
193
+	 * @throws EE_Error
194
+	 */
195
+	public function get_all_event_dates($EVT_ID = 0)
196
+	{
197
+		if (! $EVT_ID) { // on add_new_event event_id gets set to 0
198
+			return $this->create_new_blank_datetime();
199
+		}
200
+		$results = $this->get_datetimes_for_event_ordered_by_DTT_order($EVT_ID);
201
+		if (empty($results)) {
202
+			return $this->create_new_blank_datetime();
203
+		}
204
+		return $results;
205
+	}
206
+
207
+
208
+	/**
209
+	 * get all datetimes attached to an event ordered by the DTT_order field
210
+	 *
211
+	 * @public
212
+	 * @param  int    $EVT_ID     event id
213
+	 * @param boolean $include_expired
214
+	 * @param boolean $include_deleted
215
+	 * @param  int    $limit      If included then limit the count of results by
216
+	 *                            the given number
217
+	 * @return EE_Datetime[]
218
+	 * @throws EE_Error
219
+	 */
220
+	public function get_datetimes_for_event_ordered_by_DTT_order(
221
+		$EVT_ID,
222
+		$include_expired = true,
223
+		$include_deleted = true,
224
+		$limit = null
225
+	) {
226
+		// sanitize EVT_ID
227
+		$EVT_ID         = absint($EVT_ID);
228
+		$old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
229
+		$this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
230
+		$where_params = array('Event.EVT_ID' => $EVT_ID);
231
+		$query_params = ! empty($limit)
232
+			? array(
233
+				$where_params,
234
+				'limit'                    => $limit,
235
+				'order_by'                 => array('DTT_order' => 'ASC'),
236
+				'default_where_conditions' => 'none',
237
+			)
238
+			: array(
239
+				$where_params,
240
+				'order_by'                 => array('DTT_order' => 'ASC'),
241
+				'default_where_conditions' => 'none',
242
+			);
243
+		if (! $include_expired) {
244
+			$query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
245
+		}
246
+		if ($include_deleted) {
247
+			$query_params[0]['DTT_deleted'] = array('IN', array(true, false));
248
+		}
249
+		/** @var EE_Datetime[] $result */
250
+		$result = $this->get_all($query_params);
251
+		$this->assume_values_already_prepared_by_model_object($old_assumption);
252
+		return $result;
253
+	}
254
+
255
+
256
+	/**
257
+	 * Gets the datetimes for the event (with the given limit), and orders them by "importance".
258
+	 * By importance, we mean that the primary datetimes are most important (DEPRECATED FOR NOW),
259
+	 * and then the earlier datetimes are the most important.
260
+	 * Maybe we'll want this to take into account datetimes that haven't already passed, but we don't yet.
261
+	 *
262
+	 * @param int $EVT_ID
263
+	 * @param int $limit
264
+	 * @return EE_Datetime[]|EE_Base_Class[]
265
+	 * @throws EE_Error
266
+	 */
267
+	public function get_datetimes_for_event_ordered_by_importance($EVT_ID = 0, $limit = null)
268
+	{
269
+		return $this->get_all(
270
+			array(
271
+				array('Event.EVT_ID' => $EVT_ID),
272
+				'limit'                    => $limit,
273
+				'order_by'                 => array('DTT_EVT_start' => 'ASC'),
274
+				'default_where_conditions' => 'none',
275
+			)
276
+		);
277
+	}
278
+
279
+
280
+	/**
281
+	 * @param int     $EVT_ID
282
+	 * @param boolean $include_expired
283
+	 * @param boolean $include_deleted
284
+	 * @return EE_Datetime
285
+	 * @throws EE_Error
286
+	 */
287
+	public function get_oldest_datetime_for_event($EVT_ID, $include_expired = false, $include_deleted = false)
288
+	{
289
+		$results = $this->get_datetimes_for_event_ordered_by_start_time(
290
+			$EVT_ID,
291
+			$include_expired,
292
+			$include_deleted,
293
+			1
294
+		);
295
+		if ($results) {
296
+			return array_shift($results);
297
+		}
298
+		return null;
299
+	}
300
+
301
+
302
+	/**
303
+	 * Gets the 'primary' datetime for an event.
304
+	 *
305
+	 * @param int  $EVT_ID
306
+	 * @param bool $try_to_exclude_expired
307
+	 * @param bool $try_to_exclude_deleted
308
+	 * @return \EE_Datetime
309
+	 * @throws EE_Error
310
+	 */
311
+	public function get_primary_datetime_for_event(
312
+		$EVT_ID,
313
+		$try_to_exclude_expired = true,
314
+		$try_to_exclude_deleted = true
315
+	) {
316
+		if ($try_to_exclude_expired) {
317
+			$non_expired = $this->get_oldest_datetime_for_event($EVT_ID, false, false);
318
+			if ($non_expired) {
319
+				return $non_expired;
320
+			}
321
+		}
322
+		if ($try_to_exclude_deleted) {
323
+			$expired_even = $this->get_oldest_datetime_for_event($EVT_ID, true);
324
+			if ($expired_even) {
325
+				return $expired_even;
326
+			}
327
+		}
328
+		return $this->get_oldest_datetime_for_event($EVT_ID, true, true);
329
+	}
330
+
331
+
332
+	/**
333
+	 * Gets ALL the datetimes for an event (including trashed ones, for now), ordered
334
+	 * only by start date
335
+	 *
336
+	 * @param int     $EVT_ID
337
+	 * @param boolean $include_expired
338
+	 * @param boolean $include_deleted
339
+	 * @param int     $limit
340
+	 * @return EE_Datetime[]
341
+	 * @throws EE_Error
342
+	 */
343
+	public function get_datetimes_for_event_ordered_by_start_time(
344
+		$EVT_ID,
345
+		$include_expired = true,
346
+		$include_deleted = true,
347
+		$limit = null
348
+	) {
349
+		// sanitize EVT_ID
350
+		$EVT_ID         = absint($EVT_ID);
351
+		$old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
352
+		$this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
353
+		$query_params = array(array('Event.EVT_ID' => $EVT_ID), 'order_by' => array('DTT_EVT_start' => 'asc'));
354
+		if (! $include_expired) {
355
+			$query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
356
+		}
357
+		if ($include_deleted) {
358
+			$query_params[0]['DTT_deleted'] = array('IN', array(true, false));
359
+		}
360
+		if ($limit) {
361
+			$query_params['limit'] = $limit;
362
+		}
363
+		/** @var EE_Datetime[] $result */
364
+		$result = $this->get_all($query_params);
365
+		$this->assume_values_already_prepared_by_model_object($old_assumption);
366
+		return $result;
367
+	}
368
+
369
+
370
+	/**
371
+	 * Gets ALL the datetimes for an ticket (including trashed ones, for now), ordered
372
+	 * only by start date
373
+	 *
374
+	 * @param int     $TKT_ID
375
+	 * @param boolean $include_expired
376
+	 * @param boolean $include_deleted
377
+	 * @param int     $limit
378
+	 * @return EE_Datetime[]
379
+	 * @throws EE_Error
380
+	 */
381
+	public function get_datetimes_for_ticket_ordered_by_start_time(
382
+		$TKT_ID,
383
+		$include_expired = true,
384
+		$include_deleted = true,
385
+		$limit = null
386
+	) {
387
+		// sanitize TKT_ID
388
+		$TKT_ID         = absint($TKT_ID);
389
+		$old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
390
+		$this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
391
+		$query_params = array(array('Ticket.TKT_ID' => $TKT_ID), 'order_by' => array('DTT_EVT_start' => 'asc'));
392
+		if (! $include_expired) {
393
+			$query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
394
+		}
395
+		if ($include_deleted) {
396
+			$query_params[0]['DTT_deleted'] = array('IN', array(true, false));
397
+		}
398
+		if ($limit) {
399
+			$query_params['limit'] = $limit;
400
+		}
401
+		/** @var EE_Datetime[] $result */
402
+		$result = $this->get_all($query_params);
403
+		$this->assume_values_already_prepared_by_model_object($old_assumption);
404
+		return $result;
405
+	}
406
+
407
+
408
+	/**
409
+	 * Gets all the datetimes for a ticket (including trashed ones, for now), ordered by the DTT_order for the
410
+	 * datetimes.
411
+	 *
412
+	 * @param  int      $TKT_ID          ID of ticket to retrieve the datetimes for
413
+	 * @param  boolean  $include_expired whether to include expired datetimes or not
414
+	 * @param  boolean  $include_deleted whether to include trashed datetimes or not.
415
+	 * @param  int|null $limit           if null, no limit, if int then limit results by
416
+	 *                                   that number
417
+	 * @return EE_Datetime[]
418
+	 * @throws EE_Error
419
+	 */
420
+	public function get_datetimes_for_ticket_ordered_by_DTT_order(
421
+		$TKT_ID,
422
+		$include_expired = true,
423
+		$include_deleted = true,
424
+		$limit = null
425
+	) {
426
+		// sanitize id.
427
+		$TKT_ID         = absint($TKT_ID);
428
+		$old_assumption = $this->get_assumption_concerning_values_already_prepared_by_model_object();
429
+		$this->assume_values_already_prepared_by_model_object(EEM_Base::prepared_for_use_in_db);
430
+		$where_params = array('Ticket.TKT_ID' => $TKT_ID);
431
+		$query_params = array($where_params, 'order_by' => array('DTT_order' => 'ASC'));
432
+		if (! $include_expired) {
433
+			$query_params[0]['DTT_EVT_end'] = array('>=', current_time('mysql', true));
434
+		}
435
+		if ($include_deleted) {
436
+			$query_params[0]['DTT_deleted'] = array('IN', array(true, false));
437
+		}
438
+		if ($limit) {
439
+			$query_params['limit'] = $limit;
440
+		}
441
+		/** @var EE_Datetime[] $result */
442
+		$result = $this->get_all($query_params);
443
+		$this->assume_values_already_prepared_by_model_object($old_assumption);
444
+		return $result;
445
+	}
446
+
447
+
448
+	/**
449
+	 * Gets the most important datetime for a particular event (ie, the primary event usually. But if for some WACK
450
+	 * reason it doesn't exist, we consider the earliest event the most important)
451
+	 *
452
+	 * @param int $EVT_ID
453
+	 * @return EE_Datetime
454
+	 * @throws EE_Error
455
+	 */
456
+	public function get_most_important_datetime_for_event($EVT_ID)
457
+	{
458
+		$results = $this->get_datetimes_for_event_ordered_by_importance($EVT_ID, 1);
459
+		if ($results) {
460
+			return array_shift($results);
461
+		}
462
+		return null;
463
+	}
464
+
465
+
466
+	/**
467
+	 * This returns a wpdb->results        Array of all DTT month and years matching the incoming query params and
468
+	 * grouped by month and year.
469
+	 *
470
+	 * @param  array  $where_params      @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md#0-where-conditions
471
+	 * @param  string $evt_active_status A string representing the evt active status to filter the months by.
472
+	 *                                   Can be:
473
+	 *                                   - '' = no filter
474
+	 *                                   - upcoming = Published events with at least one upcoming datetime.
475
+	 *                                   - expired = Events with all datetimes expired.
476
+	 *                                   - active = Events that are published and have at least one datetime that
477
+	 *                                   starts before now and ends after now.
478
+	 *                                   - inactive = Events that are either not published.
479
+	 * @return EE_Base_Class[]
480
+	 * @throws EE_Error
481
+	 * @throws InvalidArgumentException
482
+	 * @throws InvalidArgumentException
483
+	 */
484
+	public function get_dtt_months_and_years($where_params, $evt_active_status = '')
485
+	{
486
+		$current_time_for_DTT_EVT_start = $this->current_time_for_query('DTT_EVT_start');
487
+		$current_time_for_DTT_EVT_end   = $this->current_time_for_query('DTT_EVT_end');
488
+		switch ($evt_active_status) {
489
+			case 'upcoming':
490
+				$where_params['Event.status'] = 'publish';
491
+				// if there are already query_params matching DTT_EVT_start then we need to modify that to add them.
492
+				if (isset($where_params['DTT_EVT_start'])) {
493
+					$where_params['DTT_EVT_start*****'] = $where_params['DTT_EVT_start'];
494
+				}
495
+				$where_params['DTT_EVT_start'] = array('>', $current_time_for_DTT_EVT_start);
496
+				break;
497
+			case 'expired':
498
+				if (isset($where_params['Event.status'])) {
499
+					unset($where_params['Event.status']);
500
+				}
501
+				// get events to exclude
502
+				$exclude_query[0] = array_merge(
503
+					$where_params,
504
+					array('DTT_EVT_end' => array('>', $current_time_for_DTT_EVT_end))
505
+				);
506
+				// first get all events that have datetimes where its not expired.
507
+				$event_ids = $this->_get_all_wpdb_results(
508
+					$exclude_query,
509
+					OBJECT_K,
510
+					'Datetime.EVT_ID'
511
+				);
512
+				$event_ids = array_keys($event_ids);
513
+				if (isset($where_params['DTT_EVT_end'])) {
514
+					$where_params['DTT_EVT_end****'] = $where_params['DTT_EVT_end'];
515
+				}
516
+				$where_params['DTT_EVT_end']  = array('<', $current_time_for_DTT_EVT_end);
517
+				$where_params['Event.EVT_ID'] = array('NOT IN', $event_ids);
518
+				break;
519
+			case 'active':
520
+				$where_params['Event.status'] = 'publish';
521
+				if (isset($where_params['DTT_EVT_start'])) {
522
+					$where_params['Datetime.DTT_EVT_start******'] = $where_params['DTT_EVT_start'];
523
+				}
524
+				if (isset($where_params['Datetime.DTT_EVT_end'])) {
525
+					$where_params['Datetime.DTT_EVT_end*****'] = $where_params['DTT_EVT_end'];
526
+				}
527
+				$where_params['DTT_EVT_start'] = array('<', $current_time_for_DTT_EVT_start);
528
+				$where_params['DTT_EVT_end']   = array('>', $current_time_for_DTT_EVT_end);
529
+				break;
530
+			case 'inactive':
531
+				if (isset($where_params['Event.status'])) {
532
+					unset($where_params['Event.status']);
533
+				}
534
+				if (isset($where_params['OR'])) {
535
+					$where_params['AND']['OR'] = $where_params['OR'];
536
+				}
537
+				if (isset($where_params['DTT_EVT_end'])) {
538
+					$where_params['AND']['DTT_EVT_end****'] = $where_params['DTT_EVT_end'];
539
+					unset($where_params['DTT_EVT_end']);
540
+				}
541
+				if (isset($where_params['DTT_EVT_start'])) {
542
+					$where_params['AND']['DTT_EVT_start'] = $where_params['DTT_EVT_start'];
543
+					unset($where_params['DTT_EVT_start']);
544
+				}
545
+				$where_params['AND']['Event.status'] = array('!=', 'publish');
546
+				break;
547
+		}
548
+		$query_params[0]          = $where_params;
549
+		$query_params['group_by'] = array('dtt_year', 'dtt_month');
550
+		$query_params['order_by'] = array('DTT_EVT_start' => 'DESC');
551
+		$query_interval           = EEH_DTT_Helper::get_sql_query_interval_for_offset(
552
+			$this->get_timezone(),
553
+			'DTT_EVT_start'
554
+		);
555
+		$columns_to_select        = array(
556
+			'dtt_year'      => array('YEAR(' . $query_interval . ')', '%s'),
557
+			'dtt_month'     => array('MONTHNAME(' . $query_interval . ')', '%s'),
558
+			'dtt_month_num' => array('MONTH(' . $query_interval . ')', '%s'),
559
+		);
560
+		return $this->_get_all_wpdb_results($query_params, OBJECT, $columns_to_select);
561
+	}
562
+
563
+
564
+	/**
565
+	 * Updates the DTT_sold attribute on each datetime (based on the registrations
566
+	 * for the tickets for each datetime)
567
+	 *
568
+	 * @param EE_Base_Class[]|EE_Datetime[] $datetimes
569
+	 * @throws EE_Error
570
+	 */
571
+	public function update_sold($datetimes)
572
+	{
573
+		EE_Error::doing_it_wrong(
574
+			__FUNCTION__,
575
+			esc_html__(
576
+				'Please use \EEM_Ticket::update_tickets_sold() instead which will in turn correctly update both the Ticket AND Datetime counts.',
577
+				'event_espresso'
578
+			),
579
+			'4.9.32.rc.005'
580
+		);
581
+		foreach ($datetimes as $datetime) {
582
+			$datetime->update_sold();
583
+		}
584
+	}
585
+
586
+
587
+	/**
588
+	 *    Gets the total number of tickets available at a particular datetime
589
+	 *    (does NOT take into account the datetime's spaces available)
590
+	 *
591
+	 * @param int   $DTT_ID
592
+	 * @param array $query_params
593
+	 * @return int of tickets available. If sold out, return less than 1. If infinite, returns EE_INF,  IF there are NO
594
+	 *             tickets attached to datetime then FALSE is returned.
595
+	 */
596
+	public function sum_tickets_currently_available_at_datetime($DTT_ID, array $query_params = array())
597
+	{
598
+		$datetime = $this->get_one_by_ID($DTT_ID);
599
+		if ($datetime instanceof EE_Datetime) {
600
+			return $datetime->tickets_remaining($query_params);
601
+		}
602
+		return 0;
603
+	}
604
+
605
+
606
+	/**
607
+	 * This returns an array of counts of datetimes in the database for each Datetime status that can be queried.
608
+	 *
609
+	 * @param  array $stati_to_include If included you can restrict the statuses we return counts for by including the
610
+	 *                                 stati you want counts for as values in the array.  An empty array returns counts
611
+	 *                                 for all valid stati.
612
+	 * @param  array $query_params     If included can be used to refine the conditions for returning the count (i.e.
613
+	 *                                 only for Datetimes connected to a specific event, or specific ticket.
614
+	 * @return array  The value returned is an array indexed by Datetime Status and the values are the counts.  The
615
+	 * @throws EE_Error
616
+	 *                                 stati used as index keys are: EE_Datetime::active EE_Datetime::upcoming
617
+	 *                                 EE_Datetime::expired
618
+	 */
619
+	public function get_datetime_counts_by_status(array $stati_to_include = array(), array $query_params = array())
620
+	{
621
+		// only accept where conditions for this query.
622
+		$_where            = isset($query_params[0]) ? $query_params[0] : array();
623
+		$status_query_args = array(
624
+			EE_Datetime::active   => array_merge(
625
+				$_where,
626
+				array('DTT_EVT_start' => array('<', time()), 'DTT_EVT_end' => array('>', time()))
627
+			),
628
+			EE_Datetime::upcoming => array_merge(
629
+				$_where,
630
+				array('DTT_EVT_start' => array('>', time()))
631
+			),
632
+			EE_Datetime::expired  => array_merge(
633
+				$_where,
634
+				array('DTT_EVT_end' => array('<', time()))
635
+			),
636
+		);
637
+		if (! empty($stati_to_include)) {
638
+			foreach (array_keys($status_query_args) as $status) {
639
+				if (! in_array($status, $stati_to_include, true)) {
640
+					unset($status_query_args[ $status ]);
641
+				}
642
+			}
643
+		}
644
+		// loop through and query counts for each stati.
645
+		$status_query_results = array();
646
+		foreach ($status_query_args as $status => $status_where_conditions) {
647
+			$status_query_results[ $status ] = EEM_Datetime::count(
648
+				array($status_where_conditions),
649
+				'DTT_ID',
650
+				true
651
+			);
652
+		}
653
+		return $status_query_results;
654
+	}
655
+
656
+
657
+	/**
658
+	 * Returns the specific count for a given Datetime status matching any given query_params.
659
+	 *
660
+	 * @param string $status Valid string representation for Datetime status requested. (Defaults to Active).
661
+	 * @param array  $query_params
662
+	 * @return int
663
+	 * @throws EE_Error
664
+	 */
665
+	public function get_datetime_count_for_status($status = EE_Datetime::active, array $query_params = array())
666
+	{
667
+		$count = $this->get_datetime_counts_by_status(array($status), $query_params);
668
+		return ! empty($count[ $status ]) ? $count[ $status ] : 0;
669
+	}
670 670
 }
Please login to merge, or discard this patch.
espresso.php 1 patch
Indentation   +80 added lines, -80 removed lines patch added patch discarded remove patch
@@ -38,103 +38,103 @@
 block discarded – undo
38 38
  * @since           4.0
39 39
  */
40 40
 if (function_exists('espresso_version')) {
41
-    if (! function_exists('espresso_duplicate_plugin_error')) {
42
-        /**
43
-         *    espresso_duplicate_plugin_error
44
-         *    displays if more than one version of EE is activated at the same time
45
-         */
46
-        function espresso_duplicate_plugin_error()
47
-        {
48
-            ?>
41
+	if (! function_exists('espresso_duplicate_plugin_error')) {
42
+		/**
43
+		 *    espresso_duplicate_plugin_error
44
+		 *    displays if more than one version of EE is activated at the same time
45
+		 */
46
+		function espresso_duplicate_plugin_error()
47
+		{
48
+			?>
49 49
             <div class="error">
50 50
                 <p>
51 51
                     <?php
52
-                    echo esc_html__(
53
-                        'Can not run multiple versions of Event Espresso! One version has been automatically deactivated. Please verify that you have the correct version you want still active.',
54
-                        'event_espresso'
55
-                    ); ?>
52
+					echo esc_html__(
53
+						'Can not run multiple versions of Event Espresso! One version has been automatically deactivated. Please verify that you have the correct version you want still active.',
54
+						'event_espresso'
55
+					); ?>
56 56
                 </p>
57 57
             </div>
58 58
             <?php
59
-            espresso_deactivate_plugin(plugin_basename(__FILE__));
60
-        }
61
-    }
62
-    add_action('admin_notices', 'espresso_duplicate_plugin_error', 1);
59
+			espresso_deactivate_plugin(plugin_basename(__FILE__));
60
+		}
61
+	}
62
+	add_action('admin_notices', 'espresso_duplicate_plugin_error', 1);
63 63
 } else {
64
-    define('EE_MIN_PHP_VER_REQUIRED', '5.4.0');
65
-    if (! version_compare(PHP_VERSION, EE_MIN_PHP_VER_REQUIRED, '>=')) {
66
-        /**
67
-         * espresso_minimum_php_version_error
68
-         *
69
-         * @return void
70
-         */
71
-        function espresso_minimum_php_version_error()
72
-        {
73
-            ?>
64
+	define('EE_MIN_PHP_VER_REQUIRED', '5.4.0');
65
+	if (! version_compare(PHP_VERSION, EE_MIN_PHP_VER_REQUIRED, '>=')) {
66
+		/**
67
+		 * espresso_minimum_php_version_error
68
+		 *
69
+		 * @return void
70
+		 */
71
+		function espresso_minimum_php_version_error()
72
+		{
73
+			?>
74 74
             <div class="error">
75 75
                 <p>
76 76
                     <?php
77
-                    printf(
78
-                        esc_html__(
79
-                            'We\'re sorry, but Event Espresso requires PHP version %1$s or greater in order to operate. You are currently running version %2$s.%3$sIn order to update your version of PHP, you will need to contact your current hosting provider.%3$sFor information on stable PHP versions, please go to %4$s.',
80
-                            'event_espresso'
81
-                        ),
82
-                        EE_MIN_PHP_VER_REQUIRED,
83
-                        PHP_VERSION,
84
-                        '<br/>',
85
-                        '<a href="http://php.net/downloads.php">http://php.net/downloads.php</a>'
86
-                    );
87
-                    ?>
77
+					printf(
78
+						esc_html__(
79
+							'We\'re sorry, but Event Espresso requires PHP version %1$s or greater in order to operate. You are currently running version %2$s.%3$sIn order to update your version of PHP, you will need to contact your current hosting provider.%3$sFor information on stable PHP versions, please go to %4$s.',
80
+							'event_espresso'
81
+						),
82
+						EE_MIN_PHP_VER_REQUIRED,
83
+						PHP_VERSION,
84
+						'<br/>',
85
+						'<a href="http://php.net/downloads.php">http://php.net/downloads.php</a>'
86
+					);
87
+					?>
88 88
                 </p>
89 89
             </div>
90 90
             <?php
91
-            espresso_deactivate_plugin(plugin_basename(__FILE__));
92
-        }
91
+			espresso_deactivate_plugin(plugin_basename(__FILE__));
92
+		}
93 93
 
94
-        add_action('admin_notices', 'espresso_minimum_php_version_error', 1);
95
-    } else {
96
-        define('EVENT_ESPRESSO_MAIN_FILE', __FILE__);
97
-        /**
98
-         * espresso_version
99
-         * Returns the plugin version
100
-         *
101
-         * @return string
102
-         */
103
-        function espresso_version()
104
-        {
105
-            return apply_filters('FHEE__espresso__espresso_version', '4.9.79.rc.014');
106
-        }
94
+		add_action('admin_notices', 'espresso_minimum_php_version_error', 1);
95
+	} else {
96
+		define('EVENT_ESPRESSO_MAIN_FILE', __FILE__);
97
+		/**
98
+		 * espresso_version
99
+		 * Returns the plugin version
100
+		 *
101
+		 * @return string
102
+		 */
103
+		function espresso_version()
104
+		{
105
+			return apply_filters('FHEE__espresso__espresso_version', '4.9.79.rc.014');
106
+		}
107 107
 
108
-        /**
109
-         * espresso_plugin_activation
110
-         * adds a wp-option to indicate that EE has been activated via the WP admin plugins page
111
-         */
112
-        function espresso_plugin_activation()
113
-        {
114
-            update_option('ee_espresso_activation', true);
115
-        }
108
+		/**
109
+		 * espresso_plugin_activation
110
+		 * adds a wp-option to indicate that EE has been activated via the WP admin plugins page
111
+		 */
112
+		function espresso_plugin_activation()
113
+		{
114
+			update_option('ee_espresso_activation', true);
115
+		}
116 116
 
117
-        register_activation_hook(EVENT_ESPRESSO_MAIN_FILE, 'espresso_plugin_activation');
117
+		register_activation_hook(EVENT_ESPRESSO_MAIN_FILE, 'espresso_plugin_activation');
118 118
 
119
-        require_once __DIR__ . '/core/bootstrap_espresso.php';
120
-        bootstrap_espresso();
121
-    }
119
+		require_once __DIR__ . '/core/bootstrap_espresso.php';
120
+		bootstrap_espresso();
121
+	}
122 122
 }
123 123
 if (! function_exists('espresso_deactivate_plugin')) {
124
-    /**
125
-     *    deactivate_plugin
126
-     * usage:  espresso_deactivate_plugin( plugin_basename( __FILE__ ));
127
-     *
128
-     * @access public
129
-     * @param string $plugin_basename - the results of plugin_basename( __FILE__ ) for the plugin's main file
130
-     * @return    void
131
-     */
132
-    function espresso_deactivate_plugin($plugin_basename = '')
133
-    {
134
-        if (! function_exists('deactivate_plugins')) {
135
-            require_once ABSPATH . 'wp-admin/includes/plugin.php';
136
-        }
137
-        unset($_GET['activate'], $_REQUEST['activate']);
138
-        deactivate_plugins($plugin_basename);
139
-    }
124
+	/**
125
+	 *    deactivate_plugin
126
+	 * usage:  espresso_deactivate_plugin( plugin_basename( __FILE__ ));
127
+	 *
128
+	 * @access public
129
+	 * @param string $plugin_basename - the results of plugin_basename( __FILE__ ) for the plugin's main file
130
+	 * @return    void
131
+	 */
132
+	function espresso_deactivate_plugin($plugin_basename = '')
133
+	{
134
+		if (! function_exists('deactivate_plugins')) {
135
+			require_once ABSPATH . 'wp-admin/includes/plugin.php';
136
+		}
137
+		unset($_GET['activate'], $_REQUEST['activate']);
138
+		deactivate_plugins($plugin_basename);
139
+	}
140 140
 }
Please login to merge, or discard this patch.