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