| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 6 | | This work is published under the GNU AGPLv3 license with some | |
| 7 | | permitted exceptions and without any warranty. For full license | |
| 8 | | and copyright information, see https://civicrm.org/licensing | |
| 9 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * |
| 14 | * @package CRM |
| 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
| 16 | */ |
| 17 | |
| 18 | /** |
| 19 | * Cache is an empty base object, we'll modify the scheme when we have different caching schemes |
| 20 | */ |
| 21 | class CRM_Utils_Cache { |
| 22 | |
| 23 | const DELIMITER = '/'; |
| 24 | |
| 25 | /** |
| 26 | * (Quasi-Private) Treat this as private. It is marked public to facilitate testing. |
| 27 | * |
| 28 | * We only need one instance of this object. So we use the singleton |
| 29 | * pattern and cache the instance in this variable |
| 30 | * |
| 31 | * @var object |
| 32 | */ |
| 33 | public static $_singleton = NULL; |
| 34 | |
| 35 | /** |
| 36 | * Constructor. |
| 37 | * |
| 38 | * @param array $config |
| 39 | * An array of configuration params. |
| 40 | * |
| 41 | * @return \CRM_Utils_Cache |
| 42 | * @throws \CRM_Core_Exception |
| 43 | */ |
| 44 | public function __construct(&$config) { |
| 45 | throw new CRM_Core_Exception(ts('this is just an interface and should not be called directly')); |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Singleton function used to manage this object. |
| 50 | * |
| 51 | * @return CRM_Utils_Cache_Interface |
| 52 | */ |
| 53 | public static function &singleton() { |
| 54 | if (self::$_singleton === NULL) { |
| 55 | $className = self::getCacheDriver(); |
| 56 | // a generic method for utilizing any of the available db caches. |
| 57 | $dbCacheClass = 'CRM_Utils_Cache_' . $className; |
| 58 | $settings = self::getCacheSettings($className); |
| 59 | $settings['prefix'] = CRM_Utils_Array::value('prefix', $settings, '') . self::DELIMITER . 'default' . self::DELIMITER; |
| 60 | self::$_singleton = new $dbCacheClass($settings); |
| 61 | } |
| 62 | return self::$_singleton; |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Get cache relevant settings. |
| 67 | * |
| 68 | * @param $cachePlugin |
| 69 | * |
| 70 | * @return array |
| 71 | * associative array of settings for the cache |
| 72 | */ |
| 73 | public static function getCacheSettings($cachePlugin) { |
| 74 | switch ($cachePlugin) { |
| 75 | case 'ArrayCache': |
| 76 | case 'NoCache': |
| 77 | $defaults = []; |
| 78 | break; |
| 79 | |
| 80 | case 'Redis': |
| 81 | case 'Memcache': |
| 82 | case 'Memcached': |
| 83 | $defaults = [ |
| 84 | 'host' => 'localhost', |
| 85 | 'port' => 11211, |
| 86 | 'timeout' => 3600, |
| 87 | 'prefix' => '', |
| 88 | ]; |
| 89 | |
| 90 | // Use old constants if needed to ensure backward compatibility |
| 91 | if (defined('CIVICRM_MEMCACHE_HOST')) { |
| 92 | $defaults['host'] = CIVICRM_MEMCACHE_HOST; |
| 93 | } |
| 94 | |
| 95 | if (defined('CIVICRM_MEMCACHE_PORT')) { |
| 96 | $defaults['port'] = CIVICRM_MEMCACHE_PORT; |
| 97 | } |
| 98 | |
| 99 | if (defined('CIVICRM_MEMCACHE_TIMEOUT')) { |
| 100 | $defaults['timeout'] = CIVICRM_MEMCACHE_TIMEOUT; |
| 101 | } |
| 102 | |
| 103 | if (defined('CIVICRM_MEMCACHE_PREFIX')) { |
| 104 | $defaults['prefix'] = CIVICRM_MEMCACHE_PREFIX; |
| 105 | } |
| 106 | |
| 107 | // Use new constants if possible |
| 108 | if (defined('CIVICRM_DB_CACHE_HOST')) { |
| 109 | $defaults['host'] = CIVICRM_DB_CACHE_HOST; |
| 110 | } |
| 111 | |
| 112 | if (defined('CIVICRM_DB_CACHE_PORT')) { |
| 113 | $defaults['port'] = CIVICRM_DB_CACHE_PORT; |
| 114 | } |
| 115 | |
| 116 | if (defined('CIVICRM_DB_CACHE_TIMEOUT')) { |
| 117 | $defaults['timeout'] = CIVICRM_DB_CACHE_TIMEOUT; |
| 118 | } |
| 119 | |
| 120 | if (defined('CIVICRM_DB_CACHE_PREFIX')) { |
| 121 | $defaults['prefix'] = CIVICRM_DB_CACHE_PREFIX; |
| 122 | } |
| 123 | |
| 124 | break; |
| 125 | |
| 126 | case 'APCcache': |
| 127 | $defaults = []; |
| 128 | if (defined('CIVICRM_DB_CACHE_TIMEOUT')) { |
| 129 | $defaults['timeout'] = CIVICRM_DB_CACHE_TIMEOUT; |
| 130 | } |
| 131 | if (defined('CIVICRM_DB_CACHE_PREFIX')) { |
| 132 | $defaults['prefix'] = CIVICRM_DB_CACHE_PREFIX; |
| 133 | } |
| 134 | break; |
| 135 | } |
| 136 | return $defaults; |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Create a new, named, limited-use cache. |
| 141 | * |
| 142 | * This is a factory function. Generally, you should use Civi::cache($name) |
| 143 | * to locate managed cached instance. |
| 144 | * |
| 145 | * @param array $params |
| 146 | * Array with keys: |
| 147 | * - name: string, unique symbolic name. |
| 148 | * For a naming convention, use `snake_case` or `CamelCase` to maximize |
| 149 | * portability/cleanliness. Any other punctuation or whitespace |
| 150 | * should function correctly, but it can be harder to inspect/debug. |
| 151 | * - type: array|string, list of acceptable cache types, in order of preference. |
| 152 | * - prefetch: bool, whether to prefetch all data in cache (if possible). |
| 153 | * - withArray: bool|null|'fast', whether to setup a thread-local array-cache in front of the cache driver. |
| 154 | * Note that cache-values may be passed to the underlying driver with extra metadata, |
| 155 | * so this will slightly change/enlarge the on-disk format. |
| 156 | * Support varies by driver: |
| 157 | * - For most memory backed caches, this option is meaningful. |
| 158 | * - For SqlGroup, this option is ignored. SqlGroup has equivalent behavior built-in. |
| 159 | * - For ArrayCache, this option is ignored. It's redundant. |
| 160 | * If this is a short-lived process in which TTL's don't matter, you might |
| 161 | * use 'fast' mode. It sacrifices some PSR-16 compliance and cache-coherency |
| 162 | * protections to improve performance. |
| 163 | * @return CRM_Utils_Cache_Interface |
| 164 | * @throws CRM_Core_Exception |
| 165 | * @see Civi::cache() |
| 166 | */ |
| 167 | public static function create($params = []) { |
| 168 | $types = (array) $params['type']; |
| 169 | |
| 170 | if (!empty($params['name'])) { |
| 171 | $params['name'] = self::cleanKey($params['name']); |
| 172 | } |
| 173 | |
| 174 | foreach ($types as $type) { |
| 175 | switch ($type) { |
| 176 | case '*memory*': |
| 177 | if (defined('CIVICRM_DB_CACHE_CLASS') && in_array(CIVICRM_DB_CACHE_CLASS, ['Memcache', 'Memcached', 'Redis'])) { |
| 178 | $dbCacheClass = 'CRM_Utils_Cache_' . CIVICRM_DB_CACHE_CLASS; |
| 179 | $settings = self::getCacheSettings(CIVICRM_DB_CACHE_CLASS); |
| 180 | $settings['prefix'] = CRM_Utils_Array::value('prefix', $settings, '') . self::DELIMITER . $params['name'] . self::DELIMITER; |
| 181 | $cache = new $dbCacheClass($settings); |
| 182 | if (!empty($params['withArray'])) { |
| 183 | $cache = $params['withArray'] === 'fast' ? new CRM_Utils_Cache_FastArrayDecorator($cache) : new CRM_Utils_Cache_ArrayDecorator($cache); |
| 184 | } |
| 185 | return $cache; |
| 186 | } |
| 187 | break; |
| 188 | |
| 189 | case 'SqlGroup': |
| 190 | if (defined('CIVICRM_DSN') && CIVICRM_DSN) { |
| 191 | return new CRM_Utils_Cache_SqlGroup([ |
| 192 | 'group' => $params['name'], |
| 193 | 'prefetch' => $params['prefetch'] ?? FALSE, |
| 194 | ]); |
| 195 | } |
| 196 | break; |
| 197 | |
| 198 | case 'Arraycache': |
| 199 | case 'ArrayCache': |
| 200 | return new CRM_Utils_Cache_ArrayCache([]); |
| 201 | |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | throw new CRM_Core_Exception("Failed to instantiate cache. No supported cache type found. " . print_r($params, 1)); |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Normalize a cache key. |
| 210 | * |
| 211 | * This bridges an impedance mismatch between our traditional caching |
| 212 | * and PSR-16 -- PSR-16 accepts a narrower range of cache keys. |
| 213 | * |
| 214 | * @param string $key |
| 215 | * Ex: 'ab/cd:ef' |
| 216 | * @return string |
| 217 | * Ex: '_abcd1234abcd1234' or 'ab_xx/cd_xxef'. |
| 218 | * A similar key, but suitable for use with PSR-16-compliant cache providers. |
| 219 | */ |
| 220 | public static function cleanKey($key) { |
| 221 | if (!is_string($key) && !is_int($key)) { |
| 222 | throw new \RuntimeException("Malformed cache key"); |
| 223 | } |
| 224 | |
| 225 | $maxLen = 64; |
| 226 | $escape = '-'; |
| 227 | |
| 228 | if (strlen($key) >= $maxLen) { |
| 229 | return $escape . md5($key); |
| 230 | } |
| 231 | |
| 232 | $r = preg_replace_callback(';[^A-Za-z0-9_\.];', function($m) use ($escape) { |
| 233 | return $escape . dechex(ord($m[0])); |
| 234 | }, $key); |
| 235 | |
| 236 | return strlen($r) >= $maxLen ? $escape . md5($key) : $r; |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Assert that a key is well-formed. |
| 241 | * |
| 242 | * @param string $key |
| 243 | * @return string |
| 244 | * Same $key, if it's valid. |
| 245 | * @throws \CRM_Utils_Cache_InvalidArgumentException |
| 246 | */ |
| 247 | public static function assertValidKey($key) { |
| 248 | $strict = CRM_Utils_Constant::value('CIVICRM_PSR16_STRICT', FALSE) || defined('CIVICRM_TEST'); |
| 249 | |
| 250 | if (!is_string($key)) { |
| 251 | throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Not a string"); |
| 252 | } |
| 253 | |
| 254 | if ($strict && !preg_match(';^[A-Za-z0-9_\-\. ]+$;', $key)) { |
| 255 | throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Illegal characters"); |
| 256 | } |
| 257 | |
| 258 | if ($strict && strlen($key) > 255) { |
| 259 | throw new CRM_Utils_Cache_InvalidArgumentException("Invalid cache key: Too long"); |
| 260 | } |
| 261 | |
| 262 | return $key; |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * @return string |
| 267 | * Ex: 'ArrayCache', 'Memcache', 'Redis'. |
| 268 | */ |
| 269 | public static function getCacheDriver() { |
| 270 | // default to ArrayCache for now |
| 271 | $className = 'ArrayCache'; |
| 272 | |
| 273 | // Maintain backward compatibility for now. |
| 274 | // Setting CIVICRM_USE_MEMCACHE or CIVICRM_USE_ARRAYCACHE will |
| 275 | // override the CIVICRM_DB_CACHE_CLASS setting. |
| 276 | // Going forward, CIVICRM_USE_xxxCACHE should be deprecated. |
| 277 | if (defined('CIVICRM_USE_MEMCACHE') && CIVICRM_USE_MEMCACHE) { |
| 278 | $className = 'Memcache'; |
| 279 | return $className; |
| 280 | } |
| 281 | elseif (defined('CIVICRM_USE_ARRAYCACHE') && CIVICRM_USE_ARRAYCACHE) { |
| 282 | $className = 'ArrayCache'; |
| 283 | return $className; |
| 284 | } |
| 285 | elseif (defined('CIVICRM_DB_CACHE_CLASS') && CIVICRM_DB_CACHE_CLASS) { |
| 286 | $className = CIVICRM_DB_CACHE_CLASS; |
| 287 | return $className; |
| 288 | } |
| 289 | return $className; |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Generate a unique negative-acknowledgement token (NACK). |
| 294 | * |
| 295 | * When using PSR-16 to read a value, the `$cahce->get()` will a return a default |
| 296 | * value on cache-miss, so it's hard to know if you've gotten a geniune value |
| 297 | * from the cache or just a default. If you're in an edge-case where it matters |
| 298 | * (and you want to do has()+get() in a single roundtrip), use the nack() as |
| 299 | * the default: |
| 300 | * |
| 301 | * $nack = CRM_Utils_Cache::nack(); |
| 302 | * $value = $cache->get('foo', $nack); |
| 303 | * echo ($value === $nack) ? "Cache has a value, and we got it" : "Cache has no value". |
| 304 | * |
| 305 | * The value should be unique to avoid accidental matches. |
| 306 | * |
| 307 | * @return string |
| 308 | * Unique nonce value indicating a "negative acknowledgement" (failed read). |
| 309 | * If we need to accurately perform has($key)+get($key), we can |
| 310 | * use `get($key,$nack)`. |
| 311 | */ |
| 312 | public static function nack() { |
| 313 | $st =& Civi::$statics[__CLASS__]; |
| 314 | if (!isset($st['nack-c'])) { |
| 315 | $st['nack-c'] = md5(CRM_Utils_Request::id() . CIVICRM_SITE_KEY . CIVICRM_DSN . mt_rand(0, 10000)); |
| 316 | $st['nack-i'] = 0; |
| 317 | } |
| 318 | return 'NACK:' . $st['nack-c'] . $st['nack-i']++; |
| 319 | } |
| 320 | |
| 321 | } |