1<?php
2
3
4namespace ComboStrap;
5
6use ComboStrap\TagAttribute\StyleAttribute;
7
8/**
9 * Class SectionEdit
10 * @package ComboStrap
11 * Manage the edit button
12 * (ie add HTML comment that are parsed into forms
13 * for editor user)
14 */
15class EditButton
16{
17
18
19    const SEC_EDIT_PATTERN = "/" . self::ENTER_HTML_COMMENT . "\s*" . self::EDIT_BUTTON_PREFIX . "({.*?})\s*" . self::CLOSE_HTML_COMMENT . "/";
20    const EDIT_BUTTON_PREFIX = "EDIT";
21    const WIKI_ID = "wiki-id";
22
23    const FORM_ID = "hid"; // id to be dokuwiki conform
24    const EDIT_MESSAGE = "name"; // name to be dokuwiki conform
25
26    const CANONICAL = "edit-button";
27    const ENTER_HTML_COMMENT = "<!--";
28    const CLOSE_HTML_COMMENT = "-->";
29    const SNIPPET_ID = "edit-button";
30
31
32    /**
33     * The target drive the type of editor
34     * As of today, there is two type
35     * section and table
36     */
37    const TARGET_ATTRIBUTE_NAME = "target";
38    const TARGET_SECTION_VALUE = "section";
39    /**
40     * The table does not have an edit form at all
41     * It's created by {@link \Doku_Renderer_xhtml::table_close()}
42     * They are not printed by default via CSS. Edittable show them by default via Javascript
43     */
44    const TARGET_TABLE_VALUE = "table";
45    public const EDIT_SECTION_TARGET = 'section';
46    const RANGE = "range";
47    const DOKUWIKI_FORMAT = "dokuwiki";
48    const COMBO_FORMAT = "combo";
49    const TAG = "edit-button";
50    const CLASS_SUFFIX = "edit-button";
51
52
53    private $label;
54    /**
55     * @var string
56     */
57    private $wikiId;
58
59    /**
60     * Edit type
61     * @var string
62     * This is the default
63     */
64    private string $target = self::TARGET_SECTION_VALUE;
65    /**
66     * @var int
67     */
68    private int $startPosition;
69
70    private ?int $endPosition;
71    /**
72     * @var string $format - to conform or not to dokuwiki format
73     */
74    private string $format = self::COMBO_FORMAT;
75
76    /**
77     * the id of the heading, ie the id of the section
78     * Not really needed, just to be conform with Dokuwiki
79     * When the edit button is not for an outline may be null
80     */
81    private ?string $outlineHeadingId = null;
82    /**
83     * @var ?int $sectionid - sequence id of the section used only by dokuwiki
84     * When the edit button is not for an outline may be null
85     */
86    private ?int $outlineSectionId = null;
87
88
89    /**
90     * Section constructor.
91     */
92    public function __construct($label)
93    {
94        $this->label = $label;
95    }
96
97
98    public static function create($label): EditButton
99    {
100        return new EditButton($label);
101    }
102
103    public static function createFromCallStackArray($attributes): EditButton
104    {
105        $label = $attributes[\syntax_plugin_combo_edit::LABEL];
106        $startPosition = $attributes[\syntax_plugin_combo_edit::START_POSITION];
107        $endPosition = $attributes[\syntax_plugin_combo_edit::END_POSITION];
108        $wikiId = $attributes[TagAttributes::WIKI_ID];
109        $editButton = EditButton::create($label)
110            ->setStartPosition($startPosition)
111            ->setEndPosition($endPosition)
112            ->setWikiId($wikiId);
113        $headingId = $attributes[\syntax_plugin_combo_edit::HEADING_ID];
114        if ($headingId !== null) {
115            $editButton->setOutlineHeadingId($headingId);
116        }
117        $sectionId = $attributes[\syntax_plugin_combo_edit::SECTION_ID];
118        if ($sectionId !== null) {
119            $editButton->setOutlineSectionId($sectionId);
120        }
121        $format = $attributes[\syntax_plugin_combo_edit::FORMAT];
122        if ($format !== null) {
123            $editButton->setFormat($format);
124        }
125        return $editButton;
126
127
128    }
129
130    public static function deleteAll(string $html)
131    {
132        // Dokuwiki way is to delete
133        // but because they are comment, they are not shown
134        // We delete to serve clean page to search engine
135        return preg_replace(SEC_EDIT_PATTERN, '', $html);
136    }
137
138    public static function replaceOrDeleteAll(string $html_output)
139    {
140        try {
141            return EditButton::replaceAll($html_output);
142        } catch (ExceptionNotAuthorized|ExceptionBadState $e) {
143            return EditButton::deleteAll($html_output);
144        }
145    }
146
147    /**
148     * See {@link \Doku_Renderer_xhtml::finishSectionEdit()}
149     */
150    public function toTag(): string
151    {
152
153        /**
154         * The following data are mandatory from:
155         * {@link html_secedit_get_button}
156         */
157        $wikiId = $this->getWikiId();
158
159
160        /**
161         * We follow the order of Dokuwiki for compatibility purpose
162         */
163        $data[self::TARGET_ATTRIBUTE_NAME] = $this->target;
164
165        if ($this->format === self::COMBO_FORMAT) {
166            /**
167             * In the combo edit format, we had the dokuwiki id
168             * because the edit button may also be on the secondary slot
169             */
170            $data[self::WIKI_ID] = $wikiId;
171        }
172        $data[self::EDIT_MESSAGE] = $this->label;
173        if ($this->format === self::COMBO_FORMAT) {
174            /**
175             * In the combo edit format, we had the dokuwiki id as form id
176             * to make it unique on the whole page
177             * because the edit button may also be on the secondary slot
178             */
179            $slotPath = WikiPath::createMarkupPathFromId($wikiId);
180            $formId = ExecutionContext::getActualOrCreateFromEnv()
181                ->getIdManager()
182                ->generateNewHtmlIdForComponent(self::CANONICAL, $slotPath);
183            $data[self::FORM_ID] = $formId;
184
185
186        } else {
187            $data[self::FORM_ID] = $this->getHeadingId();
188            $data["codeblockOffset"] = 0; // what is that ?
189            $data["secid"] = $this->getSectionId();
190        }
191        $data[self::RANGE] = $this->getRange();
192
193
194        return self::EDIT_BUTTON_PREFIX . Html::encode(json_encode($data));
195    }
196
197    /**
198     *
199     * @throws ExceptionBadArgument - if the wiki id could not be found
200     * @throws ExceptionNotEnabled
201     */
202    public function toHtmlComment(): string
203    {
204        global $ACT;
205        if ($ACT === FetcherMarkup::MARKUP_DYNAMIC_EXECUTION_NAME) {
206            // ie weblog, they are generated via dynamic markup
207            // meaning that there is no button to edit the file
208            if (!PluginUtility::isTest()) {
209                return "";
210            }
211        }
212        /**
213         * We don't encode there is only internal information
214         * and this is easier to see / debug the output
215         */
216        return self::ENTER_HTML_COMMENT . " " . $this->toTag() . " " . self::CLOSE_HTML_COMMENT;
217    }
218
219    public function __toString()
220    {
221        return "Section Edit $this->label";
222    }
223
224
225    /**
226     * @throws ExceptionNotAuthorized - if the user cannot modify the page
227     * @throws ExceptionBadState - if the page is a revision page or the HTML is not the output of a page
228     */
229    public static function replaceAll($html)
230    {
231
232        if (!Identity::isWriter()) {
233            throw new ExceptionNotAuthorized("Page is not writable by the user");
234        }
235        /**
236         * Delete the edit comment
237         *   * if not writable
238         *   * or an old revision
239         * Original: {@link html_secedit()} {@link html_secedit_get_button()}
240         */
241        global $INFO;
242        if (isset($INFO)) {
243            // the page is a revision page
244            $rev = $INFO['rev'] ?? 0;
245            if ($rev !== 0) {
246                throw new ExceptionBadState("Internal Error: No edit button can be added to a revision page");
247            }
248        }
249
250
251        /**
252         * Request based because the button are added only for a user that can write
253         */
254        $snippetManager = PluginUtility::getSnippetManager();
255        $snippetManager->attachCssInternalStylesheet(self::SNIPPET_ID);
256        $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_ID);
257
258        /**
259         * The callback function on all edit comment
260         * @param $matches
261         * @return string
262         */
263        $editFormCallBack = function ($matches) {
264            $json = Html::decode($matches[1]);
265            $data = json_decode($json, true);
266
267            $target = $data[self::TARGET_ATTRIBUTE_NAME];
268
269            $message = $data[self::EDIT_MESSAGE];
270            unset($data[self::EDIT_MESSAGE]);
271            if ($message === null || trim($message) === "") {
272                $message = "Edit {$target}";
273            }
274
275            if ($data === NULL) {
276                LogUtility::internalError("No data found in the edit comment", self::CANONICAL);
277                return "";
278            }
279            $wikiId = $data[self::WIKI_ID] ?? null;
280            unset($data[self::WIKI_ID]);
281            if ($wikiId === null) {
282                try {
283                    $page = MarkupPath::createPageFromExecutingId();
284                } catch (ExceptionNotFound $e) {
285                    LogUtility::internalError("A page id is mandatory for a edit button (no wiki id, no global ID were found). No edit buttons was created then.", self::CANONICAL);
286                    return "";
287                }
288            } else {
289                $page = MarkupPath::createMarkupFromId($wikiId);
290            }
291            $formId = $data[self::FORM_ID];
292            unset($data[self::FORM_ID]);
293            $data["summary"] = $message;
294            try {
295                $data['rev'] = $page->getPathObject()->getRevisionOrDefault();
296            } catch (ExceptionNotFound $e) {
297                //LogUtility::internalError("The file ({$page->getPathObject()}) does not exist, we cannot set the last modified time on the edit buttons.", self::CANONICAL);
298            }
299            $hiddenInputs = "";
300            foreach ($data as $key => $val) {
301                $inputAttributes = TagAttributes::createEmpty()
302                    ->addOutputAttributeValue("name", $key)
303                    ->addOutputAttributeValue("value", $val)
304                    ->addOutputAttributeValue("type", "hidden");
305                $hiddenInputs .= $inputAttributes->toHtmlEmptyTag("input");
306            }
307            $url = $page->getUrl()
308                ->withoutRewrite()
309                ->toHtmlString();
310            $classPageEdit = StyleAttribute::addComboStrapSuffix(self::CLASS_SUFFIX);
311
312            /**
313             * Important Note: the first div and the public class is mandatory for the edittable plugin
314             * See {@link editbutton.js file}
315             */
316            $editTableClass = "editbutton_{$target}";
317            return <<<EOF
318<div class="$classPageEdit $editTableClass">
319    <form id="$formId" method="post" action="{$url}">
320    $hiddenInputs
321    <input name="do" type="hidden" value="edit"/>
322    <button type="submit" title="$message">
323    </button>
324    </form>
325</div>
326EOF;
327        };
328
329        /**
330         * The replacement
331         */
332        return preg_replace_callback(self::SEC_EDIT_PATTERN, $editFormCallBack, $html);
333    }
334
335
336    public function setWikiId(string $id): EditButton
337    {
338        $this->wikiId = $id;
339        return $this;
340    }
341
342    /**
343     * Page / Section edit
344     * (This is known as the target for dokuwiki)
345     * @param string $target
346     * @return $this
347     *
348     */
349    public function setTarget(string $target): EditButton
350    {
351        $this->target = $target;
352        return $this;
353    }
354
355    public function setStartPosition(int $startPosition): EditButton
356    {
357        $this->startPosition = $startPosition;
358        return $this;
359    }
360
361    public function setEndPosition(?int $endPosition): EditButton
362    {
363        $this->endPosition = $endPosition;
364        return $this;
365    }
366
367    /**
368     * @return string the file character position range of the section to edit
369     */
370    private function getRange(): string
371    {
372        $range = "";
373        if (isset($this->startPosition)) {
374            $range = $this->startPosition;
375        }
376        $range = "$range-";
377        if (isset($this->endPosition)) {
378            $range = "$range{$this->endPosition}";
379        }
380        return $range;
381
382    }
383
384    public function toComboCallComboFormat(): Call
385    {
386        return $this->toComboCall(self::COMBO_FORMAT);
387    }
388
389    public function toComboCall($format): Call
390    {
391        return Call::createComboCall(
392            \syntax_plugin_combo_edit::TAG,
393            DOKU_LEXER_SPECIAL,
394            [
395                \syntax_plugin_combo_edit::START_POSITION => $this->startPosition,
396                \syntax_plugin_combo_edit::END_POSITION => $this->endPosition,
397                \syntax_plugin_combo_edit::LABEL => $this->label,
398                \syntax_plugin_combo_edit::FORMAT => $format,
399                \syntax_plugin_combo_edit::HEADING_ID => $this->getHeadingId(),
400                \syntax_plugin_combo_edit::SECTION_ID => $this->getSectionId(),
401                TagAttributes::WIKI_ID => $this->getWikiId()
402            ]
403        );
404    }
405
406
407    /**
408     *
409     */
410    private function getWikiId(): string
411    {
412
413        $wikiId = $this->wikiId;
414        if ($wikiId !== null) {
415            return $wikiId;
416        }
417
418        return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId();
419
420
421    }
422
423
424    public function toComboCallDokuWikiForm(): Call
425    {
426        return $this->toComboCall(self::DOKUWIKI_FORMAT);
427    }
428
429    /** @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */
430    private function setFormat($format): EditButton
431    {
432
433        if (!in_array($format, [self::DOKUWIKI_FORMAT, self::COMBO_FORMAT])) {
434            LogUtility::internalError("The tag format ($format) is not valid", self::CANONICAL);
435            return $this;
436        }
437        $this->format = $format;
438        return $this;
439    }
440
441    public function setOutlineHeadingId($id): EditButton
442    {
443        $this->outlineHeadingId = $id;
444        return $this;
445    }
446
447    /**
448     * @return string|null
449     */
450    private function getHeadingId(): ?string
451    {
452        return $this->outlineHeadingId;
453    }
454
455    private function getSectionId(): ?int
456    {
457        return $this->outlineSectionId;
458    }
459
460    public function setOutlineSectionId(int $sectionSequenceId): EditButton
461    {
462        $this->outlineSectionId = $sectionSequenceId;
463        return $this;
464    }
465
466}
467