Completed
Push — EZP-29891 ( 916cf6...0402ff )
by
unknown
16:53
created

LocationAwareStore   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 236
rs 10
c 0
b 0
f 0
wmc 30
lcom 1
cbo 7

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setFilesystem() 0 4 1
A getFilesystem() 0 8 2
A generateContentDigest() 0 9 2
B getPath() 0 38 7
B purgeByRequest() 0 33 10
A purgeAllContent() 0 4 1
A purgeLocation() 0 32 3
A getLocationCacheLockName() 0 6 2
A getLocationCacheDir() 0 9 2
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
namespace eZ\Publish\Core\MVC\Symfony\Cache\Http;
10
11
use Symfony\Component\HttpKernel\HttpCache\Store;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\Filesystem\Filesystem;
15
use Symfony\Component\Filesystem\Exception\IOException;
16
17
/**
18
 * LocationAwareStore implements all the logic for storing cache metadata regarding locations.
19
 */
20
class LocationAwareStore extends Store implements ContentPurger
21
{
22
    const LOCATION_CACHE_DIR = 'ezlocation';
23
    const LOCATION_STALE_CACHE_DIR = 'ezlocation_stale';
24
25
    /**
26
     * @var \Symfony\Component\Filesystem\Filesystem
27
     */
28
    private $fs;
29
30
    /**
31
     * Injects a Filesystem instance
32
     * For unit tests only.
33
     *
34
     * @internal
35
     *
36
     * @param \Symfony\Component\Filesystem\Filesystem $fs
37
     */
38
    public function setFilesystem(Filesystem $fs)
39
    {
40
        $this->fs = $fs;
41
    }
42
43
    /**
44
     * @return \Symfony\Component\Filesystem\Filesystem
45
     */
46
    public function getFilesystem()
47
    {
48
        if (!isset($this->fs)) {
49
            $this->fs = new Filesystem();
50
        }
51
52
        return $this->fs;
53
    }
54
55
    /**
56
     * Injects eZ Publish specific information in the content digest if needed.
57
     * X-Location-Id response header is set in the ViewController.
58
     *
59
     * @see \eZ\Publish\Core\MVC\Symfony\Controller\Content\ViewController::viewLocation()
60
     *
61
     * @param \Symfony\Component\HttpFoundation\Response $response
62
     *
63
     * @return string
64
     */
65
    protected function generateContentDigest(Response $response)
66
    {
67
        $digest = parent::generateContentDigest($response);
68
        if (!$response->headers->has('X-Location-Id')) {
69
            return $digest;
70
        }
71
72
        return static::LOCATION_CACHE_DIR . "/{$response->headers->get('X-Location-Id')}/$digest";
73
    }
74
75
    /**
76
     * Returns the right path where cache is being stored.
77
     * Will detect if $key is eZ Publish specific.
78
     *
79
     * @param string $key
80
     *
81
     * @return string
82
     */
83
    public function getPath($key)
84
    {
85
        if (strpos($key, static::LOCATION_CACHE_DIR) === false) {
86
            return parent::getPath($key);
87
        }
88
89
        $prefix = '';
90
        if (($pos = strrpos($key, DIRECTORY_SEPARATOR)) !== false) {
91
            $prefix = substr($key, 0, $pos) . DIRECTORY_SEPARATOR;
92
            $key = substr($key, $pos + 1);
93
94
            list($locationCacheDir, $locationId) = explode(DIRECTORY_SEPARATOR, $prefix);
95
            unset($locationCacheDir);
96
            // If cache purge is in progress, serve stale cache instead of regular cache.
97
            // We first check for a global cache purge, then for the current location.
98
            foreach (array($this->getLocationCacheLockName(), $this->getLocationCacheLockName($locationId)) as $cacheLockFile) {
99
                if (is_file($cacheLockFile)) {
100
                    if (function_exists('posix_kill')) {
101
                        // Check if purge process is still running. If not, remove the lock file to unblock future cache purge
102
                        if (!posix_kill(file_get_contents($cacheLockFile), 0)) {
103
                            $fs = $this->getFilesystem();
104
                            $fs->remove(array($cacheLockFile, $this->getLocationCacheDir($locationId)));
105
                            goto returnCachePath;
106
                        }
107
                    }
108
109
                    $prefix = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $prefix);
110
                }
111
            }
112
        }
113
114
        returnCachePath:
115
        return $this->root . DIRECTORY_SEPARATOR . $prefix .
116
           substr($key, 0, 2) . DIRECTORY_SEPARATOR .
117
           substr($key, 2, 2) . DIRECTORY_SEPARATOR .
118
           substr($key, 4, 2) . DIRECTORY_SEPARATOR .
119
           substr($key, 6);
120
    }
