Completed
Push — EZP-25696 ( 1029f9 )
by André
98:28 queued 64:36
created

LocationAwareStore::getLocationCacheLockName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
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
        $tags = $request->headers->get('xkey', null, false);
76
77
        if ($request->headers->has('X-Location-Id')) {
78
            $tags[] = 'location-' . $request->headers->get('X-Location-Id');
79
        }
80
81
        foreach (array_unique($tags) as $tag) {
82
            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...
83
                throw new \RuntimeException('Unable to store the cache tag meta information.');
84
            }
85
        }
86
    }
87
88
    /**
89
     * Save digest for the given tag.
90
     *
91
     * @internal This is almost verbatim copy of save() from parent class as it is private.
92
     *
93
     * @param string $tag    The tag key
94
     * @param string $digest The digest hash to store representing the cache item.
95
     *
96
     * @return bool|void
97
     */
98
    private function saveTag($tag, $digest)
99
    {
100
        $path = $this->getPath($this->getCacheTagDir($tag));
101
        if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
102
            return false;
103
        }
104
105
        $tmpFile = tempnam(dirname($path), basename($path));
106
        if (false === $fp = @fopen($tmpFile, 'wb')) {
107
            return false;
108
        }
109
        @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...
110
        @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...
111
112
        if ($digest != file_get_contents($tmpFile)) {
113
            return false;
114
        }
115
116
        if (false === @rename($tmpFile, $path)) {
117
            return false;
118
        }
119
120
        @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...
121
    }
122
123
    /**
124
     * Purges data from $request.
125
     * If xkey or X-Location-Id (deprecated) header is present, the store will purge cache for given locationId or group of locationIds.
126
     * If not, regular purge by URI will occur.
127
     *
128
     * @param \Symfony\Component\HttpFoundation\Request $request
129
     *
130
     * @return bool True if purge was successful. False otherwise
131
     */
132
    public function purgeByRequest(Request $request)
133
    {
134
        if (!$request->headers->has('X-Location-Id') && !$request->headers->has('xkey')) {
135
            return $this->purge($request->getUri());
136
        }
137
138
        // Deprecated, see purgeAllContent(): Purge everything
139
        $locationId = $request->headers->get('X-Location-Id');
140
        if ($locationId === '*' || $locationId === '.*') {
141
            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...
142
        }
143
144
        if ($request->headers->has('xkey')) {
145
            $tags = explode(', ', $request->headers->get('xkey'));
146
        } else if ($locationId[0] === '(' && substr($locationId, -1) === ')') {
147
            // Deprecated: (123|456|789) => Purge for #123, #456 and #789 location IDs.
148
            $tags = array_map(
149
                function($id){return 'location-' . $id;},
150
                explode('|', substr($locationId, 1, -1))
151
            );
152
        } else {
153
            $tags = array('location-' . $locationId);
154
        }
155
156
        if (empty($tags)) {
157
            return false;
158
        }
159
160
        foreach ($tags as $tag) {
161
            $this->purgeByCacheTag($tag);
162
        }
163
164
        return true;
165
    }
166
167
    /**
168
     * Purges all cached content.
169
     *
170
     * @deprecated Use cache:clear, with multi tagging theoretically there shouldn't be need to delete all anymore from core.
171
     *
172
     * @return bool
173
     */
174
    public function purgeAllContent()
175
    {
176
        $cacheTagsCacheDir = $this->getCacheTagDir();
177
        $this->getFilesystem()->remove($cacheTagsCacheDir);
178
    }
179
180
    /**
181
     * Purges cache for tag.
182
     *
183
     * @param string $tag
184
     *
185
     * @return bool
186
     */
187
    private function purgeByCacheTag($tag)
188
    {
189
        $fs = $this->getFilesystem();
190
        $cacheTagsCacheDir = $this->getCacheTagDir($tag);
191
        if (!$fs->exists($cacheTagsCacheDir)) {
192
            return false;
193
        }
194
195
        $files = Finder::create()->files()->in($cacheTagsCacheDir)->getIterator();
196
        try {
197
            foreach ($files as $file) {
198
                if ($digest = file_get_contents($file)) {
199
                    $fs->remove($this->getPath($digest));
200
                }
201
            }
202
            $fs->remove($files);
203
            // we let folder stay in case another process have just written new cache tags
204
        } catch (IOException $e) {
205
            // Log the error in the standard error log and at least try to remove the lock file
206
            error_log($e->getMessage());
207
208
            return false;
209
        }
210
    }
211
212
    /**
213
     * Returns cache dir for $tag.
214
     *
215
     * This method is public only for unit tests.
216
     * Use it only if you know what you are doing.
217
     *
218
     * @internal
219
     *
220
     * @param int $tag
221
     *
222
     * @return string
223
     */
224
    public function getCacheTagDir($tag = null)
225
    {
226
        $cacheDir = "$this->root/" . static::TAG_CACHE_DIR;
227
        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...
228
            $cacheDir .= "/$tag";
229
        }
230
231
        return $cacheDir;
232
    }
233
}
234