Completed
Push — master ( a18851...eb3437 )
by Lars
02:11
created

Cache::getClientIp()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 1 Features 0
Metric Value
dl 0
loc 18
ccs 0
cts 9
cp 0
rs 9.2
c 2
b 1
f 0
cc 4
eloc 11
nc 4
nop 1
crap 20
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\cache;
6
7
use voku\cache\Exception\InvalidArgumentException;
8
9
/**
10
 * Cache: global-cache class
11
 *
12
 * can use different cache-adapter:
13
 * - Redis
14
 * - Memcache / Memcached
15
 * - APC / APCu
16
 * - Xcache
17
 * - Array
18
 * - File
19
 *
20
 * @package   voku\cache
21
 */
22
class Cache implements iCache
23
{
24
25
  /**
26
   * @var iAdapter
27
   */
28
  protected $adapter;
29
30
  /**
31
   * @var iSerializer
32
   */
33
  protected $serializer;
34
35
  /**
36
   * @var string
37
   */
38
  protected $prefix = '';
39
40
  /**
41
   * @var bool
42
   */
43
  protected $isReady = false;
44
45
  /**
46
   * @var bool
47
   */
48
  protected $isActive = true;
49
50
  /**
51
   * @var mixed no cache, if admin-session is set
52
   */
53
  protected $isAdminSession = false;
54
55
  /**
56
   * @var array
57
   */
58
  protected static $STATIC_CACHE = [];
59
60
  /**
61
   * @var array
62
   */
63
  protected static $STATIC_CACHE_EXPIRE = [];
64
65
  /**
66
   * @var array
67
   */
68
  protected static $STATIC_CACHE_COUNTER = [];
69
70
  /**
71
   * @var int
72
   */
73
  protected $staticCacheHitCounter = 10;
74
75
  /**
76
   * __construct
77
   *
78
   * @param null|iAdapter    $adapter
79
   * @param null|iSerializer $serializer
80
   * @param bool             $checkForUser   check for dev-ip or if cms-user is logged-in
81
   * @param bool             $cacheEnabled   false will disable the cache (use it e.g. for global settings)
82
   * @param string|bool      $isAdminSession set a user-id, if the user is a admin (so we can disable cache for this
83 51
   *                                         user)
84
   */
85 51
  public function __construct(iAdapter $adapter = null, iSerializer $serializer = null, bool $checkForUser = true, bool $cacheEnabled = true, $isAdminSession = false)
86
  {
87
    $this->isAdminSession = $isAdminSession;
88 51
89
    // First check if the cache is active at all.
90 51
    $this->setActive($cacheEnabled);
91 51
    if (
92
        $this->isActive === true
93 51
        &&
94
        $checkForUser === true
95
    ) {
96
      $this->setActive($this->isCacheActiveForTheCurrentUser());
97
    }
98 51
99
    // If the cache is active, then try to auto-connect to the best possible cache-system.
100 51
    if ($this->isActive === true) {
101
102
      $this->setPrefix($this->getTheDefaultPrefix());
103
104 51
      if (
105 51
          $adapter === null
106 51
          ||
107
          !\is_object($adapter)
108 51
          ||
109
          !$adapter instanceof iAdapter
110
      ) {
111
        $adapter = $this->autoConnectToAvailableCacheSystem();
112
      }
113 51
114
      // INFO: Memcache(d) has his own "serializer", so don't use it twice
115
      if (!\is_object($serializer) && $serializer === null) {
116
        if (
117
            $adapter instanceof AdapterMemcached
118
            ||
119
            $adapter instanceof AdapterMemcache
120
        ) {
121
          $serializer = new SerializerNo();
122
        } else {
123
          // set default serializer
124
          $serializer = new SerializerIgbinary();
125 51
        }
126
      }
127
    }
128
129
    // Final checks ...
130 51
    if (
131
        $serializer instanceof iSerializer
132 51
        &&
133 51
        $adapter instanceof iAdapter
134
    ) {
135 51
      $this->setCacheIsReady(true);
136 51
137 51
      $this->adapter = $adapter;
138 51
      $this->serializer = $serializer;
139
    }
140
  }
141
142
  /**
143
   * enable / disable the cache
144
   *
145 51
   * @param bool $isActive
146
   */
147 51
  public function setActive(bool $isActive)
148 51
  {
149
    $this->isActive = $isActive;
150
  }
151
152
  /**
153
   * check if the current use is a admin || dev || server == client
154
   *
155
   * @return bool
156
   */
157
  public function isCacheActiveForTheCurrentUser(): bool
158
  {
159
    $active = true;
160
161
    // test the cache, with this GET-parameter
162
    $testCache = isset($_GET['testCache']) ? (int)$_GET['testCache'] : 0;
163
164
    if ($testCache != 1) {
165
      if (
166
        // admin is logged-in
167
          $this->isAdminSession
168
          ||
169
          // server == client
170
          (
171
              isset($_SERVER['SERVER_ADDR'])
172
              &&
173
              $_SERVER['SERVER_ADDR'] == $this->getClientIp()
174
          )
175
          ||
176
          // user is a dev
177
          $this->checkForDev() === true
178
      ) {
179
        $active = false;
180
      }
181
    }
182
183
    return $active;
184
  }
185
186
  /**
187
   * returns the IP address of the client
188
   *
189
   * @param bool $trust_proxy_headers     <p>
190
   *                                      Whether or not to trust the
191
   *                                      proxy headers HTTP_CLIENT_IP
192
   *                                      and HTTP_X_FORWARDED_FOR. ONLY
193
   *                                      use if your $_SERVER is behind a
194
   *                                      proxy that sets these values
195
   *                                      </p>
196
   *
197
   * @return string
198
   */
199
  protected function getClientIp(bool $trust_proxy_headers = false): string
200
  {
201
    $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'NO_REMOTE_ADDR';
202
203
    if ($trust_proxy_headers) {
204
      return $remoteAddr;
205
    }
206
207
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
208
      $ip = $_SERVER['HTTP_CLIENT_IP'];
209
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
210
      $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
211
    } else {
212
      $ip = $remoteAddr;
213
    }
214
215
    return $ip;
216
  }
