Passed
Push — master ( 7c2cc7...4ff029 )
by Nicolaas
04:00
created

updateHtaccess()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 11
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 24
rs 9.9
1
<?php
2
3
namespace Sunnysideup\SimpleTemplateCaching\Extensions;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Forms\CheckboxField;
9
use SilverStripe\Forms\FieldList;
10
use SilverStripe\Forms\GridField\GridField;
11
use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer;
12
use SilverStripe\Forms\NumericField;
13
use SilverStripe\Forms\ReadonlyField;
14
use SilverStripe\Core\Extension;
15
use SilverStripe\ORM\DB;
16
use SilverStripe\ORM\FieldType\DBDatetime;
17
use SilverStripe\SiteConfig\SiteConfig;
18
use Sunnysideup\SimpleTemplateCaching\Model\ObjectsUpdated;
19
20
/**
21
 * Class \Sunnysideup\SimpleTemplateCaching\Extensions\SimpleTemplateCachingSiteConfigExtension.
22
 *
23
 * @property SiteConfig|SimpleTemplateCachingSiteConfigExtension $owner
24
 * @property bool $HasCaching
25
 * @property int $PublicCacheDurationInSeconds
26
 * @property bool $RecordCacheUpdates
27
 * @property string $CacheKeyLastEdited
28
 * @property string $ClassNameLastEdited
29
 */
