Passed
Pull Request — master (#118)
by Nicolaas
02:30 queued 26s
created

FilesystemPublisher::publishPage()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 7
b 0
f 0
nc 18
nop 2
dl 0
loc 23
rs 8.8333
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
            $body = '';
194
            if ($this->Config()->get('lazy_form_recognition')) {
195
                $id = Config::inst()->get(SecurityToken::class, 'default_name') ?? 'SecurityID';
196
                // little hack to make sure we do not include pages with live forms.
197
                $body = $response->getBody();
198
                if (stripos($body, '<input type="hidden" name="'.$id.'"')) {
199
                    return false;
200
                }
201
            }
202
            if ($this->getFileExtension() === 'php') {
203
                $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

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

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

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

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

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