1 | <?php |
||
2 | |||
3 | namespace Sunnysideup\SimpleTemplateCaching\Extensions; |
||
4 | |||
5 | use Exception; |
||
6 | use Page; |
||
0 ignored issues
–
show
|
|||
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 | * @property int $ResourceCachingTimeInSeconds |
||
36 | */ |
||
37 | class SimpleTemplateCachingSiteConfigExtension extends Extension |
||
38 | { |
||
39 | private const MAX_OBJECTS_UPDATED = 1000; |
||
40 | |||
41 | private static string $image_cache_directive = ' |
||
42 | <IfModule mod_headers.c> |
||
43 | <FilesMatch "^(?:_resources/themes|assets)/.*\.(jpg|jpeg|png|gif|webp|svg|avif)$"> |
||
44 | Header set Cache-Control "public, max-age=600" |
||
45 | </FilesMatch> |
||
46 | </IfModule> |
||
47 | '; |
||
48 | |||
49 | private static string $css_and_js_cache_directive = ' |
||
50 | <IfModule mod_headers.c> |
||
51 | <FilesMatch "^_resources/themes/.*\.(js|css)$"> |
||
52 | Header set Cache-Control "public, max-age=600" |
||
53 | </FilesMatch> |
||
54 | </IfModule> |
||
55 | '; |
||
56 | |||
57 | private static string $font_cache_directive = ' |
||
58 | <IfModule mod_headers.c> |
||
59 | <FilesMatch "^_resources/themes/.*\.(woff|woff2|ttf|otf|eot)$"> |
||
60 | Header set Cache-Control "public, max-age=600" |
||
61 | </FilesMatch> |
||
62 | </IfModule> |
||
63 | '; |
||
64 | |||
65 | private static $db = [ |
||
66 | 'HasCaching' => 'Boolean(1)', |
||
67 | 'HasPartialCaching' => 'Boolean(1)', |
||
68 | 'HasResourceCaching' => 'Boolean(1)', |
||
69 | 'PublicCacheDurationInSeconds' => 'Int', |
||
70 | 'RecordCacheUpdates' => 'Boolean(0)', |
||
71 | 'CacheKeyLastEdited' => 'DBDatetime', |
||
72 | 'ClassNameLastEdited' => 'Varchar(200)', |
||
73 | 'ResourceCachingTimeInSeconds' => 'Int', |
||
74 | ]; |
||
75 | |||
76 | public function updateCMSFields(FieldList $fields) |
||
77 | { |
||
78 | $owner = $this->getOwner(); |
||
79 | $name = '[none]'; |
||
80 | if (class_exists((string) $owner->ClassNameLastEdited)) { |
||
81 | $name = Injector::inst()->get($owner->ClassNameLastEdited)->i18n_singular_name(); |
||
82 | } |
||
83 | |||
84 | // page caching |
||
85 | $fields->addFieldsToTab( |
||
86 | 'Root.Caching', |
||
87 | [ |
||
88 | HeaderField::create('FullPageCachingHeader', 'Full Page Caching'), |
||
89 | CheckboxField::create('HasCaching', 'Allow caching of entire pages?') |
||
90 | ->setDescription( |
||
91 | 'You will also need to set up the cache time below for it to be enabled. |
||
92 | You can set a default time below, but you can also set the time for individual pages.' |
||
93 | ), |
||
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 | NumericField::create('ResourceCachingTimeInSeconds', 'Cache time for resources') |
||
174 | ->setDescription( |
||
175 | 'Time is in seconds (e.g. 600 = 10 minutes). |
||
176 | This will be used for all resources on the site (fonts, images, styles, and scripts).' |
||
177 | ), |
||
178 | ] |
||
179 | ); |
||
180 | } |
||
181 | |||
182 | public static function site_cache_key(): string |
||
183 | { |
||
184 | $obj = SiteConfig::current_site_config(); |
||
185 | if ($obj->HasPartialCaching) { |
||
186 | return 'ts_' . strtotime((string) $obj->CacheKeyLastEdited); |
||
187 | } |
||
188 | |||
189 | return 'ts_' . time(); |
||
190 | } |
||
191 | |||
192 | public static function update_cache_key(?string $className = '') |
||
193 | { |
||
194 | // important - avoid endless loop! |
||
195 | if (SiteConfig::get()->exists()) { |
||
196 | $howOldIsIt = DB::query('SELECT Created FROM SiteConfig LIMIT 1')->value(); |
||
197 | if ($howOldIsIt && strtotime((string) $howOldIsIt) > strtotime('-5 minutes')) { |
||
198 | return; |
||
199 | } |
||
200 | } else { |
||
201 | return; |
||
202 | } |
||
203 | $obj = null; |
||
0 ignored issues
–
show
|
|||
204 | try { |
||
205 | $obj = SiteConfig::current_site_config(); |
||
206 | if ($obj->HasPartialCaching) { |
||
207 | DB::query(' |
||
208 | UPDATE "SiteConfig" |
||
209 | SET |
||
210 | "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\', |
||
211 | "ClassNameLastEdited" = \'' . addslashes((string) $className) . '\' |
||
212 | WHERE ID = ' . $obj->ID . ' |
||
213 | LIMIT 1 |
||
214 | ;'); |
||
215 | if ($obj->RecordCacheUpdates) { |
||
216 | $recordId = Injector::inst() |
||
217 | ->create(ObjectsUpdated::class, ['ClassNameLastEdited' => $className]) |
||
218 | ->write(); |
||
219 | DB::query('DELETE FROM "ObjectsUpdated" WHERE "ID" < ' . (int) ($recordId - self::MAX_OBJECTS_UPDATED)); |
||
220 | } |
||
221 | } else { |
||
222 | DB::query('TRUNCATE "ObjectsUpdated";'); |
||
223 | } |
||
224 | } catch (Exception $e) { |
||
225 | if (isset($obj) && $obj && $obj->ID) { |
||
226 | DB::query(' |
||
227 | UPDATE "SiteConfig" |
||
228 | SET |
||
229 | "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\', |
||
230 | "ClassNameLastEdited" = \'ERROR\' |
||
231 | WHERE ID = ' . $obj->ID . ' |
||
232 | LIMIT 1 |
||
233 | ;'); |
||
234 | } |
||
235 | } |
||
236 | } |
||
237 | |||
238 | public function onAfterWrite() |
||
239 | { |
||
240 | $this->updateHtaccess(); |
||
241 | } |
||
242 | |||
243 | public function requireDefaultRecords() |
||
244 | { |
||
245 | $this->updateHtaccess(); |
||
246 | } |
||
247 | |||
248 | protected function updateHtaccess() |
||
249 | { |
||
250 | $owner = $this->getOwner(); |
||
251 | $currentSiteConfig = SiteConfig::current_site_config(); |
||
252 | if ((int) SiteConfig::get()->count() > 100) { |
||
253 | if ($currentSiteConfig) { |
||
0 ignored issues
–
show
|
|||
254 | DB::alteration_message('Deleting all SiteConfig records except for the current one.', 'deleted'); |
||
255 | DB::query('DELETE FROM "SiteConfig" WHERE ID <> ' . $currentSiteConfig->ID); |
||
256 | } |
||
257 | } |
||
258 | foreach ( |
||
259 | [ |
||
260 | 'IMAGE_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('image_cache_directive'), |
||
261 | 'CSS_JS_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('css_and_js_cache_directive'), |
||
262 | 'FONT_CACHE_DIRECTIVE' => $currentSiteConfig->config()->get('font_cache_directive'), |
||
263 | ] as $key => $value |
||
264 | ) { |
||
265 | if (! $currentSiteConfig->HasResourceCaching) { |
||
266 | $value = ''; |
||
267 | } |
||
268 | if ($owner->ResourceCachingTimeInSeconds) { |
||
269 | $value = str_replace('max-age=600', 'max-age=' . $owner->ResourceCachingTimeInSeconds, $value); |
||
270 | } |
||
271 | |||
272 | $this->updateHtaccessForOne($key, $value); |
||
273 | } |
||
274 | } |
||
275 | |||
276 | public function DoesNotHaveCaching(): bool |
||
277 | { |
||
278 | $owner = $this->getOwner(); |
||
279 | return ! $owner->HasCaching; |
||
280 | } |
||
281 | |||
282 | protected function updateHtaccessForOne(string $code, string $toAdd) |
||
283 | { |
||
284 | $htaccessPath = Controller::join_links(Director::publicFolder(), '.htaccess'); |
||
285 | $htaccessContent = file_get_contents($htaccessPath); |
||
286 | $originalContent = $htaccessContent; |
||
287 | |||
288 | // Define start and end comments |
||
289 | $startComment = PHP_EOL . "# auto add start " . $code . PHP_EOL; |
||
290 | $endComment = PHP_EOL . "# auto add end " . $code . PHP_EOL; |
||
291 | |||
292 | // Full content to replace or add |
||
293 | $toAddFull = $startComment . $toAdd . $endComment; |
||
294 | |||
295 | // Check if the section already exists |
||
296 | $pattern = "/" . preg_quote($startComment, '/') . ".*?" . preg_quote($endComment, '/') . "/s"; |
||
297 | if (preg_match($pattern, $htaccessContent)) { |
||
298 | // Replace existing content between the start and end comments |
||
299 | $htaccessContent = preg_replace($pattern, $toAddFull, $htaccessContent); |
||
300 | } else { |
||
301 | // Prepend the new content if not found |
||
302 | $htaccessContent = $toAddFull . $htaccessContent; |
||
303 | } |
||
304 | if ($originalContent !== $htaccessContent) { |
||
305 | // Save the updated .htaccess file |
||
306 | DB::alteration_message('Updating .htaccess file with ' . $code . ' cache directive', 'created'); |
||
307 | file_put_contents($htaccessPath, $htaccessContent); |
||
308 | } |
||
309 | } |
||
310 | } |
||
311 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths