Completed
Push — feature-EZP-25696 ( e2f6d5...042f22 )
by André
13:06
created

LocationAwareStore::purgeLocation()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 1
dl 0
loc 32
rs 8.8571
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
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
 * @deprecated As of 6.7, use {@link \eZ\Publish\Core\MVC\Symfony\Cache\Http\Proxy\TagAwareStore}.
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
    public function __construct($root)
33
    {
34
        parent::__construct($root);
35
        @error_log("LocationAwareStore is deprecated and not supported anymore, please use TagAwareStore", E_USER_DEPRECATED);
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...
36
    }
37
38
    /**
39
     * Injects a Filesystem instance
40
     * For unit tests only.
41
     *
42
     * @internal
43
     *
44
     * @param \Symfony\Component\Filesystem\Filesystem $fs
45
     */
46
    public function setFilesystem(Filesystem $fs)
47
    {
48
        $this->fs = $fs;
49
    }
50
51
    /**
52
     * @return \Symfony\Component\Filesystem\Filesystem
53
     */
54
    public function getFilesystem()
55
    {
56
        if (!isset($this->fs)) {
57
            $this->fs = new Filesystem();
58
        }
59
60
        return $this->fs;
61
    }
62
63
    /**
64
     * Injects eZ Publish specific information in the content digest if needed.
65
     * X-Location-Id response header is set in the ViewController.
66
     *
67
     * @see \eZ\Publish\Core\MVC\Symfony\Controller\Content\ViewController::viewLocation()
68
     *
69
     * @param \Symfony\Component\HttpFoundation\Response $response
70
     *
71
     * @return string
72
     */
73
    protected function generateContentDigest(Response $response)
74
    {
75
        $digest = parent::generateContentDigest($response);
76
        if (!$response->headers->has('X-Location-Id')) {
77
            return $digest;
78
        }
79
80
        return static::LOCATION_CACHE_DIR . "/{$response->headers->get('X-Location-Id')}/$digest";
81
    }
82
83
    /**
84
     * Returns the right path where cache is being stored.
85
     * Will detect if $key is eZ Publish specific.
86
     *
87
     * @param string $key
88
     *
89
     * @return string
90
     */
91
    public function getPath($key)
92
    {
93
        if (strpos($key, static::LOCATION_CACHE_DIR) === false) {
94
            return parent::getPath($key);
95
        }
96
97
        $prefix = '';
98
        if (($pos = strrpos($key, DIRECTORY_SEPARATOR)) !== false) {
99
            $prefix = substr($key, 0, $pos) . DIRECTORY_SEPARATOR;
100
            $key = substr($key, $pos + 1);
101
102
            list($locationCacheDir, $locationId) = explode(DIRECTORY_SEPARATOR, $prefix);
103
            unset($locationCacheDir);
104
            // If cache purge is in progress, serve stale cache instead of regular cache.
105
            // We first check for a global cache purge, then for the current location.
106
            foreach (array($this->getLocationCacheLockName(), $this->getLocationCacheLockName($locationId)) as $cacheLockFile) {
107
                if (is_file($cacheLockFile)) {
108
                    if (function_exists('posix_kill')) {
109
                        // Check if purge process is still running. If not, remove the lock file to unblock future cache purge
110
                        if (!posix_kill(file_get_contents($cacheLockFile), 0)) {
111
                            $fs = $this->getFilesystem();
112
                            $fs->remove(array($cacheLockFile, $this->getLocationCacheDir($locationId)));
113
                            goto returnCachePath;
114
                        }
115
                    }
116
117
                    $prefix = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $prefix);
118
                }
119
            }
120
        }
121
122
        returnCachePath:
123
        return $this->root . DIRECTORY_SEPARATOR . $prefix .
124
           substr($key, 0, 2) . DIRECTORY_SEPARATOR .
125
           substr($key, 2, 2) . DIRECTORY_SEPARATOR .
126
           substr($key, 4, 2) . DIRECTORY_SEPARATOR .
127
           substr($key, 6);
128
    }
129
130
    /**
131
     * Purges data from $request.
132
     * If X-Location-Id or X-Group-Location-Id header is present, the store will purge cache for given locationId or group of locationIds.
133
     * If not, regular purge by URI will occur.
134
     *
135
     * @param \Symfony\Component\HttpFoundation\Request $request
136
     *
137
     * @return bool True if purge was successful. False otherwise
138
     */
