Passed
Push — master ( a96d2e...0ccacc )
by Nicolaas
03:59 queued 14s
created

DoesNotHaveCaching()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
3
namespace Sunnysideup\SimpleTemplateCaching\Extensions;
4
5
use Exception;
6
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Forms\CheckboxField;
12
use SilverStripe\Forms\FieldList;
13
use SilverStripe\Forms\GridField\GridField;
14
use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer;
15
use SilverStripe\Forms\NumericField;
16
use SilverStripe\Forms\ReadonlyField;
17
use SilverStripe\Core\Extension;
18
use SilverStripe\Forms\HeaderField;
19
use SilverStripe\ORM\DB;
20
use SilverStripe\ORM\FieldType\DBDatetime;
21
use SilverStripe\SiteConfig\SiteConfig;
22
use Sunnysideup\SimpleTemplateCaching\Model\ObjectsUpdated;
23
24
/**
25
 * Class \Sunnysideup\SimpleTemplateCaching\Extensions\SimpleTemplateCachingSiteConfigExtension.
26
 *
27
 * @property SiteConfig|SimpleTemplateCachingSiteConfigExtension $owner
28
 * @property bool $HasCaching
29
 * @property bool $HasPartialCaching
30
 * @property bool $HasResourceCaching
31
 * @property int $PublicCacheDurationInSeconds
32
 * @property bool $RecordCacheUpdates
33
 * @property string $CacheKeyLastEdited
34
 * @property string $ClassNameLastEdited
35
 */
