Passed
Pull Request — master (#118)
by Daniel
01:52
created

FilesystemPublisher::purgeURL()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 13
nc 4
nop 1
dl 0
loc 18
rs 9.8333
c 2
b 0
f 0
1
<?php
2
3
namespace SilverStripe\StaticPublishQueue\Publisher;
4
5
use SilverStripe\Assets\Filesystem;
6
use SilverStripe\Control\HTTPResponse;
7
use function SilverStripe\StaticPublishQueue\PathToURL;
8
use SilverStripe\StaticPublishQueue\Publisher;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Security\SecurityToken;
11
use function SilverStripe\StaticPublishQueue\URLtoPath;
12
13
class FilesystemPublisher extends Publisher
14
{
15
    /**
16
     * @var string
17
     */
18
    protected $destFolder = 'cache';
19
20
    /**
21
     * @var string
22
     */
23
    protected $fileExtension = 'php';
24
25
    /**
26
     * @var bool
27
     */
28
    private static $use_gzip_compression = false;
0 ignored issues
show
introduced by
The private property $use_gzip_compression is not used, and could be removed.
Loading history...
29
30
    /**
31
     * avoid caching any pages with name"SecurityID" - an indication that a
32
     * form my be present that requires a fresh SecurityID
33
     * @var bool
34
     */
35
    private static $lazy_form_recognition = false;
0 ignored issues
show
introduced by
The private property $lazy_form_recognition is not used, and could be removed.
Loading history...
36
37
    /**
38
     * @return string
39
     */
40
    public function getDestPath()
41
    {
42
        $base = defined('PUBLIC_PATH') ? PUBLIC_PATH : BASE_PATH;
43
        return $base . DIRECTORY_SEPARATOR . $this->getDestFolder();
44
    }
45
46
    public function setDestFolder($destFolder)
47
    {
48
        $this->destFolder = $destFolder;
49
        return $this;
50
    }
51
52
    public function getDestFolder()
53
    {
54
        return $this->destFolder;
55
    }
56
57
    public function setFileExtension($fileExtension)
58
    {
59
        $fileExtension = strtolower($fileExtension);
60
        if (!in_array($fileExtension, ['html', 'php'], true)) {
61
            throw new \InvalidArgumentException(
62
                sprintf(
63
                    'Bad file extension "%s" passed to %s::%s',
64
                    $fileExtension,
65
                    static::class,
66
                    __FUNCTION__
67
                )
68
            );
69
        }
70
        $this->fileExtension = $fileExtension;
71
        return $this;
72
    }
73
74
    public function getFileExtension()
75
    {
76
        return $this->fileExtension;
77
    }
78
79
    public function purgeURL($url)
80
    {
81
        if (!$url) {
82
            user_error('Bad url:' . var_export($url, true), E_USER_WARNING);
83
            return;
84
        }
85
        if ($path = $this->URLtoPath($url)) {
86
            $success = $this->deleteFromPath($path . '.html') && $this->deleteFromPath($path . '.php');
87
            return [
88
                'success' => $success,
89
                'url' => $url,
90
                'path' => $this->getDestPath() . DIRECTORY_SEPARATOR . $path,
91
            ];
92
        }
93
        return [
94
            'success' => false,
95
            'url' => $url,
96
            'path' => false,
97
        ];
98
    }
99
100
    public function purgeAll()
101
    {
102
        Filesystem::removeFolder($this->getDestPath());
103
104
        return file_exists($this->getDestPath()) ? false : true;
105
    }
106
107
    /**
108
     * @param string $url
109
     * @param bool $forcePublish (optional)
110
     * @return array A result array
111
     */
112
    public function publishURL($url, $forcePublish = false)
113
    {
114
        if (!$url) {
115
            user_error('Bad url:' . var_export($url, true), E_USER_WARNING);
116
            return;
117
        }
118
        $success = false;
119
        $response = $this->generatePageResponse($url);
120
        $statusCode = $response->getStatusCode();
121
        $doPublish = ($forcePublish && $this->getFileExtension() === 'php') || $statusCode < 400;
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: $doPublish = ($forcePubl...' || $statusCode < 400), Probably Intended Meaning: $doPublish = $forcePubli...' || $statusCode < 400)
Loading history...
122
123
        if ($statusCode < 300) {
124
            // publish success response
125
            $success = $this->publishPage($response, $url);
126
        } elseif ($statusCode < 400) {
127
            // publish redirect response
128
            $success = $this->publishRedirect($response, $url);
129
        } elseif ($doPublish) {
130
            // only publish error pages if we are able to send status codes via PHP
131
            $success = $this->publishPage($response, $url);
132
        }
133
        return [
134
            'published' => $doPublish,
135
            'success' => $success,
136
            'responsecode' => $statusCode,
137
            'url' => $url,
138
        ];
139
    }
140
141
    /**
142
     * @param HTTPResponse $response
143
     * @param string       $url
144
     * @return bool
145
     */
146
    protected function publishRedirect($response, $url)
147
    {
148
        $success = true;
149
        if ($path = $this->URLtoPath($url)) {
150
            $location = $response->getHeader('Location');
151
            if ($this->getFileExtension() === 'php') {
152
                $phpContent = $this->generatePHPCacheFile($response);
153
                $success = $this->saveToPath($phpContent, $path . '.php');
154
            }
155
            return $this->saveToPath($this->generateHTMLCacheRedirection($location), $path . '.html') && $success;
156
        }
157
        return false;
158
    }
159
160
    /**
161
     * @param HTTPResponse $response
162
     * @param string       $url
163
     * @return bool
164
     */
165
    protected function publishPage($response, $url)
166
    {
167
        $success = true;
168
        if ($path = $this->URLtoPath($url)) {
169
            $body = '';
170
            if ($this->Config()->get('lazy_form_recognition')) {
171
                $id = Config::inst()->get(SecurityToken::class, 'default_name') ?? 'SecurityID';
172
                // little hack to make sure we do not include pages with live forms.
173
                $body = $response->getBody();
174
                if (stripos($body, '<input type="hidden" name="' . $id . '"')) {
175
                    return false;
176
                }
177
            }
178
            if ($this->getFileExtension() === 'php') {
179
                $phpContent = $this->generatePHPCacheFile($response);
180
                $success = $this->saveToPath($phpContent, $path . '.php');
181
            }
182
            if (!$body) {
183
                $body = $response->getBody();
184
            }
185
            return $this->saveToPath($body, $path . '.html') && $success;
186
        }
187
        return false;
188
    }
189
190
    /**
191
     * returns true on access and false on failure
192
     * @param string $content
193
     * @param string $filePath
194
     * @return bool
195
     */
196
    protected function saveToPath($content, $filePath)
197
    {
198
        if (empty($content)) {
199
            return false;
200
        }
201
202
        // Write to a temporary file first
203
        $temporaryPath = tempnam(TEMP_PATH, 'filesystempublisher_');
204
        if (file_put_contents($temporaryPath, $content) === false) {
205
            return false;
206
        }
207
208
        // Move the temporary file to the desired location (prevents unlocked files from being read during write)
209
        $publishPath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
210
        Filesystem::makeFolder(dirname($publishPath));
211
        $successWithPublish = rename($temporaryPath, $publishPath);
212
        if ($successWithPublish) {
213
            if (FilesystemPublisher::config()->get('use_gzip_compression')) {
214
                $publishPath = $this->compressFile($publishPath);
215
            }
216
        }
217
218
        return file_exists($publishPath);
219
    }
220
221
    protected function compressFile(string $publishPath): string
222
    {
223
        // read original
224
        $data = implode('', file($publishPath));
0 ignored issues
show
Bug introduced by
It seems like file($publishPath) can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
        $data = implode('', /** @scrutinizer ignore-type */ file($publishPath));
Loading history...
225
        // encode it with highest level
226
        $gzdata = gzencode($data, 9);
227
        // new file
228
        $publishPathGZipped = $publishPath . '.gz';
229
        // remove
230
        unlink($publishPathGZipped);
231
        // open a new one
232
        $fp = fopen($publishPathGZipped, 'w');
233
        // write it
234
        fwrite($fp, $gzdata);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

234
        fwrite(/** @scrutinizer ignore-type */ $fp, $gzdata);
Loading history...
235
        // close it
236
        fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

236
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
237
        @chmod($file, 0664);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

237
        /** @scrutinizer ignore-unhandled */ @chmod($file, 0664);

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...
Comprehensibility Best Practice introduced by
The variable $file seems to be never defined.
Loading history...
238
239
        return $publishPathGZipped;
240
    }
241
242
    protected function deleteFromPath($filePath)
243
    {
244
        $deletePath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
245
        if (file_exists($deletePath)) {
246
            $success = unlink($deletePath);
247
        } else {
248
            $success = true;
249
        }
250
        if (file_exists($deletePath . '.gz')) {
251
            return $this->deleteFromPath($deletePath . '.gz');
252
        }
253
254
        return $success;
255
    }
256
257
    protected function URLtoPath($url)
258
    {
259
        return URLtoPath($url, BASE_URL, FilesystemPublisher::config()->get('domain_based_caching'));
260
    }
261
262
    protected function pathToURL($path)
263
    {
264
        return PathToURL($path, $this->getDestPath(), FilesystemPublisher::config()->get('domain_based_caching'));
265
    }
266
267
    public function getPublishedURLs($dir = null, &$result = [])
268
    {
269
        if ($dir === null) {
270
            $dir = $this->getDestPath();
271
        }
272
273
        $root = scandir($dir);
274
        foreach ($root as $fileOrDir) {
275
            if (strpos($fileOrDir, '.') === 0) {
276
                continue;
277
            }
278
            $fullPath = $dir . DIRECTORY_SEPARATOR . $fileOrDir;
279
            // we know html will always be generated, this prevents double ups
280
            if (is_file($fullPath) && pathinfo($fullPath, PATHINFO_EXTENSION) === 'html') {
281
                $result[] = $this->pathToURL($fullPath);
282
                continue;
283
            }
284
285
            if (is_dir($fullPath)) {
286
                $this->getPublishedURLs($fullPath, $result);
287
            }
288
        }
289
        return $result;
290
    }
291
}
292