class-sitemaps-cache-validator.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\XML_Sitemaps
  6. */
  7. /**
  8. * Handles storage keys for sitemaps caching and invalidation.
  9. *
  10. * @since 3.2
  11. */
  12. class WPSEO_Sitemaps_Cache_Validator {
  13. /**
  14. * Prefix of the transient key for sitemap caches.
  15. *
  16. * @var string
  17. */
  18. const STORAGE_KEY_PREFIX = 'yst_sm_';
  19. /**
  20. * Name of the option that holds the global validation value.
  21. *
  22. * @var string
  23. */
  24. const VALIDATION_GLOBAL_KEY = 'wpseo_sitemap_cache_validator_global';
  25. /**
  26. * The format which creates the key of the option that holds the type validation value.
  27. *
  28. * @var string
  29. */
  30. const VALIDATION_TYPE_KEY_FORMAT = 'wpseo_sitemap_%s_cache_validator';
  31. /**
  32. * Get the cache key for a certain type and page.
  33. *
  34. * A type of cache would be something like 'page', 'post' or 'video'.
  35. *
  36. * Example key format for sitemap type "post", page 1: wpseo_sitemap_post_1:akfw3e_23azBa .
  37. *
  38. * @since 3.2
  39. *
  40. * @param null|string $type The type to get the key for. Null or self::SITEMAP_INDEX_TYPE for index cache.
  41. * @param int $page The page of cache to get the key for.
  42. *
  43. * @return bool|string The key where the cache is stored on. False if the key could not be generated.
  44. */
  45. public static function get_storage_key( $type = null, $page = 1 ) {
  46. // Using SITEMAP_INDEX_TYPE for sitemap index cache.
  47. $type = is_null( $type ) ? WPSEO_Sitemaps::SITEMAP_INDEX_TYPE : $type;
  48. $global_cache_validator = self::get_validator();
  49. $type_cache_validator = self::get_validator( $type );
  50. $prefix = self::STORAGE_KEY_PREFIX;
  51. $postfix = sprintf( '_%d:%s_%s', $page, $global_cache_validator, $type_cache_validator );
  52. try {
  53. $type = self::truncate_type( $type, $prefix, $postfix );
  54. } catch ( OutOfBoundsException $exception ) {
  55. // Maybe do something with the exception, for now just mark as invalid.
  56. return false;
  57. }
  58. // Build key.
  59. $full_key = $prefix . $type . $postfix;
  60. return $full_key;
  61. }
  62. /**
  63. * If the type is over length make sure we compact it so we don't have any database problems.
  64. *
  65. * When there are more 'extremely long' post types, changes are they have variations in either the start or ending.
  66. * Because of this, we cut out the excess in the middle which should result in less chance of collision.
  67. *
  68. * @since 3.2
  69. *
  70. * @param string $type The type of sitemap to be used.
  71. * @param string $prefix The part before the type in the cache key. Only the length is used.
  72. * @param string $postfix The part after the type in the cache key. Only the length is used.
  73. *
  74. * @return string The type with a safe length to use
  75. *
  76. * @throws OutOfRangeException When there is less than 15 characters of space for a key that is originally longer.
  77. */
  78. public static function truncate_type( $type, $prefix = '', $postfix = '' ) {
  79. /*
  80. * This length has been restricted by the database column length of 64 in the past.
  81. * The prefix added by WordPress is '_transient_' because we are saving to a transient.
  82. * We need to use a timeout on the transient, otherwise the values get autoloaded, this adds
  83. * another restriction to the length.
  84. */
  85. $max_length = 45; // 64 - 19 ('_transient_timeout_')
  86. $max_length -= strlen( $prefix );
  87. $max_length -= strlen( $postfix );
  88. if ( strlen( $type ) > $max_length ) {
  89. if ( $max_length < 15 ) {
  90. /*
  91. * If this happens the most likely cause is a page number that is too high.
  92. *
  93. * So this would not happen unintentionally.
  94. * Either by trying to cause a high server load, finding backdoors or misconfiguration.
  95. */
  96. throw new OutOfRangeException(
  97. __(
  98. 'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.',
  99. 'wordpress-seo'
  100. )
  101. );
  102. }
  103. $half = ( $max_length / 2 );
  104. $first_part = substr( $type, 0, ( ceil( $half ) - 1 ) );
  105. $last_part = substr( $type, ( 1 - floor( $half ) ) );
  106. $type = $first_part . '..' . $last_part;
  107. }
  108. return $type;
  109. }
  110. /**
  111. * Invalidate sitemap cache.
  112. *
  113. * @since 3.2
  114. *
  115. * @param null|string $type The type to get the key for. Null for all caches.
  116. *
  117. * @return void
  118. */
  119. public static function invalidate_storage( $type = null ) {
  120. // Global validator gets cleared when no type is provided.
  121. $old_validator = null;
  122. // Get the current type validator.
  123. if ( ! is_null( $type ) ) {
  124. $old_validator = self::get_validator( $type );
  125. }
  126. // Refresh validator.
  127. self::create_validator( $type );
  128. if ( ! wp_using_ext_object_cache() ) {
  129. // Clean up current cache from the database.
  130. self::cleanup_database( $type, $old_validator );
  131. }
  132. // External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that.
  133. }
  134. /**
  135. * Cleanup invalidated database cache.
  136. *
  137. * @since 3.2
  138. *
  139. * @param null|string $type The type of sitemap to clear cache for.
  140. * @param null|string $validator The validator to clear cache of.
  141. *
  142. * @return void
  143. */
  144. public static function cleanup_database( $type = null, $validator = null ) {
  145. global $wpdb;
  146. if ( is_null( $type ) ) {
  147. // Clear all cache if no type is provided.
  148. $like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX );
  149. }
  150. else {
  151. // Clear type cache for all type keys.
  152. $like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type );
  153. }
  154. /*
  155. * Add slashes to the LIKE "_" single character wildcard.
  156. *
  157. * We can't use `esc_like` here because we need the % in the query.
  158. */
  159. $where = [];
  160. $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) );
  161. $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) );
  162. // Delete transients.
  163. $query = sprintf( 'DELETE FROM %1$s WHERE %2$s', $wpdb->options, implode( ' OR ', $where ) );
  164. $wpdb->query( $query );
  165. wp_cache_delete( 'alloptions', 'options' );
  166. }
  167. /**
  168. * Get the current cache validator.
  169. *
  170. * Without the type the global validator is returned.
  171. * This can invalidate -all- keys in cache at once.
  172. *
  173. * With the type parameter the validator for that specific type can be invalidated.
  174. *
  175. * @since 3.2
  176. *
  177. * @param string $type Provide a type for a specific type validator, empty for global validator.
  178. *
  179. * @return null|string The validator for the supplied type.
  180. */
  181. public static function get_validator( $type = '' ) {
  182. $key = self::get_validator_key( $type );
  183. $current = get_option( $key, null );
  184. if ( ! is_null( $current ) ) {
  185. return $current;
  186. }
  187. if ( self::create_validator( $type ) ) {
  188. return self::get_validator( $type );
  189. }
  190. return null;
  191. }
  192. /**
  193. * Get the cache validator option key for the specified type.
  194. *
  195. * @since 3.2
  196. *
  197. * @param string $type Provide a type for a specific type validator, empty for global validator.
  198. *
  199. * @return string Validator to be used to generate the cache key.
  200. */
  201. public static function get_validator_key( $type = '' ) {
  202. if ( empty( $type ) ) {
  203. return self::VALIDATION_GLOBAL_KEY;
  204. }
  205. return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type );
  206. }
  207. /**
  208. * Refresh the cache validator value.
  209. *
  210. * @since 3.2
  211. *
  212. * @param string $type Provide a type for a specific type validator, empty for global validator.
  213. *
  214. * @return bool True if validator key has been saved as option.
  215. */
  216. public static function create_validator( $type = '' ) {
  217. $key = self::get_validator_key( $type );
  218. // Generate new validator.
  219. $microtime = microtime();
  220. // Remove space.
  221. list( $milliseconds, $seconds ) = explode( ' ', $microtime );
  222. // Transients are purged every 24h.
  223. $seconds = ( $seconds % DAY_IN_SECONDS );
  224. $milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 );
  225. // Combine seconds and milliseconds and convert to integer.
  226. $validator = intval( $seconds . '' . $milliseconds, 10 );
  227. // Apply base 61 encoding.
  228. $compressed = self::convert_base10_to_base61( $validator );
  229. return update_option( $key, $compressed, false );
  230. }
  231. /**
  232. * Encode to base61 format.
  233. *
  234. * @since 3.2
  235. *
  236. * This is base64 (numeric + alpha + alpha upper case) without the 0.
  237. *
  238. * @param int $base10 The number that has to be converted to base 61.
  239. *
  240. * @return string Base 61 converted string.
  241. *
  242. * @throws InvalidArgumentException When the input is not an integer.
  243. */
  244. public static function convert_base10_to_base61( $base10 ) {
  245. if ( ! is_int( $base10 ) ) {
  246. throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) );
  247. }
  248. // Characters that will be used in the conversion.
  249. $characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  250. $length = strlen( $characters );
  251. $remainder = $base10;
  252. $output = '';
  253. do {
  254. // Building from right to left in the result.
  255. $index = ( $remainder % $length );
  256. // Prepend the character to the output.
  257. $output = $characters[ $index ] . $output;
  258. // Determine the remainder after removing the applied number.
  259. $remainder = floor( $remainder / $length );
  260. // Keep doing it until we have no remainder left.
  261. } while ( $remainder );
  262. return $output;
  263. }
  264. }