Passed
Pull Request — master (#118)
by Nicolaas
03:00 queued 37s
created

FilesystemPublisher::purgeAll()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 5
rs 10
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 function SilverStripe\StaticPublishQueue\URLtoPath;
10
11
class FilesystemPublisher extends Publisher
12
{
13
    /**
14
     * @var string
15
     */
16
    protected $destFolder = 'cache';
17
18
    /**
19
     * @var string
20
     */
21
    protected $fileExtension = 'html';
22
23
    /**
24
     * @var bool
25
     */
26
    private static $use_gzip_compression = true;
0 ignored issues
show
introduced by
The private property $use_gzip_compression is not used, and could be removed.
Loading history...
27
28
    /**
29
     * @return string
30
     */
31
    public function getDestPath()
32
    {
33
        $base = defined('PUBLIC_PATH') ? PUBLIC_PATH : BASE_PATH;
34
        return $base . DIRECTORY_SEPARATOR . $this->getDestFolder();
35
    }
36
37
    public function setDestFolder($destFolder)
38
    {
39
        $this->destFolder = $destFolder;
40
        return $this;
41
    }
42
43
    public function getDestFolder()
44
    {
45
        return $this->destFolder;
46
    }
47
48
    public function setFileExtension($fileExtension)
49
    {
50
        $fileExtension = strtolower($fileExtension);
51
        if (! in_array($fileExtension, ['html', 'php'], true)) {
52
            throw new \InvalidArgumentException(
53
                sprintf(
54
                    'Bad file extension "%s" passed to %s::%s',
55
                    $fileExtension,
56
                    static::class,
57
                    __FUNCTION__
58
                )
59
            );
60
        }
61
        $this->fileExtension = $fileExtension;
62
        return $this;
63
    }
64
65
    public function getFileExtension()
66
    {
67
        return $this->fileExtension;
68
    }
69
70
    public function purgeURL(string $url): array
71
    {
72
        if (! $url) {
73
            user_error('Bad url:' . var_export($url, true), E_USER_WARNING);
74
            return [];
75
        }
76
        if ($path = $this->URLtoPath($url)) {
77
            $success = $this->deleteFromPath($path . '.html') && $this->deleteFromPath($path . '.php');
78
            return [
79
                'success' => $success,
80
                'url' => $url,
81
                'path' => $this->getDestPath() . DIRECTORY_SEPARATOR . $path,
82
            ];
83
        }
84
        return [
85
            'success' => false,
86
            'url' => $url,
87
            'path' => false,
88
        ];
89
    }
90
91
    public function purgeAll(): bool
92
    {
93
        Filesystem::removeFolder($this->getDestPath());
94
95
        return file_exists($this->getDestPath()) ? false : true;
96
    }
97
98
    /**
99
     * @param string $url
100
     * @return array A result array
101
     */
102
    public function publishURL(string $url, ?bool $forcePublish = false): array
103
    {
104
        if (!$url) {
105
            user_error('Bad url:' . var_export($url, true), E_USER_WARNING);
106
            return [];
107
        }
108
        $success = false;
109
        $response = $this->generatePageResponse($url);
110
        $statusCode = $response->getStatusCode();
111
        $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...
112
113
        if ($statusCode < 300) {
114
            // publish success response
115
            $success = $this->publishPage($response, $url);
116
        } elseif ($statusCode < 400) {
117
            // publish redirect response
118
            $success = $this->publishRedirect($response, $url);
119
        } elseif ($doPublish) {
120
            // only publish error pages if we are able to send status codes via PHP
121
            $success = $this->publishPage($response, $url);
122
        }
123
        return [
124
            'published' => $doPublish,
125
            'success' => $success,
126
            'responsecode' => $statusCode,
127
            'url' => $url,
128
        ];
129
    }
130
131
    public function getPublishedURLs(?string $dir = null, array &$result = []): array
132
    {
133
        if ($dir === null) {
134
            $dir = $this->getDestPath();
135
        }
136
137
        $root = scandir($dir);
138
        foreach ($root as $fileOrDir) {
139
            if (strpos($fileOrDir, '.') === 0) {
140
                continue;
141
            }
142
            $fullPath = $dir . DIRECTORY_SEPARATOR . $fileOrDir;
143
            // we know html will always be generated, this prevents double ups
144
            if (is_file($fullPath) && pathinfo($fullPath, PATHINFO_EXTENSION) === 'html') {
145
                $result[] = $this->pathToURL($fullPath);
146
                continue;
147
            }
148
149
            if (is_dir($fullPath)) {
150
                $this->getPublishedURLs($fullPath, $result);
151
            }
152
        }
153
        return $result;
154
    }
155
156
    /**
157
     * @param HTTPResponse $response
158
     * @param string       $url
159
     * @return bool
160
     */
161
    protected function publishRedirect($response, string $url) : bool
162
    {
163
        $success = true;
164
        if ($path = $this->URLtoPath($url)) {
165
            $location = $response->getHeader('Location');
166
            if ($this->getFileExtension() === 'php') {
167
                $phpContent = $this->generatePHPCacheFile($response);
168
                $success = $this->saveToPath($phpContent, $path . '.php');
169
            }
170
            return $this->saveToPath($this->generateHTMLCacheRedirection($location), $path . '.html') && $success;
171
        }
172
        return false;
173
    }
174
175
    /**
176
     * @param HTTPResponse $response
177
     * @param string       $url
178
     * @return bool
179
     */
180
    protected function publishPage(string $response, string $url) : bool
181
    {
182
        $success = true;
183
        if ($path = $this->URLtoPath($url)) {
184
            // little hack to make sure we do not include pages with live forms.
185
            if (stripos($response->getBody(), 'name="SecurityID"')) {
186
                return false;
187
            }
188
            if ($this->getFileExtension() === 'php') {
189
                $phpContent = $this->generatePHPCacheFile($response);
0 ignored issues
show
Bug introduced by
$response of type string is incompatible with the type SilverStripe\Control\HTTPResponse expected by parameter $response of SilverStripe\StaticPubli...:generatePHPCacheFile(). ( Ignorable by Annotation )

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

189
                $phpContent = $this->generatePHPCacheFile(/** @scrutinizer ignore-type */ $response);
Loading history...
190
                $success = $this->saveToPath($phpContent, $path . '.php');
191
            }
192
            return $this->saveToPath($response->getBody(), $path . '.html') && $success;
193
        }
194
        return false;
195
    }
196
197
    /**
198
     * returns true on access and false on failure
199
     * @param string $content
200
     * @param string $filePath
201
     * @return bool
202
     */
203
    protected function saveToPath(string $content, string $filePath): bool
204
    {
205
        if (empty($content)) {
206
            return false;
207
        }
208
209
        // Write to a temporary file first
210
        $temporaryPath = tempnam(TEMP_PATH, 'filesystempublisher_');
211
        if (file_put_contents($temporaryPath, $content) === false) {
212
            return false;
213
        }
214
215
        // Move the temporary file to the desired location (prevents unlocked files from being read during write)
216
        $publishPath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
217
        Filesystem::makeFolder(dirname($publishPath));
218
        $successWithPublish = rename($temporaryPath, $publishPath);
219
        if ($successWithPublish) {
220
            if (FilesystemPublisher::config()->get('use_gzip_compression')) {
221
                $publishPath = $this->compressFile($publishPath);
222
            }
223
        }
224
225
        return file_exists($publishPath);
226
    }
227
228
    protected function compressFile(string $publishPath): string
229
    {
230
        // read original
231
        $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

231
        $data = implode('', /** @scrutinizer ignore-type */ file($publishPath));
Loading history...
232
        // encode it with highest level
233
        $gzdata = gzencode($data, 9);
234
        // new file
235
        $publishPathGZipped = $publishPath . '.gz';
236
        // remove
237
        unlink($publishPathGZipped);
238
        // open a new one
239
        $fp = fopen($publishPathGZipped, 'w');
240
        // write it
241
        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

241
        fwrite(/** @scrutinizer ignore-type */ $fp, $gzdata);
Loading history...
242
        // close it
243
        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

243
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
244
        @chmod($file, 0664);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $file seems to be never defined.
Loading history...
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

244
        /** @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...
245
246
        return $publishPathGZipped;
247
    }
248
249
    protected function deleteFromPath(string $filePath) : bool
250
    {
251
        $deletePath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
252
        if (file_exists($deletePath)) {
253
            $success = unlink($deletePath);
254
        } else {
255
            $success = true;
256
        }
257
        if (file_exists($deletePath . '.gz')) {
258
            return $this->deleteFromPath($deletePath . '.gz');
259
        }
260
261
        return $success;
262
    }
263
264
    protected function URLtoPath(string $url): string
265
    {
266
        return URLtoPath($url, BASE_URL, FilesystemPublisher::config()->get('domain_based_caching'));
267
    }
268
269
    protected function pathToURL(string $path): string
270
    {
271
        return PathToURL($path, $this->getDestPath(), FilesystemPublisher::config()->get('domain_based_caching'));
272
    }
273
}
274