xref: /plugin/renderrevisions/action/save.php (revision 6ba0f847dd4a9b7b89db62dac4d9f6bdae0b1998)
1*6ba0f847SSzymon Olewniczak<?php
2*6ba0f847SSzymon Olewniczak
3*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\ActionPlugin;
4*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\Event;
5*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\EventHandler;
6*6ba0f847SSzymon Olewniczakuse dokuwiki\File\PageFile;
7*6ba0f847SSzymon Olewniczak
8*6ba0f847SSzymon Olewniczak/**
9*6ba0f847SSzymon Olewniczak * DokuWiki Plugin renderrevisions (Action Component)
10*6ba0f847SSzymon Olewniczak *
11*6ba0f847SSzymon Olewniczak * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
12*6ba0f847SSzymon Olewniczak * @author Andreas Gohr <dokuwiki@cosmocode.de>
13*6ba0f847SSzymon Olewniczak */
14*6ba0f847SSzymon Olewniczakclass action_plugin_renderrevisions_save extends ActionPlugin
15*6ba0f847SSzymon Olewniczak{
16*6ba0f847SSzymon Olewniczak    /** @var array list of pages that are processed by the plugin */
17*6ba0f847SSzymon Olewniczak    protected $pages = [];
18*6ba0f847SSzymon Olewniczak
19*6ba0f847SSzymon Olewniczak    /** @var string|null  the current page being saved, used to overwrite the contentchanged check */
20*6ba0f847SSzymon Olewniczak    protected $current = null;
21*6ba0f847SSzymon Olewniczak
22*6ba0f847SSzymon Olewniczak    /** @inheritDoc */
23*6ba0f847SSzymon Olewniczak    public function register(EventHandler $controller)
24*6ba0f847SSzymon Olewniczak    {
25*6ba0f847SSzymon Olewniczak        $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handleParserCacheUse');
26*6ba0f847SSzymon Olewniczak
27*6ba0f847SSzymon Olewniczak        $controller->register_hook(
28*6ba0f847SSzymon Olewniczak            'RENDERER_CONTENT_POSTPROCESS',
29*6ba0f847SSzymon Olewniczak            'AFTER',
30*6ba0f847SSzymon Olewniczak            $this,
31*6ba0f847SSzymon Olewniczak            'handleRenderContent',
32*6ba0f847SSzymon Olewniczak            null,
33*6ba0f847SSzymon Olewniczak            PHP_INT_MAX // other plugins might want to change the content before we see it
34*6ba0f847SSzymon Olewniczak        );
35*6ba0f847SSzymon Olewniczak
36*6ba0f847SSzymon Olewniczak        $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handleCommonWikipageSave');
37*6ba0f847SSzymon Olewniczak    }
38*6ba0f847SSzymon Olewniczak
39*6ba0f847SSzymon Olewniczak    /**
40*6ba0f847SSzymon Olewniczak     * Event handler for PARSER_CACHE_USE
41*6ba0f847SSzymon Olewniczak     *
42*6ba0f847SSzymon Olewniczak     * @see https://www.dokuwiki.org/devel:event:PARSER_CACHE_USE
43*6ba0f847SSzymon Olewniczak     * @param Event $event Event object
44*6ba0f847SSzymon Olewniczak     * @param mixed $param optional parameter passed when event was registered
45*6ba0f847SSzymon Olewniczak     * @return void
46*6ba0f847SSzymon Olewniczak     */
47*6ba0f847SSzymon Olewniczak    public function handleParserCacheUse(Event $event, $param)
48*6ba0f847SSzymon Olewniczak    {
49*6ba0f847SSzymon Olewniczak        $cacheObject = $event->data;
50*6ba0f847SSzymon Olewniczak
51*6ba0f847SSzymon Olewniczak        if (!$cacheObject->page) return;
52*6ba0f847SSzymon Olewniczak        if ($cacheObject->mode !== 'xhtml') return;
53*6ba0f847SSzymon Olewniczak
54*6ba0f847SSzymon Olewniczak        // only process pages that match both the skip and match regex
55*6ba0f847SSzymon Olewniczak
56*6ba0f847SSzymon Olewniczak        $page = $cacheObject->page;
57*6ba0f847SSzymon Olewniczak        try {
58*6ba0f847SSzymon Olewniczak            [$skipRE, $matchRE] = $this->getRegexps();
59*6ba0f847SSzymon Olewniczak        } catch (\Exception $e) {
60*6ba0f847SSzymon Olewniczak            msg(hsc($e->getMessage()), -1);
61*6ba0f847SSzymon Olewniczak            return;
62*6ba0f847SSzymon Olewniczak        }
63*6ba0f847SSzymon Olewniczak        if (
64*6ba0f847SSzymon Olewniczak            ($skipRE && preg_match($skipRE, ":$page")) ||
65*6ba0f847SSzymon Olewniczak            ($matchRE && !preg_match($matchRE, ":$page"))
66*6ba0f847SSzymon Olewniczak        ) {
67*6ba0f847SSzymon Olewniczak            return;
68*6ba0f847SSzymon Olewniczak        }
69*6ba0f847SSzymon Olewniczak
70*6ba0f847SSzymon Olewniczak        // remember that this page was processed
71*6ba0f847SSzymon Olewniczak        // This is a somewhat ugly workaround for when text snippets are rendered within the same page.
72*6ba0f847SSzymon Olewniczak        // Those snippets will not have a page context set during cache use event and thus not be processed
73*6ba0f847SSzymon Olewniczak        // later on in the RENDERER_CONTENT_POSTPROCESS event
74*6ba0f847SSzymon Olewniczak        $this->pages[$page] = true;
75*6ba0f847SSzymon Olewniczak    }
76*6ba0f847SSzymon Olewniczak
77*6ba0f847SSzymon Olewniczak
78*6ba0f847SSzymon Olewniczak    /**
79*6ba0f847SSzymon Olewniczak     * Event handler for RENDERER_CONTENT_POSTPROCESS
80*6ba0f847SSzymon Olewniczak     *
81*6ba0f847SSzymon Olewniczak     * @see https://www.dokuwiki.org/devel:event:RENDERER_CONTENT_POSTPROCESS
82*6ba0f847SSzymon Olewniczak     * @param Event $event Event object
83*6ba0f847SSzymon Olewniczak     * @param mixed $param optional parameter passed when event was registered
84*6ba0f847SSzymon Olewniczak     * @return void
85*6ba0f847SSzymon Olewniczak     */
86*6ba0f847SSzymon Olewniczak    public function handleRenderContent(Event $event, $param)
87*6ba0f847SSzymon Olewniczak    {
88*6ba0f847SSzymon Olewniczak        [$format, $xhtml] = $event->data;
89*6ba0f847SSzymon Olewniczak        if ($format !== 'xhtml') return;
90*6ba0f847SSzymon Olewniczak
91*6ba0f847SSzymon Olewniczak        // thanks to the $this->pages property we might be able to skip some of those checks, but they don't hurt
92*6ba0f847SSzymon Olewniczak        global $ACT;
93*6ba0f847SSzymon Olewniczak        global $REV;
94*6ba0f847SSzymon Olewniczak        global $DATE_AT;
95*6ba0f847SSzymon Olewniczak        global $ID;
96*6ba0f847SSzymon Olewniczak        global $INFO;
97*6ba0f847SSzymon Olewniczak        if ($ACT !== 'show') return;
98*6ba0f847SSzymon Olewniczak        if ($REV) return;
99*6ba0f847SSzymon Olewniczak        if ($DATE_AT) return;
100*6ba0f847SSzymon Olewniczak        if (!$INFO['exists']) return;
101*6ba0f847SSzymon Olewniczak        if (!$ID) return;
102*6ba0f847SSzymon Olewniczak        if (!isset($this->pages[$ID])) return;
103*6ba0f847SSzymon Olewniczak
104*6ba0f847SSzymon Olewniczak        // all the above still does not ensure we skip sub renderings, so this is our last resort
105*6ba0f847SSzymon Olewniczak        if (count(array_filter(debug_backtrace(), fn($t) => $t['function'] === 'p_render')) > 1) return;
106*6ba0f847SSzymon Olewniczak
107*6ba0f847SSzymon Olewniczak        $md5cache = getCacheName($ID, '.renderrevision');
108*6ba0f847SSzymon Olewniczak
109*6ba0f847SSzymon Olewniczak        // no or outdated MD5 cache, create new one
110*6ba0f847SSzymon Olewniczak        // this means a new revision of the page has been created naturally
111*6ba0f847SSzymon Olewniczak        // we store the new render result and are done
112*6ba0f847SSzymon Olewniczak        if (!file_exists($md5cache) || filemtime(wikiFN($ID)) > filemtime($md5cache)) {
113*6ba0f847SSzymon Olewniczak            file_put_contents($md5cache, md5($xhtml));
114*6ba0f847SSzymon Olewniczak
115*6ba0f847SSzymon Olewniczak            if($this->getConf('store')) {
116*6ba0f847SSzymon Olewniczak                /** @var helper_plugin_renderrevisions_storage $storage */
117*6ba0f847SSzymon Olewniczak                $storage = plugin_load('helper', 'renderrevisions_storage');
118*6ba0f847SSzymon Olewniczak                $storage->saveRevision($ID, filemtime(wikiFN($ID)), $xhtml);
119*6ba0f847SSzymon Olewniczak                $storage->cleanUp($ID);
120*6ba0f847SSzymon Olewniczak            }
121*6ba0f847SSzymon Olewniczak
122*6ba0f847SSzymon Olewniczak            return;
123*6ba0f847SSzymon Olewniczak        }
124*6ba0f847SSzymon Olewniczak
125*6ba0f847SSzymon Olewniczak        // only act on pages that have not been changed very recently
126*6ba0f847SSzymon Olewniczak        if (time() - filemtime(wikiFN($ID)) < $this->getConf('maxfrequency')) {
127*6ba0f847SSzymon Olewniczak            return;
128*6ba0f847SSzymon Olewniczak        }
129*6ba0f847SSzymon Olewniczak
130*6ba0f847SSzymon Olewniczak        // get the render result as it were when the page was last changed
131*6ba0f847SSzymon Olewniczak        $oldMd5 = file_get_contents($md5cache);
132*6ba0f847SSzymon Olewniczak
133*6ba0f847SSzymon Olewniczak        // did the rendered content change?
134*6ba0f847SSzymon Olewniczak        if ($oldMd5 === md5($xhtml)) {
135*6ba0f847SSzymon Olewniczak            return;
136*6ba0f847SSzymon Olewniczak        }
137*6ba0f847SSzymon Olewniczak
138*6ba0f847SSzymon Olewniczak        // time to create a new revision
139*6ba0f847SSzymon Olewniczak        $this->current = $ID;
140*6ba0f847SSzymon Olewniczak        (new PageFile($ID))->saveWikiText(rawWiki($ID), $this->getLang('summary'));
141*6ba0f847SSzymon Olewniczak        $this->current = null;
142*6ba0f847SSzymon Olewniczak    }
143*6ba0f847SSzymon Olewniczak
144*6ba0f847SSzymon Olewniczak
145*6ba0f847SSzymon Olewniczak    /**
146*6ba0f847SSzymon Olewniczak     * Event handler for COMMON_WIKIPAGE_SAVE
147*6ba0f847SSzymon Olewniczak     *
148*6ba0f847SSzymon Olewniczak     * Overwrite the contentChanged flag to force a new revision even though the content did not change
149*6ba0f847SSzymon Olewniczak     *
150*6ba0f847SSzymon Olewniczak     * @see https://www.dokuwiki.org/devel:event:COMMON_WIKIPAGE_SAVE
151*6ba0f847SSzymon Olewniczak     * @param Event $event Event object
152*6ba0f847SSzymon Olewniczak     * @param mixed $param optional parameter passed when event was registered
153*6ba0f847SSzymon Olewniczak     * @return void
154*6ba0f847SSzymon Olewniczak     */
155*6ba0f847SSzymon Olewniczak    public function handleCommonWikipageSave(Event $event, $param)
156*6ba0f847SSzymon Olewniczak    {
157*6ba0f847SSzymon Olewniczak        if ($this->current !== $event->data['id']) return;
158*6ba0f847SSzymon Olewniczak        $event->data['contentChanged'] = true;
159*6ba0f847SSzymon Olewniczak    }
160*6ba0f847SSzymon Olewniczak
161*6ba0f847SSzymon Olewniczak
162*6ba0f847SSzymon Olewniczak    /**
163*6ba0f847SSzymon Olewniczak     * Read the skip and match regex from the config
164*6ba0f847SSzymon Olewniczak     *
165*6ba0f847SSzymon Olewniczak     * Ensures the regular expressions are valid
166*6ba0f847SSzymon Olewniczak     *
167*6ba0f847SSzymon Olewniczak     * @return string[] [$skipRE, $matchRE]
168*6ba0f847SSzymon Olewniczak     * @throws \Exception if the regular expressions are invalid
169*6ba0f847SSzymon Olewniczak     */
170*6ba0f847SSzymon Olewniczak    public function getRegexps()
171*6ba0f847SSzymon Olewniczak    {
172*6ba0f847SSzymon Olewniczak        $skip = $this->getConf('skipRegex');
173*6ba0f847SSzymon Olewniczak        $skipRE = '';
174*6ba0f847SSzymon Olewniczak        $match = $this->getConf('matchRegex');
175*6ba0f847SSzymon Olewniczak        $matchRE = '';
176*6ba0f847SSzymon Olewniczak
177*6ba0f847SSzymon Olewniczak        if ($skip) {
178*6ba0f847SSzymon Olewniczak            $skipRE = '/' . $skip . '/';
179*6ba0f847SSzymon Olewniczak            if (@preg_match($skipRE, '') === false) {
180*6ba0f847SSzymon Olewniczak                throw new \Exception('Invalid regular expression in $conf[\'skipRegex\']. ' . preg_last_error_msg());
181*6ba0f847SSzymon Olewniczak            }
182*6ba0f847SSzymon Olewniczak        }
183*6ba0f847SSzymon Olewniczak
184*6ba0f847SSzymon Olewniczak        if ($match) {
185*6ba0f847SSzymon Olewniczak            $matchRE = '/' . $match . '/';
186*6ba0f847SSzymon Olewniczak            if (@preg_match($matchRE, '') === false) {
187*6ba0f847SSzymon Olewniczak                throw new \Exception('Invalid regular expression in $conf[\'matchRegex\']. ' . preg_last_error_msg());
188*6ba0f847SSzymon Olewniczak            }
189*6ba0f847SSzymon Olewniczak        }
190*6ba0f847SSzymon Olewniczak        return [$skipRE, $matchRE];
191*6ba0f847SSzymon Olewniczak    }
192*6ba0f847SSzymon Olewniczak}
193