36
class SimpleTemplateCachingSiteConfigExtension extends Extension
37
{
38
    private const MAX_OBJECTS_UPDATED = 1000;
39
40
    private static string $image_cache_directive = '
41
<IfModule mod_headers.c>
42
  <FilesMatch "^(?:_resources/themes|assets)/.*\.(jpg|jpeg|png|gif|webp|svg|avif)$">
43
    Header set Cache-Control "public, max-age=600"
44
  </FilesMatch>
45
</IfModule>
46
    ';
47
48
    private static string $css_and_js_cache_directive = '
49
<IfModule mod_headers.c>
50
  <FilesMatch "^_resources/themes/.*\.(js|css)$">
51
    Header set Cache-Control "public, max-age=600"
52
  </FilesMatch>
53
</IfModule>
54
    ';
55
56
    private static string $font_cache_directive = '
57
    <IfModule mod_headers.c>
58
      <FilesMatch "^_resources/themes/.*\.(woff|woff2|ttf|otf|eot)$">
59
        Header set Cache-Control "public, max-age=600"
60
      </FilesMatch>
61
    </IfModule>
62
    ';
63
64
    private static $db = [
65
        'HasCaching' => 'Boolean(1)',
66
        'HasPartialCaching' => 'Boolean(1)',
67
        'HasResourceCaching' => 'Boolean(1)',
68
        'PublicCacheDurationInSeconds' => 'Int',
69
        'RecordCacheUpdates' => 'Boolean(0)',
70
        'CacheKeyLastEdited' => 'DBDatetime',
71
        'ClassNameLastEdited' => 'Varchar(200)',
72
    ];
73
74
    public function updateCMSFields(FieldList $fields)
75
    {
76
        $owner = $this->getOwner();
77
        $name = '[none]';
78
        if (class_exists((string) $owner->ClassNameLastEdited)) {
79
            $name = Injector::inst()->get($owner->ClassNameLastEdited)->i18n_singular_name();
80
        }
81
82
        // page caching
83
        $fields->addFieldsToTab(
84
            'Root.Caching',
85
            [
86
                HeaderField::create('FullPageCachingHeader', 'Full Page Caching'),
87
                CheckboxField::create('HasCaching', 'Allow caching of entire pages?')
88
                    ->setDescription(
89
                        'You will also need to set up the cache time below for it to be enabled.
90
                        You can set a default time below, but you can also set the time for individual pages.'
91
                    ),
92
            ]
93
        );
94
        if ($owner->HasCaching) {
95
            $fields->addFieldsToTab(
96
                'Root.Caching',
97
                [
98
                    NumericField::create('PublicCacheDurationInSeconds', 'Cache time for ALL pages')
99
                        ->setDescription(
100
                            'USE WITH CARE - This will apply caching to ALL pages on the site.
101
                            Time is in seconds (e.g. 600 = 10 minutes).
102
                            Cache time on individual pages will override this value set here.
103
                            The total number of pages on the site with an individual caching time is: ' . Page::get()->filter('PublicCacheDurationInSeconds:GreaterThan', 0)->count()
104
                        ),
105
                ]
106
            );
107
        }
108
109
        //partial caching
110
        $fields->addFieldsToTab(
111
            'Root.Caching',
112
            [
113
                HeaderField::create('PartialCachingHeader', 'Partial Caching'),
114
                CheckboxField::create('HasPartialCaching', 'Allow partial template caching?')
115
                    ->setDescription(
116
                        'This should usually be turned on unless you want to make sure no templates are cached in any part at all.'
117
                    ),
118
            ]
119
        );
120
        if ($owner->HasPartialCaching) {
121
            $fields->addFieldsToTab(
122
                'Root.Caching',
123
                [
124
                    CheckboxField::create('RecordCacheUpdates', 'Keep a record of what is being changed?')
125
                        ->setDescription(
126
                            'To work out when the cache is being cleared,
127
                            you can keep a record of the last ' . self::MAX_OBJECTS_UPDATED . ' records changed.
128
                            Only turn this on temporarily for tuning purposes.'
129
                        ),
130
                ]
131
            );
132
            if ($this->getOwner()->RecordCacheUpdates) {
133
                $fields->addFieldsToTab(
134
                    'Root.Caching',
135
                    [
136
137
                        ReadonlyField::create('CacheKeyLastEditedNice', 'Last database change', $owner->dbObject('CacheKeyLastEdited')->ago())
138
                            ->setDescription(
139
                                'The frontend template cache will be invalidated every time this value changes.
140
                                                The value changes every time anything is changed in the database.'
141
                            ),
142
                        ReadonlyField::create('ClassNameLastEditedNice', 'Last record updated', $name)
143
                            ->setDescription('The last record to invalidate the cache.'),
144
145
                        GridField::create(
146
                            'ObjectsUpdated',
147
                            'Last ' . self::MAX_OBJECTS_UPDATED . ' records updated',
148
                            ObjectsUpdated::get()->limit(self::MAX_OBJECTS_UPDATED),
149
                            GridFieldConfig_RecordViewer::create()
150
                        )
151
                            ->setDescription(
152
                                '
153
                                This is a list of the last ' . self::MAX_OBJECTS_UPDATED . ' records updated.
154
                                It is used to track changes to the database.
155
                                It includes: ' . ObjectsUpdated::classes_edited()
156
                            ),
157
                    ]
158
                );
159
            }
160
        }
161
        // resource caching
162
        $fields->addFieldsToTab(
163
            'Root.Caching',
164
            [
165
                HeaderField::create('ResourceCachingHeader', 'Resource Caching'),
166
                CheckboxField::create('HasResourceCaching', 'Allow caching of resources (e.g. images, styles, etc.). ')
167
                    ->setDescription(
168
                        'This will add cache control headers to your .htaccess file for images, styles, and scripts.
169
                        This will help with performance, but once cached, a cache can not be cleared without changing the file name.'
170
                    ),
171
            ]
172
        );
173
    }
174
175
    public static function site_cache_key(): string
176
    {
177
        $obj = SiteConfig::current_site_config();
178
        if ($obj->HasPartialCaching) {
179
            return 'ts_' . strtotime((string) $obj->CacheKeyLastEdited);
180
        }
181
182
        return 'ts_' . time();
183
    }
184
185
    public static function update_cache_key(?string $className = '')
186
    {
187
        // important - avoid endless loop!
188
        if (SiteConfig::get()->exists()) {
189
            $howOldIsIt = DB::query('SELECT Created FROM SiteConfig LIMIT 1')->value();
190
            if ($howOldIsIt && strtotime((string) $howOldIsIt) > strtotime('-5 minutes')) {
191
                return;
192
            }
193
        } else {
194
            return;
195
        }
196
        try {
197
            $obj = SiteConfig::current_site_config();
198
            if ($obj->HasPartialCaching) {
199
                DB::query('
200
                    UPDATE "SiteConfig"
201
                    SET
202
                        "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\',
203
                        "ClassNameLastEdited" = \'' . addslashes((string) $className) . '\'
204
                    WHERE ID = ' . $obj->ID . '
205
                    LIMIT 1
206
                ;');
207
                if ($obj->RecordCacheUpdates) {
208
                    $recordId = Injector::inst()
209
                        ->create(ObjectsUpdated::class, ['ClassNameLastEdited' => $className])
210
                        ->write();
211
                    DB::query('DELETE FROM "ObjectsUpdated" WHERE "ID" < ' . (int) ($recordId - self::MAX_OBJECTS_UPDATED));
212
                }
213
            } else {
214
                DB::query('TRUNCATE "ObjectsUpdated";');
215
            }
216
        } catch (Exception $e) {
217
            DB::query('
218
                UPDATE "SiteConfig"
219
                SET
220
                    "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\',
221
                    "ClassNameLastEdited" = \'ERROR\'
222
                WHERE ID = ' . $obj->ID . '
223
                LIMIT 1
224
        ;');
225
        }
226
    }
227
228
    public function onBeforeWrite()
229
    {
230
        $this->requireDefaultRecords();
231
    }
232
233
    public function requireDefaultRecords()
234
    {
235
        $currentSiteConfig = SiteConfig::current_site_config();
236
        if ((int) SiteConfig::get()->count() > 100) {
237
            if ($currentSiteConfig) {
0 ignored issues
show
introduced by
$currentSiteConfig is of type SilverStripe\SiteConfig\SiteConfig, thus it always evaluated to true.
Loading history...
238
                DB::alteration_message('Deleting all SiteConfig records except for the current one.', 'deleted');
239
                DB::query('DELETE FROM "SiteConfig" WHERE ID <> ' . $currentSiteConfig->ID);
240
            }
241
        }
242
        foreach (
243
            [
244
                'IMAGE_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('image_cache_directive'),
245
                'CSS_JS_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('css_and_js_cache_directive'),
246
                'FONT_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('font_cache_directive'),
247
            ] as $key => $value
248
        ) {
249
            if (! $currentSiteConfig->HasResourceCaching) {
250
                $value = '';
251
            }
252
            $this->updateHtaccess($key, $value);
253
        }
254
    }
255
256
    public function DoesNotHaveCaching(): bool
257
    {
258
        $owner = $this->getOwner();
259
        return ! $owner->HasCaching;
260
    }
261
262
    protected function updateHtaccess(string $code, string $toAdd)
263
    {
264
        $htaccessPath = Controller::join_links(Director::publicFolder(), '.htaccess');
265
        $htaccessContent = file_get_contents($htaccessPath);
266
        $originalContent = $htaccessContent;
267
268
        // Define start and end comments
269
        $startComment = PHP_EOL . "# auto add start " . $code . PHP_EOL;
270
        $endComment = PHP_EOL . "# auto add end " . $code . PHP_EOL;
271
272
        // Full content to replace or add
273
        $toAddFull = $startComment . $toAdd . $endComment;
274
275
        // Check if the section already exists
276
        $pattern = "/" . preg_quote($startComment, '/') . ".*?" . preg_quote($endComment, '/') . "/s";
277
        if (preg_match($pattern, $htaccessContent)) {
278
            // Replace existing content between the start and end comments
279
            $htaccessContent = preg_replace($pattern, $toAddFull, $htaccessContent);
280
        } else {
281
            // Prepend the new content if not found
282
            $htaccessContent = $toAddFull . $htaccessContent;
283
        }
284
        if ($originalContent !== $htaccessContent) {
285
            // Save the updated .htaccess file
286
            DB::alteration_message('Updating .htaccess file with ' . $code . ' cache directive', 'created');
287
            file_put_contents($htaccessPath, $htaccessContent);
288
        }
289
    }
290
}
291