217
218
  /**
219
   * Check for local developer.
220
   *
221
   * @return bool
222
   */
223
  protected function checkForDev(): bool
224
  {
225
    $return = false;
226
227
    if (\function_exists('checkForDev')) {
228
      $return = checkForDev();
229
    } else {
230
231
      // for testing with dev-address
232
      $noDev = isset($_GET['noDev']) ? (int)$_GET['noDev'] : 0;
233
      $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'NO_REMOTE_ADDR';
234
235
      if (
236
          $noDev != 1
237
          &&
238
          (
239
              $remoteAddr === '127.0.0.1'
240
              ||
241
              $remoteAddr === '::1'
242
              ||
243
              PHP_SAPI === 'cli'
244
          )
245
      ) {
246
        $return = true;
247
      }
248
    }
249
250
    return $return;
251
  }
252 51
253
  /**
254 51
   * Set the default-prefix via "SERVER"-var + "SESSION"-language.
255 51
   */
256 51
  protected function getTheDefaultPrefix()
257 51
  {
258 51
    return ($_SERVER['SERVER_NAME'] ?? '') . '_' .
259
           ($_SERVER['THEME'] ?? '') . '_' .
260
           ($_SERVER['STAGE'] ?? '') . '_' .
261
           ($_SESSION['language'] ?? '') . '_' .
262
           ($_SESSION['language_extra'] ?? '');
263
  }
264
265
  /**
266
   * Auto-connect to the available cache-system on the server.
267
   *
268
   * @return iAdapter
269
   */
270
  protected function autoConnectToAvailableCacheSystem(): iAdapter
