|
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() ); |
|
|
|
|
|
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
$query = $this->build_checksum_query( $table, $this->checksum_fields, $this->range_field, $range_from, $range_to, $salt, $granular_result ); |
|
|
|
|
|
|
180
|
|
|
|
|
181
|
|
|
// TODO fix |
|
182
|
|
|
return false; |
|
183
|
|
|
} |
|
184
|
|
|
} |
|
185
|
|
|
|
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
@ignorePhpDoc annotation to the duplicate definition and it will be ignored.