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

FilesystemPublisher::compressFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 19
rs 9.9666
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(string $url): array
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(): bool
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): array
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
    public function getPublishedURLs(?string $dir = null, array &$result = []): array
142
    {
143
        if ($dir === null) {
144
            $dir = $this->getDestPath();
145
        }
146
147
        $root = scandir($dir);
148
        foreach ($root as $fileOrDir) {
149
            if (strpos($fileOrDir, '.') === 0) {
150
                continue;
151
            }
152
            $fullPath = $dir . DIRECTORY_SEPARATOR . $fileOrDir;
153
            // we know html will always be generated, this prevents double ups
154
            if (is_file($fullPath) && pathinfo($fullPath, PATHINFO_EXTENSION) === 'html') {
155
                $result[] = $this->pathToURL($fullPath);
156
                continue;
157
            }
158
159
            if (is_dir($fullPath)) {
160
                $this->getPublishedURLs($fullPath, $result);
161
            }
162
        }
163
        return $result;
164
    }
165
166
    /**
167
     * @param HTTPResponse $response
168
     * @param string       $url
169
     * @return bool
170
     */
171
    protected function publishRedirect($response, string $url) : bool
172
    {
173
        $success = true;
174
        if ($path = $this->URLtoPath($url)) {
175
            $location = $response->getHeader('Location');
176
            if ($this->getFileExtension() === 'php') {
177
                $phpContent = $this->generatePHPCacheFile($response);
178
                $success = $this->saveToPath($phpContent, $path . '.php');
179
            }
180
            return $this->saveToPath($this->generateHTMLCacheRedirection($location), $path . '.html') && $success;
181
        }
182
        return false;
183
    }
184
185
    /**
186
     * @param HTTPResponse $response
187
     * @param string       $url
188
     * @return bool
189
     */
190
    protected function publishPage(string $response, string $url) : bool
191
    {
192
        $success = true;
193
        if ($path = $this->URLtoPath($url)) {
194
            $body = '';
195
            if ($this->Config()->get('lazy_form_recognition')) {
196
                $id = Config::inst()->get(SecurityToken::class, 'default_name') ?? 'SecurityID';
197
                // little hack to make sure we do not include pages with live forms.
198
                $body = $response->getBody();
199
                if (stripos($body, '<input type="hidden" name="' . $id . '"')) {
200
                    return false;
201
                }
202
            }
203
            if ($this->getFileExtension() === 'php') {
204
                $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

204
                $phpContent = $this->generatePHPCacheFile(/** @scrutinizer ignore-type */ $response);
Loading history...
205
                $success = $this->saveToPath($phpContent, $path . '.php');
206
            }
207
            if (! $body) {
208
                $body = $response->getBody();
209
            }
210
            return $this->saveToPath($body, $path . '.html') && $success;
211
        }
212
        return false;
213
    }
214
215
    /**
216
     * returns true on access and false on failure
217
     * @param string $content
218
     * @param string $filePath
219
     * @return bool
220
     */
221
    protected function saveToPath(string $content, string $filePath): bool
222
    {
223
        if (empty($content)) {
224
            return false;
225
        }
226
227
        // Write to a temporary file first
228
        $temporaryPath = tempnam(TEMP_PATH, 'filesystempublisher_');
229
        if (file_put_contents($temporaryPath, $content) === false) {
230
            return false;
231
        }
232
233
        // Move the temporary file to the desired location (prevents unlocked files from being read during write)
234
        $publishPath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
235
        Filesystem::makeFolder(dirname($publishPath));
236
        $successWithPublish = rename($temporaryPath, $publishPath);
237
        if ($successWithPublish) {
238
            if (FilesystemPublisher::config()->get('use_gzip_compression')) {
239
                $publishPath = $this->compressFile($publishPath);
240
            }
241
        }
242
243
        return file_exists($publishPath);
244
    }
245
246
    protected function compressFile(string $publishPath): string
247
    {
248
        // read original
249
        $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

249
        $data = implode('', /** @scrutinizer ignore-type */ file($publishPath));
Loading history...
250
        // encode it with highest level
251
        $gzdata = gzencode($data, 9);
252
        // new file
253
        $publishPathGZipped = $publishPath . '.gz';
254
        // remove
255
        unlink($publishPathGZipped);
256
        // open a new one
257
        $fp = fopen($publishPathGZipped, 'w');
258
        // write it
259
        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

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

261
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
262
        @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

262
        /** @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...
263
264
        return $publishPathGZipped;
265
    }
266
267
    protected function deleteFromPath(string $filePath) : bool
268
    {
269
        $deletePath = $this->getDestPath() . DIRECTORY_SEPARATOR . $filePath;
270
        if (file_exists($deletePath)) {
271
            $success = unlink($deletePath);
272
        } else {
273
            $success = true;
274
        }
275
        if (file_exists($deletePath . '.gz')) {
276
            return $this->deleteFromPath($deletePath . '.gz');
277
        }
278
279
        return $success;
280
    }
281
282
    protected function URLtoPath(string $url): string
283
    {
284
        return URLtoPath($url, BASE_URL, FilesystemPublisher::config()->get('domain_based_caching'));
285
    }
286
287
    protected function pathToURL(string $path): string
288
    {
289
        return PathToURL($path, $this->getDestPath(), FilesystemPublisher::config()->get('domain_based_caching'));
290
    }
291
}
292