271
  {
272
    static $adapterCache;
273
274
    if (\is_object($adapterCache) && $adapterCache instanceof iAdapter) {
275
      return $adapterCache;
276
    }
277
278
    $memcached = null;
279
    $isMemcachedAvailable = false;
280
    if (\extension_loaded('memcached')) {
281
      $memcached = new \Memcached();
282
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
283
      $isMemcachedAvailable = @$memcached->addServer('127.0.0.1', 11211);
284
    }
285
286
    if ($isMemcachedAvailable === false) {
287
      $memcached = null;
288
    }
289
290
    $adapterMemcached = new AdapterMemcached($memcached);
291
    if ($adapterMemcached->installed() === true) {
292
293
      // -------------------------------------------------------------
294
      // "Memcached"
295
      // -------------------------------------------------------------
296
      $adapter = $adapterMemcached;
297
298
    } else {
299
300
      $memcache = null;
301
      $isMemcacheAvailable = false;
302
      if (class_exists('\Memcache')) {
303
        $memcache = new \Memcache;
304
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
305
        $isMemcacheAvailable = @$memcache->connect('127.0.0.1', 11211);
306
      }
307
308
      if ($isMemcacheAvailable === false) {
309
        $memcache = null;
310
      }
311
312
      $adapterMemcache = new AdapterMemcache($memcache);
313
      if ($adapterMemcache->installed() === true) {
314
315
        // -------------------------------------------------------------
316
        // "Memcache"
317
        // -------------------------------------------------------------
318
        $adapter = $adapterMemcache;
319
320
      } else {
321
322
        $redis = null;
323
        $isRedisAvailable = false;
324
        if (
325
            \extension_loaded('redis')
326
            &&
327
            class_exists('\Predis\Client')
328
        ) {
329
          /** @noinspection PhpUndefinedNamespaceInspection */
330
          /** @noinspection PhpUndefinedClassInspection */
331
          $redis = new \Predis\Client(
332
              [
333
                  'scheme'  => 'tcp',
334
                  'host'    => '127.0.0.1',
335
                  'port'    => 6379,
336
                  'timeout' => '2.0',
337
              ]
338
          );
339
          try {
340
            /** @noinspection PhpUndefinedMethodInspection */
341
            $redis->connect();
342
            /** @noinspection PhpUndefinedMethodInspection */
343
            $isRedisAvailable = $redis->getConnection()->isConnected();
344
          } catch (\Exception $e) {
345
            // nothing
346
          }
347
        }
348
349
        if ($isRedisAvailable === false) {
350
          $redis = null;
351
        }
352
353
        $adapterRedis = new AdapterPredis($redis);
354
        if ($adapterRedis->installed() === true) {
355
356
          // -------------------------------------------------------------
357
          // Redis
358
          // -------------------------------------------------------------
359
          $adapter = $adapterRedis;
360
361
        } else {
362
363
          $adapterXcache = new AdapterXcache();
364
          if ($adapterXcache->installed() === true) {
365
366
            // -------------------------------------------------------------
367
            // "Xcache"
368
            // -------------------------------------------------------------
369
            $adapter = $adapterXcache;
370
371
          } else {
372
373
            $adapterApc = new AdapterApc();
374
            if ($adapterApc->installed() === true) {
375
376
              // -------------------------------------------------------------
377
              // "APC"
378
              // -------------------------------------------------------------
379
              $adapter = $adapterApc;
380
381
            } else {
382
383
              $adapterApcu = new AdapterApcu();
384
              if ($adapterApcu->installed() === true) {
385
386
                // -------------------------------------------------------------
387
                // "APCu"
388
                // -------------------------------------------------------------
389
                $adapter = $adapterApcu;
390
391
              } else {
392
393
                $adapterFile = new AdapterFile();
394
                if ($adapterFile->installed() === true) {
395
396
                  // -------------------------------------------------------------
397
                  // File-Cache
398
                  // -------------------------------------------------------------
399
                  $adapter = $adapterFile;
400
401
                } else {
402
403
                  // -------------------------------------------------------------
404
                  // Static-PHP-Cache
405
                  // -------------------------------------------------------------
406
                  $adapter = new AdapterArray();
407
                }
408
              }
409
            }
410
          }
411
        }
412
      }
413
    }
414
415
    // save to static cache
416
    $adapterCache = $adapter;
417
418
    return $adapter;
419
  }
