Passed
Push — master ( 407d85...7d4e8a )
by Nicolaas
04:17 queued 15s
created

SimpleTemplateCachingSiteConfigExtension   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 171
Duplicated Lines 0 %

Importance

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