139
    public function purgeByRequest(Request $request)
140
    {
141 View Code Duplication
        if (!$request->headers->has('X-Location-Id') && !$request->headers->has('X-Group-Location-Id')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
142
            return $this->purge($request->getUri());
143
        }
144
145
        // Purge everything
146
        $locationId = $request->headers->get('X-Location-Id');
147
        if ($locationId === '*' || $locationId === '.*') {
148
            return $this->purgeAllContent();
149
        }
150
151
        // Usage of X-Group-Location-Id is deprecated.
152
        if ($request->headers->has('X-Group-Location-Id')) {
153
            $aLocationId = explode('; ', $request->headers->get('X-Group-Location-Id'));
154 View Code Duplication
        } elseif ($locationId[0] === '(' && substr($locationId, -1) === ')') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
            // Equivalent to X-Group-Location-Id, using a simple Regexp:
156
            // (123|456|789) => Purge for #123, #456 and #789 location IDs.
157
            $aLocationId = explode('|', substr($locationId, 1, -1));
158
        } else {
159
            $aLocationId = array($locationId);
160
        }
161
162
        if (empty($aLocationId)) {
163
            return false;
164
        }
165
166
        foreach ($aLocationId as $locationId) {
167
            $this->purgeLocation($locationId);
168
        }
169
170
        return true;
171
    }
172
173
    /**
174
     * Purges all cached content.
175
     *
176
     * @return bool
177
     */
178
    public function purgeAllContent()
179
    {
180
        return $this->purgeLocation(null);
181
    }
182
183
    /**
184
     * Purges cache for $locationId.
185
     *
186
     * @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...
187
     *
188
     * @return bool
189
     */
190
    private function purgeLocation($locationId)
191
    {
192
        $fs = $this->getFilesystem();
193
        $locationCacheDir = $this->getLocationCacheDir($locationId);
194
        if ($fs->exists($locationCacheDir)) {
195
            // 1. Copy cache files to stale cache dir
196
            // 2. Place a lock file indicating to use the stale cache
197
            // 3. Remove real cache dir
198
            // 4. Remove lock file
199
            // 5. Remove stale cache dir
200
            // Note that there is no need to remove the meta-file
201
            $staleCacheDir = str_replace(static::LOCATION_CACHE_DIR, static::LOCATION_STALE_CACHE_DIR, $locationCacheDir);
202
            $fs->mkdir($staleCacheDir);
203
            $fs->mirror($locationCacheDir, $staleCacheDir);
204
            $lockFile = $this->getLocationCacheLockName($locationId);
205
            file_put_contents($lockFile, getmypid());
206
            try {
207
                // array of removal is in reverse order on purpose since remove() starts from the end.
208
                $fs->remove(array($staleCacheDir, $lockFile, $locationCacheDir));
209
210
                return true;
211
            } catch (IOException $e) {
212
                // Log the error in the standard error log and at least try to remove the lock file
213
                error_log($e->getMessage());
214
                @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...
215
216
                return false;
217
            }
218
        }
219
220
        return false;
221
    }
222
223
    /**
224
     * Returns cache lock name for $locationId.
225
     *
226
     * This method is public only for unit tests.
227
     * Use it only if you know what you are doing.
228
     *
229
     * @internal
230
     *
231
     * @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...
232
     *
233
     * @return string
234
     */
235
    public function getLocationCacheLockName($locationId = null)
236
    {
237
        $locationId = $locationId ?: 'all';
238
239
        return "$this->root/_ezloc_$locationId.purging";
240
    }
241
242
    /**
243
     * Returns cache dir for $locationId.
244
     *
245
     * This method is public only for unit tests.
246
     * Use it only if you know what you are doing.
247
     *
248
     * @internal
249
     *
250
     * @param int $locationId
251
     *
252
     * @return string
253
     */
254
    public function getLocationCacheDir($locationId = null)
255
    {
256
        $cacheDir = "$this->root/" . static::LOCATION_CACHE_DIR;
257
        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...
258
            $cacheDir .= "/$locationId";
259
        }
260
261
        return $cacheDir;
262
    }
263
}
264