<?php if ( ! defined( 'ABSPATH' ) ) exit;
* Class NF_Updates_CacheCollateCleanup
* This class manages the step process of running through the CacheCollateCleanup required update.
* It will define an object to pull data from (if necessary) to pick back up if exited early.
* It will then step over each table in our structure and ensure that orphan records are removed from storage.
* It will then step over all submissions, removing any orphans.
* It will then step over all submissions (by form), updating or removing any orphan field records.
* After completing the above for every form on the site, it will remove the data object that manages its location.
class NF_Updates_CacheCollateCleanup extends NF_Abstracts_RequiredUpdate
private $running = array();
* The denominator object for calculating our steps.
private $stage_complete = false;
* @param $data (Array) The data object passed in by the AJAX call.
* @param $running (Array) The array of required updates being run.
public function __construct( $data = array(), $running )
// Build our arguments array.
'slug' => 'CacheCollateCleanup',
'class_name' => 'NF_Updates_CacheCollateCleanup',
$this->running = $running;
// Call the parent constructor.
parent::__construct( $args );
* Function to loop over the batch.
public function process()
// If we've not already started...
if ( ! isset( $this->running[ 0 ][ 'running' ] ) ) {
// Run our startup method.
$this->stage = $this->running[ 0 ][ 'stages' ][ 0 ];
// Determine what process to run.
switch ( $this->stage[ 'table' ] ) {
// If we've cleaned out all orphan submissions...
if ( $this->stage[ 'purged' ] ) {
// Work on clearing out orphan field records.
} // Otherwise... (We still have orphan submissions.)
// Remove orphan submissions.
// If we need to step over this process...
if ( $this->divisor < $this->stage[ 'parent_total' ] ) {
// Call stepped deletion.
} // Otherwise... (We don't need to step over this.)
// Increment our step count.
$this->running[ 0 ][ 'current' ] += 1;
// If we have completed the current stage...
if ( $this->stage_complete ) {
// Remove it from our list.
array_shift( $this->running[ 0 ][ 'stages' ] );
} // Otherwise... (We have not completed the stage)
$this->running[ 0 ][ 'stages' ][ 0 ] = $this->stage;
// Prepare to output our number of steps and current step.
$this->response[ 'stepsTotal' ] = $this->running[ 0 ][ 'steps' ];
$this->response[ 'currentStep' ] = $this->running[ 0 ][ 'current' ];
// If we have no stages left...
if ( empty( $this->running[ 0 ][ 'stages' ] ) ) {
// Run our cleanup method.
// Prepare to output the number of updates remaining.
$this->response[ 'updatesRemaining' ] = count( $this->running );
// Record our current location in the process.
update_option( 'ninja_forms_doing_required_updates', $this->running );
// Respond to the AJAX call.
* Function to run any setup steps necessary to begin processing.
public function startup()
// Record that we're processing the update.
$this->running[ 0 ][ 'running' ] = true;
// Get the number of records in the forms table.
$form_count = $this->get_total( 'nf3_forms' );
// Get the number of records in the fields table.
$action_count = $this->get_total( 'nf3_actions' );
// Get the number of records in the actions table.
$field_count = $this->get_total( 'nf3_fields' );
// Get the number of records in the objects table.
$object_count = $this->get_total( 'nf3_objects' );
// Get all form_ids from the db for the submissions portion.
$sql = "SELECT `id` FROM `{$this->db->prefix}nf3_forms`";
$forms = $this->db->get_results( $sql, 'ARRAY_A' );
'table' => $this->db->prefix . 'nf3_form_meta',
'parent' => $this->db->prefix . 'nf3_forms',
'parent_total' => $form_count,
'table' => $this->db->prefix . 'nf3_actions',
'parent' => $this->db->prefix . 'nf3_forms',
'parent_total' => $form_count,
'table' => $this->db->prefix . 'nf3_action_meta',
'parent' => $this->db->prefix . 'nf3_actions',
'parent_total' => $action_count,
'table' => $this->db->prefix . 'nf3_fields',
'parent' => $this->db->prefix . 'nf3_forms',
'parent_total' => $form_count,
'table' => $this->db->prefix . 'nf3_field_meta',
'parent' => $this->db->prefix . 'nf3_fields',
'parent_total' => $field_count,
'table' => $this->db->prefix . 'nf3_object_meta',
'parent' => $this->db->prefix . 'nf3_objects',
'parent_total' => $object_count,
'table' => 'submissions',
'parent_total' => $form_count,
// Set the steps for form meta (enforcing a minimum step count).
$add = ceil( $form_count / $this->divisor );
$steps = ( 0 == $add ) ? 1 : $add;
// Add actions and fields.
// Add action meta (enforcing a minimum step count).
$add = ceil( $action_count / $this->divisor );
$add = ( 0 == $add ) ? 1 : $add;
// Add field meta (enforcing a minimum step count).
$add = ceil( $field_count / $this->divisor );
$add = ( 0 == $add ) ? 1 : $add;
// Add object meta (enforcing a minimum step count).
$add = ceil( $object_count / $this->divisor );
$add = ( 0 == $add ) ? 1 : $add;
// Add one plus the form count for submissions (enforcing a minimum step count).
$add = ( 1 == $add ) ? 2 : $add;
$this->running[ 0 ][ 'stages' ] = $stages;
// Record the total number of steps in this batch.
$this->running[ 0 ][ 'steps' ] = $steps;
// Record our current step (defaulted to 0 here).
$this->running[ 0 ][ 'current' ] = 0;
* Function to cleanup any lingering temporary elements of a required update after completion.
public function cleanup()
// Remove the current process from the array.
array_shift( $this->running );
// Record to our updates setting that this update is complete.
$this->confirm_complete();
// If we have no updates left to process...
if ( empty( $this->running ) ) {
// Call the parent cleanup method.
* Function to get the number of objects in a table.
* @param $table (String) The name of the target table.
* @return (Int) The count of rows.
public function get_total( $table )
$sql = "SELECT COUNT( `id` ) AS Total FROM `{$this->db->prefix}{$table}`;";
$result = $this->db->get_results( $sql, 'ARRAY_A' );
return intval( $result[ 0 ][ 'Total' ] );
* Function to perform a simple, single-step delete of orphan data.
private function do_easy_delete()
// Remove the orphan records.
$sql = "DELETE FROM `{$this->stage[ 'table' ]}`
WHERE `parent_id` NOT IN(
SELECT `id` FROM `{$this->stage[ 'parent' ]}`
// Protect favorite fields from being removed.
if ( false !== strpos( $this->stage[ 'table' ], 'nf3_fields' ) ) {
$sql .= " AND `parent_id` <> 0";
// Confirm that this stage is complete.
$this->stage_complete = true;
* Function to perform a multi-step delete of orphan data.
private function do_step_delete()
// Get records from our table.
$sub_sql = "SELECT DISTINCT( `parent_id` ) AS id FROM `{$this->stage[ 'table' ]}` ";
// If we have a previous last record...
if ( isset( $this->stage[ 'last' ] ) ) {
// Make sure we exclude anything before it from the result.
$sub_sql .= "WHERE `parent_id` > " . $this->stage[ 'last' ] . " ";
} // Protect favorite fields from being removed.
elseif ( false !== strpos( $this->stage[ 'table' ], 'nf3_fields' ) ) {
$sub_sql .= "WHERE `parent_id` <> 0 ";
$sub_sql .= "ORDER BY `parent_id` ASC
LIMIT {$this->divisor};";
$result = $this->db->get_results( $sub_sql, 'ARRAY_A' );
// Get the last affected row.
// Squash our results to get a non-associative array.
$result = $this->array_squash( $result );
$this->stage[ 'last' ] = intval( $last[ 'id' ] );
// Get records from the parent table.
$sql = "SELECT `id` FROM `{$this->stage[ 'parent' ]}`
WHERE `id` IN(" . implode( ', ', $result ) .
$parent_result = $this->db->get_results( $sql, 'ARRAY_A' );
// If we didn't get the same number of results...
if ( count( $result ) !== count( $parent_result ) ) {
$result = array_merge( $result, $parent_result );
// Convert the array to something we can sort by duplicates.
$temp = array_count_values( array_column( $result, 'id' ) );
// Get rid of all values that had more than 1 result.
$temp = array_filter( $temp, array( $this, 'uniquify' ) );
// Schedule all of the single result values for deletion.
$delete = implode( ', ', array_keys( $temp ) );
$sql = "DELETE FROM `{$this->stage[ 'table' ]}`
WHERE `parent_id` IN( {$delete} );";
// If we've not already determined the limit of this table...
if ( ! isset( $this->stage[ 'max' ] ) ) {
// Fetch the maximum value.
$sql = "SELECT MAX( `parent_id` ) as target FROM `{$this->stage[ 'table' ]}`";
$result = $this->db->get_results( $sql, 'ARRAY_A' );
// Save a reference to it.
$this->stage[ 'max' ] = intval( $result[ 0 ][ 'target' ] );
// If our last record is equal to our maximum record...
if ( $this->stage[ 'last' ] == $this->stage[ 'max' ] ) {
// Record that we're done with this stage.
$this->stage_complete = true;
* Function called by array_filter to remove ALL duplicate results.
* @param $v (Int) The number of results.
private function uniquify( $v )
// If we have 1 result, keep it.
* Function to remove orphan submissions from the posts and postmeta tables.
private function adopt_subs()
// Fetch a limited number of orphan subs to delete.
$sub_sql = "SELECT m.post_id AS id from `{$this->db->prefix}postmeta` AS m
LEFT OUTER JOIN `{$this->db->prefix}posts` AS p
WHERE m.meta_key = '_form_id'
AND p.post_type = 'nf_sub'
SELECT `id` from `{$this->db->prefix}nf3_forms`
LIMIT {$this->divisor};";
$result = $this->db->get_results( $sub_sql, 'ARRAY_A' );
$count = count( $result );
// Squash our results to get a non-associative array.
$result = $this->array_squash( $result );
// If we have records to be deleted...
// Remove their postmeta.
$sql = "DELETE FROM `{$this->db->prefix}postmeta`
WHERE post_id IN(" . implode( ', ', $result ) . ");";
$sql = "DELETE FROM `{$this->db->prefix}posts`
WHERE id IN(" . implode( ', ', $result ) . ");";
// If the number of affected rows was less than our divisor...
if ( $count < $this->divisor ) {
// Mark that we've completed the process.
$this->stage[ 'purged' ] = true;
// Increment our step count.
$this->running[ 0 ][ 'current' ] += 1;
* Function to remove orphan field data that's still attached
* to valid submissions from the postmeta table.
private function adopt_fields()
// Get the form to work with.
$form = array_pop( $this->stage[ 'forms' ] );
// Fetch meta rows that match our criteria.
$sql = "SELECT r.meta_id, r.meta_key AS field_id
FROM `{$this->db->prefix}posts` AS p
LEFT JOIN `{$this->db->prefix}postmeta` AS r
LEFT JOIN `{$this->db->prefix}postmeta` AS f
WHERE p.post_type = 'nf_sub'
AND f.meta_key = '_form_id'
AND f.meta_value = " . intval( $form[ 'id' ] ) . "
AND r.meta_key LIKE '_field_%' ";
if ( isset( $form[ 'last' ] ) ) {
// Make sure we're getting new results instead of old ones.
$sql .= "AND r.meta_id > {$form[ 'last' ]} ";
$sql .= "ORDER BY r.meta_id ASC
LIMIT {$this->divisor};";
$results = $this->db->get_results( $sql, 'ARRAY_A' );
$count = count( $results );
$last = $last[ 'meta_id' ];
// Get all fields associated with this form.
$sql = "SELECT id FROM `{$this->db->prefix}nf3_fields` WHERE parent_id = " . intval( $form[ 'id' ] ) . ";";
$fields = $this->db->get_results( $sql, 'ARRAY_A' );
// Squash our results to get a non-associative array.
$fields = $this->array_squash( $fields );
foreach( $results as $result ) {
// Pull the text out of our meta.
$id = str_replace( '_field_', '', $result[ 'field_id' ] );
// If this field isn't on the form...
if ( ! in_array( $id, $fields ) ) {
// Add it to our list to check later.
$bad_records[ $id ][] = $result[ 'meta_id' ];
// Get a list of any old_field_ids from our field table update.
$sql = "SELECT f.id AS new_id, m.meta_value AS id
FROM `{$this->db->prefix}nf3_fields` AS f
LEFT JOIN `{$this->db->prefix}nf3_field_meta` AS m
WHERE f.parent_id = " . intval( $form[ 'id' ] ) . "
AND m.meta_key = 'old_field_id';";
$old_ids = $this->db->get_results( $sql, 'ARRAY_A' );
// Squash our results to get an associative array.
$old_ids = $this->array_squash( $old_ids );
// For each id in the bad records list...
foreach ( $bad_records as $field => $meta ) {
// If we have a new ID for that record...
if ( isset( $old_ids[ $field ] ) ) {
// Update our submissions.
$sql = "UPDATE `{$this->db->prefix}postmeta`
SET `meta_key` = '_field_" . $old_ids[ $field ] . "'
WHERE `meta_id` IN(" . implode( ', ', $meta ) . ");";
} // Otherwise... (We don't have a new ID for it.)
// Delete the orphan record.
$sql = "DELETE FROM `{$this->db->prefix}postmeta`
WHERE `meta_id` IN(" . implode( ', ', $meta ) . ");";
// If the number of affected rows was less than our divisor...
if ( $count < $this->divisor ) {
// Increment our step count.
$this->running[ 0 ][ 'current' ] += 1;
} // Otherwise... (We need to continue.)
// Record where we stopped.
$form[ 'last' ] = intval( $last );
// Put our form back on the stack.
array_push( $this->stage[ 'forms' ], $form );
// If there are no forms left to process...
if ( empty( $this->stage[ 'forms' ] ) ) {
// Mark that this stage is done.
$this->stage_complete = true;
* Function to compress our db results into a more useful format.
* @param $data (Array) The result to be compressed.
* @return (Array) Associative if our data was complex.
* Non-associative if our data was a single item.