420 51
421
  /**
422 51
   * Set "isReady" state.
423 51
   *
424
   * @param bool $isReady
425
   */
426
  protected function setCacheIsReady(bool $isReady)
427
  {
428
    $this->isReady = $isReady;
429
  }
430 4
431
  /**
432 4
   * Get the "isReady" state.
433
   *
434
   * @return bool
435
   */
436
  public function getCacheIsReady(): bool
437
  {
438
    return $this->isReady;
439
  }
440
441
  /**
442
   * Get cached-item by key.
443 22
   *
444
   * @param string $key
445
   * @param int    $forceStaticCacheHitCounter
446 22
   *
447
   * @return mixed
448 22
   */
449
  public function getItem(string $key, int $forceStaticCacheHitCounter = 0)
450
  {
451
    if (!$this->adapter instanceof iAdapter) {
452 22
      return null;
453
    }
454
455 22
    $storeKey = $this->calculateStoreKey($key);
456 22
457 6
    // check if we already using static-cache
458 6
    $useStaticCache = true;
459
    if ($this->adapter instanceof AdapterArray) {
460 22
      $useStaticCache = false;
461 13
    }
462 13
463
    if (!isset(self::$STATIC_CACHE_COUNTER[$storeKey])) {
464
      self::$STATIC_CACHE_COUNTER[$storeKey] = 0;
465
    }
466
467 22
    // get from static-cache
468 16
    if (
469 22
        $useStaticCache === true
470 4
        &&
471
        $this->checkForStaticCache($storeKey) === true
472
    ) {
473 20
      return self::$STATIC_CACHE[$storeKey];
474 20
    }
475
476 20
    $serialized = $this->adapter->get($storeKey);
477
    $value = $serialized ? $this->serializer->unserialize($serialized) : null;
478
479
    self::$STATIC_CACHE_COUNTER[$storeKey]++;
480
481 20
    // save into static-cache if needed
482
    if (
483
        $useStaticCache === true
484
        &&
485 14
        (
486
            (
487
                $forceStaticCacheHitCounter !== 0
488
                &&
489
                self::$STATIC_CACHE_COUNTER[$storeKey] >= $forceStaticCacheHitCounter
490 14
            )
491 14
            ||
492 14
            (
493 14
                $this->staticCacheHitCounter !== 0
494 14
                &&
495 20
                self::$STATIC_CACHE_COUNTER[$storeKey] >= $this->staticCacheHitCounter
496 3
            )
497 3
        )
498
    ) {
499 20
      self::$STATIC_CACHE[$storeKey] = $value;
500
    }
501
502
    return $value;
503
  }
504
505
  /**
506
   * Calculate store-key (prefix + $rawKey).
507
   *
508
   * @param string $rawKey
509 42
   *
510
   * @return string
511 42
   */
512
  protected function calculateStoreKey(string $rawKey): string
513 42
  {
514 8
    $str = $this->getPrefix() . $rawKey;
515 8
516
    if ($this->adapter instanceof AdapterFile) {
517 42
      $str = $this->cleanStoreKey($str);
518
    }
519
520
    return $str;
521
  }
522
523
  /**
524
   * Clean store-key (required e.g. for the "File"-Adapter).
525
   *
526
   * @param string $str
527 8
   *
528
   * @return string
529 8
   */
530 8
  protected function cleanStoreKey(string $str): string
