Completed
Push — ezp26352-skip_csrf_check_on_re... ( 19f37a...6abe82 )
by
unknown
79:54 queued 33:48
created

LocationAwareStore::getLocationCacheDir()   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
19
/**
20
 * LocationAwareStore implements all the logic for storing cache metadata regarding locations.
21
 */
22
class LocationAwareStore extends Store implements ContentPurger
23
{
24
    const LOCATION_CACHE_DIR = 'ezlocation',
25
          LOCATION_STALE_CACHE_DIR = 'ezlocation_stale';
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
     * Injects eZ Publish specific information in the content digest if needed.
59
     * X-Location-Id response header is set in the ViewController.
60
     *
61
     * @see \eZ\Publish\Core\MVC\Symfony\Controller\Content\ViewController::viewLocation()
62
     *
63
     * @param \Symfony\Component\HttpFoundation\Response $response
64
     *
65
     * @return string
66
     */
67
    protected function generateContentDigest(Response $response)
68
    {
69
        $digest = parent::generateContentDigest($response);
70
        if (!$response->headers->has('X-Location-Id')) {
71
            return $digest;
72
        }
73
74
        return static::LOCATION_CACHE_DIR . "/{$response->headers->get('X-Location-Id')}/$digest";
75
    }
76
77
    /**
78
     * Returns the right path where cache is being stored.
79
     * Will detect if $key is eZ Publish specific.
80
     *
81
     * @param string $key
82
     *
83
     * @return string
84
     */
85
    public function getPath($key)
86
    {
87
        if (strpos($key, static::LOCATION_CACHE_DIR) === false) {
88
            return parent::getPath($key);
89
        }
90
91
        $prefix = '';
92
        if (($pos = strrpos($key, DIRECTORY_SEPARATOR)) !== false) {
93
            $prefix = substr($key, 0, $pos) . DIRECTORY_SEPARATOR;
94
            $key = substr($key, $pos + 1);
95
96
            list($locationCacheDir, $locationId) = explode(DIRECTORY_SEPARATOR, $prefix);
97
            unset($locationCacheDir);
98
            // If cache purge is in progress, serve stale cache instead of regular cache.
99
            // We first check for a global cache purge, then for the current location.
100
            foreach (array($this->getLocationCacheLockName(), $this->getLocationCacheLockName($locationId)) as $cacheLockFile) {
101
                if (is_file($cacheLockFile)) {
102
                    if (function_exists('posix_kill')) {
103
                        // Check if purge process is still running. If not, remove the lock file to unblock future cache purge
104
                        if (!posix_kill(file_get_contents($cacheLockFile), 0)) {
105
                            $fs = $this->getFilesystem();
106
                            $fs->remove(array($cacheLockFile, $this->getLocationCacheDir($locationId)));
107
                            goto returnCachePath;
108
                        }
109
                    }
110
111
                    $prefix = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $prefix);
112
                }
113
            }
114
        }
115
116
        returnCachePath:
117
        return $this->root . DIRECTORY_SEPARATOR . $prefix .
118
           substr($key, 0, 2) . DIRECTORY_SEPARATOR .
119
           substr($key, 2, 2) . DIRECTORY_SEPARATOR .
120
           substr($key, 4, 2) . DIRECTORY_SEPARATOR .
121
           substr($key, 6);
122
    }
123
124
    /**
125
     * Purges data from $request.
126
     * If X-Location-Id or X-Group-Location-Id header is present, the store will purge cache for given locationId or group of locationIds.
127
     * If not, regular purge by URI will occur.
128
     *
129
     * @param \Symfony\Component\HttpFoundation\Request $request
130
     *
131
     * @return bool True if purge was successful. False otherwise
132
     */
133
    public function purgeByRequest(Request $request)
