Completed
Push — add/sync-partial-sync-checksum... ( bd259b...b214f6 )
by
unknown
08:24
created

Table_Checksum::validate_input()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 6
rs 10
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 $table_configuration = array();
13
	public $range_field     = '';
14
	public $key_fields      = array();
15
	public $checksum_fields = array();
16
	public $default_tables  = array();
17
18
	public $salt = '';
19
20
	public $allowed_tables = array();
21
22
	/**
23
	 * Table_Checksum constructor.
24
	 *
25
	 * @param string $table
26
	 * @param string $salt
27
	 * @param string $range_field
0 ignored issues
show
Documentation introduced by
Should the type for parameter $salt not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
28
	 * @param null   $key_fields
0 ignored issues
show
Bug introduced by
There is no parameter named $range_field. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
29
	 * @param null   $filter_field
0 ignored issues
show
Bug introduced by
There is no parameter named $key_fields. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
30
	 * @param array  $checksum_fields
0 ignored issues
show
Bug introduced by
There is no parameter named $filter_field. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
31
	 */
0 ignored issues
show
Bug introduced by
There is no parameter named $checksum_fields. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
32
33
	public function __construct( $table, $salt = null ) {
34
		$this->salt            = $salt;
35
36
		global $wpdb;
37
38
		$this->default_tables = array(
39
			'posts'              => array(
40
				'table'           => $wpdb->posts,
41
				'range_field'     => 'ID',
42
				'key_fields'      => array( 'ID' ),
43
				'checksum_fields' => array( 'post_modified' ),
44
			),
45
			'postmeta'           => array(
46
				'table'           => $wpdb->postmeta,
47
				'range_field'     => 'post_id',
48
				'key_fields'      => array( 'post_id', 'meta_key' ),
49
				'checksum_fields' => array( 'meta_key', 'meta_value' ),
50
			),
51
			'comments'           => array(
52
				'table'           => $wpdb->comments,
53
				'range_field'     => 'comment_ID',
54
				'key_fields'      => array( 'comment_ID' ),
55
				'checksum_fields' => array( 'comment_content' ),
56
			),
57
			'commentmeta'        => array(
58
				'table'           => $wpdb->commentmeta,
59
				'range_field'     => 'comment_id',
60
				'key_fields'      => array( 'comment_id', 'meta_key' ),
61
				'checksum_fields' => array( 'meta_key', 'meta_value' ),
62
			),
63
			'terms'              => array(
64
				'table'           => $wpdb->terms,
65
				'range_field'     => 'term_id',
66
				'key_fields'      => array( 'term_id' ),
67
				'checksum_fields' => array( 'term_id', 'name', 'slug' ),
68
			),
69
			'termmeta'           => array(
70
				'table'           => $wpdb->termmeta,
71
				'range_field'     => 'term_id',
72
				'key_fields'      => array( 'term_id', 'meta_key' ),
73
				'checksum_fields' => array( 'meta_key', 'meta_value' ),
74
			),
75
			'term_relationships' => $wpdb->term_relationships, // TODO describe in the array format or add exceptions
76
			'term_taxonomy'      => $wpdb->term_taxonomy, // TODO describe in the array format or add exceptions
77
			'links'              => $wpdb->links, // TODO describe in the array format or add exceptions
78
			'options'            => $wpdb->options, // TODO describe in the array format or add exceptions
79
		);
80
81
		// TODO change filters to allow the array format
82
		// TODO add get_fields or similar method to get things out of the table
83
		// TODO extract this configuration in a better way, still make it work with `$wpdb` names.
84
		// TODO take over the replicastore functions and move them over to this class
85
		// TODO make the API work
86
87
		$this->allowed_tables = apply_filters( 'jetpack_sync_checksum_allowed_tables', $this->default_tables );
88
89
		$this->table               = $this->validate_table_name( $table );
90
		$this->table_configuration = $this->allowed_tables[ $table ];
91
92
		$this->prepare_fields( $this->table_configuration );
93
94
	}
95
96
	private function prepare_fields( $table_configuration ) {
97
		$this->key_fields = $table_configuration['key_fields'];
98
		$this->range_field = $table_configuration['range_field'];
99
		$this->checksum_fields = $table_configuration['checksum_fields'];
100
	}
101
102
	private function validate_table_name( $table ) {
103
		if ( empty( $table ) ) {
104
			throw new Exception( 'Invalid table name: empty' );
105
		}
106
107
		if ( ! array_key_exists( $table, $this->allowed_tables ) ) {
108
			throw new Exception( 'Invalid table name: not allowed' );
109
		}
110
111
		// TODO other checks if such are needed.
112
113
		return $this->allowed_tables[$table]['table'];
114
	}
115
116
	private function validate_fields( $fields ) {
117
		foreach ( $fields as $field ) {
118
			if ( ! preg_match( '/^[0-9,a-z,A-Z$_]+$/i', $field ) ) {
119
				throw new Exception( "Invalid field name: {$field} is not allowed" );
120
			}
121
122
			// TODO other verifications of the field names
123
		}
124
	}