531 8
  {
532
    $str = preg_replace("/[\r\n\t ]+/", ' ', $str);
533 8
    $str = str_replace(
534 8
        ['"', '*', ':', '<', '>', '?', "'", '|'],
535 8
        [
536 8
            '-+-',
537 8
            '-+-+-',
538 8
            '-+-+-+-',
539 8
            '-+-+-+-+-',
540 8
            '-+-+-+-+-+-',
541 8
            '-+-+-+-+-+-+-',
542
            '-+-+-+-+-+-+-+-',
543 8
            '-+-+-+-+-+-+-+-+-',
544 8
        ],
545 8
        $str
546 8
    );
547 8
    $str = html_entity_decode($str, ENT_QUOTES, 'UTF-8');
548 8
    $str = htmlentities($str, ENT_QUOTES, 'UTF-8');
549 8
    $str = preg_replace('/(&)([a-z])([a-z]+;)/i', '$2', $str);
550
    $str = str_replace(' ', '-', $str);
551 8
    $str = rawurlencode($str);
552
    $str = str_replace('%', '-', $str);
553
554
    return $str;
555
  }
556
557
  /**
558
   * Get the prefix.
559 42
   *
560
   * @return string
561 42
   */
562
  public function getPrefix(): string
563
  {
564
    return $this->prefix;
565
  }
566
567
  /**
568
   * !!! Set the prefix. !!!
569
   *
570
   * WARNING: Do not use if you don't know what you do. Because this will overwrite the default prefix.
571 51
   *
572
   * @param string $prefix
573 51
   */
574 51
  public function setPrefix(string $prefix)
575
  {
576
    $this->prefix = $prefix;
577
  }
578
579
  /**
580
   * Get the current value, when the static cache is used.
581
   *
582
   * @return int
583
   */
584
  public function getStaticCacheHitCounter(): int
585
  {
586
    return $this->staticCacheHitCounter;
587
  }
588
589
  /**
590
   * Set the static-hit-counter: Who often do we hit the cache, before we use static cache?
591
   *
592
   * @param int $staticCacheHitCounter
593
   */
594
  public function setStaticCacheHitCounter(int $staticCacheHitCounter)
595
  {
596
    $this->staticCacheHitCounter = $staticCacheHitCounter;
597
  }
598
599
  /**
600
   * Set cache-item by key => value + date.
601
   *
602
   * @param string    $key
603
   * @param mixed     $value
604
   * @param \DateTime $date <p>If the date is in the past, we will remove the existing cache-item.</p>
605
   *
606 8
   * @return bool
607
   * @throws \Exception
608 8
   */
609
  public function setItemToDate(string $key, $value, \DateTime $date): bool
610 8
  {
611 1
    $ttl = $date->getTimestamp() - time();
612
613
    if ($ttl <= 0) {
614 7
      throw new InvalidArgumentException('Date in the past.');
615
    }
616 7
617
    $storeKey = $this->calculateStoreKey($key);
618
619
    return $this->setItem($storeKey, $value, $ttl);
620
  }
621
622
  /**
623
   * Set cache-item by key => value + ttl.
624
   *
625
   * @param string                 $key
626
   * @param mixed                  $value
627
   * @param null|int|\DateInterval $ttl
628 23
   *
629
   * @return bool
630
   */
631 23
  public function setItem(string $key, $value, $ttl = 0): bool
