1<?php
2/**
3 * Copyright 2018 Google Inc. All Rights Reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17namespace Google\Auth\Cache;
18
19use Psr\Cache\CacheItemInterface;
20use Psr\Cache\CacheItemPoolInterface;
21
22/**
23 * SystemV shared memory based CacheItemPool implementation.
24 *
25 * This CacheItemPool implementation can be used among multiple processes, but
26 * it doesn't provide any locking mechanism. If multiple processes write to
27 * this ItemPool, you have to avoid race condition manually in your code.
28 */
29class SysVCacheItemPool implements CacheItemPoolInterface
30{
31    const VAR_KEY = 1;
32
33    const DEFAULT_PROJ = 'A';
34
35    const DEFAULT_MEMSIZE = 10000;
36
37    const DEFAULT_PERM = 0600;
38
39    /**
40     * @var int
41     */
42    private $sysvKey;
43
44    /**
45     * @var CacheItemInterface[]
46     */
47    private $items;
48
49    /**
50     * @var CacheItemInterface[]
51     */
52    private $deferredItems;
53
54    /**
55     * @var array<mixed>
56     */
57    private $options;
58
59    /**
60     * @var bool
61     */
62    private $hasLoadedItems = false;
63
64    /**
65     * Create a SystemV shared memory based CacheItemPool.
66     *
67     * @param array<mixed> $options {
68     *     [optional] Configuration options.
69     *
70     *     @type int    $variableKey The variable key for getting the data from the shared memory. **Defaults to** 1.
71     *     @type string $proj        The project identifier for ftok. This needs to be a one character string. **Defaults to** 'A'.
72     *     @type int    $memsize     The memory size in bytes for shm_attach. **Defaults to** 10000.
73     *     @type int    $perm        The permission for shm_attach. **Defaults to** 0600.
74     * }
75     */
76    public function __construct($options = [])
77    {
78        if (! extension_loaded('sysvshm')) {
79            throw new \RuntimeException(
80                'sysvshm extension is required to use this ItemPool'
81            );
82        }
83        $this->options = $options + [
84            'variableKey' => self::VAR_KEY,
85            'proj' => self::DEFAULT_PROJ,
86            'memsize' => self::DEFAULT_MEMSIZE,
87            'perm' => self::DEFAULT_PERM
88        ];
89        $this->items = [];
90        $this->deferredItems = [];
91        $this->sysvKey = ftok(__FILE__, $this->options['proj']);
92    }
93
94    /**
95     * @param mixed $key
96     * @return CacheItemInterface
97     */
98    public function getItem($key): CacheItemInterface
99    {
100        $this->loadItems();
101        return current($this->getItems([$key])); // @phpstan-ignore-line
102    }
103
104    /**
105     * @param array<mixed> $keys
106     * @return iterable<CacheItemInterface>
107     */
108    public function getItems(array $keys = []): iterable
109    {
110        $this->loadItems();
111        $items = [];
112        $itemClass = \PHP_VERSION_ID >= 80000 ? TypedItem::class : Item::class;
113        foreach ($keys as $key) {
114            $items[$key] = $this->hasItem($key) ?
115                clone $this->items[$key] :
116                new $itemClass($key);
117        }
118        return $items;
119    }
120
121    /**
122     * {@inheritdoc}
123     */
124    public function hasItem($key): bool
125    {
126        $this->loadItems();
127        return isset($this->items[$key]) && $this->items[$key]->isHit();
128    }
129
130    /**
131     * {@inheritdoc}
132     */
133    public function clear(): bool
134    {
135        $this->items = [];
136        $this->deferredItems = [];
137        return $this->saveCurrentItems();
138    }
139
140    /**
141     * {@inheritdoc}
142     */
143    public function deleteItem($key): bool
144    {
145        return $this->deleteItems([$key]);
146    }
147
148    /**
149     * {@inheritdoc}
150     */
151    public function deleteItems(array $keys): bool
152    {
153        if (!$this->hasLoadedItems) {
154            $this->loadItems();
155        }
156
157        foreach ($keys as $key) {
158            unset($this->items[$key]);
159        }
160        return $this->saveCurrentItems();
161    }
162
163    /**
164     * {@inheritdoc}
165     */
166    public function save(CacheItemInterface $item): bool
167    {
168        if (!$this->hasLoadedItems) {
169            $this->loadItems();
170        }
171
172        $this->items[$item->getKey()] = $item;
173        return $this->saveCurrentItems();
174    }
175
176    /**
177     * {@inheritdoc}
178     */
179    public function saveDeferred(CacheItemInterface $item): bool
180    {
181        $this->deferredItems[$item->getKey()] = $item;
182        return true;
183    }
184
185    /**
186     * {@inheritdoc}
187     */
188    public function commit(): bool
189    {
190        foreach ($this->deferredItems as $item) {
191            if ($this->save($item) === false) {
192                return false;
193            }
194        }
195        $this->deferredItems = [];
196        return true;
197    }
198
199    /**
200     * Save the current items.
201     *
202     * @return bool true when success, false upon failure
203     */
204    private function saveCurrentItems()
205    {
206        $shmid = shm_attach(
207            $this->sysvKey,
208            $this->options['memsize'],
209            $this->options['perm']
210        );
211        if ($shmid !== false) {
212            $ret = shm_put_var(
213                $shmid,
214                $this->options['variableKey'],
215                $this->items
216            );
217            shm_detach($shmid);
218            return $ret;
219        }
220        return false;
221    }
222
223    /**
224     * Load the items from the shared memory.
225     *
226     * @return bool true when success, false upon failure
227     */
228    private function loadItems()
229    {
230        $shmid = shm_attach(
231            $this->sysvKey,
232            $this->options['memsize'],
233            $this->options['perm']
234        );
235        if ($shmid !== false) {
236            $data = @shm_get_var($shmid, $this->options['variableKey']);
237            if (!empty($data)) {
238                $this->items = $data;
239            } else {
240                $this->items = [];
241            }
242            shm_detach($shmid);
243            $this->hasLoadedItems = true;
244            return true;
245        }
246        return false;
247    }
248}
249