Completed
Push — EZP-25003 ( 9557aa )
by André
23:09
created

LocationAwareStore::generateContentDigest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the LocationAwareStore class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 *
9
 * @version //autogentag//
10
 */
11
namespace eZ\Publish\Core\MVC\Symfony\Cache\Http;
12
13
use Symfony\Component\HttpKernel\HttpCache\Store;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\Filesystem\Filesystem;
17
use Symfony\Component\Filesystem\Exception\IOException;
18
use Symfony\Component\Finder\Finder;
19
20
/**
21
 * LocationAwareStore implements all the logic for storing cache metadata regarding locations.
22
 */
23
class TagAwareStore extends Store implements ContentPurger
24
{
25
    const TAG_CACHE_DIR = 'ezcachetag';
26
27
    /**
28
     * @var \Symfony\Component\Filesystem\Filesystem
29
     */
30
    private $fs;
31
32
    /**
33
     * Injects a Filesystem instance
34
     * For unit tests only.
35
     *
36
     * @internal
37
     *
38
     * @param \Symfony\Component\Filesystem\Filesystem $fs
39
     */
40
    public function setFilesystem(Filesystem $fs)
41
    {
42
        $this->fs = $fs;
43
    }
44
45
    /**
46
     * @return \Symfony\Component\Filesystem\Filesystem
47
     */
48
    public function getFilesystem()
49
    {
50
        if (!isset($this->fs)) {
51
            $this->fs = new Filesystem();
52
        }
53
54
        return $this->fs;
55
    }
56
57
    /**
58
     * Writes a cache entry to the store for the given Request and Response.
59
     *
60
     * Existing entries are read and any that match the response are removed. This
61
     * method calls write with the new list of cache entries.
62
     *
63
     * @param Request  $request  A Request instance
64
     * @param Response $response A Response instance
65
     *
66
     * @return string The key under which the response is stored
67
     *
68
     * @throws \RuntimeException
69
     */
70
    public function write(Request $request, Response $response)
71
    {
72
        parent::write($request, $response);
73
74
        $digest = $response->headers->get('X-Content-Digest');
75
        if ($request->headers->has('X-Cache-Tags')) {
76
            $tags = explode(', ', $request->headers->get('X-Cache-Tags'));
77
        } else {
78
            $tags = [];
79
        }
80
81
        if ($request->headers->has('X-Location-Id')) {
82
            $tags[] = 'location-' . $request->headers->get('X-Location-Id');
83
        }
84
85
        foreach (array_unique($tags) as $tag) {
86
            if (false === $this->saveTag($tag, $digest)) {
0 ignored issues
show
Bug introduced by
It seems like $digest defined by $response->headers->get('X-Content-Digest') on line 74 can also be of type array; however, eZ\Publish\Core\MVC\Symf...agAwareStore::saveTag() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
87
                throw new \RuntimeException('Unable to store the cache tag meta information.');
88
            }
89
        }
90
    }
91
92
    /**
93
     * Save digest for the given tag.
94
     *
95
     * @internal This is almost verbatim copy of save() from parent class as it is private.
96
     *
97
     * @param string $tag    The tag key
98
     * @param string $digest The digest hash to store representing the cache item.
99
     *
100
     * @return bool|void
101
     */
102
    private function saveTag($tag, $digest)
103
    {
104
        $path = $this->getPath($this->getCacheTagDir($tag));
105
        if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
106
            return false;
107
        }
108
109
        $tmpFile = tempnam(dirname($path), basename($path));
110
        if (false === $fp = @fopen($tmpFile, 'wb')) {
111
            return false;
112
        }
113
        @fwrite($fp, $digest);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
114
        @fclose($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
115
116
        if ($digest != file_get_contents($tmpFile)) {
117
            return false;
118
        }
119
120
        if (false === @rename($tmpFile, $path)) {
121
            return false;
122
        }
123
124
        @chmod($path, 0666 & ~umask());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
125
    }
126
127
    /**
128
     * Purges data from $request.
129
     * If X-Cache-Tags or X-Location-Id (deprecated) header is present, the store will purge cache for given locationId or group of locationIds.
130
     * If not, regular purge by URI will occur.
131
     *
132
     * @param \Symfony\Component\HttpFoundation\Request $request
133
     *
134
     * @return bool True if purge was successful. False otherwise
135
     */
136
    public function purgeByRequest(Request $request)
137
    {
138
        if (!$request->headers->has('X-Location-Id') && !$request->headers->has('X-Cache-Tags')) {
139
            return $this->purge($request->getUri());
140
        }
141
142
        // Deprecated, see purgeAllContent(): Purge everything
143
        $locationId = $request->headers->get('X-Location-Id');
144
        if ($locationId === '*' || $locationId === '.*') {
145
            return $this->purgeAllContent();
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\Core\MVC\Symf...tore::purgeAllContent() has been deprecated with message: Use cache:clear, with multi tagging theoretically there shouldn't be need to delete all anymore from core.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
146
        }
147
148
        if ($request->headers->has('X-Cache-Tags')) {
149
            $tags = explode(', ', $request->headers->get('X-Cache-Tags'));
150
        } else if ($locationId[0] === '(' && substr($locationId, -1) === ')') {
151
            // Deprecated: (123|456|789) => Purge for #123, #456 and #789 location IDs.
152
            $tags = array_map(
153
                function($id){return 'location-' . $id;},
154
                explode('|', substr($locationId, 1, -1))
155
            );
156
        } else {
157
            $tags = array('location-' . $locationId);
158
        }
159
160
        if (empty($tags)) {
161
            return false;
162
        }
163
164
        foreach ($tags as $tag) {
165
            $this->purgeByCacheTag($tag);
166
        }
167
168
        return true;
169
    }
170
171
    /**
172
     * Purges all cached content.
173
     *
174
     * @deprecated Use cache:clear, with multi tagging theoretically there shouldn't be need to delete all anymore from core.
175
     *
176
     * @return bool
177
     */
178
    public function purgeAllContent()
179
    {
180
        $cacheTagsCacheDir = $this->getCacheTagDir();
181
        $this->getFilesystem()->remove($cacheTagsCacheDir);
182
    }
183
184
    /**
185
     * Purges cache for tag.
186
     *
187
     * @param string $tag
188
     *
189
     * @return bool
190
     */
191
    private function purgeByCacheTag($tag)
192
    {
193
        $fs = $this->getFilesystem();
194
        $cacheTagsCacheDir = $this->getCacheTagDir($tag);
195
        if (!$fs->exists($cacheTagsCacheDir)) {
196
            return false;
197
        }
198
199
        $files = Finder::create()->files()->in($cacheTagsCacheDir)->getIterator();
200
        try {
201
            foreach ($files as $file) {
202
                if ($digest = file_get_contents($file)) {
203
                    $fs->remove($this->getPath($digest));
204
                }
205
            }
206
            $fs->remove($files);
207
            // we let folder stay in case another process have just written new cache tags
208
        } catch (IOException $e) {
209
            // Log the error in the standard error log and at least try to remove the lock file
210
            error_log($e->getMessage());
211
212
            return false;
213
        }
214
    }
215
216
    /**
217
     * Returns cache dir for $tag.
218
     *
219
     * This method is public only for unit tests.
220
     * Use it only if you know what you are doing.
221
     *
222
     * @internal
223
     *
224
     * @param int $tag
225
     *
226
     * @return string
227
     */
228
    public function getCacheTagDir($tag = null)
229
    {
230
        $cacheDir = "$this->root/" . static::TAG_CACHE_DIR;
231
        if ($tag) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tag of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
232
            $cacheDir .= "/$tag";
233
        }
234
235
        return $cacheDir;
236
    }
237
}
238