632 23
  {
633 23
    if (
634 23
        !$this->adapter instanceof iAdapter
635
        ||
636
        !$this->serializer instanceof iSerializer
637
    ) {
638 23
      return false;
639 23
    }
640
641
    $storeKey = $this->calculateStoreKey($key);
642 23
    $serialized = $this->serializer->serialize($value);
643 3
644 3
    // update static-cache, if it's exists
645
    if (array_key_exists($storeKey, self::$STATIC_CACHE) === true) {
646 23
      self::$STATIC_CACHE[$storeKey] = $value;
647
    }
648 11
649
    if ($ttl) {
650 1
651 1
      if ($ttl instanceof \DateInterval) {
652
        // Converting to a TTL in seconds
653
        $dateTimeNow = new \DateTime('now');
654 11
        $ttl = $dateTimeNow->add($ttl)->getTimestamp() - time();
655
      }
656 11
657
      // always cache the TTL time, maybe we need this later ...
658
      self::$STATIC_CACHE_EXPIRE[$storeKey] = ($ttl ? (int)$ttl + time() : 0);
659 12
660
      return $this->adapter->setExpired($storeKey, $serialized, $ttl);
661
    }
662
663
    return $this->adapter->set($storeKey, $serialized);
664
  }
665
666
  /**
667
   * Remove a cached-item.
668
   *
669 3
   * @param string $key
670
   *
671 3
   * @return bool
672
   */
673
  public function removeItem(string $key): bool
674
  {
675 3
    if (!$this->adapter instanceof iAdapter) {
676
      return false;
677
    }
678
679 3
    $storeKey = $this->calculateStoreKey($key);
680 3
681 1
    // remove static-cache
682 3
    if (
683
        !empty(self::$STATIC_CACHE)
684
        &&
685
        array_key_exists($storeKey, self::$STATIC_CACHE) === true
686
    ) {
687
      unset(
688
          self::$STATIC_CACHE[$storeKey],
689
          self::$STATIC_CACHE_COUNTER[$storeKey],
690 3
          self::$STATIC_CACHE_EXPIRE[$storeKey]
691
      );
692
    }
693
694
    return $this->adapter->remove($storeKey);
695
  }
696
697
  /**
698 1
   * Remove all cached-items.
699
   *
700 1
   * @return bool
701
   */
702
  public function removeAll(): bool
703
  {
704
    if (!$this->adapter instanceof iAdapter) {
705 1
      return false;
706 1
    }
707 1
708 1
    // remove static-cache
709 1
    if (!empty(self::$STATIC_CACHE)) {
710
      self::$STATIC_CACHE = [];
711 1
      self::$STATIC_CACHE_COUNTER = [];
712
      self::$STATIC_CACHE_EXPIRE = [];
713
    }
714
715
    return $this->adapter->removeAll();
716
  }
717
718
  /**
719
   * Check if cached-item exists.
720
   *
721 10
   * @param string $key
722
   *
723 10
   * @return bool
724
   */
725
  public function existsItem(string $key): bool
726
  {
727 10
    if (!$this->adapter instanceof iAdapter) {
728
      return false;
729
    }
730 10
731
    $storeKey = $this->calculateStoreKey($key);
732
733
    // check static-cache
734 10
    if ($this->checkForStaticCache($storeKey) === true) {
735
      return true;
736
    }
737
738
    return $this->adapter->exists($storeKey);
739
  }
740
741
  /**
742 26
   * @param string $storeKey
743
   *
744
   * @return bool
745 26
   */
746 26
  protected function checkForStaticCache(string $storeKey): bool
747 17
  {
748 26
    return !empty(self::$STATIC_CACHE)
749 6
           &&
750 26
           array_key_exists($storeKey, self::$STATIC_CACHE) === true
751 4
           &&
752 26
           array_key_exists($storeKey, self::$STATIC_CACHE_EXPIRE) === true
753 4
           &&
754
           time() <= self::$STATIC_CACHE_EXPIRE[$storeKey];
755
  }
756 24
757
  /**
758
   * Get the current adapter class-name.
759
   *
760
   * @return string
761
   */
762
  public function getUsedAdapterClassName(): string
763
  {
764 2
    return \get_class($this->adapter);
765
  }
766 2
767
  /**
768
   * Get the current serializer class-name.
769
   *
770
   * @return string
771
   */
772
  public function getUsedSerializerClassName(): string
773
  {
774 2
    return \get_class($this->serializer);
775
  }
776
}
777