xref: /plugin/combo/ComboStrap/EditButton.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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            if ($INFO['rev']) {
245                throw new ExceptionBadState("Internal Error: No edit button can be added to a revision page");
246            }
247        }
248
249
250        /**
251         * Request based because the button are added only for a user that can write
252         */
253        $snippetManager = PluginUtility::getSnippetManager();
254        $snippetManager->attachCssInternalStylesheet(self::SNIPPET_ID);
255        $snippetManager->attachJavascriptFromComponentId(self::SNIPPET_ID);
256
257        /**
258         * The callback function on all edit comment
259         * @param $matches
260         * @return string
261         */
262        $editFormCallBack = function ($matches) {
263            $json = Html::decode($matches[1]);
264            $data = json_decode($json, true);
265
266            $target = $data[self::TARGET_ATTRIBUTE_NAME];
267
268            $message = $data[self::EDIT_MESSAGE];
269            unset($data[self::EDIT_MESSAGE]);
270            if ($message === null || trim($message) === "") {
271                $message = "Edit {$target}";
272            }
273
274            if ($data === NULL) {
275                LogUtility::internalError("No data found in the edit comment", self::CANONICAL);
276                return "";
277            }
278            $wikiId = $data[self::WIKI_ID];
279            unset($data[self::WIKI_ID]);
280            if ($wikiId === null) {
281                try {
282                    $page = MarkupPath::createPageFromExecutingId();
283                } catch (ExceptionNotFound $e) {
284                    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);
285                    return "";
286                }
287            } else {
288                $page = MarkupPath::createMarkupFromId($wikiId);
289            }
290            $formId = $data[self::FORM_ID];
291            unset($data[self::FORM_ID]);
292            $data["summary"] = $message;
293            try {
294                $data['rev'] = $page->getPathObject()->getRevisionOrDefault();
295            } catch (ExceptionNotFound $e) {
296                //LogUtility::internalError("The file ({$page->getPathObject()}) does not exist, we cannot set the last modified time on the edit buttons.", self::CANONICAL);
297            }
298            $hiddenInputs = "";
299            foreach ($data as $key => $val) {
300                $inputAttributes = TagAttributes::createEmpty()
301                    ->addOutputAttributeValue("name", $key)
302                    ->addOutputAttributeValue("value", $val)
303                    ->addOutputAttributeValue("type", "hidden");
304                $hiddenInputs .= $inputAttributes->toHtmlEmptyTag("input");
305            }
306            $url = $page->getUrl()
307                ->withoutRewrite()
308                ->toHtmlString();
309            $classPageEdit = StyleAttribute::addComboStrapSuffix(self::CLASS_SUFFIX);
310
311            /**
312             * Important Note: the first div and the public class is mandatory for the edittable plugin
313             * See {@link editbutton.js file}
314             */
315            $editTableClass = "editbutton_{$target}";
316            return <<<EOF
317<div class="$classPageEdit $editTableClass">
318    <form id="$formId" method="post" action="{$url}">
319    $hiddenInputs
320    <input name="do" type="hidden" value="edit"/>
321    <button type="submit" title="$message">
322    </button>
323    </form>
324</div>
325EOF;
326        };
327
328        /**
329         * The replacement
330         */
331        return preg_replace_callback(self::SEC_EDIT_PATTERN, $editFormCallBack, $html);
332    }
333
334
335    public function setWikiId(string $id): EditButton
336    {
337        $this->wikiId = $id;
338        return $this;
339    }
340
341    /**
342     * Page / Section edit
343     * (This is known as the target for dokuwiki)
344     * @param string $target
345     * @return $this
346     *
347     */
348    public function setTarget(string $target): EditButton
349    {
350        $this->target = $target;
351        return $this;
352    }
353
354    public function setStartPosition(int $startPosition): EditButton
355    {
356        $this->startPosition = $startPosition;
357        return $this;
358    }
359
360    public function setEndPosition(?int $endPosition): EditButton
361    {
362        $this->endPosition = $endPosition;
363        return $this;
364    }
365
366    /**
367     * @return string the file character position range of the section to edit
368     */
369    private function getRange(): string
370    {
371        $range = "";
372        if (isset($this->startPosition)) {
373            $range = $this->startPosition;
374        }
375        $range = "$range-";
376        if (isset($this->endPosition)) {
377            $range = "$range{$this->endPosition}";
378        }
379        return $range;
380
381    }
382
383    public function toComboCallComboFormat(): Call
384    {
385        return $this->toComboCall(self::COMBO_FORMAT);
386    }
387
388    public function toComboCall($format): Call
389    {
390        return Call::createComboCall(
391            \syntax_plugin_combo_edit::TAG,
392            DOKU_LEXER_SPECIAL,
393            [
394                \syntax_plugin_combo_edit::START_POSITION => $this->startPosition,
395                \syntax_plugin_combo_edit::END_POSITION => $this->endPosition,
396                \syntax_plugin_combo_edit::LABEL => $this->label,
397                \syntax_plugin_combo_edit::FORMAT => $format,
398                \syntax_plugin_combo_edit::HEADING_ID => $this->getHeadingId(),
399                \syntax_plugin_combo_edit::SECTION_ID => $this->getSectionId(),
400                TagAttributes::WIKI_ID => $this->getWikiId()
401            ]
402        );
403    }
404
405
406    /**
407     *
408     */
409    private function getWikiId(): string
410    {
411
412        $wikiId = $this->wikiId;
413        if ($wikiId !== null) {
414            return $wikiId;
415        }
416
417        return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId();
418
419
420    }
421
422
423    public function toComboCallDokuWikiForm(): Call
424    {
425        return $this->toComboCall(self::DOKUWIKI_FORMAT);
426    }
427
428    /** @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */
429    private function setFormat($format): EditButton
430    {
431
432        if (!in_array($format, [self::DOKUWIKI_FORMAT, self::COMBO_FORMAT])) {
433            LogUtility::internalError("The tag format ($format) is not valid", self::CANONICAL);
434            return $this;
435        }
436        $this->format = $format;
437        return $this;
438    }
439
440    public function setOutlineHeadingId($id): EditButton
441    {
442        $this->outlineHeadingId = $id;
443        return $this;
444    }
445
446    /**
447     * @return string|null
448     */
449    private function getHeadingId(): ?string
450    {
451        return $this->outlineHeadingId;
452    }
453
454    private function getSectionId(): ?int
455    {
456        return $this->outlineSectionId;
457    }
458
459    public function setOutlineSectionId(int $sectionSequenceId): EditButton
460    {
461        $this->outlineSectionId = $sectionSequenceId;
462        return $this;
463    }
464
465}
466