class.jetpack-sync-wp-replicastore.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <?php
  2. require_once dirname( __FILE__ ) . '/interface.jetpack-sync-replicastore.php';
  3. require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
  4. /**
  5. * An implementation of iJetpack_Sync_Replicastore which returns data stored in a WordPress.org DB.
  6. * This is useful to compare values in the local WP DB to values in the synced replica store
  7. */
  8. class Jetpack_Sync_WP_Replicastore implements iJetpack_Sync_Replicastore {
  9. public function reset() {
  10. global $wpdb;
  11. $wpdb->query( "DELETE FROM $wpdb->posts" );
  12. $wpdb->query( "DELETE FROM $wpdb->comments" );
  13. // also need to delete terms from cache
  14. $term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" );
  15. foreach ( $term_ids as $term_id ) {
  16. wp_cache_delete( $term_id, 'terms' );
  17. }
  18. $wpdb->query( "DELETE FROM $wpdb->terms" );
  19. $wpdb->query( "DELETE FROM $wpdb->term_taxonomy" );
  20. $wpdb->query( "DELETE FROM $wpdb->term_relationships" );
  21. // callables and constants
  22. $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" );
  23. $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" );
  24. }
  25. function full_sync_start( $config ) {
  26. $this->reset();
  27. }
  28. function full_sync_end( $checksum ) {
  29. // noop right now
  30. }
  31. public function post_count( $status = null, $min_id = null, $max_id = null ) {
  32. global $wpdb;
  33. $where = '';
  34. if ( $status ) {
  35. $where = "post_status = '" . esc_sql( $status ) . "'";
  36. } else {
  37. $where = '1=1';
  38. }
  39. if ( null != $min_id ) {
  40. $where .= ' AND ID >= ' . intval( $min_id );
  41. }
  42. if ( null != $max_id ) {
  43. $where .= ' AND ID <= ' . intval( $max_id );
  44. }
  45. return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" );
  46. }
  47. // TODO: actually use max_id/min_id
  48. public function get_posts( $status = null, $min_id = null, $max_id = null ) {
  49. $args = array( 'orderby' => 'ID', 'posts_per_page' => -1 );
  50. if ( $status ) {
  51. $args['post_status'] = $status;
  52. } else {
  53. $args['post_status'] = 'any';
  54. }
  55. return get_posts( $args );
  56. }
  57. public function get_post( $id ) {
  58. return get_post( $id );
  59. }
  60. public function upsert_post( $post, $silent = false ) {
  61. global $wpdb;
  62. // reject the post if it's not a WP_Post
  63. if ( ! $post instanceof WP_Post ) {
  64. return;
  65. }
  66. $post = $post->to_array();
  67. // reject posts without an ID
  68. if ( ! isset( $post['ID'] ) ) {
  69. return;
  70. }
  71. $now = current_time( 'mysql' );
  72. $now_gmt = get_gmt_from_date( $now );
  73. $defaults = array(
  74. 'ID' => 0,
  75. 'post_author' => '0',
  76. 'post_content' => '',
  77. 'post_content_filtered' => '',
  78. 'post_title' => '',
  79. 'post_name' => '',
  80. 'post_excerpt' => '',
  81. 'post_status' => 'draft',
  82. 'post_type' => 'post',
  83. 'comment_status' => 'closed',
  84. 'comment_count' => '0',
  85. 'ping_status' => '',
  86. 'post_password' => '',
  87. 'to_ping' => '',
  88. 'pinged' => '',
  89. 'post_parent' => 0,
  90. 'menu_order' => 0,
  91. 'guid' => '',
  92. 'post_date' => $now,
  93. 'post_date_gmt' => $now_gmt,
  94. 'post_modified' => $now,
  95. 'post_modified_gmt' => $now_gmt,
  96. );
  97. $post = array_intersect_key( $post, $defaults );
  98. $post = sanitize_post( $post, 'db' );
  99. unset( $post['filter'] );
  100. $exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) );
  101. if ( $exists ) {
  102. $wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) );
  103. } else {
  104. $wpdb->insert( $wpdb->posts, $post );
  105. }
  106. clean_post_cache( $post['ID'] );
  107. }
  108. public function delete_post( $post_id ) {
  109. wp_delete_post( $post_id, true );
  110. }
  111. public function posts_checksum( $min_id = null, $max_id = null ) {
  112. global $wpdb;
  113. return $this->table_checksum( $wpdb->posts, Jetpack_Sync_Defaults::$default_post_checksum_columns , 'ID', Jetpack_Sync_Settings::get_blacklisted_post_types_sql(), $min_id, $max_id );
  114. }
  115. public function post_meta_checksum( $min_id = null, $max_id = null ) {
  116. global $wpdb;
  117. return $this->table_checksum( $wpdb->postmeta, Jetpack_Sync_Defaults::$default_post_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id );
  118. }
  119. public function comment_count( $status = null, $min_id = null, $max_id = null ) {
  120. global $wpdb;
  121. $comment_approved = $this->comment_status_to_approval_value( $status );
  122. if ( $comment_approved !== false ) {
  123. $where = "comment_approved = '" . esc_sql( $comment_approved ) . "'";
  124. } else {
  125. $where = '1=1';
  126. }
  127. if ( $min_id != null ) {
  128. $where .= ' AND comment_ID >= ' . intval( $min_id );
  129. }
  130. if ( $max_id != null ) {
  131. $where .= ' AND comment_ID <= ' . intval( $max_id );
  132. }
  133. return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" );
  134. }
  135. private function comment_status_to_approval_value( $status ) {
  136. switch ( $status ) {
  137. case 'approve':
  138. return '1';
  139. case 'hold':
  140. return '0';
  141. case 'spam':
  142. return 'spam';
  143. case 'trash':
  144. return 'trash';
  145. case 'any':
  146. return false;
  147. case 'all':
  148. return false;
  149. default:
  150. return false;
  151. }
  152. }
  153. // TODO: actually use max_id/min_id
  154. public function get_comments( $status = null, $min_id = null, $max_id = null ) {
  155. $args = array( 'orderby' => 'ID', 'status' => 'all' );
  156. if ( $status ) {
  157. $args['status'] = $status;
  158. }
  159. return get_comments( $args );
  160. }
  161. public function get_comment( $id ) {
  162. return WP_Comment::get_instance( $id );
  163. }
  164. public function upsert_comment( $comment ) {
  165. global $wpdb, $wp_version;
  166. if ( version_compare( $wp_version, '4.4', '<' ) ) {
  167. $comment = (array) $comment;
  168. } else {
  169. // WP 4.4 introduced the WP_Comment Class
  170. $comment = $comment->to_array();
  171. }
  172. // filter by fields on comment table
  173. $comment_fields_whitelist = array(
  174. 'comment_ID',
  175. 'comment_post_ID',
  176. 'comment_author',
  177. 'comment_author_email',
  178. 'comment_author_url',
  179. 'comment_author_IP',
  180. 'comment_date',
  181. 'comment_date_gmt',
  182. 'comment_content',
  183. 'comment_karma',
  184. 'comment_approved',
  185. 'comment_agent',
  186. 'comment_type',
  187. 'comment_parent',
  188. 'user_id',
  189. );
  190. foreach ( $comment as $key => $value ) {
  191. if ( ! in_array( $key, $comment_fields_whitelist ) ) {
  192. unset( $comment[ $key ] );
  193. }
  194. }
  195. $exists = $wpdb->get_var(
  196. $wpdb->prepare(
  197. "SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )",
  198. $comment['comment_ID']
  199. )
  200. );
  201. if ( $exists ) {
  202. $wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) );
  203. } else {
  204. $wpdb->insert( $wpdb->comments, $comment );
  205. }
  206. wp_update_comment_count( $comment['comment_post_ID'] );
  207. }
  208. public function trash_comment( $comment_id ) {
  209. wp_delete_comment( $comment_id );
  210. }
  211. public function delete_comment( $comment_id ) {
  212. wp_delete_comment( $comment_id, true );
  213. }
  214. public function spam_comment( $comment_id ) {
  215. wp_spam_comment( $comment_id );
  216. }
  217. public function trashed_post_comments( $post_id, $statuses ) {
  218. wp_trash_post_comments( $post_id );
  219. }
  220. public function untrashed_post_comments( $post_id ) {
  221. wp_untrash_post_comments( $post_id );
  222. }
  223. public function comments_checksum( $min_id = null, $max_id = null ) {
  224. global $wpdb;
  225. return $this->table_checksum( $wpdb->comments, Jetpack_Sync_Defaults::$default_comment_checksum_columns, 'comment_ID', Jetpack_Sync_Settings::get_comments_filter_sql(), $min_id, $max_id );
  226. }
  227. public function comment_meta_checksum( $min_id = null, $max_id = null ) {
  228. global $wpdb;
  229. return $this->table_checksum( $wpdb->commentmeta, Jetpack_Sync_Defaults::$default_comment_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id );
  230. }
  231. public function options_checksum() {
  232. global $wpdb;
  233. $options_whitelist = "'" . implode( "', '", Jetpack_Sync_Defaults::$default_options_whitelist ) . "'";
  234. $where_sql = "option_name IN ( $options_whitelist )";
  235. return $this->table_checksum( $wpdb->options, Jetpack_Sync_Defaults::$default_option_checksum_columns, null, $where_sql, null, null );
  236. }
  237. public function update_option( $option, $value ) {
  238. return update_option( $option, $value );
  239. }
  240. public function get_option( $option, $default = false ) {
  241. return get_option( $option, $default );
  242. }
  243. public function delete_option( $option ) {
  244. return delete_option( $option );
  245. }
  246. public function set_theme_support( $theme_support ) {
  247. // noop
  248. }
  249. public function current_theme_supports( $feature ) {
  250. return current_theme_supports( $feature );
  251. }
  252. public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) {
  253. return get_metadata( $type, $object_id, $meta_key, $single );
  254. }
  255. /**
  256. *
  257. * Stores remote meta key/values alongside an ID mapping key
  258. *
  259. * @param $type
  260. * @param $object_id
  261. * @param $meta_key
  262. * @param $meta_value
  263. * @param $meta_id
  264. *
  265. * @return bool
  266. */
  267. public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) {
  268. $table = _get_meta_table( $type );
  269. if ( ! $table ) {
  270. return false;
  271. }
  272. global $wpdb;
  273. $exists = $wpdb->get_var( $wpdb->prepare(
  274. "SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )",
  275. $meta_id
  276. ) );
  277. if ( $exists ) {
  278. $wpdb->update( $table, array(
  279. 'meta_key' => $meta_key,
  280. 'meta_value' => maybe_serialize( $meta_value ),
  281. ), array( 'meta_id' => $meta_id ) );
  282. } else {
  283. $object_id_field = $type . '_id';
  284. $wpdb->insert( $table, array(
  285. 'meta_id' => $meta_id,
  286. $object_id_field => $object_id,
  287. 'meta_key' => $meta_key,
  288. 'meta_value' => maybe_serialize( $meta_value ),
  289. ) );
  290. }
  291. wp_cache_delete( $object_id, $type . '_meta' );
  292. return true;
  293. }
  294. public function delete_metadata( $type, $object_id, $meta_ids ) {
  295. global $wpdb;
  296. $table = _get_meta_table( $type );
  297. if ( ! $table ) {
  298. return false;
  299. }
  300. foreach ( $meta_ids as $meta_id ) {
  301. $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) );
  302. }
  303. // if we don't have an object ID what do we do - invalidate ALL meta?
  304. if ( $object_id ) {
  305. wp_cache_delete( $object_id, $type . '_meta' );
  306. }
  307. }
  308. // todo: test this out to make sure it works as expected.
  309. public function delete_batch_metadata( $type, $object_ids, $meta_key ) {
  310. global $wpdb;
  311. $table = _get_meta_table( $type );
  312. if ( ! $table ) {
  313. return false;
  314. }
  315. $column = sanitize_key($type . '_id' );
  316. $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) );
  317. // if we don't have an object ID what do we do - invalidate ALL meta?
  318. foreach ( $object_ids as $object_id ) {
  319. wp_cache_delete( $object_id, $type . '_meta' );
  320. }
  321. }
  322. // constants
  323. public function get_constant( $constant ) {
  324. $value = get_option( 'jetpack_constant_' . $constant );
  325. if ( $value ) {
  326. return $value;
  327. }
  328. return null;
  329. }
  330. public function set_constant( $constant, $value ) {
  331. update_option( 'jetpack_constant_' . $constant, $value );
  332. }
  333. public function get_updates( $type ) {
  334. $all_updates = get_option( 'jetpack_updates', array() );
  335. if ( isset( $all_updates[ $type ] ) ) {
  336. return $all_updates[ $type ];
  337. } else {
  338. return null;
  339. }
  340. }
  341. public function set_updates( $type, $updates ) {
  342. $all_updates = get_option( 'jetpack_updates', array() );
  343. $all_updates[ $type ] = $updates;
  344. update_option( 'jetpack_updates', $all_updates );
  345. }
  346. // functions
  347. public function get_callable( $name ) {
  348. $value = get_option( 'jetpack_' . $name );
  349. if ( $value ) {
  350. return $value;
  351. }
  352. return null;
  353. }
  354. public function set_callable( $name, $value ) {
  355. update_option( 'jetpack_' . $name, $value );
  356. }
  357. // network options
  358. public function get_site_option( $option ) {
  359. return get_option( 'jetpack_network_' . $option );
  360. }
  361. public function update_site_option( $option, $value ) {
  362. return update_option( 'jetpack_network_' . $option, $value );
  363. }
  364. public function delete_site_option( $option ) {
  365. return delete_option( 'jetpack_network_' . $option );
  366. }
  367. // terms
  368. // terms
  369. public function get_terms( $taxonomy ) {
  370. return get_terms( $taxonomy );
  371. }
  372. public function get_term( $taxonomy, $term_id, $is_term_id = true ) {
  373. $t = $this->ensure_taxonomy( $taxonomy );
  374. if ( ! $t || is_wp_error( $t ) ) {
  375. return $t;
  376. }
  377. return get_term( $term_id, $taxonomy );
  378. }
  379. private function ensure_taxonomy( $taxonomy ) {
  380. if ( ! taxonomy_exists( $taxonomy ) ) {
  381. // try re-registering synced taxonomies
  382. $taxonomies = $this->get_callable( 'taxonomies' );
  383. if ( ! isset( $taxonomies[ $taxonomy ] ) ) {
  384. // doesn't exist, or somehow hasn't been synced
  385. return new WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" );
  386. }
  387. $t = $taxonomies[ $taxonomy ];
  388. return register_taxonomy(
  389. $taxonomy,
  390. $t->object_type,
  391. (array) $t
  392. );
  393. }
  394. return true;
  395. }
  396. public function get_the_terms( $object_id, $taxonomy ) {
  397. return get_the_terms( $object_id, $taxonomy );
  398. }
  399. public function update_term( $term_object ) {
  400. $taxonomy = $term_object->taxonomy;
  401. global $wpdb;
  402. $exists = $wpdb->get_var( $wpdb->prepare(
  403. "SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )",
  404. $term_object->term_id
  405. ) );
  406. if ( ! $exists ) {
  407. $term_object = sanitize_term( clone( $term_object ), $taxonomy, 'db' );
  408. $term = array(
  409. 'term_id' => $term_object->term_id,
  410. 'name' => $term_object->name,
  411. 'slug' => $term_object->slug,
  412. 'term_group' => $term_object->term_group,
  413. );
  414. $term_taxonomy = array(
  415. 'term_taxonomy_id' => $term_object->term_taxonomy_id,
  416. 'term_id' => $term_object->term_id,
  417. 'taxonomy' => $term_object->taxonomy,
  418. 'description' => $term_object->description,
  419. 'parent' => (int) $term_object->parent,
  420. 'count' => (int) $term_object->count,
  421. );
  422. $wpdb->insert( $wpdb->terms, $term );
  423. $wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy );
  424. return true;
  425. }
  426. return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object );
  427. }
  428. public function delete_term( $term_id, $taxonomy ) {
  429. return wp_delete_term( $term_id, $taxonomy );
  430. }
  431. public function update_object_terms( $object_id, $taxonomy, $terms, $append ) {
  432. wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
  433. }
  434. public function delete_object_terms( $object_id, $tt_ids ) {
  435. global $wpdb;
  436. if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) {
  437. // escape
  438. $tt_ids_sanitized = array_map( 'intval', $tt_ids );
  439. $taxonomies = array();
  440. foreach ( $tt_ids_sanitized as $tt_id ) {
  441. $term = get_term_by( 'term_taxonomy_id', $tt_id );
  442. $taxonomies[ $term->taxonomy ][] = $tt_id;
  443. }
  444. $in_tt_ids = implode( ", ", $tt_ids_sanitized );
  445. /**
  446. * Fires immediately before an object-term relationship is deleted.
  447. *
  448. * @since 2.9.0
  449. *
  450. * @param int $object_id Object ID.
  451. * @param array $tt_ids An array of term taxonomy IDs.
  452. */
  453. do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized );
  454. $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
  455. foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) {
  456. $this->ensure_taxonomy( $taxonomy );
  457. wp_cache_delete( $object_id, $taxonomy . '_relationships' );
  458. /**
  459. * Fires immediately after an object-term relationship is deleted.
  460. *
  461. * @since 2.9.0
  462. *
  463. * @param int $object_id Object ID.
  464. * @param array $tt_ids An array of term taxonomy IDs.
  465. */
  466. do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids );
  467. wp_update_term_count( $taxonomy_tt_ids, $taxonomy );
  468. }
  469. return (bool) $deleted;
  470. }
  471. return false;
  472. }
  473. // users
  474. public function user_count() {
  475. }
  476. public function get_user( $user_id ) {
  477. return WP_User::get_instance( $user_id );
  478. }
  479. public function upsert_user( $user ) {
  480. $this->invalid_call();
  481. }
  482. public function delete_user( $user_id ) {
  483. $this->invalid_call();
  484. }
  485. public function upsert_user_locale( $user_id, $local ) {
  486. $this->invalid_call();
  487. }
  488. public function delete_user_locale( $user_id ) {
  489. $this->invalid_call();
  490. }
  491. public function get_user_locale( $user_id ) {
  492. return jetpack_get_user_locale( $user_id );
  493. }
  494. public function get_allowed_mime_types( $user_id ) {
  495. }
  496. public function checksum_all() {
  497. $post_meta_checksum = $this->checksum_histogram( 'post_meta', 1 );
  498. $comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 );
  499. return array(
  500. 'posts' => $this->posts_checksum(),
  501. 'comments' => $this->comments_checksum(),
  502. 'post_meta'=> reset( $post_meta_checksum ),
  503. 'comment_meta'=> reset( $comment_meta_checksum ),
  504. );
  505. }
  506. function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true ) {
  507. global $wpdb;
  508. $wpdb->queries = array();
  509. switch( $object_type ) {
  510. case "posts":
  511. $object_count = $this->post_count( null, $start_id, $end_id );
  512. $object_table = $wpdb->posts;
  513. $id_field = 'ID';
  514. $where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
  515. if ( empty( $columns ) ) {
  516. $columns = Jetpack_Sync_Defaults::$default_post_checksum_columns;
  517. }
  518. break;
  519. case "post_meta":
  520. $object_table = $wpdb->postmeta;
  521. $where_sql = Jetpack_Sync_Settings::get_whitelisted_post_meta_sql();
  522. $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
  523. $id_field = 'meta_id';
  524. if ( empty( $columns ) ) {
  525. $columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
  526. }
  527. break;
  528. case "comments":
  529. $object_count = $this->comment_count( null, $start_id, $end_id );
  530. $object_table = $wpdb->comments;
  531. $id_field = 'comment_ID';
  532. $where_sql = Jetpack_Sync_Settings::get_comments_filter_sql();
  533. if ( empty( $columns ) ) {
  534. $columns = Jetpack_Sync_Defaults::$default_comment_checksum_columns;
  535. }
  536. break;
  537. case "comment_meta":
  538. $object_table = $wpdb->commentmeta;
  539. $where_sql = Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql();
  540. $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
  541. $id_field = 'meta_id';
  542. if ( empty( $columns ) ) {
  543. $columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
  544. }
  545. break;
  546. default:
  547. return false;
  548. }
  549. $bucket_size = intval( ceil( $object_count / $buckets ) );
  550. $previous_max_id = 0;
  551. $histogram = array();
  552. $where = '1=1';
  553. if ( $start_id ) {
  554. $where .= " AND $id_field >= " . intval( $start_id );
  555. }
  556. if ( $end_id ) {
  557. $where .= " AND $id_field <= " . intval( $end_id );
  558. }
  559. do {
  560. list( $first_id, $last_id ) = $wpdb->get_row(
  561. "SELECT MIN($id_field) as min_id, MAX($id_field) as max_id FROM ( SELECT $id_field FROM $object_table WHERE $where AND $id_field > $previous_max_id ORDER BY $id_field ASC LIMIT $bucket_size ) as ids",
  562. ARRAY_N
  563. );
  564. // get the checksum value
  565. $value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $first_id, $last_id, $strip_non_ascii );
  566. if ( is_wp_error( $value ) ) {
  567. return $value;
  568. }
  569. if ( $first_id === null || $last_id === null ) {
  570. break;
  571. } elseif ( $first_id === $last_id ) {
  572. $histogram[ $first_id ] = $value;
  573. } else {
  574. $histogram[ "{$first_id}-{$last_id}" ] = $value;
  575. }
  576. $previous_max_id = $last_id;
  577. } while ( true );
  578. return $histogram;
  579. }
  580. private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true ) {
  581. global $wpdb;
  582. // sanitize to just valid MySQL column names
  583. $sanitized_columns = preg_grep ( '/^[0-9,a-z,A-Z$_]+$/i', $columns );
  584. if ( $strip_non_ascii ) {
  585. $columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns ) );
  586. } else {
  587. $columns_sql = implode( ',', $sanitized_columns );
  588. }
  589. if ( $min_id !== null ) {
  590. $min_id = intval( $min_id );
  591. $where_sql .= " AND $id_column >= $min_id";
  592. }
  593. if ( $max_id !== null ) {
  594. $max_id = intval( $max_id );
  595. $where_sql .= " AND $id_column <= $max_id";
  596. }
  597. $query = <<<ENDSQL
  598. SELECT CONV(BIT_XOR(CRC32(CONCAT({$columns_sql}))), 10, 16)
  599. FROM $table
  600. WHERE $where_sql
  601. ENDSQL;
  602. $result = $wpdb->get_var( $query );
  603. if ( $wpdb->last_error ) {
  604. return new WP_Error( 'database_error', $wpdb->last_error );
  605. }
  606. return $result;
  607. }
  608. private function meta_count( $table, $where_sql, $min_id, $max_id ) {
  609. global $wpdb;
  610. if ( $min_id != null ) {
  611. $where_sql .= ' AND meta_id >= ' . intval( $min_id );
  612. }
  613. if ( $max_id != null ) {
  614. $where_sql .= ' AND meta_id <= ' . intval( $max_id );
  615. }
  616. return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where_sql" );
  617. }
  618. /**
  619. * Wraps a column name in SQL which strips non-ASCII chars.
  620. * This helps normalize data to avoid checksum differences caused by
  621. * badly encoded data in the DB
  622. */
  623. function strip_non_ascii_sql( $column_name ) {
  624. return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )";
  625. }
  626. private function invalid_call() {
  627. $backtrace = debug_backtrace();
  628. $caller = $backtrace[1]['function'];
  629. throw new Exception( "This function $caller is not supported on the WP Replicastore" );
  630. }
  631. }