1<?php
2
3
4use ComboStrap\ExceptionBadArgument;
5use ComboStrap\ExceptionNotFound;
6use ComboStrap\ExceptionSqliteNotAvailable;
7use ComboStrap\ExecutionContext;
8use ComboStrap\FetcherMarkup;
9use ComboStrap\FragmentTag;
10use ComboStrap\MarkupCacheDependencies;
11use ComboStrap\CacheManager;
12use ComboStrap\Call;
13use ComboStrap\CallStack;
14use ComboStrap\MarkupDynamicRender;
15use ComboStrap\ExceptionCompile;
16use ComboStrap\LogUtility;
17use ComboStrap\MarkupPath;
18use ComboStrap\PageImageTag;
19use ComboStrap\PagePath;
20use ComboStrap\PageSql;
21use ComboStrap\PageSqlTreeListener;
22use ComboStrap\PluginUtility;
23use ComboStrap\Sqlite;
24use ComboStrap\TagAttributes;
25use ComboStrap\WikiPath;
26use ComboStrap\XmlTagProcessing;
27
28require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
29
30
31/**
32 *
33 * An iterator to iterate over templates.
34 *
35 * *******************
36 * Iteration driver
37 * *******************
38 * The end tag of the template node is driving the iteration.
39 * This way, the tags just after the template
40 * sees them in the {@link CallStack} and can change their context
41 *
42 * For instance, a {@link syntax_plugin_combo_masonry}
43 * component will change the context of all card inside it.
44 *
45 * ********************
46 * Header and footer delimitation
47 * ********************
48 * The iterator delimits also the header and footer.
49 * Some component needs the header to be generate completely.
50 * This is the case of a complex markup such as a table
51 *
52 * ******************************
53 * Delete if no data
54 * ******************************
55 * It gives also the possibility to {@link syntax_plugin_combo_iterator::EMPTY_ROWS_COUNT_ATTRIBUTE
56 * delete the whole block}
57 * (header and footer also) if there is no data
58 *
59 * *****************************
60 * Always Contextual
61 * *****************************
62 * We don't capture the text markup such as in a {@link syntax_plugin_combo_code}
63 * in order to loop because you can't pass the actual handler (ie callstack)
64 * when you {@link p_get_instructions() parse again} a markup.
65 *
66 * The markup is then seen as a new single page without any context.
67 * That may lead to problems.
68 * Example: `heading` may then think that they are `outline heading` ...
69 *
70 */
71class syntax_plugin_combo_iterator extends DokuWiki_Syntax_Plugin
72{
73
74    /**
75     * Tag in Dokuwiki cannot have a `-`
76     * This is the last part of the class
77     */
78    const TAG = "iterator";
79
80    /**
81     * Page canonical and tag pattern
82     */
83    const CANONICAL = "iterator";
84    const PAGE_SQL = "page-sql";
85    const PAGE_SQL_ATTRIBUTES = "page-sql-attributes";
86    const COMPLEX_MARKUP_FOUND = "complex-markup-found";
87    const BEFORE_TEMPLATE_CALLSTACK = "header-callstack";
88    const AFTER_TEMPLATE_CALLSTACK = "footer-callstack";
89    const TEMPLATE_CALLSTACK = "template-callstack";
90
91
92    /**
93     * @param TagAttributes $tagAttributes
94     * @return WikiPath the context path for element that are in a fragment
95     */
96    public static function getContextPathForComponentThatMayBeInFragment(TagAttributes $tagAttributes): WikiPath
97    {
98        $pathString = $tagAttributes->getComponentAttributeValueAndRemoveIfPresent(PagePath::PROPERTY_NAME);
99        if ($pathString != null) {
100            try {
101                return WikiPath::createMarkupPathFromPath($pathString);
102            } catch (ExceptionBadArgument $e) {
103                LogUtility::warning("Error while creating the path for the page image with the path value ($pathString)", PageImageTag::CANONICAL, $e);
104            }
105        }
106
107        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
108
109        try {
110            $markupHandler = $executionContext->getExecutingMarkupHandler();
111            $contextData = $markupHandler
112                ->getContextData();
113            $path = $contextData[PagePath::PROPERTY_NAME];
114            if ($path !== null) {
115                try {
116                    return WikiPath::createMarkupPathFromPath($path);
117                } catch (ExceptionBadArgument $e) {
118                    LogUtility::internalError("The path string should be absolute, we should not get this error", PageImageTag::CANONICAL, $e);
119                }
120            }
121            return $markupHandler->getRequestedContextPath();
122        } catch (ExceptionNotFound $e) {
123            // no markup handler
124        }
125        return $executionContext->getContextPath();
126
127    }
128
129
130    /**
131     * Syntax Type.
132     *
133     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
134     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
135     * @see DokuWiki_Syntax_Plugin::getType()
136     */
137    function getType(): string
138    {
139        return 'container';
140    }
141
142    /**
143     * How Dokuwiki will add P element
144     *
145     * * 'normal' - Inline
146     *  * 'block' - Block (p are not created inside)
147     *  * 'stack' - Block (p can be created inside)
148     *
149     * @see DokuWiki_Syntax_Plugin::getPType()
150     * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype
151     */
152    function getPType(): string
153    {
154        return 'block';
155    }
156
157    /**
158     * @return array
159     * Allow which kind of plugin inside
160     *
161     * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
162     * because we manage self the content and we call self the parser
163     *
164     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
165     */
166    function getAllowedTypes(): array
167    {
168        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
169    }
170
171    function getSort(): int
172    {
173        return 201;
174    }
175
176    public function accepts($mode): bool
177    {
178        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
179    }
180
181
182    function connectTo($mode)
183    {
184
185
186        $pattern = XmlTagProcessing::getContainerTagPattern(self::TAG);
187        $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
188
189
190    }
191
192
193    public function postConnect()
194    {
195
196        $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
197
198
199    }
200
201
202    /**
203     *
204     * The handle function goal is to parse the matched syntax through the pattern function
205     * and to return the result for use in the renderer
206     * This result is always cached until the page is modified.
207     * @param string $match
208     * @param int $state
209     * @param int $pos - byte position in the original source file
210     * @param Doku_Handler $handler
211     * @return array
212     * @see DokuWiki_Syntax_Plugin::handle()
213     *
214     */
215    function handle($match, $state, $pos, Doku_Handler $handler): array
216    {
217
218        switch ($state) {
219
220            case DOKU_LEXER_ENTER :
221
222                $tagAttributes = TagAttributes::createFromTagMatch($match);
223                $callStackArray = $tagAttributes->toCallStackArray();
224                return array(
225                    PluginUtility::STATE => $state,
226                    PluginUtility::ATTRIBUTES => $callStackArray
227                );
228
229            case DOKU_LEXER_UNMATCHED :
230
231                // We should not ever come here but a user does not not known that
232                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
233
234
235            case DOKU_LEXER_EXIT :
236
237                $callStack = CallStack::createFromHandler($handler);
238                $openTag = $callStack->moveToPreviousCorrespondingOpeningCall();
239                /**
240                 * Scanning the callstack and extracting the information
241                 * such as sql and template instructions
242                 */
243                $pageSql = null;
244                $pageSqlAttribute = [];
245                $beforeTemplateCallStack = [];
246                $templateStack = [];
247                $afterTemplateCallStack = [];
248                $parsingState = "before";
249                $complexMarkupFound = false;
250                while ($actualCall = $callStack->next()) {
251                    $tagName = $actualCall->getTagName();
252
253                    if ($tagName === syntax_plugin_combo_edit::TAG) {
254                        /**
255                         * Not capturing the edit button because the markup is generated
256                         */
257                        continue;
258                    }
259
260                    switch ($tagName) {
261                        case syntax_plugin_combo_iteratordata::TAG:
262                            switch ($actualCall->getState()) {
263                                case DOKU_LEXER_UNMATCHED:
264                                    $pageSql = $actualCall->getCapturedContent();
265                                    break;
266                                case DOKU_LEXER_ENTER:
267                                    $pageSqlAttribute = $actualCall->getAttributes();
268                                    break;
269                            }
270                            continue 2;
271                        case FragmentTag::FRAGMENT_TAG:
272                            $parsingState = "after";
273                            if ($actualCall->getState() === DOKU_LEXER_EXIT) {
274                                $templateStack = $actualCall->getPluginData(FragmentTag::CALLSTACK);
275                                /**
276                                 * Do we have markup where the instructions should be generated at once
277                                 * and not line by line
278                                 *
279                                 * ie a list or a table
280                                 */
281                                foreach ($templateStack as $templateInstructions) {
282                                    $templateCall = Call::createFromInstruction($templateInstructions);
283                                    if (in_array($templateCall->getComponentName(), Call::BLOCK_MARKUP_DOKUWIKI_COMPONENTS)) {
284                                        $complexMarkupFound = true;
285                                    }
286
287                                }
288                            }
289                            continue 2;
290                        default:
291                            if ($parsingState === "before") {
292                                $beforeTemplateCallStack[] = $actualCall->toCallArray();
293                            } else {
294                                $afterTemplateCallStack[] = $actualCall->toCallArray();
295                            };
296                            break;
297                    }
298                }
299
300                /**
301                 * Wipe the content of iterator
302                 */
303                $callStack->deleteAllCallsAfter($openTag);
304
305                /**
306                 * Enter Tag is the driver tag
307                 * (To be able to add class by third party component)
308                 */
309                $openTag->setPluginData(self::PAGE_SQL, $pageSql);
310                $openTag->setPluginData(self::PAGE_SQL_ATTRIBUTES, $pageSqlAttribute);
311                $openTag->setPluginData(self::COMPLEX_MARKUP_FOUND, $complexMarkupFound);
312                $openTag->setPluginData(self::BEFORE_TEMPLATE_CALLSTACK, $beforeTemplateCallStack);
313                $openTag->setPluginData(self::AFTER_TEMPLATE_CALLSTACK, $afterTemplateCallStack);
314                $openTag->setPluginData(self::TEMPLATE_CALLSTACK, $templateStack);
315
316                return array(
317                    PluginUtility::STATE => $state
318                );
319
320        }
321        return array();
322
323    }
324
325    /**
326     * Render the output
327     * @param string $format
328     * @param Doku_Renderer $renderer
329     * @param array $data - what the function handle() return'ed
330     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
331     * @see DokuWiki_Syntax_Plugin::render()
332     *
333     *
334     */
335    function render($format, Doku_Renderer $renderer, $data): bool
336    {
337        if ($format === "xhtml") {
338            $state = $data[PluginUtility::STATE];
339            switch ($state) {
340                case DOKU_LEXER_EXIT:
341                    return true;
342                case DOKU_LEXER_UNMATCHED:
343                    $renderer->doc .= PluginUtility::renderUnmatched($data);
344                    return true;
345                case DOKU_LEXER_ENTER:
346
347                    $pageSql = $data[self::PAGE_SQL];
348
349                    /**
350                     * Data Processing
351                     */
352                    if ($pageSql === null) {
353                        $renderer->doc .= "A data node could not be found as a child of the iterator.";
354                        return false;
355                    }
356                    if (empty($pageSql)) {
357                        $renderer->doc .= "The data node definition needs a logical sql content";
358                        return false;
359                    }
360
361                    /**
362                     * Sqlite available ?
363                     */
364                    try {
365                        $sqlite = Sqlite::createOrGetSqlite();
366                    } catch (ExceptionSqliteNotAvailable $e) {
367                        $renderer->doc .= "The iterator component needs Sqlite to be able to work";
368                        return false;
369                    }
370
371                    $executionContext = ExecutionContext::getActualOrCreateFromEnv();
372
373                    /**
374                     * Create the SQL
375                     */
376                    try {
377                        $tagAttributes = TagAttributes::createFromCallStackArray($data[self::PAGE_SQL_ATTRIBUTES]);
378                        $path = $tagAttributes->getValue(PagePath::PROPERTY_NAME);
379                        if ($path !== null) {
380                            $contextualPage = MarkupPath::createPageFromAbsoluteId($path);
381                        } else {
382                            $contextualPage = MarkupPath::createPageFromPathObject($executionContext->getContextPath());
383                        }
384                        $pageSql = PageSql::create($pageSql, $contextualPage);
385                    } catch (Exception $e) {
386                        $renderer->doc .= "The page sql is not valid. Error Message: {$e->getMessage()}. Page Sql: ($pageSql)";
387                        return false;
388                    }
389
390                    $table = $pageSql->getTable();
391
392                    try {
393                        $cacheDependencies = $executionContext
394                            ->getExecutingMarkupHandler()
395                            ->getOutputCacheDependencies();
396
397                        switch ($table) {
398                            case PageSqlTreeListener::BACKLINKS:
399                                $cacheDependencies->addDependency(MarkupCacheDependencies::BACKLINKS_DEPENDENCY);
400                                // The requested page dependency could be determined by the backlinks dependency
401                                $cacheDependencies->addDependency(MarkupCacheDependencies::REQUESTED_PAGE_DEPENDENCY);
402                                break;
403                            case PageSqlTreeListener::DESCENDANTS:
404                                $cacheDependencies->addDependency(MarkupCacheDependencies::PAGE_SYSTEM_DEPENDENCY);
405                                break;
406                            default:
407                        }
408                    } catch (ExceptionNotFound $e) {
409                        // not a fetcher markup run
410                    }
411
412                    /**
413                     * Execute the generated SQL
414                     */
415                    try {
416                        $executableSql = $pageSql->getExecutableSql();
417                        $parameters = $pageSql->getParameters();
418                        $request = $sqlite
419                            ->createRequest()
420                            ->setQueryParametrized($executableSql, $parameters);
421                        $rowsInDb = [];
422                        try {
423                            $rowsInDb = $request
424                                ->execute()
425                                ->getRows();
426                        } catch (ExceptionCompile $e) {
427                            $renderer->doc .= "The sql statement generated returns an error. Sql statement: $executableSql";
428                            return false;
429                        } finally {
430                            $request->close();
431                        }
432
433                        $rows = [];
434                        foreach ($rowsInDb as $sourceRow) {
435
436                            /**
437                             * We use id until path is full in the database
438                             */
439                            $id = $sourceRow["ID"];
440                            $contextualPage = MarkupPath::createMarkupFromId($id);
441                            if ($contextualPage->isHidden()) {
442                                continue;
443                            }
444                            if (!$contextualPage->exists()) {
445                                LogUtility::error("Internal Error: the page selected ($contextualPage) was not added. It does not exist and was deleted from the database index.", self::CANONICAL);
446                                $contextualPage->getDatabasePage()->delete();
447                                continue;
448                            }
449                            $standardMetadata = $contextualPage->getMetadataForRendering();
450                            $rows[] = $standardMetadata;
451                        }
452                    } catch (Exception $e) {
453                        $renderer->doc .= "Error during Sql Execution. Error: {$e->getMessage()}";
454                        return false;
455                    }
456
457
458                    /**
459                     * Loop
460                     */
461                    $elementCounts = sizeof($rows);
462                    if ($elementCounts === 0) {
463                        $parametersString = implode(", ", $parameters);
464                        LogUtility::debug("The physical query (Sql: {$pageSql->getExecutableSql()}, Parameters: $parametersString) does not return any data", syntax_plugin_combo_iterator::CANONICAL);
465                        return true;
466                    }
467
468
469                    /**
470                     * Template stack processing
471                     */
472                    $iteratorTemplateInstructions = $data[self::TEMPLATE_CALLSTACK];
473                    if ($iteratorTemplateInstructions === null) {
474                        $renderer->doc .= "No template was found in this iterator.";
475                        return false;
476                    }
477
478
479                    /**
480                     * Split template
481                     * Splits the template into header, main and footer
482                     * in case of complex header
483                     */
484                    $templateHeader = array();
485                    $templateMain = $iteratorTemplateInstructions;
486                    $templateFooter = array();
487                    $complexMarkupFound = $data[self::COMPLEX_MARKUP_FOUND];
488                    if ($complexMarkupFound) {
489
490                        /**
491                         * @var Call $actualCall
492                         */
493                        $templateCallStack = CallStack::createFromInstructions($iteratorTemplateInstructions);
494
495                        $actualStack = array();
496                        $templateCallStack->moveToStart();
497                        while ($actualCall = $templateCallStack->next()) {
498                            switch ($actualCall->getComponentName()) {
499                                case "listitem_open":
500                                case "tablerow_open":
501                                    $templateHeader = $actualStack;
502                                    $actualStack = [$actualCall->toCallArray()];
503                                    continue 2;
504                                case "listitem_close":
505                                case "tablerow_close":
506                                    $actualStack[] = $actualCall->toCallArray();
507                                    $templateMain = $actualStack;
508                                    $actualStack = [];
509                                    continue 2;
510                                default:
511                                    $actualStack[] = $actualCall->toCallArray();
512                            }
513                        }
514                        $templateFooter = $actualStack;
515                    }
516
517                    $contextPath = $executionContext->getContextPath();
518
519                    /**
520                     * Rendering
521                     */
522                    $renderDoc = "";
523
524                    /**
525                     * Header
526                     */
527                    $iteratorHeaderInstructions = $data[self::BEFORE_TEMPLATE_CALLSTACK];
528                    if (!empty($iteratorHeaderInstructions)) {
529                        /**
530                         * Table with an header
531                         * If this is the case, the table_close of the header
532                         * and the table_open of the template should be
533                         * deleted to create one table
534                         */
535                        if (!empty($templateHeader)) {
536                            $firstTemplateCall = Call::createFromInstruction($templateHeader[0]);
537                            if ($firstTemplateCall->getComponentName() === "table_open") {
538                                $lastIterationHeaderElement = sizeof($iteratorHeaderInstructions) - 1;
539                                $lastIterationHeaderInstruction = Call::createFromInstruction($iteratorHeaderInstructions[$lastIterationHeaderElement]);
540                                if ($lastIterationHeaderInstruction->getComponentName() === "table_close") {
541                                    unset($iteratorHeaderInstructions[$lastIterationHeaderElement]);
542                                    unset($templateHeader[0]);
543                                }
544                            }
545                        }
546                    }
547
548                    /**
549                     * Template
550                     */
551                    try {
552                        if (!empty($iteratorHeaderInstructions)) {
553                            /**
554                             * Hack: Rebuild the whole header to have also the table open
555                             * to avoid the row_counter problem
556                             */
557                            $templateHeader = array_merge($iteratorHeaderInstructions, $templateHeader);
558                        }
559                        $fetcherMarkup = FetcherMarkup::confChild()
560                            ->setRequestedContextPath($contextPath)
561                            ->setRequestedMimeToXhtml()
562                            ->setIsDocument(false)
563                            ->setRequestedInstructions($templateHeader)
564                            ->build();
565                        $renderDoc .= $fetcherMarkup->getFetchString();
566                    } catch (ExceptionCompile $e) {
567                        LogUtility::error("Error while rendering the template header. Error: {$e->getMessage()}", self::CANONICAL);
568                        return false;
569                    }
570                    foreach ($rows as $row) {
571                        try {
572                            $renderDoc .= $fetcherMarkup
573                                ->setNextIteratorInstructionsWithContext($templateMain, $row)
574                                ->getFetchString();
575                        } catch (ExceptionCompile $e) {
576                            LogUtility::error("Error while rendering a data row. Error: {$e->getMessage()}", self::CANONICAL, $e);
577                            continue;
578                        }
579                    }
580                    try {
581                        $renderDoc .= $fetcherMarkup
582                            ->setNextIteratorInstructionsWithContext($templateFooter)
583                            ->getFetchString();
584                    } catch (ExceptionCompile $e) {
585                        LogUtility::error("Error while rendering the template footer. Error: {$e->getMessage()}", self::CANONICAL);
586                        return false;
587                    }
588
589
590                    /**
591                     * Iterator Footer
592                     */
593                    $callStackFooterInstructions = $data[self::AFTER_TEMPLATE_CALLSTACK];
594                    if (!empty($callStackFooterInstructions)) {
595                        try {
596                            $renderDoc .= $fetcherMarkup
597                                ->setNextIteratorInstructionsWithContext($callStackFooterInstructions)
598                                ->getFetchString();
599                        } catch (ExceptionCompile $e) {
600                            LogUtility::error("Error while rendering the iterator footer. Error: {$e->getMessage()}", self::CANONICAL);
601                            return false;
602                        }
603                    }
604
605                    /**
606                     * Renderer
607                     */
608                    $renderer->doc .= $renderDoc;
609                    return true;
610
611            }
612        }
613        // unsupported $mode
614        return false;
615    }
616
617
618}
619
620