Passed
Pull Request — master (#118)
by Nicolaas
01:41
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
     * @return array A result array
110
     */
111
    public function publishURL(string $url, ?bool $forcePublish = false): array
112
    {
113
        if (!$url) {
114
            user_error('Bad url:' . var_export($url, true), E_USER_WARNING);
115
            return [];
116
        }
117
        $success = false;
118
        $response = $this->generatePageResponse($url);
119
        $statusCode = $response->getStatusCode();
120
        $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...
121
122
        if ($statusCode < 300) {
123
            // publish success response
124
            $success = $this->publishPage($response, $url);
125
        } elseif ($statusCode < 400) {
126
            // publish redirect response
127
            $success = $this->publishRedirect($response, $url);
128
        } elseif ($doPublish) {
129
            // only publish error pages if we are able to send status codes via PHP
130
            $success = $this->publishPage($response, $url);
131
        }
132
        return [
133
            'published' => $doPublish,
134
            'success' => $success,
135
            'responsecode' => $statusCode,
136
            'url' => $url,
137
        ];
138
    }
139
140
    public function getPublishedURLs(?string $dir = null, array &$result = []): array
141
    {
142
        if ($dir === null) {
143
            $dir = $this->getDestPath();
144
        }
145
146
        $root = scandir($dir);
147
        foreach ($root as $fileOrDir) {
148
            if (strpos($fileOrDir, '.') === 0) {
149
                continue;
150
            }
151
            $fullPath = $dir . DIRECTORY_SEPARATOR . $fileOrDir;
152
            // we know html will always be generated, this prevents double ups
153
            if (is_file($fullPath) && pathinfo($fullPath, PATHINFO_EXTENSION) === 'html') {
154
                $result[] = $this->pathToURL($fullPath);
155
                continue;
156
            }
157
158
            if (is_dir($fullPath)) {
159
                $this->getPublishedURLs($fullPath, $result);
160
            }
161
        }
162
        return $result;
163
    }
164
165
    /**
166
     * @param HTTPResponse $response
167
     * @param string       $url
168
     * @return bool
169
     */
170
    protected function publishRedirect($response, string $url) : bool
171
    {
172
        $success = true;
173
        if ($path = $this->URLtoPath($url)) {
174
            $location = $response->getHeader('Location');
175
            if ($this->getFileExtension() === 'php') {
176
                $phpContent = $this->generatePHPCacheFile($response);
177
                $success = $this->saveToPath($phpContent, $path . '.php');
178
            }
179
            return $this->saveToPath($this->generateHTMLCacheRedirection($location), $path . '.html') && $success;
180
        }
181
        return false;
182
    }
183
184
    /**
185
     * @param HTTPResponse $response
186
     * @param string       $url
187
     * @return bool
188
     */
189
    protected function publishPage(string $response, string $url) : bool
190
    {
191
        $success = true;
192
        if ($path = $this->URLtoPath($url)) {
193
            if ($this->Config()->get('lazy_form_recognition')) {
194
                $id = Config::inst()->get(SecurityToken::class, 'default_name');
195
                // little hack to make sure we do not include pages with live forms.
196
                if (stripos($response->getBody(), '<input type="hidden" name="'.$id.'"')) {
197
                    return false;
198
                }
199
            }
200
            if ($this->getFileExtension() === 'php') {
201
                $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

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

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

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

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

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