1<?php 2 3namespace ComboStrap; 4 5 6use ComboStrap\Meta\Field\PageTemplateName; 7use ComboStrap\Web\Url; 8 9/** 10 * Bundle page from the same namespace 11 * with {@link FetcherPageBundler::getBundledOutline() corrected outline} 12 * 13 * From a wiki app, just add: `?do=combo_pagebundler` 14 * 15 */ 16class FetcherPageBundler extends IFetcherAbs implements IFetcherString 17{ 18 19 use FetcherTraitWikiPath; 20 21 const CANONICAL = self::NAME; 22 const NAME = "pagebundler"; 23 private ?Outline $bundledOutline = null; 24 /** 25 * @var int - the maximum number of pages to bundle 26 * Security to not get DDOS by a Search engine 27 */ 28 private int $maxPages = 5; 29 /** 30 * @var int the number of pages processed (ie actually added to the outline) 31 */ 32 private int $countPageProcessed = 0; 33 34 public static function createPageBundler(): FetcherPageBundler 35 { 36 return new FetcherPageBundler(); 37 } 38 39 public function buildFromUrl(Url $url): FetcherPageBundler 40 { 41 /** 42 * Just to return the good type 43 */ 44 parent::buildFromUrl($url); 45 return $this; 46 } 47 48 /** 49 * @throws ExceptionBadArgument 50 * @throws ExceptionBadSyntax 51 * @throws ExceptionNotExists 52 * @throws ExceptionNotFound 53 */ 54 public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherPageBundler 55 { 56 parent::buildFromTagAttributes($tagAttributes); 57 $this->buildOriginalPathFromTagAttributes($tagAttributes); 58 return $this; 59 } 60 61 62 function getBuster(): string 63 { 64 return ""; 65 } 66 67 /** 68 * @return Mime 69 */ 70 public function getMime(): Mime 71 { 72 return Mime::getHtml(); 73 } 74 75 public function getFetcherName(): string 76 { 77 return self::NAME; 78 } 79 80 public function getFetchString(): string 81 { 82 83 $outline = $this->getBundledOutline(); 84 $instructionsCalls = $outline->toHtmlSectionOutlineCalls(); 85 $mainContent = MarkupRenderer::createFromInstructions($instructionsCalls) 86 ->setRequestedExecutingPath($this->getStartPath()) 87 ->setRequestedContextPath($this->getRequestedContextPath()) 88 ->setRequestedMime($this->getMime()) 89 ->getOutput(); 90 91 92 $startMarkup = $this->getStartPath(); 93 $title = PageTitle::createForMarkup($startMarkup)->getValueOrDefault(); 94 $lang = Lang::createForMarkup($startMarkup); 95 try { 96 $startMarkupWikiPath = WikiPath::createFromPathObject($startMarkup->getPathObject()); 97 } catch (ExceptionBadArgument $e) { 98 /** 99 * should not happen as this class accepts only wiki path as {@link FetcherPageBundler::setContextPath() context path} 100 */ 101 throw new ExceptionRuntimeInternal("We were unable to get the start markup wiki path. Error:{$e->getMessage()}", self::CANONICAL); 102 } 103 104 $layoutName = PageTemplateName::BLANK_TEMPLATE_VALUE; 105 try { 106 $toc = Toc::createEmpty() 107 ->setValue($this->getBundledOutline()->toTocDokuwikiFormat()); 108 } catch (ExceptionBadArgument $e) { 109 // this is an array 110 throw new ExceptionRuntimeInternal("The toc could not be created. Error:{$e->getMessage()}", self::CANONICAL, 1, $e); 111 } 112 try { 113 return TemplateForWebPage::create() 114 ->setRequestedTemplateName($layoutName) 115 ->setRequestedContextPath($startMarkupWikiPath) 116 ->setRequestedTitle($title) 117 ->setRequestedLang($lang) 118 ->setToc($toc) 119 ->setIsSocial(false) 120 ->setRequestedEnableTaskRunner(false) 121 ->setMainContent($mainContent) 122 ->render(); 123 } catch (ExceptionBadSyntax|ExceptionNotFound|ExceptionBadArgument $e) { 124 // layout should be good 125 throw new ExceptionRuntimeInternal("The $layoutName template returns an error", self::CANONICAL, 1, $e); 126 } 127 128 129 } 130 131 public function getBundledOutline(): Outline 132 { 133 134 if (isset($this->bundledOutline)) { 135 return $this->bundledOutline; 136 } 137 138 if (!Identity::isAnonymous()) { 139 $this->maxPages = 99999; 140 set_time_limit(5 * 60); 141 } 142 $startPath = $this->getStartPath(); 143 $actualLevel = 0; 144 $this->buildOutlineRecursive($startPath, $actualLevel); 145 146 return $this->bundledOutline; 147 148 } 149 150 /** 151 * The path from where the bundle should start 152 * If this is not an index markup, the index markup will be chosen {@link FetcherPageBundler::getStartPath()} 153 * 154 * @throws ExceptionBadArgument - if the path is not a {@link WikiPath web path} 155 */ 156 public function setContextPath(Path $requestedPath): FetcherPageBundler 157 { 158 $this->setSourcePath(WikiPath::createFromPathObject($requestedPath)); 159 return $this; 160 } 161 162 private function getRequestedContextPath(): WikiPath 163 { 164 return $this->getSourcePath(); 165 } 166 167 168 /** 169 * 170 * @return MarkupPath The index path or the request path is none 171 * 172 */ 173 private function getStartPath(): MarkupPath 174 { 175 $requestedPath = MarkupPath::createPageFromPathObject($this->getRequestedContextPath()); 176 if ($requestedPath->isIndexPage()) { 177 return $requestedPath; 178 } 179 try { 180 /** 181 * Parent is an index path in the {@link MarkupFileSystem} 182 */ 183 return $requestedPath->getParent(); 184 } catch (ExceptionNotFound $e) { 185 // home markup case (should not happen - home page is a index page) 186 return $requestedPath; 187 } 188 189 } 190 191 /** 192 * If a page does not have any h1 193 * (Case of index page for instance) 194 * 195 * If this is the case, the outline is broken. 196 * @param Outline $outline 197 * @return Outline 198 */ 199 private function addFirstSectionIfMissing(Outline $outline): Outline 200 { 201 $rootOutlineSection = $outline->getRootOutlineSection(); 202 $addFirstSection = false; 203 try { 204 $firstChild = $rootOutlineSection->getFirstChild(); 205 if ($firstChild->getLevel() >= 2) { 206 $addFirstSection = true; 207 } 208 } catch (ExceptionNotFound $e) { 209 $addFirstSection = true; 210 } 211 if ($addFirstSection) { 212 $enterHeading = Call::createComboCall( 213 HeadingTag::HEADING_TAG, 214 DOKU_LEXER_ENTER, 215 array(HeadingTag::LEVEL => 1), 216 HeadingTag::TYPE_OUTLINE, 217 null, 218 null, 219 null, 220 \syntax_plugin_combo_xmlblocktag::TAG 221 ); 222 $title = PageTitle::createForMarkup($outline->getMarkupPath())->getValueOrDefault(); 223 $unmatchedHeading = Call::createComboCall( 224 HeadingTag::HEADING_TAG, 225 DOKU_LEXER_UNMATCHED, 226 [], 227 null, 228 $title, 229 $title, 230 null, 231 \syntax_plugin_combo_xmlblocktag::TAG 232 ); 233 $exitHeading = Call::createComboCall( 234 HeadingTag::HEADING_TAG, 235 DOKU_LEXER_EXIT, 236 array(HeadingTag::LEVEL => 1), 237 null, 238 null, 239 null, 240 null, 241 \syntax_plugin_combo_xmlblocktag::TAG 242 ); 243 $h1Section = OutlineSection::createFromEnterHeadingCall($outline, $enterHeading) 244 ->addHeaderCall($unmatchedHeading) 245 ->addHeaderCall($exitHeading); 246 $children = $rootOutlineSection->getChildren(); 247 foreach ($children as $child) { 248 $child->detachBeforeAppend(); 249 try { 250 $h1Section->appendChild($child); 251 } catch (ExceptionBadState $e) { 252 LogUtility::error("An error occurs when trying to move the h2 children below the recreated heading title ($title)", self::CANONICAL); 253 } 254 } 255 /** 256 * Without h1 257 * The content is in the root heading 258 */ 259 foreach ($rootOutlineSection->getContentCalls() as $rootHeadingCall) { 260 $h1Section->addContentCall($rootHeadingCall); 261 } 262 $rootOutlineSection->deleteContentCalls(); 263 try { 264 $rootOutlineSection->appendChild($h1Section); 265 } catch (ExceptionBadState $e) { 266 LogUtility::error("An error occurs when trying to add the recreated title heading ($title) to the root", self::CANONICAL); 267 } 268 } 269 return $outline; 270 } 271 272 public function getLabel(): string 273 { 274 return self::CANONICAL; 275 } 276 277 private function buildOutlineRecursive(MarkupPath $indexPath, int $actualLevel) 278 { 279 /** 280 * Index Page 281 */ 282 if (FileSystems::exists($indexPath)) { 283 $outline = FetcherMarkup::confRoot() 284 ->setRequestedExecutingPath($indexPath) 285 ->setRequestedContextPath($indexPath->toWikiPath()) 286 ->setRequestedMimeToInstructions() 287 ->build() 288 ->getOutline(); 289 $indexOutline = $this->addFirstSectionIfMissing($outline); 290 foreach ($indexOutline->getRootOutlineSection()->getChildren() as $childOuterSection) { 291 $childOuterSection->updatePageLinkToInternal($indexPath); 292 } 293 } else { 294 $title = PageTitle::createForMarkup($indexPath)->getValueOrDefault(); 295 $content = <<<EOF 296====== $title ====== 297EOF; 298 $indexOutline = Outline::createFromMarkup($content, $indexPath, $this->getRequestedContextPath()); 299 $indexOutline = $this->addFirstSectionIfMissing($indexOutline); 300 } 301 302 /** 303 * Start of bundled outline or not 304 */ 305 if ($this->bundledOutline === null) { 306 $this->bundledOutline = $indexOutline; 307 } else { 308 Outline::merge($this->bundledOutline, $indexOutline, $actualLevel); 309 } 310 $this->countPageProcessed = +1; 311 if ($this->countPageProcessed > $this->maxPages) { 312 return; 313 } 314 315 /** 316 * Children Pages (Same level) 317 */ 318 $childrenPages = MarkupFileSystem::getOrCreate()->getChildren($indexPath, FileSystems::LEAF); 319 foreach ($childrenPages as $child) { 320 if ($child->isSlot()) { 321 continue; 322 } 323 try { 324 $outline = FetcherMarkup::confRoot() 325 ->setRequestedExecutingPath($child) 326 ->setRequestedContextPath($child->toWikiPath()) 327 ->setRequestedMimeToInstructions() 328 ->build() 329 ->getOutline(); 330 } catch (ExceptionNotExists $e) { 331 // as it's in a file system loop, the page should exist 332 continue; 333 } 334 $outer = $this->addFirstSectionIfMissing($outline); 335 Outline::merge($this->bundledOutline, $outer, $actualLevel); 336 $this->countPageProcessed = +1; 337 if ($this->countPageProcessed > $this->maxPages) { 338 return; 339 } 340 } 341 $containerPages = MarkupFileSystem::getOrCreate()->getChildren($indexPath, FileSystems::CONTAINER); 342 $nextLevel = $actualLevel + 1; 343 foreach ($containerPages as $child) { 344 $this->buildOutlineRecursive($child, $nextLevel); 345 } 346 347 } 348} 349