class-sitemaps-cache-validator.php 9.0 KB

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