121
122
    /**
123
     * Purges data from $request.
124
     * If X-Location-Id or X-Group-Location-Id header is present, the store will purge cache for given locationId or group of locationIds.
125
     * If not, regular purge by URI will occur.
126
     *
127
     * @param \Symfony\Component\HttpFoundation\Request $request
128
     *
129
     * @return bool True if purge was successful. False otherwise
130
     */
131
    public function purgeByRequest(Request $request)
132
    {
133
        if (!$request->headers->has('X-Location-Id') && !$request->headers->has('X-Group-Location-Id')) {
134
            return $this->purge($request->getUri());
135
        }
136
137
        // Purge everything
138
        $locationId = $request->headers->get('X-Location-Id');
139
        if ($locationId === '*' || $locationId === '.*') {
140
            return $this->purgeAllContent();
141
        }
142
143
        // Usage of X-Group-Location-Id is deprecated.
144
        if ($request->headers->has('X-Group-Location-Id')) {
145
            $aLocationId = explode('; ', $request->headers->get('X-Group-Location-Id'));
146
        } elseif ($locationId[0] === '(' && substr($locationId, -1) === ')') {
147
            // Equivalent to X-Group-Location-Id, using a simple Regexp:
148
            // (123|456|789) => Purge for #123, #456 and #789 location IDs.
149
            $aLocationId = explode('|', substr($locationId, 1, -1));
150
        } else {
151
            $aLocationId = array($locationId);
152
        }
153
154
        if (empty($aLocationId)) {
155
            return false;
156
        }
157
158
        foreach ($aLocationId as $locationId) {
159
            $this->purgeLocation($locationId);
160
        }
161
162
        return true;
163
    }
164
165
    /**
166
     * Purges all cached content.
167
     *
168
     * @return bool
169
     */
170
    public function purgeAllContent()
171
    {
172
        return $this->purgeLocation(null);
173
    }
174
175
    /**
176
     * Purges cache for $locationId.
177
     *
178
     * @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...
179
     *
180
     * @return bool
181
     */
182
    private function purgeLocation($locationId)
183
    {
184
        $fs = $this->getFilesystem();
185
        $locationCacheDir = $this->getLocationCacheDir($locationId);
186
        if ($fs->exists($locationCacheDir)) {
187
            // 1. Copy cache files to stale cache dir
188
            // 2. Place a lock file indicating to use the stale cache
189
            // 3. Remove real cache dir
190
            // 4. Remove lock file
191
            // 5. Remove stale cache dir
192
            // Note that there is no need to remove the meta-file
193
            $staleCacheDir = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $locationCacheDir);
194
            $fs->mkdir($staleCacheDir);
195
            $fs->mirror($locationCacheDir, $staleCacheDir);
196
            $lockFile = $this->getLocationCacheLockName($locationId);
197
            file_put_contents($lockFile, getmypid());
198
            try {
199
                // array of removal is in reverse order on purpose since remove() starts from the end.
200
                $fs->remove(array($staleCacheDir, $lockFile, $locationCacheDir));
201
202
                return true;
203
            } catch (IOException $e) {
204
                // Log the error in the standard error log and at least try to remove the lock file
205
                error_log($e->getMessage());
206
                @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...
207
208
                return false;
209
            }
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
     * Returns cache lock name for $locationId.
217
     *
218
     * This method is public only for unit tests.
219
     * Use it only if you know what you are doing.
220
     *
221
     * @internal
222
     *
223
     * @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...
224
     *
225
     * @return string
226
     */
227
    public function getLocationCacheLockName($locationId = null)
228
    {
229
        $locationId = $locationId ?: 'all';
230
231
        return "$this->root/_ezloc_$locationId.purging";
232
    }
233
234
    /**
235
     * Returns cache dir for $locationId.
236
     *
237
     * This method is public only for unit tests.
238
     * Use it only if you know what you are doing.
239
     *
240
     * @internal
241
     *
242
     * @param int $locationId
243
     *
244
     * @return string
245
     */
246
    public function getLocationCacheDir($locationId = null)
247
    {
248
        $cacheDir = "$this->root/" . static::LOCATION_CACHE_DIR;
249
        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...
250
            $cacheDir .= "/$locationId";
251
        }
252
253
        return $cacheDir;
254
    }
255
}
256