134
    {
135
        if (!$request->headers->has('X-Location-Id') && !$request->headers->has('X-Group-Location-Id')) {
136
            return $this->purge($request->getUri());
137
        }
138
139
        // Purge everything
140
        $locationId = $request->headers->get('X-Location-Id');
141
        if ($locationId === '*' || $locationId === '.*') {
142
            return $this->purgeAllContent();
143
        }
144
145
        // Usage of X-Group-Location-Id is deprecated.
146
        if ($request->headers->has('X-Group-Location-Id')) {
147
            $aLocationId = explode('; ', $request->headers->get('X-Group-Location-Id'));
148
        } elseif ($locationId[0] === '(' && substr($locationId, -1) === ')') {
149
            // Equivalent to X-Group-Location-Id, using a simple Regexp:
150
            // (123|456|789) => Purge for #123, #456 and #789 location IDs.
151
            $aLocationId = explode('|', substr($locationId, 1, -1));
152
        } else {
153
            $aLocationId = array($locationId);
154
        }
155
156
        if (empty($aLocationId)) {
157
            return false;
158
        }
159
160
        foreach ($aLocationId as $locationId) {
161
            $this->purgeLocation($locationId);
162
        }
163
164
        return true;
165
    }
166
167
    /**
168
     * Purges all cached content.
169
     *
170
     * @return bool
171
     */
172
    public function purgeAllContent()
173
    {
174
        return $this->purgeLocation(null);
175
    }
176
177
    /**
178
     * Purges cache for $locationId.
179
     *
180
     * @param int|null $locationId. If null, all locations will be purged.
0 ignored issues
show
Documentation introduced by
There is no parameter named $locationId.. Did you maybe mean $locationId?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
181
     *
182
     * @return bool
183
     */
184
    private function purgeLocation($locationId)
185
    {
186
        $fs = $this->getFilesystem();
187
        $locationCacheDir = $this->getLocationCacheDir($locationId);
188
        if ($fs->exists($locationCacheDir)) {
189
            // 1. Copy cache files to stale cache dir
190
            // 2. Place a lock file indicating to use the stale cache
191
            // 3. Remove real cache dir
192
            // 4. Remove lock file
193
            // 5. Remove stale cache dir
194
            // Note that there is no need to remove the meta-file
195
            $staleCacheDir = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $locationCacheDir);
196
            $fs->mkdir($staleCacheDir);
197
            $fs->mirror($locationCacheDir, $staleCacheDir);
198
            $lockFile = $this->getLocationCacheLockName($locationId);
199
            file_put_contents($lockFile, getmypid());
200
            try {
201
                // array of removal is in reverse order on purpose since remove() starts from the end.
202
                $fs->remove(array($staleCacheDir, $lockFile, $locationCacheDir));
203
204
                return true;
205
            } catch (IOException $e) {
206
                // Log the error in the standard error log and at least try to remove the lock file
207
                error_log($e->getMessage());
208
                @unlink($lockFile);
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...
209
210
                return false;
211
            }
212
        }
213
214
        return false;
215
    }
216
217
    /**
218
     * Returns cache lock name for $locationId.
219
     *
220
     * This method is public only for unit tests.
221
     * Use it only if you know what you are doing.
222
     *
223
     * @internal
224
     *
225
     * @param int $locationId. If null, will return a global cache lock name (purging all content)
0 ignored issues
show
Documentation introduced by
There is no parameter named $locationId.. Did you maybe mean $locationId?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
226
     *
227
     * @return string
228
     */
229
    public function getLocationCacheLockName($locationId = null)
230
    {
231
        $locationId = $locationId ?: 'all';
232
233
        return "$this->root/_ezloc_$locationId.purging";
234
    }
235
236
    /**
237
     * Returns cache dir for $locationId.
238
     *
239
     * This method is public only for unit tests.
240
     * Use it only if you know what you are doing.
241
     *
242
     * @internal
243
     *
244
     * @param int $locationId
245
     *
246
     * @return string
247
     */
248
    public function getLocationCacheDir($locationId = null)
249
    {
250
        $cacheDir = "$this->root/" . static::LOCATION_CACHE_DIR;
251
        if ($locationId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $locationId 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...
252
            $cacheDir .= "/$locationId";
253
        }
254
255
        return $cacheDir;
256
    }
257
}
258