30
class SimpleTemplateCachingSiteConfigExtension extends Extension
31
{
32
    private const MAX_OBJECTS_UPDATED = 1000;
33
34
    private static $image_cache_duration_in_seconds = 3600;
35
    private static $image_cache_directive = '<IfModule mod_headers.c> <FilesMatch "\.(jpg|jpeg|png|gif|webp|svg)$"> Header set Cache-Control "public, max-age=[SECONDS]" </FilesMatch> </IfModule>';
36
37
    private static $css_and_js_cache_duration_in_seconds = 3600;
38
    private static $css_and_js_cache_directive = '<IfModule mod_headers.c> <FilesMatch "\.(js|css|)$"> Header set Cache-Control "public, max-age=[SECONDS]" </FilesMatch> </IfModule>';
39
40
    private static $db = [
41
        'HasCaching' => 'Boolean(1)',
42
        'PublicCacheDurationInSeconds' => 'Int',
43
        'RecordCacheUpdates' => 'Boolean(0)',
44
        'CacheKeyLastEdited' => 'DBDatetime',
45
        'ClassNameLastEdited' => 'Varchar(200)',
46
    ];
47
48
    public function updateCMSFields(FieldList $fields)
49
    {
50
        $name = '';
51
        if (class_exists((string) $this->getOwner()->ClassNameLastEdited)) {
52
            $name = Injector::inst()->get($this->getOwner()->ClassNameLastEdited)->i18n_singular_name();
53
        }
54
        $fields->addFieldsToTab(
55
            'Root.Caching',
56
            [
57
                CheckboxField::create('HasCaching', 'Use caching'),
58
                NumericField::create('PublicCacheDurationInSeconds', 'Cache time for ALL pages')
59
                    ->setDescription(
60
                        'USE WITH CARE - This will apply caching to ALL pages on the site. Time is in seconds (e.g. 600 = 10 minutes).'
61
                    ),
62
                CheckboxField::create('RecordCacheUpdates', 'Record every change?')
63
                    ->setDescription('To work out when the cache is being updated, you can track every change. This will slow down all your edits, so it is recommend only to turn this on temporarily - for tuning purposes.'),
64
                ReadonlyField::create('CacheKeyLastEdited', 'Content Last Edited')
65
                    ->setRightTitle('The frontend template cache will be invalidated every time this value changes. It changes every time anything is changed in the database.'),
66
                ReadonlyField::create('ClassNameLastEdited', 'Last class updated')
67
                    ->setRightTitle('Last object updated. The name of this object is: ' . $name),
68
            ]
69
        );
70
        if ($this->getOwner()->RecordCacheUpdates) {
71
            $fields->addFieldsToTab(
72
                'Root.Caching',
73
                [
74
                    GridField::create(
75
                        'ObjectsUpdated',
76
                        'Last ' . self::MAX_OBJECTS_UPDATED . ' objects updated',
77
                        ObjectsUpdated::get()->limit(self::MAX_OBJECTS_UPDATED),
78
                        GridFieldConfig_RecordViewer::create()
79
                    ),
80
                ]
81
            );
82
        }
83
    }
84
85
    public static function site_cache_key(): string
86
    {
87
        $obj = SiteConfig::current_site_config();
88
        if ($obj->HasCaching) {
89
            return 'ts_' . strtotime((string) $obj->CacheKeyLastEdited);
90
        }
91
92
        return 'ts_' . time();
93
    }
94
95
    public static function update_cache_key(?string $className = '')
96
    {
97
        // important - avoid endless loop!
98
        if (SiteConfig::get()->exists()) {
99
            $howOldIsIt = DB::query('SELECT Created FROM SiteConfig LIMIT 1')->value();
100
            if ($howOldIsIt && strtotime((string) $howOldIsIt) > strtotime('-5 minutes')) {
101
                return;
102
            }
103
        } else {
104
            return;
105
        }
106
        try {
107
            $obj = SiteConfig::current_site_config();
108
        } catch (\Exception $e) {
109
            $obj = null;
110
        }
111
        if ($obj && $obj->HasCaching) {
112
            DB::query('
113
                UPDATE "SiteConfig"
114
                SET
115
                    "CacheKeyLastEdited" = \'' . DBDatetime::now()->Rfc2822() . '\',
116
                    "ClassNameLastEdited" = \'' . addslashes((string) $className) . '\'
117
                WHERE ID = ' . $obj->ID . '
118
                LIMIT 1
119
            ;');
120
        }
121
        if ($obj && $obj->RecordCacheUpdates) {
122
            $recordId = Injector::inst()
123
                ->create(ObjectsUpdated::class, ['ClassNameLastEdited' => $className])
124
                ->write();
125
            DB::query('DELETE FROM ObjectsUpdated WHERE ID < ' . (int) ($recordId - self::MAX_OBJECTS_UPDATED));
126
        }
127
    }
128
129
    public function requireDefaultRecords()
130
    {
131
        if ((int) SiteConfig::get()->count() > 100) {
132
            $currentSiteConfig = SiteConfig::current_site_config();
133
            if ($currentSiteConfig) {
0 ignored issues
show
introduced by
$currentSiteConfig is of type SilverStripe\SiteConfig\SiteConfig, thus it always evaluated to true.
Loading history...
134
                DB::alteration_message('Deleting all SiteConfig records except for the current one.', 'deleted');
135
                DB::query('DELETE FROM "SiteConfig" WHERE ID <> ' . $currentSiteConfig->ID);
136
            }
137
        }
138
        $imageCacheDirective = $this->getOwner()->config()->get('image_cache_directive');
139
        $cssJsCacheDirective = $this->getOwner()->config()->get('css_and_js_cache_directive');
140
        $toDo = [
141
            'IMAGE_CACHE_DIRECTIVE' => $imageCacheDirective,
142
            'CSS_JS_CACHE_DIRECTIVE' => $cssJsCacheDirective,
143
        ];
144
        foreach ($toDo as $key => $value) {
145
            $this->updateHtaccess($value, $key);
146
        }
147
    }
148
149
    protected function updateHtaccess(string $toAdd, string $code)
150
    {
151
        $htaccessPath = Controller::join_links(Director::publicFolder(), '.htaccess');
152
        $htaccessContent = file_get_contents($htaccessPath);
153
154
        // Define start and end comments
155
        $startComment = PHP_EOL . "# auto add start " . $code . PHP_EOL;
156
        $endComment = PHP_EOL . "# auto add end " . $code . PHP_EOL;
157
158
        // Full content to replace or add
159
        $toAddFull = $startComment . $toAdd . $endComment;
160
161
        // Check if the section already exists
162
        $pattern = "/" . preg_quote($startComment, '/') . ".*?" . preg_quote($endComment, '/') . "/s";
163
        if (preg_match($pattern, $htaccessContent)) {
164
            // Replace existing content between the start and end comments
165
            $htaccessContent = preg_replace($pattern, $toAddFull, $htaccessContent);
166
        } else {
167
            // Append the new content at the end if the section is not found
168
            $htaccessContent = $toAddFull . $htaccessContent;
169
        }
170
171
        // Save the updated .htaccess file
172
        file_put_contents($htaccessPath, $htaccessContent);
173
    }
174
}
175