125
126
	private function validate_fields_against_table( $fields ) {
127
		global $wpdb;
128
129
		// TODO: Is this safe enough?
130
		$result = $wpdb->get_row( "SELECT * FROM {$this->table} LIMIT 1", ARRAY_A );
131
132
		if ( ! is_array( $result ) ) {
133
			throw new Exception( 'Unexpected $wpdb->query output: not array' );
134
		}
135
136
		// Check if the fields are actually contained in the table
137
		foreach ( $fields as $field_to_check ) {
138
			if ( ! array_key_exists( $field_to_check, $result ) ) {
139
				throw new Exception( "Invalid field name: field '{$field_to_check}' doesn't exist in table {$this->table}" );
140
			}
141
		}
142
143
		return true;
144
	}
145
146
	private function validate_input() {
147
		$fields = array_merge( array( $this->range_field ), $this->key_fields, $this->checksum_fields );
148
149
		$this->validate_fields( $fields );
150
		$this->validate_fields_against_table( $fields );
151
	}
152
153
	// TODO make sure the function is described as DOESN'T DO VALIDATION
154
	private function build_checksum_query( $range_from, $range_to, $filter_values, $granular_result ) {
155
		global $wpdb;
156
157
		// Make sure the range makes sense
158
		$range_start = min( $range_from, $range_to );
159
		$range_end   = max( $range_from, $range_to );
160
161
		// Escape the salt
162
		$salt = $wpdb->prepare( '%s', $this->salt ); // TODO escape or prepare statement
163
164
		// Prepare the compound key
165
		$key_fields = implode( ',', $this->key_fields );
166
167
		// Prepare the checksum fields
168
		$checksum_fields_string = implode( ',', array_merge( $this->checksum_fields, array( $salt ) ) );
169
170
		$filter_prepared_statement = '';
171
		if ( ! empty( $filter_values ) ) {
172
			// Prepare filtering
173
			$filter_placeholders       = "AND {$this->filter_field} IN(" . implode( ',', array_fill( 0, count( $filter_values ), '%s' ) ) . ')';
0 ignored issues
show
Bug introduced by
The property filter_field does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
174
			$filter_prepared_statement = $wpdb->prepare( $filter_placeholders, $filter_values );
175
		}
176
177
		$additional_fields = '';
178
		if ( $granular_result ) {
179
			// TODO uniq the fields as sometimes(most) range_index is the key and there's no need to select the same field twice
180
			$additional_fields = "
181
				{$this->range_field} as range_index,
182
			    {$key_fields},
183
			";
184
		}
185
186
		$query = "
187
			SELECT
188
				{$additional_fields}
189
				SUM(
190
					CRC32(
191
						CONCAT_WS( '#', {$salt}, {$checksum_fields_string} )
192
					)
193
				)  AS checksum
194
			 FROM
195
			    {$this->table}
196
			 WHERE
197
				{$this->range_field} > {$range_start} AND {$this->range_field} < {$range_end}
198
		        {$filter_prepared_statement}
199
		";
200
201
		/**
202
		 * We need the GROUP BY only for compound keys
203
		 */
204
		if ( $granular_result ) {
205
			$query .= "
206
				GROUP BY {$key_fields}
207
			";
208
		}
209
210
		return $query;
211
212
	}
213
214
	public function get_range_edges( $table, $range_col ) {
215
		global $wpdb;
216
217
		$this->validate_fields( array( $range_col ) );
218
219
		$result = $wpdb->get_row(
220
			"
221
			SELECT
222
			       MIN({$range_col}) as min_range,
223
			       MAX({$range_col}) as max_range,
224
			FROM
225
			     {$table}
226
	     ",
227
			ARRAY_A
228
		);
229
230
		if ( ! $result || ! is_array( $result ) ) {
231
			throw new Exception( 'Unable to get range edges' );
232
		}
233
234
		return $result;
235
	}
236
237
	public function prepare_results_for_output(&$results) {
238
		// get the compound key
239
		// only return range and compound key for granular results
240
241
		foreach ($results as &$result) {
242
			// Working on reference to save memory here.
243
244
			$key = array();
245
			foreach($this->key_fields as $field) {
246
				$key[] = $result[$field];
247
			}
248
249
			$result = array(
250
				'key' => implode('-', $key),
251
				'checksum' => $result['checksum'],
252
			);
253
254
		}
255
	}
256
257
	public function calculate_checksum( $range_from, $range_to, $filter_values, $granular_result = false ) {
258
		try {
259
			$this->validate_input();
260
		}
261
		catch ( Exception $ex ) {
262
			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...
263
		}
264
265
		$query = $this->build_checksum_query( $range_from, $range_to, $filter_values, $granular_result );
266
267
		global $wpdb;
268
269
		if ( ! $granular_result ) {
270
			$result = $wpdb->get_row( $query, ARRAY_A );
271
272
			if ( ! is_array( $result ) ) {
273
				return new WP_Error( 'invalid_query', "Result wasn't an array" );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_query'.

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...
274
			}
275
276
			return array(
277
				'range'    => $range_from . '-' . $range_to,
278
				'checksum' => $result['checksum'],
279
			);
280
		} else {
281
			$result = $wpdb->get_results( $query, ARRAY_A );
282
			$this->prepare_results_for_output( $result );
283
284
			return $result;
285
		}
286
	}
287
}
288