Completed
Push — add/sync-partial-sync-checksum... ( bd259b )
by
unknown
12:31 queued 04:22
created

Table_Checksum::get_range_edges()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 2
dl 0
loc 24
rs 9.536
c 0
b 0
f 0
1
<?php
2
3
namespace Automattic\Jetpack\Sync\Replicastore;
4
5
use Exception;
6
use WP_error;
7
8
// TODO add rest endpoints to work with this, hopefully in the same folder
9
10
class Table_Checksum {
11
	public $table           = '';
12
	public $range_field     = '';
13
	public $key_fields      = array();
14
	public $checksum_fields = array();
15
16
	public $salt = '';
17
18
	public $allowed_table_names = array();
19
20
	/**
21
	 * Table_Checksum constructor.
22
	 *
23
	 * @param string $table
24
	 * @param string $range_field
25
	 * @param array  $checksum_fields
26
	 * @param string $salt
27
	 */
28
	public function __construct( $table, $range_field, $key_fields, $checksum_fields, $salt ) {
29
		$this->table           = $table;
30
		$this->range_field     = $range_field;
31
		$this->checksum_fields = $checksum_fields;
32
		$this->key_fields      = $key_fields;
33
		$this->salt            = $salt;
34
35
		global $wpdb;
36
37
		$this->allowed_table_names = array(
38
			'posts'              => $wpdb->posts,
39
			'postmeta'           => $wpdb->postmeta,
40
			'comments'           => $wpdb->comments,
41
			'commentmeta'        => $wpdb->commentmeta,
42
			'terms'              => $wpdb->terms,
43
			'termmeta'           => $wpdb->termmeta,
44
			'term_relationships' => $wpdb->term_relationships,
45
			'term_taxonomy'      => $wpdb->term_taxonomy,
46
			'links'              => $wpdb->links,
47
			'options'            => $wpdb->options,
48
		);
49
	}
50
51
	protected function validate_table_name( $table ) {
52
		if ( empty( $table ) ) {
53
			throw new Exception( 'Invalid table name: empty' );
54
		}
55
56
		if ( ! in_array( $table, $this->allowed_table_names, true ) ) {
57
			throw new Exception( 'Invalid table name: not allowed' );
58
		}
59
60
		// TODO other checks if such are needed.
61
62
		return $table;
63
	}
64
65
	public function validate_fields( $fields ) {
66
		foreach ( $fields as $field ) {
67
			if ( ! preg_match( '/^[0-9,a-z,A-Z$_]+$/i', $field ) ) {
68
				throw new Exception( "Invalid field name: {$field} is not allowed" );
69
			}
70
71
			// TODO other verifications of the field names
72
		}
73
	}
74
75
	public function validate_fields_against_table( $table, $fields ) {
76
		global $wpdb;
77
78
		$table = $this->validate_table_name( $table );
79
80
		// TODO: Is this safe enough?
81
		$result = $wpdb->query( "SELECT * FROM {$table} LIMIT 1", ARRAY_A );
82
83
		if ( ! is_array( $result ) ) {
84
			throw new Exception( 'Unexpected $wpdb->query output: not array' );
85
		}
86
87
		// Check if the fields are actually contained in the table
88
		foreach ( $fields as $field_to_check ) {
89
			if ( ! array_key_exists( $field_to_check, $result ) ) {
90
				throw new Exception( "Invalid field name: field '{$field_to_check}' doesn't exist in table {$table}" );
91
			}
92
		}
93
94
		return true;
95
	}
96
97
	// TODO make sure the function is described as DOESN'T DO VALIDATION
98
	public function build_checksum_query( $table, $key_fields, $checksum_fields, $range_field, $range_from, $range_to, $filter_field, $filter_values, $salt, $granular_result ) {
99
		global $wpdb;
100
101
		// Make sure the range makes sense
102
		$range_start = min( $range_from, $range_to );
103
		$range_end   = max( $range_from, $range_to );
104
105
		// Escape the salt
106
		$salt = $wpdb->_real_escape( $salt ); // TODO escape or prepare statement
107
108
		// Prepare the compound key
109
		$key_fields = implode( ',', $key_fields );
110
111
		// Prepare the checksum fields
112
		$checksum_fields_string = implode( ',', array_merge( $checksum_fields, array( $salt ) ) );
113
114
		// Prepare filtering
115
		$filter_placeholders       = 'IN(' . implode( ',', array_fill( 0, count( $filter_values ), '%s' ) ) . ')';
116
		$filter_prepared_statement = $wpdb->prepare( $filter_placeholders, $filter_values );
117
118
		$query = "
119
			SELECT
120
			     {$range_field} as range_index,
121
			     {$key_fields},
122
			     SUM(
123
			         CRC32(
124
			                 CONCAT_WS( '#', '%s', {$checksum_fields_string} )
125
			             )
126
			     )  AS checksum
127
			 FROM
128
			    {$table}
129
			 WHERE
130
				{$range_field} > {$range_start} AND {$range_field} < {$range_end}
131
			   	AND {$filter_field} {$filter_prepared_statement} # Filter example
132
			GROUP BY {$key_fields};
133
		";
134
135
		return $query;
136
137
	}
138
139
	public function get_range_edges( $table, $range_col ) {
140
		global $wpdb;
141
142
		$this->validate_fields( array( $range_col ) );
143
144
		// TODO decide if we need the count column or only the range edges. Adding `COUNT(DISTINCT)` is kind of slow
145
		$result = $wpdb->get_row(
146
			"
147
			SELECT
148
			       MIN({$range_col}) as min_range,
149
			       MAX({$range_col}) as max_range,
150
			       COUNT(DISTINCT {$range_col}) as total_count
151
			FROM
152
			     {$table}
153
	     ",
154
			ARRAY_A
155
		);
156
157
		if ( ! $result || ! is_array( $result ) ) {
158
			throw new Exception( 'Unable to get range edges' );
159
		}
160
161
		return $result;
162
	}
163
164
	public function calculate_checksum( $range_from, $range_to, $salt, $granular_result = false ) {
165
		try {
166
			$table = $this->validate_table_name( $this->table );
167
168
			$fields = array_merge( array( $this->range_field ), $this->key_fields, $this->checksum_fields );
169
170
			$this->validate_fields( $fields );
171
			$this->validate_fields_against_table( $table, $fields );
172
			// TODO validate ranges?
173
			// TODO validate salt?
174
			// TODO granular/non-granular result
175
		} catch ( Exception $ex ) {
176
			return new WP_error( 'invalid_input', $ex->getMessage() );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
177
		}
178
179
		$query = $this->build_checksum_query( $table, $this->checksum_fields, $this->range_field, $range_from, $range_to, $salt, $granular_result );
0 ignored issues
show
Bug introduced by
The call to build_checksum_query() misses some required arguments starting with $filter_values.
Loading history...
Unused Code introduced by
$query is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
180
181
		// TODO fix
182
		return false;
183
	}
184
}
185