core.php (181843B)
1 <?php 2 // Core extension, https://github.com/annaesvensson/yellow-core 3 4 class YellowCore { 5 const VERSION = "0.9.26"; 6 const RELEASE = "0.9"; 7 public $content; // content files 8 public $media; // media files 9 public $system; // system settings 10 public $language; // language settings 11 public $user; // user settings 12 public $extension; // extensions 13 public $lookup; // lookup and normalisation methods 14 public $toolbox; // toolbox with helper methods 15 public $page; // current page 16 17 public function __construct() { 18 $this->content = new YellowContent($this); 19 $this->media = new YellowMedia($this); 20 $this->system = new YellowSystem($this); 21 $this->language = new YellowLanguage($this); 22 $this->user = new YellowUser($this); 23 $this->extension = new YellowExtension($this); 24 $this->lookup = new YellowLookup($this); 25 $this->toolbox = new YellowToolbox($this); 26 $this->page = new YellowPage($this); 27 $this->checkRequirements(); 28 $this->system->setDefault("sitename", "Localhost"); 29 $this->system->setDefault("author", "Datenstrom"); 30 $this->system->setDefault("email", "webmaster"); 31 $this->system->setDefault("from", "noreply"); 32 $this->system->setDefault("language", "en"); 33 $this->system->setDefault("layout", "default"); 34 $this->system->setDefault("theme", "default"); 35 $this->system->setDefault("parser", "markdown"); 36 $this->system->setDefault("status", "public"); 37 $this->system->setDefault("coreServerUrl", "auto"); 38 $this->system->setDefault("coreTimezone", "UTC"); 39 $this->system->setDefault("coreContentExtension", ".md"); 40 $this->system->setDefault("coreContentDefaultFile", "page.md"); 41 $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); 42 $this->system->setDefault("coreLanguageFile", "yellow-language.ini"); 43 $this->system->setDefault("coreUserFile", "yellow-user.ini"); 44 $this->system->setDefault("coreWebsiteFile", "yellow-website.log"); 45 $this->system->setDefault("coreAssetLocation", "/assets/"); 46 $this->system->setDefault("coreMediaLocation", "/media/"); 47 $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); 48 $this->system->setDefault("coreImageLocation", "/media/images/"); 49 $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/"); 50 $this->system->setDefault("coreMultiLanguageMode", "0"); 51 $this->system->setDefault("coreDebugMode", "0"); 52 } 53 54 public function __destruct() { 55 $this->shutdown(); 56 } 57 58 // Check requirements 59 public function checkRequirements() { 60 if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!"); 61 if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!"); 62 if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!"); 63 if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!"); 64 if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!"); 65 mb_internal_encoding("UTF-8"); 66 } 67 68 // Handle initialisation 69 public function load() { 70 $this->system->load("system/extensions/yellow-system.ini"); 71 $this->system->set("coreSystemFile", "yellow-system.ini"); 72 $this->system->set("coreContentDirectory", "content/"); 73 $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation")); 74 $this->system->set("coreSystemDirectory", "system/"); 75 $this->system->set("coreCacheDirectory", "system/cache/"); 76 $this->system->set("coreExtensionDirectory", "system/extensions/"); 77 $this->system->set("coreLayoutDirectory", "system/layouts/"); 78 $this->system->set("coreThemeDirectory", "system/themes/"); 79 $this->system->set("coreTrashDirectory", "system/trash/"); 80 $this->system->set("coreWorkerDirectory", "system/workers/"); 81 list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation(); 82 $this->system->set("coreServerInstallDirectory", $pathInstall); 83 $this->system->set("coreServerRootDirectory", $pathRoot); 84 $this->system->set("coreServerHomeDirectory", $pathHome); 85 register_shutdown_function(array($this, "processFatalError")); 86 if ($this->system->get("coreDebugMode")>=1) { 87 ini_set("display_errors", 1); 88 error_reporting(E_ALL); 89 } 90 date_default_timezone_set($this->system->get("coreTimezone")); 91 $this->extension->load($this->system->get("coreWorkerDirectory")); 92 $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile")); 93 $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile")); 94 $this->startup(); 95 } 96 97 // Handle request from web browser 98 public function request() { 99 $statusCode = 0; 100 $this->toolbox->timerStart($time); 101 ob_start(); 102 list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation(); 103 $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true); 104 foreach ($this->extension->data as $key=>$value) { 105 if (method_exists($value["object"], "onRequest")) { 106 $this->lookup->requestHandler = $key; 107 $statusCode = $value["object"]->onRequest($scheme, $address, $base, $location, $fileName); 108 if ($statusCode!=0) break; 109 } 110 } 111 if ($statusCode==0) { 112 $this->lookup->requestHandler = "core"; 113 $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true); 114 } 115 if ($this->page->isError()) $statusCode = $this->processRequestError(); 116 ob_end_flush(); 117 $this->toolbox->timerStop($time); 118 if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { 119 echo "YellowCore::request status:$statusCode time:$time ms<br />\n"; 120 } 121 return $statusCode; 122 } 123 124 // Process request 125 public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { 126 $statusCode = 0; 127 if (is_readable($fileName)) { 128 if ($this->lookup->isRequestCleanUrl($location)) { 129 $location = $location.$this->toolbox->getLocationArgumentsCleanUrl(); 130 $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); 131 $statusCode = $this->sendStatus(303, $location); 132 } 133 } else { 134 if ($this->lookup->isRedirectLocation($location)) { 135 $location = $this->lookup->getRedirectLocation($location); 136 $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); 137 $statusCode = $this->sendStatus(301, $location); 138 } 139 } 140 if ($statusCode==0) { 141 if ($this->lookup->isContentFile($fileName)) { 142 $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true); 143 } elseif (!is_string_empty($fileName)) { 144 $statusCode = $this->sendFile(200, $fileName, $cacheable); 145 } 146 if (!is_readable($fileName)) $this->page->error(404); 147 } 148 return $statusCode; 149 } 150 151 // Process request with error 152 public function processRequestError() { 153 ob_clean(); 154 $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base, 155 $this->page->location, $this->page->fileName, false, false); 156 return $statusCode; 157 } 158 159 // Process fatal runtime error 160 public function processFatalError() { 161 $error = error_get_last(); 162 if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) { 163 $fileNameAbsolute = isset($error["file"]) ? $error["file"] : ""; 164 $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory"))); 165 $this->toolbox->log("error", "Process file '$fileName' with fatal error!"); 166 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); 167 $troubleshooting = PHP_SAPI!="cli" ? 168 "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); 169 echo "<br />\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n"; 170 } 171 } 172 173 // Show error message and terminate immediately 174 public function exitFatalError($errorMessage = "") { 175 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); 176 $troubleshooting = PHP_SAPI!="cli" ? 177 "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); 178 echo "$errorMessage $troubleshooting\n"; 179 exit(1); 180 } 181 182 // Send page response 183 public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $regular) { 184 $rawData = $regular ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError(); 185 $statusCode = max($this->page->statusCode, 200); 186 $errorMessage = $this->page->errorMessage; 187 $this->page = new YellowPage($this); 188 $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable); 189 $this->page->parseMeta($rawData, $statusCode, $errorMessage); 190 $this->language->set($this->page->get("language")); 191 $this->page->parseContent(); 192 $this->page->parsePage(); 193 $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData); 194 if ($this->system->get("coreDebugMode")>=1) { 195 foreach ($this->page->headerData as $key=>$value) { 196 echo "YellowCore::sendPage $key: $value<br />\n"; 197 } 198 $fileNameResponse = $regular ? $this->page->fileName : $this->page->getFileNameError(); 199 $layout = $this->page->get("layout"); 200 $theme = $this->page->get("theme"); 201 echo "YellowCore::sendPage file:$fileNameResponse<br />\n"; 202 echo "YellowCore::sendPage layout:$layout theme:$theme<br />\n"; 203 } 204 return $statusCode; 205 } 206 207 // Send data response 208 public function sendData($statusCode, $headerData, $outputData) { 209 $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : ""; 210 if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) { 211 $statusCode = 304; 212 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); 213 } else { 214 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); 215 foreach ($headerData as $key=>$value) { 216 $this->toolbox->sendHttpHeader("$key: $value"); 217 } 218 if (!is_null($outputData)) echo $outputData; 219 } 220 return $statusCode; 221 } 222 223 // Send file response 224 public function sendFile($statusCode, $fileName, $cacheable) { 225 $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); 226 if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) { 227 $statusCode = 304; 228 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); 229 } else { 230 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); 231 if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store"); 232 $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); 233 $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted); 234 echo $this->toolbox->readFile($fileName); 235 } 236 return $statusCode; 237 } 238 239 // Send status response 240 public function sendStatus($statusCode, $location = "") { 241 if (!is_string_empty($location)) $this->page->status($statusCode, $location); 242 $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); 243 foreach ($this->page->headerData as $key=>$value) { 244 $this->toolbox->sendHttpHeader("$key: $value"); 245 } 246 return $statusCode; 247 } 248 249 // Handle command from command line 250 public function command($line = "") { 251 $statusCode = 0; 252 $this->toolbox->timerStart($time); 253 list($command, $text) = $this->lookup->getCommandInformation($line); 254 foreach ($this->extension->data as $key=>$value) { 255 if (method_exists($value["object"], "onCommand")) { 256 $this->lookup->commandHandler = $key; 257 $statusCode = $value["object"]->onCommand($command, $text); 258 if ($statusCode!=0) break; 259 } 260 } 261 if ($statusCode==0 && is_string_empty($command)) { 262 $lines = array(); 263 foreach ($this->extension->data as $key=>$value) { 264 if (method_exists($value["object"], "onCommandHelp")) { 265 $this->lookup->commandHandler = $key; 266 $output = $value["object"]->onCommandHelp(); 267 if (!is_null($output)) { 268 $lines = array_merge($lines, is_array($output) ? $output : array($output)); 269 } 270 } 271 } 272 usort($lines, "strnatcasecmp"); 273 $this->showCommandHelp($lines); 274 $statusCode = 200; 275 } 276 if ($statusCode==0) { 277 $this->lookup->commandHandler = "core"; 278 $statusCode = 400; 279 echo "Yellow $command: Command not found\n"; 280 } 281 $this->toolbox->timerStop($time); 282 if ($this->system->get("coreDebugMode")>=1) { 283 echo "YellowCore::command status:$statusCode time:$time ms<br />\n"; 284 } 285 return $statusCode<400 ? 0 : 1; 286 } 287 288 // Show command help 289 public function showCommandHelp($lines) { 290 echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; 291 $lineCounter = 0; 292 foreach ($lines as $line) { 293 echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; 294 } 295 } 296 297 // Handle startup 298 public function startup() { 299 if (isset($this->extension->data)) { 300 foreach ($this->extension->data as $key=>$value) { 301 if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup(); 302 } 303 } 304 } 305 306 // Handle shutdown 307 public function shutdown() { 308 if (isset($this->extension->data)) { 309 foreach ($this->extension->data as $key=>$value) { 310 if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown(); 311 } 312 } 313 } 314 315 // Include layout 316 public function layout($name, $arguments = null) { 317 $this->lookup->layoutArguments = func_get_args(); 318 $this->page->includeLayout($name); 319 } 320 321 // Return layout arguments 322 public function getLayoutArguments($sizeMin = 9) { 323 return array_pad($this->lookup->layoutArguments, $sizeMin, null); 324 } 325 } 326 327 class YellowContent { 328 public $yellow; // access to API 329 public $pages; // scanned pages 330 331 public function __construct($yellow) { 332 $this->yellow = $yellow; 333 $this->pages = array(); 334 } 335 336 // Scan file system on demand 337 public function scanLocation($location) { 338 if (!isset($this->pages[$location])) { 339 $this->pages[$location] = array(); 340 $scheme = $this->yellow->page->scheme; 341 $address = $this->yellow->page->address; 342 $base = $this->yellow->page->base; 343 if (is_string_empty($location)) { 344 $rootLocations = $this->yellow->lookup->findContentRootLocations(); 345 foreach ($rootLocations as $rootLocation=>$rootFileName) { 346 $page = new YellowPage($this->yellow); 347 $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false); 348 $page->parseMeta(""); 349 array_push($this->pages[$location], $page); 350 } 351 } else { 352 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br />\n"; 353 $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location); 354 foreach ($fileNames as $fileName) { 355 $page = new YellowPage($this->yellow); 356 $page->setRequestInformation($scheme, $address, $base, 357 $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false); 358 $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096)); 359 if (strlenb($page->rawData)<4096) $page->statusCode = 200; 360 array_push($this->pages[$location], $page); 361 } 362 } 363 } 364 return $this->pages[$location]; 365 } 366 367 // Return page from, null if not found 368 public function find($location, $absoluteLocation = false) { 369 $found = false; 370 if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); 371 foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { 372 if ($page->location==$location) { 373 $found = true; 374 break; 375 } 376 } 377 return $found ? $page : null; 378 } 379 380 // Return page collection with pages of the website 381 public function index($showInvisible = false) { 382 $rootLocation = $this->getRootLocation($this->yellow->page->location); 383 return $this->getChildrenRecursive($rootLocation, $showInvisible); 384 } 385 386 // Return page collection with top-level navigation 387 public function top($showInvisible = false) { 388 $rootLocation = $this->getRootLocation($this->yellow->page->location); 389 return $this->getChildren($rootLocation, $showInvisible); 390 } 391 392 // Return page collection with path ancestry 393 public function path($location, $absoluteLocation = false) { 394 $pages = new YellowPageCollection($this->yellow); 395 if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); 396 $page = null; 397 while (!$this->yellow->lookup->isRootLocation($location)) { 398 $page = $this->find($location); 399 if ($page) $pages->prepend($page); 400 $location = $this->getParentLocation($location); 401 } 402 if ($page) { 403 $home = $this->find($this->getHomeLocation($page->location)); 404 if ($home && $home->location!=$page->location) $pages->prepend($home); 405 } 406 return $pages; 407 } 408 409 // Return page collection with multiple languages 410 public function multi($location, $absoluteLocation = false, $showInvisible = false) { 411 $pages = new YellowPageCollection($this->yellow); 412 if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); 413 $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4); 414 foreach ($this->scanLocation("") as $page) { 415 if ($content = $this->find(substru($page->location, 4).$locationEnd)) { 416 if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) { 417 $pages->append($content); 418 } 419 } 420 } 421 return $pages; 422 } 423 424 // Return page collection that's empty 425 public function clean() { 426 return new YellowPageCollection($this->yellow); 427 } 428 429 // Return languages in multi language mode 430 public function getLanguages($showInvisible = false) { 431 $languages = array(); 432 if ($this->yellow->system->get("coreMultiLanguageMode")) { 433 foreach ($this->scanLocation("") as $page) { 434 if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { 435 array_push($languages, $page->get("language")); 436 } 437 } 438 } 439 return $languages; 440 } 441 442 // Return child pages 443 public function getChildren($location, $showInvisible = false) { 444 $pages = new YellowPageCollection($this->yellow); 445 foreach ($this->scanLocation($location) as $page) { 446 if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { 447 $pages->append($page); 448 } 449 } 450 return $pages; 451 } 452 453 // Return child pages recursively 454 public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { 455 --$levelMax; 456 $pages = new YellowPageCollection($this->yellow); 457 foreach ($this->scanLocation($location) as $page) { 458 if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { 459 $pages->append($page); 460 } 461 if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { 462 $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); 463 } 464 } 465 return $pages; 466 } 467 468 // Return shared pages 469 public function getShared($location) { 470 $pages = new YellowPageCollection($this->yellow); 471 $sharedLocation = $this->getHomeLocation($location)."shared/"; 472 return $pages->merge($this->scanLocation($sharedLocation)); 473 } 474 475 // Return root location 476 public function getRootLocation($location) { 477 $rootLocation = "root/"; 478 if ($this->yellow->system->get("coreMultiLanguageMode")) { 479 foreach ($this->scanLocation("") as $page) { 480 $token = substru($page->location, 4); 481 if ($token!="/" && substru($location, 0, strlenu($token))==$token) { 482 $rootLocation = "root$token"; 483 break; 484 } 485 } 486 } 487 return $rootLocation; 488 } 489 490 // Return home location 491 public function getHomeLocation($location) { 492 return substru($this->getRootLocation($location), 4); 493 } 494 495 // Return parent location 496 public function getParentLocation($location) { 497 $parentLocation = ""; 498 $token = rtrim(substru($this->getRootLocation($location), 4), "/"); 499 if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { 500 if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; 501 } 502 if (is_string_empty($parentLocation)) $parentLocation = "root$token/"; 503 return $parentLocation; 504 } 505 506 // Return top-level location 507 public function getParentTopLocation($location) { 508 $parentTopLocation = ""; 509 $token = rtrim(substru($this->getRootLocation($location), 4), "/"); 510 if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; 511 if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; 512 return $parentTopLocation; 513 } 514 } 515 516 class YellowMedia { 517 public $yellow; // access to API 518 public $files; // scanned files 519 520 public function __construct($yellow) { 521 $this->yellow = $yellow; 522 $this->files = array(); 523 } 524 525 // Scan file system on demand 526 public function scanLocation($location) { 527 if (!isset($this->files[$location])) { 528 $this->files[$location] = array(); 529 $scheme = $this->yellow->page->scheme; 530 $address = $this->yellow->page->address; 531 $base = $this->yellow->system->get("coreServerBase"); 532 if (is_string_empty($location)) { 533 $fileNames = array($this->yellow->system->get("coreMediaDirectory")); 534 } else { 535 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br />\n"; 536 $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location); 537 } 538 foreach ($fileNames as $fileName) { 539 $file = new YellowPage($this->yellow); 540 $file->setRequestInformation($scheme, $address, $base, 541 $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false); 542 $file->parseMeta(null); 543 array_push($this->files[$location], $file); 544 } 545 } 546 return $this->files[$location]; 547 } 548 549 // Return page with media file information, null if not found 550 public function find($location, $absoluteLocation = false) { 551 $found = false; 552 if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); 553 foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { 554 if ($file->location==$location) { 555 $found = true; 556 break; 557 } 558 } 559 return $found ? $file : null; 560 } 561 562 // Return page collection with media files 563 public function index($showInvisible = false) { 564 return $this->getChildrenRecursive("", $showInvisible); 565 } 566 567 // Return page collection that's empty 568 public function clean() { 569 return new YellowPageCollection($this->yellow); 570 } 571 572 // Return child files 573 public function getChildren($location, $showInvisible = false) { 574 $files = new YellowPageCollection($this->yellow); 575 foreach ($this->scanLocation($location) as $file) { 576 if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { 577 $files->append($file); 578 } 579 } 580 return $files; 581 } 582 583 // Return child files recursively 584 public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { 585 --$levelMax; 586 $files = new YellowPageCollection($this->yellow); 587 foreach ($this->scanLocation($location) as $file) { 588 if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { 589 $files->append($file); 590 } 591 if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) { 592 $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax)); 593 } 594 } 595 return $files; 596 } 597 598 // Return home location 599 public function getHomeLocation($location) { 600 return $this->yellow->system->get("coreMediaLocation"); 601 } 602 603 // Return parent location 604 public function getParentLocation($location) { 605 $parentLocation = ""; 606 $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); 607 if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { 608 if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; 609 } 610 return $parentLocation; 611 } 612 613 // Return top-level location 614 public function getParentTopLocation($location) { 615 $parentTopLocation = ""; 616 $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); 617 if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; 618 if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; 619 return $parentTopLocation; 620 } 621 } 622 623 class YellowSystem { 624 public $yellow; // access to API 625 public $modified; // system modification date 626 public $settings; // system settings 627 public $settingsDefaults; // system settings defaults 628 629 public function __construct($yellow) { 630 $this->yellow = $yellow; 631 $this->modified = 0; 632 $this->settings = new YellowArray(); 633 $this->settingsDefaults = new YellowArray(); 634 } 635 636 // Load system settings from file 637 public function load($fileName) { 638 $this->modified = $this->yellow->toolbox->getFileModified($fileName); 639 $fileData = $this->yellow->toolbox->readFile($fileName); 640 $this->settings = $this->yellow->toolbox->getTextSettings($fileData, ""); 641 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br />\n"; 642 if ($this->yellow->system->get("coreDebugMode")>=3) { 643 foreach ($this->settings as $key=>$value) { 644 echo "YellowSystem::load ".ucfirst($key).":$value<br />\n"; 645 } 646 } 647 } 648 649 // Save system settings to file 650 public function save($fileName, $settings) { 651 $this->modified = time(); 652 $settingsNew = new YellowArray(); 653 foreach ($settings as $key=>$value) { 654 if (!is_string_empty($key) && !is_string_empty($value)) { 655 $this->set($key, $value); 656 $settingsNew[$key] = $value; 657 } 658 } 659 $fileData = $this->yellow->toolbox->readFile($fileName); 660 $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew); 661 return $this->yellow->toolbox->writeFile($fileName, $fileData); 662 } 663 664 // Set default system setting 665 public function setDefault($key, $value) { 666 $this->settingsDefaults[$key] = $value; 667 } 668 669 // Set default system settings 670 public function setDefaults($lines) { 671 foreach ($lines as $line) { 672 if (preg_match("/^\#/", $line)) continue; 673 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 674 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 675 $this->settingsDefaults[$matches[1]] = $matches[2]; 676 } 677 } 678 } 679 } 680 681 // Set system setting 682 public function set($key, $value) { 683 $this->settings[$key] = $value; 684 } 685 686 // Return system setting 687 public function get($key) { 688 if (isset($this->settings[$key])) { 689 $value = $this->settings[$key]; 690 } else { 691 $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; 692 } 693 return $value; 694 } 695 696 // Return system setting, HTML encoded 697 public function getHtml($key) { 698 return htmlspecialchars($this->get($key)); 699 } 700 701 // Return different value for system setting 702 public function getDifferent($key) { 703 $array = array_diff($this->yellow->toolbox->enumerate($key), array($this->get($key))); 704 return reset($array); 705 } 706 707 // TODO: Remove later, this is only for backwards compatibility 708 public function getAvailable($key) { return $this->yellow->toolbox->enumerate($key); } 709 710 // Return system settings 711 public function getSettings($filterStart = "", $filterEnd = "") { 712 $settings = array(); 713 if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { 714 $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); 715 } else { 716 foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { 717 if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; 718 if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; 719 } 720 } 721 return $settings; 722 } 723 724 // Return system settings modification date, Unix time or HTTP format 725 public function getModified($httpFormat = false) { 726 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; 727 } 728 729 // Check if system setting exists 730 public function isExisting($key) { 731 return isset($this->settings[$key]); 732 } 733 } 734 735 class YellowLanguage { 736 public $yellow; // access to API 737 public $modified; // language modification date 738 public $settings; // language settings 739 public $settingsDefaults; // language settings defaults 740 public $language; // current language 741 742 public function __construct($yellow) { 743 $this->yellow = $yellow; 744 $this->modified = 0; 745 $this->settings = new YellowArray(); 746 $this->settingsDefaults = new YellowArray(); 747 $this->language = ""; 748 } 749 750 // Load language settings from file 751 public function load($fileName) { 752 $this->modified = $this->yellow->toolbox->getFileModified($fileName); 753 $fileData = $this->yellow->toolbox->readFile($fileName); 754 $settings = $this->yellow->toolbox->getTextSettings($fileData, "language"); 755 foreach ($settings as $language=>$block) { 756 if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); 757 foreach ($block as $key=>$value) { 758 $this->settings[$language][$key] = $value; 759 } 760 } 761 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br />\n"; 762 foreach ($this->settings->getArrayCopy() as $key=>$value) { 763 if (!isset($this->settings[$key]["languageDescription"])) { 764 unset($this->settings[$key]); 765 } 766 } 767 $callback = function ($a, $b) { 768 return strnatcmp($a["languageDescription"], $b["languageDescription"]); 769 }; 770 $this->settings->uasort($callback); 771 } 772 773 // Set current language 774 public function set($language) { 775 $this->language = $language; 776 } 777 778 // Set default language setting 779 public function setDefault($key, $value, $language) { 780 if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); 781 $this->settings[$language][$key] = $value; 782 $this->settingsDefaults[$key] = true; 783 } 784 785 // Set default language settings 786 public function setDefaults($lines) { 787 $language = ""; 788 foreach ($lines as $line) { 789 if (preg_match("/^\#/", $line)) continue; 790 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 791 if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) { 792 $language = $matches[2]; 793 if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); 794 } 795 if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 796 $this->settings[$language][$matches[1]] = $matches[2]; 797 $this->settingsDefaults[$matches[1]] = true; 798 } 799 } 800 } 801 } 802 803 // Set language setting 804 public function setText($key, $value, $language) { 805 if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); 806 $this->settings[$language][$key] = $value; 807 } 808 809 // Return language setting 810 public function getText($key, $language = "") { 811 if (is_string_empty($language)) $language = $this->language; 812 return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]"; 813 } 814 815 // Return language setting, HTML encoded 816 public function getTextHtml($key, $language = "") { 817 return htmlspecialchars($this->getText($key, $language)); 818 } 819 820 // Return text as language specific date, convert to one of the standard formats 821 public function getDateStandard($text, $language = "") { 822 if (preg_match("/^\d+$/", $text)) { 823 $output = $text; 824 } elseif (preg_match("/^\d+\-\d+$/", $text)) { 825 $format = $this->getText("coreDateFormatShort", $language); 826 $output = $this->getDateFormatted(strtotime($text), $format, $language); 827 } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { 828 $format = $this->getText("coreDateFormatMedium", $language); 829 $output = $this->getDateFormatted(strtotime($text), $format, $language); 830 } else { 831 $format = $this->getText("coreDateFormatLong", $language); 832 $output = $this->getDateFormatted(strtotime($text), $format, $language); 833 } 834 return $output; 835 } 836 837 // Return Unix time as date, relative to today 838 public function getDateRelative($timestamp, $format, $daysLimit, $language = "") { 839 $timeDifference = mktime(0, 0, 0) - strtotime(date("Y-m-d", $timestamp)); 840 $days = abs(intval($timeDifference/86400)); 841 $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; 842 $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language)); 843 if (count($tokens)>=8) { 844 if ($days<=$daysLimit || $daysLimit==0) { 845 if ($days==0) { 846 $output = $tokens[0]; 847 } elseif ($days==1) { 848 $output = $tokens[1]; 849 } elseif ($days>=2 && $days<=29) { 850 $output = preg_replace("/@x/i", $days, $tokens[2]); 851 } elseif ($days>=30 && $days<=59) { 852 $output = $tokens[3]; 853 } elseif ($days>=60 && $days<=364) { 854 $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); 855 } elseif ($days>=365 && $days<=729) { 856 $output = $tokens[5]; 857 } else { 858 $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); 859 } 860 } else { 861 $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]); 862 } 863 } else { 864 $output = "[$key]"; 865 } 866 return $output; 867 } 868 869 // Return Unix time as date 870 public function getDateFormatted($timestamp, $format, $language = "") { 871 $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language)); 872 $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language)); 873 $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language)); 874 $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1]; 875 $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1]; 876 $weekday = $dateWeekdays[date("N", $timestamp) - 1]; 877 $timeZone = $this->yellow->system->get("coreTimezone"); 878 $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone)); 879 $timeZoneOffset = $timeZoneHelper->getOffset(); 880 $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); 881 $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format); 882 $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format); 883 $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 0, 3), "A..Za..z"), $format); 884 $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); 885 $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); 886 $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); 887 return date($format, $timestamp); 888 } 889 890 // Return language settings 891 public function getSettings($filterStart = "", $filterEnd = "", $language = "") { 892 $settings = array(); 893 if (is_string_empty($language)) $language = $this->language; 894 if (isset($this->settings[$language])) { 895 if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { 896 $settings = $this->settings[$language]->getArrayCopy(); 897 } else { 898 foreach ($this->settings[$language] as $key=>$value) { 899 if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; 900 if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; 901 } 902 } 903 } 904 return $settings; 905 } 906 907 // Return language settings modification date, Unix time or HTTP format 908 public function getModified($httpFormat = false) { 909 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; 910 } 911 912 // Check if language setting exists 913 public function isText($key, $language = "") { 914 if (is_string_empty($language)) $language = $this->language; 915 return isset($this->settings[$language]) && isset($this->settings[$language][$key]); 916 } 917 918 // Check if language exists 919 public function isExisting($language) { 920 return isset($this->settings[$language]); 921 } 922 } 923 924 class YellowUser { 925 public $yellow; // access to API 926 public $modified; // user modification date 927 public $settings; // user settings 928 public $email; // current email 929 930 public function __construct($yellow) { 931 $this->yellow = $yellow; 932 $this->modified = 0; 933 $this->settings = new YellowArray(); 934 $this->email = ""; 935 } 936 937 // Load user settings from file 938 public function load($fileName) { 939 $this->modified = $this->yellow->toolbox->getFileModified($fileName); 940 $fileData = $this->yellow->toolbox->readFile($fileName); 941 $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email"); 942 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br />\n"; 943 } 944 945 // Save user settings to file 946 public function save($fileName, $email, $settings) { 947 $this->modified = time(); 948 $settingsNew = new YellowArray(); 949 $settingsNew["email"] = $email; 950 foreach ($settings as $key=>$value) { 951 if (!is_string_empty($key) && !is_string_empty($value)) { 952 $this->setUser($key, $value, $email); 953 $settingsNew[$key] = $value; 954 } 955 } 956 $fileData = $this->yellow->toolbox->readFile($fileName); 957 $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew); 958 return $this->yellow->toolbox->writeFile($fileName, $fileData); 959 } 960 961 // Remove user settings from file 962 public function remove($fileName, $email) { 963 $this->modified = time(); 964 if (isset($this->settings[$email])) unset($this->settings[$email]); 965 $fileData = $this->yellow->toolbox->readFile($fileName); 966 $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email); 967 return $this->yellow->toolbox->writeFile($fileName, $fileData); 968 } 969 970 // Set current email 971 public function set($email) { 972 $this->email = $email; 973 } 974 975 // Set user setting 976 public function setUser($key, $value, $email) { 977 if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray(); 978 $this->settings[$email][$key] = $value; 979 } 980 981 // Return user setting 982 public function getUser($key, $email = "") { 983 if (is_string_empty($email)) $email = $this->email; 984 return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : ""; 985 } 986 987 // Return user setting, HTML encoded 988 public function getUserHtml($key, $email = "") { 989 return htmlspecialchars($this->getUser($key, $email)); 990 } 991 992 // Return user settings 993 public function getSettings($email = "") { 994 $settings = array(); 995 if (is_string_empty($email)) $email = $this->email; 996 if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy(); 997 return $settings; 998 } 999 1000 // Return user settings modification date, Unix time or HTTP format 1001 public function getModified($httpFormat = false) { 1002 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; 1003 } 1004 1005 // Check if user setting exists 1006 public function isUser($key, $email = "") { 1007 if (is_string_empty($email)) $email = $this->email; 1008 return isset($this->settings[$email]) && isset($this->settings[$email][$key]); 1009 } 1010 1011 // Check if user exists 1012 public function isExisting($email) { 1013 return isset($this->settings[$email]); 1014 } 1015 } 1016 1017 class YellowExtension { 1018 public $yellow; // access to API 1019 public $modified; // extension modification date 1020 public $data; // extension data 1021 1022 public function __construct($yellow) { 1023 $this->yellow = $yellow; 1024 $this->modified = 0; 1025 $this->data = array(); 1026 } 1027 1028 // Load extensions 1029 public function load($path) { 1030 foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { 1031 $this->modified = max($this->modified, $this->yellow->toolbox->getFileModified($entry)); 1032 require_once($entry); 1033 $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); 1034 $this->register(lcfirst($name), "Yellow".ucfirst($name)); 1035 if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br />\n"; 1036 } 1037 $callback = function ($a, $b) { 1038 return $a["priority"] - $b["priority"]; 1039 }; 1040 uasort($this->data, $callback); 1041 foreach ($this->data as $key=>$value) { 1042 if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow); 1043 } 1044 } 1045 1046 // Register extension 1047 public function register($key, $class) { 1048 if (!$this->isExisting($key) && class_exists($class)) { 1049 $this->data[$key] = array(); 1050 $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class; 1051 $this->data[$key]["class"] = $class; 1052 $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; 1053 $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10; 1054 } 1055 } 1056 1057 // Return extension 1058 public function get($key) { 1059 return $this->data[$key]["object"]; 1060 } 1061 1062 // Return extensions modification date, Unix time or HTTP format 1063 public function getModified($httpFormat = false) { 1064 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; 1065 } 1066 1067 // Check if extension exists 1068 public function isExisting($key) { 1069 return isset($this->data[$key]); 1070 } 1071 } 1072 1073 class YellowLookup { 1074 public $yellow; // access to API 1075 public $requestHandler; // request handler name 1076 public $commandHandler; // command handler name 1077 public $layoutArguments; // layout arguments 1078 1079 public function __construct($yellow) { 1080 $this->yellow = $yellow; 1081 } 1082 1083 // Return file system information 1084 public function findFileSystemInformation() { 1085 $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreWorkerDirectory"))); 1086 $pathBase = $this->yellow->system->get("coreContentDirectory"); 1087 $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : ""; 1088 $pathHome = "home/"; 1089 if (!is_string_empty($pathRoot)) { 1090 $firstRoot = ""; 1091 $token = $root = rtrim($pathRoot, "/"); 1092 foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { 1093 if (is_string_empty($firstRoot)) $firstRoot = $token = $entry; 1094 if ($this->normaliseToken($entry)==$root) { 1095 $token = $entry; 1096 break; 1097 } 1098 } 1099 $pathRoot = $this->normaliseToken($token)."/"; 1100 $pathBase .= "$firstRoot/"; 1101 } 1102 if (!is_string_empty($pathHome)) { 1103 $firstHome = ""; 1104 $token = $home = rtrim($pathHome, "/"); 1105 foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { 1106 if (is_string_empty($firstHome)) $firstHome = $token = $entry; 1107 if ($this->normaliseToken($entry)==$home) { 1108 $token = $entry; 1109 break; 1110 } 1111 } 1112 $pathHome = $this->normaliseToken($token)."/"; 1113 } 1114 return array($pathInstall, $pathRoot, $pathHome); 1115 } 1116 1117 // Return content language 1118 public function findContentLanguage($fileName, $languageDefault) { 1119 $language = $languageDefault; 1120 $pathBase = $this->yellow->system->get("coreContentDirectory"); 1121 $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); 1122 if (!is_string_empty($pathRoot)) { 1123 $fileName = substru($fileName, strlenu($pathBase)); 1124 if (preg_match("/^(.+?)\//", $fileName, $matches)) { 1125 $name = $this->normaliseToken($matches[1]); 1126 if (strlenu($name)==2) $language = $name; 1127 } 1128 } 1129 return $language; 1130 } 1131 1132 // Return content root locations 1133 public function findContentRootLocations() { 1134 $rootLocations = array(); 1135 $pathBase = $this->yellow->system->get("coreContentDirectory"); 1136 $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); 1137 if (!is_string_empty($pathRoot)) { 1138 foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { 1139 $token = $this->normaliseToken($entry)."/"; 1140 if ($token==$pathRoot) $token = ""; 1141 $rootLocations["root/$token"] = "$pathBase$entry/"; 1142 } 1143 } else { 1144 $rootLocations["root/"] = "$pathBase"; 1145 } 1146 if ($this->yellow->system->get("coreDebugMode")>=3) { 1147 foreach ($rootLocations as $key=>$value) { 1148 echo "YellowLookup::findContentRootLocations $key -> $value<br />\n"; 1149 } 1150 } 1151 return $rootLocations; 1152 } 1153 1154 // Return content location from file path 1155 public function findContentLocationFromFile($fileName) { 1156 $invalid = false; 1157 $location = "/"; 1158 $pathBase = $this->yellow->system->get("coreContentDirectory"); 1159 $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); 1160 $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); 1161 $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); 1162 $fileExtension = $this->yellow->system->get("coreContentExtension"); 1163 if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { 1164 $fileName = substru($fileName, strlenu($pathBase)); 1165 $tokens = explode("/", $fileName); 1166 if (!is_string_empty($pathRoot)) { 1167 $token = $this->normaliseToken($tokens[0])."/"; 1168 if ($token!=$pathRoot) $location .= $token; 1169 array_shift($tokens); 1170 } 1171 for ($i=0; $i<count($tokens)-1; ++$i) { 1172 $token = $this->normaliseToken($tokens[$i])."/"; 1173 if ($i || $token!=$pathHome) $location .= $token; 1174 } 1175 $token = $this->normaliseToken($tokens[$i], $fileExtension); 1176 if ($token!=$fileDefault) { 1177 $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); 1178 } 1179 $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; 1180 if ($extension!=$fileExtension) $invalid = true; 1181 } else { 1182 $invalid = true; 1183 } 1184 if ($this->yellow->system->get("coreDebugMode")>=2) { 1185 $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; 1186 echo "YellowLookup::findContentLocationFromFile $debug<br />\n"; 1187 } 1188 return $invalid ? "" : $location; 1189 } 1190 1191 // Return file path from content location 1192 public function findFileFromContentLocation($location, $directory = false) { 1193 $found = $invalid = false; 1194 $path = $this->yellow->system->get("coreContentDirectory"); 1195 $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); 1196 $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); 1197 $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); 1198 $fileExtension = $this->yellow->system->get("coreContentExtension"); 1199 $tokens = explode("/", $location); 1200 if ($this->isRootLocation($location)) { 1201 if (!is_string_empty($pathRoot)) { 1202 $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); 1203 $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); 1204 } 1205 } else { 1206 if (!is_string_empty($pathRoot)) { 1207 if (count($tokens)>2) { 1208 if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; 1209 $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); 1210 if ($found) array_shift($tokens); 1211 } 1212 if (!$found) { 1213 $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); 1214 } 1215 } 1216 if (count($tokens)>2) { 1217 if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; 1218 for ($i=1; $i<count($tokens)-1; ++$i) { 1219 $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); 1220 } 1221 } else { 1222 $i = 1; 1223 $tokens[0] = rtrim($pathHome, "/"); 1224 $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); 1225 } 1226 if (!$directory) { 1227 if (!is_string_empty($tokens[$i])) { 1228 $token = $tokens[$i].$fileExtension; 1229 if ($token==$fileDefault) $invalid = true; 1230 $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); 1231 } else { 1232 $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); 1233 } 1234 if ($this->yellow->system->get("coreDebugMode")>=2) { 1235 $debug = "$location -> ".($invalid ? "INVALID" : $path); 1236 echo "YellowLookup::findFileFromContentLocation $debug<br />\n"; 1237 } 1238 } 1239 } 1240 return $invalid ? "" : $path; 1241 } 1242 1243 // Return children from content location 1244 public function findChildrenFromContentLocation($location) { 1245 $fileNames = array(); 1246 if (!$this->isFileLocation($location)) { 1247 $path = $this->findFileFromContentLocation($location, true); 1248 $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); 1249 $fileExtension = $this->yellow->system->get("coreContentExtension"); 1250 foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { 1251 $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); 1252 array_push($fileNames, $path.$entry."/".$token); 1253 } 1254 if (!$this->isRootLocation($location)) { 1255 $regex = "/^.*\\".$fileExtension."$/"; 1256 foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { 1257 if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; 1258 array_push($fileNames, $path.$entry); 1259 } 1260 } 1261 } 1262 return $fileNames; 1263 } 1264 1265 // Return media location from file path 1266 public function findMediaLocationFromFile($fileName) { 1267 $location = ""; 1268 $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); 1269 if (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) { 1270 $location = "/".$fileName; 1271 } 1272 return $location; 1273 } 1274 1275 // Return file path from media location 1276 public function findFileFromMediaLocation($location) { 1277 $fileName = ""; 1278 $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); 1279 if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { 1280 $fileName = substru($location, 1); 1281 } 1282 return $fileName; 1283 } 1284 1285 // Return children from media location 1286 public function findChildrenFromMediaLocation($location) { 1287 $fileNames = array(); 1288 if (!$this->isFileLocation($location)) { 1289 $path = $this->findFileFromMediaLocation($location); 1290 foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { 1291 array_push($fileNames, $entry."/"); 1292 } 1293 foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { 1294 array_push($fileNames, $entry); 1295 } 1296 } 1297 return $fileNames; 1298 } 1299 1300 // Return media directory from a system setting 1301 public function findMediaDirectory($key) { 1302 return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : ""; 1303 } 1304 1305 // Return system location from file path, for virtually mapped system files 1306 public function findSystemLocationFromFile($fileName) { 1307 $location = ""; 1308 $layoutDirectoryLength = strlenu($this->yellow->system->get("coreLayoutDirectory")); 1309 $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); 1310 $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory")); 1311 if (substru($fileName, 0, $layoutDirectoryLength)==$this->yellow->system->get("coreLayoutDirectory")) { 1312 if ($this->isSafeFile($fileName)) { 1313 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $layoutDirectoryLength); 1314 } 1315 } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) { 1316 if ($this->isSafeFile($fileName)) { 1317 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $themeDirectoryLength); 1318 } 1319 } elseif (substru($fileName, 0, $workerDirectoryLength)==$this->yellow->system->get("coreWorkerDirectory")) { 1320 if ($this->isSafeFile($fileName)) { 1321 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $workerDirectoryLength); 1322 } 1323 } 1324 return $location; 1325 } 1326 1327 // Return file path from system location, for virtually mapped system files 1328 public function findFileFromSystemLocation($location) { 1329 $fileName = ""; 1330 $assetLocationLength = strlenu($this->yellow->system->get("coreAssetLocation")); 1331 if (substru($location, 0, $assetLocationLength)==$this->yellow->system->get("coreAssetLocation")) { 1332 if ($this->isSafeFile($location)) { 1333 $fileNameLayout = $this->yellow->system->get("coreLayoutDirectory").substru($location, $assetLocationLength); 1334 $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").substru($location, $assetLocationLength); 1335 $fileNameWorker = $this->yellow->system->get("coreWorkerDirectory").substru($location, $assetLocationLength); 1336 if (is_file($fileNameLayout)) { 1337 $fileName = $fileNameLayout; 1338 } elseif (is_file($fileNameTheme)) { 1339 $fileName = $fileNameTheme; 1340 } elseif (is_file($fileNameWorker)) { 1341 $fileName = $fileNameWorker; 1342 } 1343 } 1344 } 1345 return $fileName; 1346 } 1347 1348 // Return file or directory that matches token 1349 public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { 1350 if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; 1351 if (!$invalid) { 1352 $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/"; 1353 foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { 1354 if ($this->normaliseToken($entry, $fileExtension)==$token) { 1355 $token = $entry; 1356 $found = true; 1357 break; 1358 } 1359 } 1360 } 1361 if ($directory) $token .= "/"; 1362 return ($default || $found) ? $token : ""; 1363 } 1364 1365 // Return default file in directory 1366 public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { 1367 $token = $fileDefault; 1368 if (!is_file($path."/".$fileDefault)) { 1369 $regex = "/^[\d\-\_\.]*($fileDefault)$/"; 1370 foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { 1371 if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { 1372 $token = $entry; 1373 break; 1374 } 1375 } 1376 } 1377 return $includePath ? "$path/$token" : $token; 1378 } 1379 1380 // Normalise file/directory token 1381 public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { 1382 if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; 1383 if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; 1384 return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); 1385 } 1386 1387 // Normalise name 1388 public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { 1389 if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; 1390 if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; 1391 if ($filterStrict) $text = strtoloweru($text); 1392 return preg_replace("/[^\pL\d\-\_]/u", "-", $text); 1393 } 1394 1395 // Normalise prefix 1396 public function normalisePrefix($text) { 1397 $prefix = ""; 1398 if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; 1399 if (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; 1400 return $prefix; 1401 } 1402 1403 // Normalise elements and attributes in HTML/SVG data 1404 public function normaliseData($text, $type = "html", $filterStrict = true) { 1405 $output = ""; 1406 $elementsHtml = array( 1407 "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); 1408 $elementsSvg = array( 1409 "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); 1410 $attributesHtml = array( 1411 "accept", "action", "align", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); 1412 $attributesSvg = array( 1413 "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); 1414 $attributesAllowEmptyString = array("alt", "download", "open", "sandbox", "value"); 1415 $elementsSafe = $elementsHtml; 1416 $attributesSafe = $attributesHtml; 1417 if ($type=="svg") { 1418 $elementsSafe = array_merge($elementsHtml, $elementsSvg); 1419 $attributesSafe = array_merge($attributesHtml, $attributesSvg); 1420 } 1421 $offsetBytes = 0; 1422 while (true) { 1423 $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); 1424 $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); 1425 $elementStart = $elementFound ? $matches[1][0] : ""; 1426 $elementName = $elementFound ? $matches[2][0]: ""; 1427 $elementMiddle = $elementFound ? $matches[3][0]: ""; 1428 $elementEnd = $elementFound ? $matches[4][0]: ""; 1429 $output .= $elementBefore; 1430 if (substrb($elementName, 0, 1)=="!") { 1431 $output .= "<$elementName$elementMiddle>"; 1432 } elseif (in_array(strtolower($elementName), $elementsSafe)) { 1433 $elementAttributes = $this->getTextAttributes($elementMiddle, $attributesAllowEmptyString); 1434 foreach ($elementAttributes as $key=>$value) { 1435 if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { 1436 unset($elementAttributes[$key]); 1437 } 1438 } 1439 if ($filterStrict) { 1440 $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; 1441 if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { 1442 $elementAttributes["href"] = "error-xss-filter"; 1443 } 1444 $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; 1445 if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { 1446 $elementAttributes["xlink:href"] = "error-xss-filter"; 1447 } 1448 } 1449 $output .= "<$elementStart$elementName"; 1450 foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; 1451 if (!is_string_empty($elementEnd)) $output .= " "; 1452 $output .= "$elementEnd>"; 1453 } 1454 if (!$elementFound) break; 1455 $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); 1456 } 1457 return $output; 1458 } 1459 1460 // Normalise name and email for a single address 1461 public function normaliseAddress($input, $type = "mail", $filterStrict = true) { 1462 $output = ""; 1463 if ($type=="mail") { 1464 if (preg_match("/^(.*?)(\s*)<(.*?)>$/", $input, $matches)) { 1465 $name = $matches[1]; 1466 $email = $matches[3]; 1467 } else { 1468 $name = ""; 1469 $email = $input; 1470 } 1471 $name = preg_replace("/[^\pL\d\-\. ]/u", "", $name); 1472 $name = preg_replace("/\s+/s", " ", $name); 1473 if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $email)) { 1474 $email = "error-mail-address"; 1475 } 1476 $output = is_string_empty($name) ? "<$email>" : "$name <$email>"; 1477 } 1478 return $output; 1479 } 1480 1481 // Normalise fields in MIME headers 1482 public function normaliseHeaders($input, $type = "mime", $filterStrict = true) { 1483 $output = ""; 1484 if ($type=="mime") { 1485 $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc"); 1486 foreach ($input as $key=>$value) { 1487 $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-"); 1488 if (in_array($key, $keysMixedEncoding)) { 1489 $text = "$key: "; 1490 foreach (preg_split("/\s*,\s*/", $value) as $email) { 1491 if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) { 1492 $matches[1] = $matches[2] = ""; 1493 $matches[3] = $email; 1494 } 1495 if (!is_string_empty($matches[1]) && !preg_match("/^[\pL\d\-\. ]+$/u", $matches[1])) { 1496 $matches[1] = $matches[2] = ""; 1497 $matches[3] = "error-mail-address"; 1498 } 1499 if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $matches[3])) { 1500 $matches[3] = "error-mail-address"; 1501 } 1502 if (substru($text, -2, 2)!=": ") $text .= ",\r\n "; 1503 $text = $this->getMimeHeader($text, $matches[1]); 1504 $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false); 1505 } 1506 $text .= "\r\n"; 1507 } else { 1508 $text = $this->getMimeHeader("$key: ", $value)."\r\n"; 1509 } 1510 $output .= $text; 1511 } 1512 } 1513 return $output; 1514 } 1515 1516 // Normalise CSS class 1517 public function normaliseClass($text) { 1518 return str_replace(array(" ", "_"), array("-", "-"), strtoloweru($text)); 1519 } 1520 1521 // Normalise relative path tokens 1522 public function normalisePath($text) { 1523 $textFiltered = ""; 1524 $textLength = strlenb($text); 1525 for ($pos=0; $pos<$textLength; ++$pos) { 1526 if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) { 1527 while ($text[$pos]==".") ++$pos; 1528 if ($text[$pos]=="/") ++$pos; 1529 --$pos; 1530 continue; 1531 } 1532 $textFiltered .= $text[$pos]; 1533 } 1534 return $textFiltered; 1535 } 1536 1537 // Normalise text lines, convert line endings 1538 public function normaliseLines($text, $endOfLine = "lf") { 1539 if ($endOfLine=="lf") { 1540 $text = preg_replace("/\R/u", "\n", $text); 1541 } else { 1542 $text = preg_replace("/\R/u", "\r\n", $text); 1543 } 1544 return $text; 1545 } 1546 1547 // Normalise text into UTF-8 NFC 1548 public function normaliseUnicode($text) { 1549 if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) { 1550 $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text); 1551 if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text); 1552 } 1553 return $text; 1554 } 1555 1556 // Normalise location, make absolute location 1557 public function normaliseLocation($location, $pageLocation, $filterStrict = true) { 1558 if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { 1559 $pageBase = $this->yellow->page->base; 1560 $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); 1561 if (!preg_match("/^\#/", $location)) { 1562 if (!preg_match("/^\//", $location)) { 1563 $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; 1564 } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { 1565 $location = $pageBase.$location; 1566 } 1567 } else { 1568 $location = $pageBase.$pageLocation.$location; 1569 } 1570 $location = str_replace("/./", "/", $location); 1571 $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); 1572 } else { 1573 if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; 1574 } 1575 return $location; 1576 } 1577 1578 // Normalise location arguments 1579 public function normaliseArguments($text, $filterStrict = true) { 1580 if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text)); 1581 $separator = $this->yellow->toolbox->getLocationArgumentsSeparator(); 1582 $text = str_replace(":", $separator, $text); 1583 if (preg_match("/^(.*\/)?page$separator.*$/", $text)) { 1584 $text = rtrim($text, "/"); 1585 } else { 1586 $text = rtrim($text, "/")."/"; 1587 } 1588 return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); 1589 } 1590 1591 // Normalise URL, make absolute URL 1592 public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { 1593 if (!preg_match("/^\w+:/", $location)) { 1594 $url = "$scheme://$address$base$location"; 1595 } else { 1596 if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; 1597 $url = $location; 1598 } 1599 return $url; 1600 } 1601 1602 // Return URL information 1603 public function getUrlInformation($url) { 1604 $scheme = $address = $base = ""; 1605 if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { 1606 $scheme = $matches[1]; 1607 $address = $matches[2]; 1608 $base = $matches[3]; 1609 } 1610 return array($scheme, $address, $base); 1611 } 1612 1613 // Return request information 1614 public function getRequestInformation($scheme = "", $address = "", $base = "") { 1615 if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) { 1616 $url = $this->yellow->system->get("coreServerUrl"); 1617 if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl(); 1618 list($scheme, $address, $base) = $this->getUrlInformation($url); 1619 $this->yellow->system->set("coreServerScheme", $scheme); 1620 $this->yellow->system->set("coreServerAddress", $address); 1621 $this->yellow->system->set("coreServerBase", $base); 1622 if ($this->yellow->system->get("coreDebugMode")>=3) { 1623 echo "YellowLookup::getRequestInformation $scheme://$address$base<br />\n"; 1624 } 1625 } 1626 $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base)); 1627 $fileName = ""; 1628 if (is_string_empty($fileName)) $fileName = $this->findFileFromSystemLocation($location); 1629 if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location); 1630 if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location); 1631 return array($scheme, $address, $base, $location, $fileName); 1632 } 1633 1634 // Return command information 1635 public function getCommandInformation($line = "") { 1636 if (is_string_empty($line)) { 1637 $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1)); 1638 if ($this->yellow->system->get("coreDebugMode")>=3) { 1639 echo "YellowLookup::getCommandInformation $line<br />\n"; 1640 } 1641 } 1642 return $this->yellow->toolbox->getTextList($line, " ", 2); 1643 } 1644 1645 // Return request handler 1646 public function getRequestHandler() { 1647 return $this->requestHandler; 1648 } 1649 1650 // Return command handler 1651 public function getCommandHandler() { 1652 return $this->commandHandler; 1653 } 1654 1655 // Return attributes from text 1656 public function getTextAttributes($text, $attributesAllowEmptyString) { 1657 $tokens = array(); 1658 $posStart = $posQuote = 0; 1659 $textLength = strlenb($text); 1660 for ($pos=0; $pos<$textLength; ++$pos) { 1661 if ($text[$pos]==" " && !$posQuote) { 1662 if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); 1663 $posStart = $pos+1; 1664 } 1665 if ($text[$pos]=="=" && !$posQuote) { 1666 if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); 1667 array_push($tokens, "="); 1668 $posStart = $pos+1; 1669 } 1670 if ($text[$pos]=="\"") { 1671 if ($posQuote) { 1672 if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); 1673 $posQuote = 0; 1674 $posStart = $pos+1; 1675 } else { 1676 if ($pos==$posStart) $posQuote = $pos; 1677 } 1678 } 1679 } 1680 if ($pos>$posStart && !$posQuote) { 1681 array_push($tokens, substrb($text, $posStart, $pos-$posStart)); 1682 } 1683 $attributes = array(); 1684 for ($i=0; $i<count($tokens); ++$i) { 1685 if ($i+2<count($tokens) && $tokens[$i+1]=="=") { 1686 $key = $tokens[$i]; 1687 $value = $tokens[$i+2]; 1688 $i += 2; 1689 } else { 1690 $key = $value = $tokens[$i]; 1691 } 1692 if (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) { 1693 $attributes[$key] = $value; 1694 } 1695 } 1696 return $attributes; 1697 } 1698 1699 // Return HTML attributes from generic Markdown attributes 1700 public function getHtmlAttributes($text) { 1701 $htmlAttributes = ""; 1702 $htmlAttributesData = array(); 1703 foreach (explode(" ", $text) as $token) { 1704 if (substru($token, 0, 1)==".") { 1705 if (!isset($htmlAttributesData["class"])) { 1706 $htmlAttributesData["class"] = substru($token, 1); 1707 } else { 1708 $htmlAttributesData["class"] .= " ".substru($token, 1); 1709 } 1710 } 1711 if (substru($token, 0, 1)=="#") $htmlAttributesData["id"] = substru($token, 1); 1712 if (preg_match("/^([\w]+)=(.+)/", $token, $matches)) $htmlAttributesData[$matches[1]] = $matches[2]; 1713 } 1714 foreach ($htmlAttributesData as $key=>$value) { 1715 $htmlAttributes .= " $key=\"".htmlspecialchars($value)."\""; 1716 } 1717 return $htmlAttributes; 1718 } 1719 1720 // Return MIME header field, encode and fold if necessary 1721 public function getMimeHeader($text, $field, $allowEncode = true) { 1722 if ($allowEncode) { 1723 $encode = preg_match("/[\x7F-\xFF]/", $field); 1724 $fieldPos = 0; 1725 while (true) { 1726 $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); 1727 $bytesAvailable = max(0, 78-$textPos); 1728 $fragment = substrb($field, $fieldPos); 1729 if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; 1730 if ($bytesAvailable<strlenb($fragment)) { 1731 $bytesHandled = $bytesAvailable; 1732 if (!$encode) { 1733 for ($pos=$bytesHandled;$pos>0;--$pos) { 1734 if ($field[$fieldPos+$pos]==" ") { 1735 $fragment = substrb($field, $fieldPos, $pos); 1736 $bytesHandled = $pos+1; 1737 break; 1738 } 1739 } 1740 if ($pos==0) $encode = true; 1741 } 1742 if ($encode) { 1743 while (true) { 1744 $fragment = substrb($field, $fieldPos, $bytesHandled); 1745 if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; 1746 if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break; 1747 --$bytesHandled; 1748 } 1749 } 1750 $text .= $fragment."\r\n "; 1751 $fieldPos += $bytesHandled; 1752 } else { 1753 $text .= $fragment; 1754 break; 1755 } 1756 } 1757 } else { 1758 $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); 1759 $bytesAvailable = max(0, 78-$textPos); 1760 if ($bytesAvailable<strlenb($field)) { 1761 $text .= "\r\n ".ltrim($field); 1762 } else { 1763 $text .= $field; 1764 } 1765 } 1766 return $text; 1767 } 1768 1769 // Return directory location 1770 public function getDirectoryLocation($location) { 1771 return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; 1772 } 1773 1774 // Return redirect location 1775 public function getRedirectLocation($location) { 1776 if ($this->isFileLocation($location)) { 1777 $location = "$location/"; 1778 } else { 1779 $languageDefault = $this->yellow->system->get("language"); 1780 $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); 1781 $location = "/$language/"; 1782 } 1783 return $location; 1784 } 1785 1786 // Check if clean URL is requested 1787 public function isRequestCleanUrl($location) { 1788 return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; 1789 } 1790 1791 // Check if location is specifying root 1792 public function isRootLocation($location) { 1793 return substru($location, 0, 1)!="/"; 1794 } 1795 1796 // Check if location is specifying file or directory 1797 public function isFileLocation($location) { 1798 return substru($location, -1, 1)!="/"; 1799 } 1800 1801 // Check if location can be redirected into directory 1802 public function isRedirectLocation($location) { 1803 $redirect = false; 1804 if ($this->isFileLocation($location)) { 1805 $redirect = is_dir($this->findFileFromContentLocation("$location/", true)); 1806 } elseif ($location=="/") { 1807 $redirect = $this->yellow->system->get("coreMultiLanguageMode"); 1808 } 1809 return $redirect; 1810 } 1811 1812 // Check if location contains nested directories 1813 public function isNestedLocation($location, $fileName, $checkHomeLocation = false) { 1814 $nested = false; 1815 if (!$checkHomeLocation || $location==$this->yellow->content->getHomeLocation($location)) { 1816 $path = dirname($fileName); 1817 if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; 1818 } 1819 return $nested; 1820 } 1821 1822 // Check if location is within shared directory 1823 public function isSharedLocation($location) { 1824 $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/"; 1825 return substru($location, 0, strlenu($sharedLocation))==$sharedLocation; 1826 } 1827 1828 // Check if location is within current HTTP request 1829 public function isActiveLocation($location, $currentLocation) { 1830 if ($this->isFileLocation($location)) { 1831 $active = $currentLocation==$location; 1832 } else { 1833 if ($location==$this->yellow->content->getHomeLocation($location)) { 1834 $active = $this->getDirectoryLocation($currentLocation)==$location; 1835 } else { 1836 $active = substru($currentLocation, 0, strlenu($location))==$location; 1837 } 1838 } 1839 return $active; 1840 } 1841 1842 // Check if URL is a well-known URL scheme 1843 public function isSafeUrl($url) { 1844 return preg_match("/^(http|https|ftp|mailto|tel):/", $url); 1845 } 1846 1847 // Check if file is a well-known file type 1848 public function isSafeFile($fileName) { 1849 return preg_match("/\.(css|gif|ico|js|jpeg|jpg|json|map|png|scss|svg|webmanifest|woff|woff2)$/", $fileName); 1850 } 1851 1852 // Check if file is valid 1853 public function isValidFile($fileName) { 1854 $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); 1855 $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); 1856 $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); 1857 return strposu($fileName, "/")===false || 1858 substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || 1859 substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || 1860 substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); 1861 } 1862 1863 // Check if content file 1864 public function isContentFile($fileName) { 1865 $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); 1866 return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); 1867 } 1868 1869 // Check if media file 1870 public function isMediaFile($fileName) { 1871 $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); 1872 return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); 1873 } 1874 1875 // Check if system file 1876 public function isSystemFile($fileName) { 1877 $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); 1878 return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); 1879 } 1880 1881 // Check if running at command line 1882 public function isCommandLine() { 1883 return isset($this->commandHandler); 1884 } 1885 } 1886 1887 class YellowToolbox { 1888 public $yellow; // access to API 1889 1890 public function __construct($yellow) { 1891 $this->yellow = $yellow; 1892 } 1893 1894 // Return browser cookie from from current HTTP request 1895 public function getCookie($key) { 1896 return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; 1897 } 1898 1899 // Return server argument from current HTTP request 1900 public function getServer($key) { 1901 return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; 1902 } 1903 1904 // Return location arguments from current HTTP request 1905 public function getLocationArguments() { 1906 return $this->getServer("LOCATION_ARGUMENTS"); 1907 } 1908 1909 // Return location arguments from current HTTP request, modify existing arguments 1910 public function getLocationArgumentsNew($key, $value) { 1911 $locationArguments = ""; 1912 $found = false; 1913 $separator = $this->getLocationArgumentsSeparator(); 1914 foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { 1915 if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { 1916 if ($matches[1]==$key) { 1917 $matches[2] = $value; 1918 $found = true; 1919 } 1920 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 1921 if (!is_string_empty($locationArguments)) $locationArguments .= "/"; 1922 $locationArguments .= "$matches[1]:$matches[2]"; 1923 } 1924 } 1925 } 1926 if (!$found && !is_string_empty($key) && !is_string_empty($value)) { 1927 if (!is_string_empty($locationArguments)) $locationArguments .= "/"; 1928 $locationArguments .= "$key:$value"; 1929 } 1930 if (!is_string_empty($locationArguments)) { 1931 $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false); 1932 } 1933 return $locationArguments; 1934 } 1935 1936 // Return location arguments from current HTTP request, convert form parameters 1937 public function getLocationArgumentsCleanUrl() { 1938 $locationArguments = ""; 1939 foreach (array_merge($_GET, $_POST) as $key=>$value) { 1940 if (!is_string_empty($key) && !is_string_empty($value)) { 1941 if (!is_string_empty($locationArguments)) $locationArguments .= "/"; 1942 $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); 1943 $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); 1944 $locationArguments .= "$key:$value"; 1945 } 1946 } 1947 if (!is_string_empty($locationArguments)) { 1948 $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false); 1949 } 1950 return $locationArguments; 1951 } 1952 1953 // Return location arguments separator 1954 public function getLocationArgumentsSeparator() { 1955 return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; 1956 } 1957 1958 // Return human readable HTTP date 1959 public function getHttpDateFormatted($timestamp) { 1960 return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; 1961 } 1962 1963 // Return human readable HTTP server status 1964 public function getHttpStatusFormatted($statusCode, $shortFormat = false) { 1965 switch ($statusCode) { 1966 case 0: $text = "No data"; break; 1967 case 200: $text = "OK"; break; 1968 case 301: $text = "Moved permanently"; break; 1969 case 302: $text = "Moved temporarily"; break; 1970 case 303: $text = "Reload please"; break; 1971 case 304: $text = "Not modified"; break; 1972 case 400: $text = "Bad request"; break; 1973 case 403: $text = "Forbidden"; break; 1974 case 404: $text = "Not found"; break; 1975 case 420: $text = "Not public"; break; 1976 case 430: $text = "Login failed"; break; 1977 case 434: $text = "Can create"; break; 1978 case 435: $text = "Can restore"; break; 1979 case 450: $text = "Update error"; break; 1980 case 500: $text = "Server error"; break; 1981 case 503: $text = "Service unavailable"; break; 1982 default: $text = "Error $statusCode"; 1983 } 1984 $serverProtocol = $this->getServer("SERVER_PROTOCOL"); 1985 if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; 1986 return $shortFormat ? $text : "$serverProtocol $statusCode $text"; 1987 } 1988 1989 // Return MIME content type 1990 public function getMimeContentType($fileName) { 1991 $contentType = ""; 1992 $contentTypes = array( 1993 "css" => "text/css", 1994 "gif" => "image/gif", 1995 "html" => "text/html; charset=utf-8", 1996 "ico" => "image/x-icon", 1997 "js" => "application/javascript", 1998 "json" => "application/json", 1999 "jpeg" => "image/jpeg", 2000 "jpg" => "image/jpeg", 2001 "md" => "text/markdown", 2002 "png" => "image/png", 2003 "scss" => "text/x-scss", 2004 "svg" => "image/svg+xml", 2005 "txt" => "text/plain", 2006 "webmanifest" => "application/manifest+json", 2007 "woff" => "application/font-woff", 2008 "woff2" => "application/font-woff2", 2009 "xml" => "text/xml; charset=utf-8"); 2010 $fileType = $this->getFileType($fileName); 2011 if (is_string_empty($fileType)) { 2012 $contentType = $contentTypes["html"]; 2013 } elseif (array_key_exists($fileType, $contentTypes)) { 2014 $contentType = $contentTypes[$fileType]; 2015 } 2016 return $contentType; 2017 } 2018 2019 // Send HTTP header 2020 public function sendHttpHeader($text) { 2021 if (!headers_sent()) header($text); 2022 } 2023 2024 // Return files and directories 2025 public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { 2026 return $this->getDirectoryEntriesRecursive($path, $regex, $sort, $directories, $includePath, 1); 2027 } 2028 2029 // Return files and directories recursively 2030 public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true, $levelMax = 0) { 2031 --$levelMax; 2032 $entries = array(); 2033 $directoryHandle = @opendir($path); 2034 if ($directoryHandle) { 2035 $path = rtrim($path, "/"); 2036 $directoryEntries = array(); 2037 while (($entry = readdir($directoryHandle))!==false) { 2038 if (substru($entry, 0, 1)==".") continue; 2039 $entry = $this->yellow->lookup->normaliseUnicode($entry); 2040 if (preg_match($regex, $entry)) { 2041 if ($directories) { 2042 if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); 2043 } else { 2044 if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); 2045 } 2046 } 2047 if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); 2048 } 2049 if ($sort) { 2050 natcasesort($entries); 2051 natcasesort($directoryEntries); 2052 } 2053 closedir($directoryHandle); 2054 foreach ($directoryEntries as $directoryEntry) { 2055 $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($directoryEntry, $regex, $sort, $directories, $includePath, $levelMax)); 2056 } 2057 } 2058 return $entries; 2059 } 2060 2061 // Return directory information, modification date and file count 2062 public function getDirectoryInformation($path) { 2063 return $this->getDirectoryInformationRecursive($path, 1); 2064 } 2065 2066 // Return directory information recursively, modification date and file count 2067 public function getDirectoryInformationRecursive($path, $levelMax = 0) { 2068 --$levelMax; 2069 $modified = $fileCount = 0; 2070 $directoryHandle = @opendir($path); 2071 if ($directoryHandle) { 2072 $path = rtrim($path, "/"); 2073 $directoryEntries = array(); 2074 while (($entry = readdir($directoryHandle))!==false) { 2075 if (substru($entry, 0, 1)==".") continue; 2076 $modified = max($modified, $this->getFileModified("$path/$entry")); 2077 if (is_file("$path/$entry")) ++$fileCount; 2078 if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); 2079 } 2080 closedir($directoryHandle); 2081 foreach ($directoryEntries as $directoryEntry) { 2082 list($modifiedBelow, $fileCountBelow) = $this->getDirectoryInformationRecursive($directoryEntry, $levelMax); 2083 $modified = max($modified, $modifiedBelow); 2084 $fileCount += $fileCountBelow; 2085 } 2086 } 2087 return array($modified, $fileCount); 2088 } 2089 2090 // Read file, empty string if not found 2091 public function readFile($fileName, $sizeMax = 0) { 2092 $fileData = ""; 2093 $fileHandle = @fopen($fileName, "rb"); 2094 if ($fileHandle) { 2095 clearstatcache(true, $fileName); 2096 if (flock($fileHandle, LOCK_SH)) { 2097 $fileSize = $sizeMax ? $sizeMax : filesize($fileName); 2098 if ($fileSize) $fileData = fread($fileHandle, $fileSize); 2099 flock($fileHandle, LOCK_UN); 2100 } 2101 fclose($fileHandle); 2102 } 2103 return $fileData; 2104 } 2105 2106 // Write file 2107 public function writeFile($fileName, $fileData, $mkdir = false) { 2108 $ok = false; 2109 if ($mkdir) { 2110 $path = dirname($fileName); 2111 if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); 2112 } 2113 $fileHandle = @fopen($fileName, "cb"); 2114 if ($fileHandle) { 2115 clearstatcache(true, $fileName); 2116 if (flock($fileHandle, LOCK_EX)) { 2117 ftruncate($fileHandle, 0); 2118 fwrite($fileHandle, $fileData); 2119 fflush($fileHandle); 2120 flock($fileHandle, LOCK_UN); 2121 } 2122 fclose($fileHandle); 2123 $ok = true; 2124 } 2125 return $ok; 2126 } 2127 2128 // Append file 2129 public function appendFile($fileName, $fileData, $mkdir = false) { 2130 $ok = false; 2131 if ($mkdir) { 2132 $path = dirname($fileName); 2133 if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); 2134 } 2135 $fileHandle = @fopen($fileName, "ab"); 2136 if ($fileHandle) { 2137 clearstatcache(true, $fileName); 2138 if (flock($fileHandle, LOCK_EX)) { 2139 fwrite($fileHandle, $fileData); 2140 fflush($fileHandle); 2141 flock($fileHandle, LOCK_UN); 2142 } 2143 fclose($fileHandle); 2144 $ok = true; 2145 } 2146 return $ok; 2147 } 2148 2149 // Copy file 2150 public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { 2151 clearstatcache(); 2152 if ($mkdir) { 2153 $path = dirname($fileNameDestination); 2154 if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); 2155 } 2156 return @copy($fileNameSource, $fileNameDestination); 2157 } 2158 2159 // Rename file 2160 public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { 2161 clearstatcache(); 2162 if ($mkdir) { 2163 $path = dirname($fileNameDestination); 2164 if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); 2165 } 2166 return @rename($fileNameSource, $fileNameDestination); 2167 } 2168 2169 // Rename directory 2170 public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { 2171 return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); 2172 } 2173 2174 // Delete file 2175 public function deleteFile($fileName, $pathTrash = "") { 2176 clearstatcache(); 2177 if (is_string_empty($pathTrash)) { 2178 $ok = @unlink($fileName); 2179 } else { 2180 if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); 2181 $fileNameDestination = $pathTrash; 2182 $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); 2183 $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); 2184 $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); 2185 $ok = @rename($fileName, $fileNameDestination); 2186 } 2187 return $ok; 2188 } 2189 2190 // Delete directory 2191 public function deleteDirectory($path, $pathTrash = "") { 2192 clearstatcache(); 2193 if (is_string_empty($pathTrash)) { 2194 $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); 2195 $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); 2196 foreach ($files as $file) { 2197 if ($file->getType()=="dir") { 2198 @rmdir($file->getPathname()); 2199 } else { 2200 @unlink($file->getPathname()); 2201 } 2202 } 2203 $ok = @rmdir($path); 2204 } else { 2205 if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); 2206 $pathDestination = $pathTrash; 2207 $pathDestination .= basename($path); 2208 $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); 2209 $ok = @rename($path, $pathDestination); 2210 } 2211 return $ok; 2212 } 2213 2214 // Set file/directory modification date, Unix time 2215 public function modifyFile($fileName, $modified) { 2216 clearstatcache(true, $fileName); 2217 return @touch($fileName, $modified); 2218 } 2219 2220 // Return file/directory modification date, Unix time 2221 public function getFileModified($fileName) { 2222 return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0; 2223 } 2224 2225 // Return file/directory deletion date, Unix time 2226 public function getFileDeleted($fileName) { 2227 $deleted = 0; 2228 $text = basename($fileName); 2229 $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; 2230 if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) { 2231 $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]"); 2232 } 2233 return $deleted; 2234 } 2235 2236 // Return file size 2237 public function getFileSize($fileName) { 2238 return is_file($fileName) ? filesize($fileName) : 0; 2239 } 2240 2241 // Return file type 2242 public function getFileType($fileName) { 2243 return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); 2244 } 2245 2246 // Return file group 2247 public function getFileGroup($fileName, $path) { 2248 $group = "none"; 2249 if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); 2250 return $group; 2251 } 2252 2253 // Return number of bytes 2254 public function getNumberBytes($text) { 2255 $bytes = intval($text); 2256 switch (strtoupperu(substru($text, -1))) { 2257 case "G": $bytes *= 1024*1024*1024; break; 2258 case "M": $bytes *= 1024*1024; break; 2259 case "K": $bytes *= 1024; break; 2260 } 2261 return $bytes; 2262 } 2263 2264 // Return lines from text, including newline 2265 public function getTextLines($text) { 2266 $lines = preg_split("/\n/", $text); 2267 foreach ($lines as &$line) { 2268 $line = $line."\n"; 2269 } 2270 if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines); 2271 return $lines; 2272 } 2273 2274 // Return settings from text 2275 function getTextSettings($text, $blockStart) { 2276 $settings = new YellowArray(); 2277 if (is_string_empty($blockStart)) { 2278 foreach ($this->getTextLines($text) as $line) { 2279 if (preg_match("/^\#/", $line)) continue; 2280 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2281 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 2282 $settings[$matches[1]] = $matches[2]; 2283 } 2284 } 2285 } 2286 } else { 2287 $blockKey = ""; 2288 foreach ($this->getTextLines($text) as $line) { 2289 if (preg_match("/^\#/", $line)) continue; 2290 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2291 if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { 2292 $blockKey = $matches[2]; 2293 $settings[$blockKey] = new YellowArray(); 2294 } 2295 if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 2296 $settings[$blockKey][$matches[1]] = $matches[2]; 2297 } 2298 } 2299 } 2300 } 2301 return $settings; 2302 } 2303 2304 // Set settings in text 2305 function setTextSettings($text, $blockStart, $blockKey, $settings) { 2306 $textNew = ""; 2307 if (is_string_empty($blockStart)) { 2308 foreach ($this->getTextLines($text) as $line) { 2309 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2310 if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { 2311 $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n"; 2312 unset($settings[$matches[1]]); 2313 continue; 2314 } 2315 } 2316 $textNew .= $line; 2317 } 2318 foreach ($settings as $key=>$value) { 2319 $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; 2320 } 2321 } else { 2322 $scan = false; 2323 $textStart = $textMiddle = $textEnd = ""; 2324 foreach ($this->getTextLines($text) as $line) { 2325 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2326 if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { 2327 $scan = lcfirst($matches[2])==lcfirst($blockKey); 2328 } 2329 } 2330 if (!$scan && is_string_empty($textMiddle)) { 2331 $textStart .= $line; 2332 } elseif ($scan) { 2333 $textMiddle .= $line; 2334 } else { 2335 $textEnd .= $line; 2336 } 2337 } 2338 $textSettings = ""; 2339 foreach ($this->getTextLines($textMiddle) as $line) { 2340 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2341 if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { 2342 $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n"; 2343 unset($settings[$matches[1]]); 2344 continue; 2345 } 2346 $textSettings .= $line; 2347 } 2348 } 2349 foreach ($settings as $key=>$value) { 2350 $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; 2351 } 2352 if (!is_string_empty($textMiddle)) { 2353 $textMiddle = $textSettings; 2354 if (!is_string_empty($textEnd)) $textMiddle .= "\n"; 2355 } else { 2356 if (!is_string_empty($textStart)) $textEnd .= "\n"; 2357 $textEnd .= $textSettings; 2358 } 2359 $textNew = $textStart.$textMiddle.$textEnd; 2360 } 2361 return $textNew; 2362 } 2363 2364 // Remove settings from text 2365 function unsetTextSettings($text, $blockStart, $blockKey) { 2366 $textNew = ""; 2367 if (!is_string_empty($blockStart)) { 2368 $scan = false; 2369 $textStart = $textMiddle = $textEnd = ""; 2370 foreach ($this->getTextLines($text) as $line) { 2371 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2372 if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { 2373 $scan = lcfirst($matches[2])==lcfirst($blockKey); 2374 } 2375 } 2376 if (!$scan && is_string_empty($textMiddle)) { 2377 $textStart .= $line; 2378 } elseif ($scan) { 2379 $textMiddle .= $line; 2380 } else { 2381 $textEnd .= $line; 2382 } 2383 } 2384 $textNew = rtrim($textStart.$textEnd)."\n"; 2385 } 2386 return $textNew; 2387 } 2388 2389 // Return array of specific size from text 2390 public function getTextList($text, $separator, $size) { 2391 $tokens = explode($separator, $text, $size); 2392 return array_pad($tokens, $size, ""); 2393 } 2394 2395 // Return array of variable size from text, space separated 2396 public function getTextArguments($text, $optional = "-", $sizeMin = 9) { 2397 $text = preg_replace("/\s+/s", " ", trim($text)); 2398 $tokens = str_getcsv($text, " ", "\"", ""); 2399 foreach ($tokens as $key=>$value) { 2400 if (is_null($value) || $value==$optional) $tokens[$key] = ""; 2401 } 2402 return array_pad($tokens, $sizeMin, ""); 2403 } 2404 2405 // Return text from array, space separated 2406 public function getTextString($tokens, $optional = "-") { 2407 $text = ""; 2408 foreach ($tokens as $token) { 2409 if (preg_match("/\s/", $token)) $token = "\"$token\""; 2410 if (is_string_empty($token)) $token = $optional; 2411 if (!is_string_empty($text)) $text .= " "; 2412 $text .= $token; 2413 } 2414 return $text; 2415 } 2416 2417 // Return number of words in text 2418 public function getTextWords($text) { 2419 $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); 2420 $text = preg_replace("/(\pL|\p{N})/u", "x", $text); 2421 return str_word_count($text); 2422 } 2423 2424 // Return text truncated at word boundary 2425 public function getTextTruncated($text, $lengthMax) { 2426 if (strlenu($text)>$lengthMax-1) { 2427 $text = substru($text, 0, $lengthMax); 2428 $pos = strrposu($text, " "); 2429 $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; 2430 } 2431 return $text; 2432 } 2433 2434 // Create text description, with or without HTML 2435 public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { 2436 $output = ""; 2437 $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); 2438 $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); 2439 if ($lengthMax==0) $lengthMax = strlenu($text); 2440 if ($removeHtml) { 2441 $hiddenLevel = 0; 2442 $offsetBytes = 0; 2443 while (true) { 2444 $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); 2445 $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); 2446 $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; 2447 $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; 2448 $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; 2449 $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : ""; 2450 $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; 2451 if (!is_string_empty($elementBefore) && !$hiddenLevel) { 2452 $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); 2453 if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; 2454 if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); 2455 $output .= $this->getTextTruncated($rawText, $lengthMax); 2456 $lengthMax -= strlenu($rawText); 2457 } 2458 if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { 2459 $output .= $endMarkerText; 2460 $lengthMax = 0; 2461 } 2462 if ($lengthMax<=0 || !$elementFound) break; 2463 if ($hiddenLevel>0 || 2464 preg_match("/aria-hidden=\"true\"/i", $elementAttributes) || 2465 preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) { 2466 if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { 2467 if (is_string_empty($elementStart)) { 2468 ++$hiddenLevel; 2469 } else { 2470 --$hiddenLevel; 2471 } 2472 } 2473 } 2474 $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); 2475 } 2476 $output = preg_replace("/\s+\…$/s", "…", $output); 2477 } else { 2478 $elementsOpen = array(); 2479 $offsetBytes = 0; 2480 while (true) { 2481 $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); 2482 $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); 2483 $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; 2484 $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; 2485 $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; 2486 $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; 2487 if (!is_string_empty($elementBefore)) { 2488 $output .= $this->getTextTruncated($elementBefore, $lengthMax); 2489 $lengthMax -= strlenu($elementBefore); 2490 } 2491 if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { 2492 $output .= $endMarkerText; 2493 $lengthMax = 0; 2494 } 2495 if ($lengthMax<=0 || !$elementFound) break; 2496 if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { 2497 if (is_string_empty($elementStart)) { 2498 array_push($elementsOpen, $elementName); 2499 } else { 2500 array_pop($elementsOpen); 2501 } 2502 } 2503 $output .= $elementRawData; 2504 if ($elementRawData[0]=="&") --$lengthMax; 2505 $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); 2506 } 2507 $output = preg_replace("/\s+\…$/s", "…", $output); 2508 for ($i=count($elementsOpen)-1; $i>=0; --$i) { 2509 $output .= "</".$elementsOpen[$i].">"; 2510 } 2511 } 2512 return trim($output); 2513 } 2514 2515 // Create title from text 2516 public function createTextTitle($text) { 2517 if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", ucfirst($matches[1])); 2518 return $text; 2519 } 2520 2521 // Create random text for cryptography 2522 public function createSalt($length, $bcryptFormat = false) { 2523 $dataBuffer = $salt = ""; 2524 $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2)); 2525 if (is_string_empty($dataBuffer) && function_exists("random_bytes")) { 2526 $dataBuffer = @random_bytes($dataBufferSize); 2527 } 2528 if (is_string_empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { 2529 $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); 2530 } 2531 if (strlenb($dataBuffer)==$dataBufferSize) { 2532 if ($bcryptFormat) { 2533 $salt = substrb(base64_encode($dataBuffer), 0, $length); 2534 $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 2535 $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 2536 $salt = strtr($salt, $base64Chars, $bcrypt64Chars); 2537 } else { 2538 $salt = substrb(bin2hex($dataBuffer), 0, $length); 2539 } 2540 } 2541 return $salt; 2542 } 2543 2544 // Create hash with random salt, bcrypt or sha256 2545 public function createHash($text, $algorithm, $cost = 0) { 2546 $hash = ""; 2547 switch ($algorithm) { 2548 case "bcrypt": $prefix = sprintf("$2y$%02d$", $cost); 2549 $salt = $this->createSalt(22, true); 2550 $hash = crypt($text, $prefix.$salt); 2551 if (is_string_empty($salt) || strlenb($hash)!=60) $hash = ""; 2552 break; 2553 case "sha256": $prefix = "$5y$"; 2554 $salt = $this->createSalt(32); 2555 $hash = "$prefix$salt".hash("sha256", $salt.$text); 2556 if (is_string_empty($salt) || strlenb($hash)!=100) $hash = ""; 2557 break; 2558 } 2559 return $hash; 2560 } 2561 2562 // Verify that text matches hash 2563 public function verifyHash($text, $algorithm, $hash) { 2564 $hashCalculated = ""; 2565 switch ($algorithm) { 2566 case "bcrypt": if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") { 2567 $hashCalculated = crypt($text, $hash); 2568 } 2569 break; 2570 case "sha256": if (substrb($hash, 0, 4)=="$5y$") { 2571 $prefix = "$5y$"; 2572 $salt = substrb($hash, 4, 32); 2573 $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text); 2574 } 2575 break; 2576 } 2577 return $this->verifyToken($hashCalculated, $hash); 2578 } 2579 2580 // Verify that token is not empty and identical, timing attack safe string comparison 2581 public function verifyToken($tokenExpected, $tokenReceived) { 2582 $ok = false; 2583 $lengthExpected = strlenb($tokenExpected); 2584 $lengthReceived = strlenb($tokenReceived); 2585 if ($lengthExpected!=0 && $lengthReceived!=0) { 2586 $ok = $lengthExpected==$lengthReceived; 2587 for ($i=0; $i<$lengthReceived; ++$i) { 2588 $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i]; 2589 } 2590 } 2591 return $ok; 2592 } 2593 2594 // Return meta data from raw data 2595 public function getMetaData($rawData, $key) { 2596 $value = ""; 2597 if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { 2598 $key = lcfirst($key); 2599 foreach ($this->getTextLines($parts[2]) as $line) { 2600 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2601 if (lcfirst($matches[1])==$key && !is_string_empty($matches[2])) { 2602 $value = $matches[2]; 2603 break; 2604 } 2605 } 2606 } 2607 } 2608 return $value; 2609 } 2610 2611 // Set meta data in raw data 2612 public function setMetaData($rawData, $key, $value) { 2613 if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { 2614 $found = false; 2615 $key = lcfirst($key); 2616 $rawDataMiddle = ""; 2617 foreach ($this->getTextLines($parts[2]) as $line) { 2618 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2619 if (lcfirst($matches[1])==$key) { 2620 $rawDataMiddle .= "$matches[1]: $value\n"; 2621 $found = true; 2622 continue; 2623 } 2624 } 2625 $rawDataMiddle .= $line; 2626 } 2627 if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; 2628 $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; 2629 } else { 2630 $rawDataNew = $rawData; 2631 } 2632 return $rawDataNew; 2633 } 2634 2635 // Remove meta data in raw data 2636 public function unsetMetaData($rawData, $key) { 2637 if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { 2638 $key = lcfirst($key); 2639 $rawDataMiddle = ""; 2640 foreach ($this->getTextLines($parts[2]) as $line) { 2641 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 2642 if (lcfirst($matches[1])==$key) continue; 2643 } 2644 $rawDataMiddle .= $line; 2645 } 2646 $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; 2647 } else { 2648 $rawDataNew = $rawData; 2649 } 2650 return $rawDataNew; 2651 } 2652 2653 // Return troubleshooting URL 2654 public function getTroubleshootingUrl() { 2655 return "https://datenstrom.se/yellow/help/troubleshooting"; 2656 } 2657 2658 // Detect server URL 2659 public function detectServerUrl() { 2660 $scheme = "http"; 2661 if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https"; 2662 if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https"; 2663 $address = $this->getServer("SERVER_NAME"); 2664 $port = $this->getServer("SERVER_PORT"); 2665 if ($port!=80 && $port!=443) $address .= ":$port"; 2666 $base = ""; 2667 if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; 2668 return "$scheme://$address$base/"; 2669 } 2670 2671 // Detect server location 2672 public function detectServerLocation() { 2673 if (isset($_SERVER["REQUEST_URI"])) { 2674 $location = $_SERVER["REQUEST_URI"]; 2675 $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); 2676 $location = $this->yellow->lookup->normalisePath($location); 2677 if (substru($location, 0, 1)!="/") $location = "/".$location; 2678 $separator = $this->getLocationArgumentsSeparator(); 2679 if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { 2680 $_SERVER["LOCATION"] = $location = $matches[1]; 2681 $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; 2682 foreach (explode("/", $matches[2]) as $token) { 2683 if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { 2684 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { 2685 $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); 2686 $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); 2687 $_REQUEST[$matches[1]] = $matches[2]; 2688 } 2689 } 2690 } 2691 } else { 2692 $_SERVER["LOCATION"] = $location; 2693 $_SERVER["LOCATION_ARGUMENTS"] = ""; 2694 } 2695 } 2696 return $this->getServer("LOCATION"); 2697 } 2698 2699 // Detect server sitename 2700 public function detectServerSitename() { 2701 $sitename = "Localhost"; 2702 if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) { 2703 $sitename = ucfirst($matches[2]); 2704 } 2705 return $sitename; 2706 } 2707 2708 // Detect server timezone 2709 public function detectServerTimezone() { 2710 $timezone = ini_get("date.timezone"); 2711 if (is_string_empty($timezone)) { 2712 if (PHP_OS=="Darwin") { 2713 if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; 2714 } else { 2715 if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1]; 2716 } 2717 } 2718 if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC"; 2719 return $timezone; 2720 } 2721 2722 // Detect server name, version and operating system 2723 public function detectServerInformation() { 2724 $name = "Unknown"; 2725 $version = "x.x.x"; 2726 $os = PHP_OS; 2727 if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { 2728 $name = $matches[1]; 2729 $version = $matches[2]; 2730 } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { 2731 $name = $matches[1]; 2732 } 2733 if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") { 2734 $name = "Built-in"; 2735 $version = PHP_VERSION; 2736 } 2737 if (PHP_OS=="Darwin") { 2738 $os = "Mac"; 2739 } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { 2740 $os = "Windows"; 2741 } 2742 return array($name, $version, $os); 2743 } 2744 2745 // Detect browser language 2746 public function detectBrowserLanguage($languages, $languageDefault) { 2747 $languageFound = $languageDefault; 2748 foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) { 2749 list($language, $dummy) = $this->getTextList($text, ";", 2); 2750 if (!is_string_empty($language) && in_array($language, $languages)) { 2751 $languageFound = $language; 2752 break; 2753 } 2754 } 2755 return $languageFound; 2756 } 2757 2758 // Detect terminal width and height 2759 public function detectTerminalInformation() { 2760 $width = $height = 0; 2761 if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { 2762 exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus); 2763 if ($returnStatus==0 && !is_array_empty($outputLines)) { 2764 $width = intval(end($outputLines)); 2765 } 2766 exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus); 2767 if ($returnStatus==0 && !is_array_empty($outputLines)) { 2768 $height = intval(end($outputLines)); 2769 } 2770 } else { 2771 exec("stty size", $outputLines, $returnStatus); 2772 if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) { 2773 $width = intval($matches[2]); 2774 $height = intval($matches[1]); 2775 } 2776 } 2777 return array($width, $height); 2778 } 2779 2780 // Detect image width, height, orientation and type for GIF/JPEG/PNG/SVG 2781 public function detectImageInformation($fileName, $fileType = "") { 2782 $width = $height = $orientation = 0; 2783 $type = ""; 2784 $fileHandle = @fopen($fileName, "rb"); 2785 if ($fileHandle) { 2786 if (is_string_empty($fileType)) $fileType = $this->getFileType($fileName); 2787 if ($fileType=="gif") { 2788 $dataSignature = fread($fileHandle, 6); 2789 $dataHeader = fread($fileHandle, 7); 2790 if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { 2791 $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); 2792 $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); 2793 $type = $fileType; 2794 } 2795 } elseif ($fileType=="jpeg" || $fileType=="jpg") { 2796 $dataBufferSizeMax = filesize($fileName); 2797 $dataBufferSize = min($dataBufferSizeMax, 4096); 2798 if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); 2799 $dataSignature = substrb($dataBuffer, 0, 2); 2800 if (!feof($fileHandle) && $dataSignature=="\xff\xd8") { 2801 for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { 2802 if ($dataBuffer[$pos]!="\xff") break; 2803 $dataMarker = $dataBuffer[$pos+1]; 2804 if ($dataMarker=="\xe1") { 2805 $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize); 2806 } 2807 if (($dataMarker>="\xc0" && $dataMarker<="\xc3") || 2808 ($dataMarker>="\xc5" && $dataMarker<="\xc7") || 2809 ($dataMarker>="\xc9" && $dataMarker<="\xcb") || 2810 ($dataMarker>="\xcd" && $dataMarker<="\xcf")) { 2811 $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); 2812 $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); 2813 $type = "jpeg"; 2814 break; 2815 } 2816 $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; 2817 while ($pos+$length+8>=$dataBufferSize) { 2818 if ($dataBufferSize==$dataBufferSizeMax) break; 2819 $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; 2820 $dataBufferSize += $dataBufferDiff; 2821 $dataBufferChunk = fread($fileHandle, $dataBufferDiff); 2822 if (feof($fileHandle) || $dataBufferChunk===false) { 2823 $dataBufferSize = 0; 2824 break; 2825 } 2826 $dataBuffer .= $dataBufferChunk; 2827 } 2828 } 2829 } 2830 } elseif ($fileType=="png") { 2831 $dataSignature = fread($fileHandle, 8); 2832 $dataHeader = fread($fileHandle, 16); 2833 if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { 2834 $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); 2835 $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); 2836 $type = $fileType; 2837 } 2838 } elseif ($fileType=="svg") { 2839 $dataBufferSizeMax = filesize($fileName); 2840 $dataBufferSize = min($dataBufferSizeMax, 4096); 2841 if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); 2842 if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { 2843 if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; 2844 if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; 2845 $type = $fileType; 2846 } 2847 } 2848 fclose($fileHandle); 2849 } 2850 return array($width, $height, $orientation, $type); 2851 } 2852 2853 // Return image orientation from Exif 2854 public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) { 2855 $orientation = 0; 2856 $dataSignature = substrb($dataBuffer, $pos, 6); 2857 if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) { 2858 $startPos = $pos+6; 2859 $bigEndian = $dataBuffer[$startPos]=="M"; 2860 $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian); 2861 $ifdStartPos = $startPos+$ifdOffset; 2862 $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; 2863 $pos = $ifdStartPos+2; 2864 while ($ifdCount && $pos+12<=$size) { 2865 $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian); 2866 $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian); 2867 if ($ifdTag==0x8769 && $ifdFormat==4) { 2868 $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian); 2869 $ifdStartPos = $startPos+$ifdOffset; 2870 $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; 2871 $pos = $ifdStartPos+2; 2872 continue; 2873 } 2874 if ($ifdTag==0x0112 && $ifdFormat==3) { 2875 $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian); 2876 break; 2877 } 2878 --$ifdCount; 2879 $pos += 12; 2880 } 2881 } 2882 return $orientation; 2883 } 2884 2885 // Return unsigned short value from buffer 2886 public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) { 2887 if ($bigEndian) { 2888 $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]); 2889 } else { 2890 $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); 2891 } 2892 return $value; 2893 } 2894 2895 // Return unsigned long value from buffer 2896 public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) { 2897 if ($bigEndian) { 2898 $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) + 2899 (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]); 2900 } else { 2901 $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) + 2902 (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); 2903 } 2904 return $value; 2905 } 2906 2907 // Return possible values 2908 public function enumerate($action, $context = "") { 2909 $values = array(); 2910 foreach ($this->yellow->extension->data as $key=>$value) { 2911 if (method_exists($value["object"], "onEnumerate")) { 2912 $output = $value["object"]->onEnumerate($action, $context); 2913 if (!is_null($output)) { 2914 $values = array_merge($values, is_array($output) ? $output : array($output)); 2915 } 2916 } 2917 } 2918 if ($action=="email") { 2919 foreach ($this->yellow->user->settings as $userKey=>$userValue) { 2920 array_push($values, $userKey); 2921 } 2922 } elseif ($action=="language") { 2923 foreach ($this->yellow->language->settings as $languageKey=>$languageValue) { 2924 array_push($values, $languageKey); 2925 } 2926 } elseif ($action=="theme") { 2927 $path = $this->yellow->system->get("coreThemeDirectory"); 2928 foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) { 2929 array_push($values, substru($entry, 0, -4)); 2930 } 2931 } 2932 usort($values, "strnatcasecmp"); 2933 return $values; 2934 } 2935 2936 // Send email message 2937 public function mail($action, $headers, $message) { 2938 $statusCode = 0; 2939 foreach ($this->yellow->extension->data as $key=>$value) { 2940 if (method_exists($value["object"], "onMail")) { 2941 $statusCode = $value["object"]->onMail($action, $headers, $message); 2942 if ($statusCode!=0) break; 2943 } 2944 } 2945 if ($statusCode==0) { 2946 $text = $this->yellow->lookup->normaliseHeaders($headers, "mime"); 2947 $to = $subject = $remaining = $key = ""; 2948 foreach (preg_split("/\r\n/", $text) as $line) { 2949 if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) { 2950 $key = $matches[1]; 2951 $fragment = $matches[2]; 2952 } else { 2953 $fragment = $line; 2954 } 2955 if ($key=="To") { $to .= $fragment; continue; } 2956 if ($key=="Subject") { $subject .= $fragment; continue; } 2957 $remaining .= $line."\r\n"; 2958 } 2959 $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500; 2960 } 2961 return $statusCode==200; 2962 } 2963 2964 // Write information to log file 2965 public function log($action, $message) { 2966 $statusCode = 0; 2967 foreach ($this->yellow->extension->data as $key=>$value) { 2968 if (method_exists($value["object"], "onLog")) { 2969 $statusCode = $value["object"]->onLog($action, $message); 2970 if ($statusCode!=0) break; 2971 } 2972 } 2973 if ($statusCode==0 && $this->yellow->system->get("coreWebsiteFile")!="none") { 2974 $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; 2975 $this->appendFile($this->yellow->system->get("coreServerInstallDirectory"). 2976 $this->yellow->system->get("coreExtensionDirectory"). 2977 $this->yellow->system->get("coreWebsiteFile"), $line); 2978 } 2979 } 2980 2981 // Start timer 2982 public function timerStart(&$time) { 2983 $time = microtime(true); 2984 } 2985 2986 // Stop timer and calculate elapsed time in milliseconds 2987 public function timerStop(&$time) { 2988 $time = intval((microtime(true)-$time) * 1000); 2989 } 2990 2991 // Check if there are location arguments in current HTTP request 2992 public function isLocationArguments($location = "") { 2993 if (is_string_empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); 2994 $separator = $this->getLocationArgumentsSeparator(); 2995 return preg_match("/[^\/]+$separator.*$/", $location); 2996 } 2997 2998 // Check if there are pagination arguments in current HTTP request 2999 public function isLocationArgumentsPagination($location) { 3000 $separator = $this->getLocationArgumentsSeparator(); 3001 return preg_match("/^(.*\/)?page$separator\d+$/", $location); 3002 } 3003 3004 // Check if unmodified since last HTTP request 3005 public function isNotModified($lastModifiedFormatted) { 3006 return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; 3007 } 3008 } 3009 3010 class YellowPage { 3011 public $yellow; // access to API 3012 public $scheme; // server scheme 3013 public $address; // server address 3014 public $base; // base location 3015 public $location; // page location 3016 public $fileName; // content file name 3017 public $rawData; // raw data of page 3018 public $metaDataOffsetBytes; // meta data offset 3019 public $metaData; // meta data 3020 public $pageCollections; // additional pages 3021 public $sharedPages; // shared pages 3022 public $headerData; // response header 3023 public $outputData; // response output 3024 public $parser; // content parser 3025 public $parserData; // content data of page 3026 public $statusCode; // status code 3027 public $errorMessage; // error message 3028 public $lastModified; // last modification date 3029 public $available; // page is available? (boolean) 3030 public $visible; // page is visible location? (boolean) 3031 public $cacheable; // page is cacheable? (boolean) 3032 3033 public function __construct($yellow) { 3034 $this->yellow = $yellow; 3035 $this->scheme = ""; 3036 $this->address = ""; 3037 $this->base = ""; 3038 $this->location = ""; 3039 $this->fileName = ""; 3040 $this->metaData = new YellowArray(); 3041 $this->pageCollections = array(); 3042 $this->sharedPages = array(); 3043 $this->headerData = array(); 3044 } 3045 3046 // Set request information 3047 public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) { 3048 $this->scheme = $scheme; 3049 $this->address = $address; 3050 $this->base = $base; 3051 $this->location = $location; 3052 $this->fileName = $fileName; 3053 $this->cacheable = $cacheable; 3054 } 3055 3056 // Parse page meta 3057 public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") { 3058 $this->rawData = $rawData; 3059 $this->parser = null; 3060 $this->parserData = ""; 3061 $this->statusCode = $statusCode; 3062 $this->errorMessage = $errorMessage; 3063 $this->lastModified = 0; 3064 $this->available = true; 3065 $this->visible = true; 3066 $this->parseMetaData(); 3067 } 3068 3069 // Parse page meta update 3070 public function parseMetaUpdate() { 3071 if ($this->statusCode==0) { 3072 $this->rawData = $this->yellow->toolbox->readFile($this->fileName); 3073 $this->statusCode = 200; 3074 $this->parseMetaData(); 3075 } 3076 } 3077 3078 // Parse page meta data 3079 public function parseMetaData() { 3080 $this->metaData = new YellowArray(); 3081 $this->metaDataOffsetBytes = 0; 3082 if (!is_null($this->rawData)) { 3083 $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); 3084 $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"))); 3085 $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); 3086 $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status")); 3087 $this->parseMetaDataShared(); 3088 $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? 3089 $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); 3090 if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); 3091 if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); 3092 if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); 3093 if ($this->yellow->lookup->isRootLocation($this->location) || !is_readable($this->fileName)) $this->available = false; 3094 if ($this->get("status")=="shared") $this->available = false; 3095 if ($this->get("status")=="unlisted") $this->visible = false; 3096 } else { 3097 $this->set("size", $this->yellow->toolbox->getFileSize($this->fileName)); 3098 $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); 3099 $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); 3100 $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); 3101 if (!$this->yellow->lookup->isFileLocation($this->location)) $this->available = false; 3102 } 3103 foreach ($this->yellow->extension->data as $key=>$value) { 3104 if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this); 3105 } 3106 } 3107 3108 // Parse page meta data from raw data 3109 public function parseMetaDataRaw($defaultKeys) { 3110 foreach ($defaultKeys as $key) { 3111 $value = $this->yellow->system->get($key); 3112 if (!is_string_empty($key) && !is_string_empty($value)) $this->set($key, $value); 3113 } 3114 if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { 3115 $this->metaDataOffsetBytes = strlenb($parts[0]); 3116 foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { 3117 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { 3118 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) $this->set($matches[1], $matches[2]); 3119 } 3120 } 3121 } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { 3122 $this->metaDataOffsetBytes = strlenb($parts[0]); 3123 $this->set("title", $parts[2]); 3124 } 3125 } 3126 3127 // Parse page meta data for shared pages 3128 public function parseMetaDataShared() { 3129 $this->sharedPages["main"] = $this; 3130 if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) { 3131 foreach ($this->yellow->content->getShared($this->location) as $page) { 3132 $this->sharedPages[basename($page->location)] = $page; 3133 $page->sharedPages["main"] = $this; 3134 } 3135 } 3136 if ($this->yellow->lookup->isSharedLocation($this->location)) { 3137 $this->set("status", "shared"); 3138 } 3139 } 3140 3141 // Parse page content on demand 3142 public function parseContent() { 3143 if (!is_null($this->rawData) && !is_object($this->parser)) { 3144 if ($this->yellow->extension->isExisting($this->get("parser"))) { 3145 $value = $this->yellow->extension->data[$this->get("parser")]; 3146 if (method_exists($value["object"], "onParseContentRaw")) { 3147 $this->parser = $value["object"]; 3148 $this->parserData = $this->getContentRaw(); 3149 $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); 3150 foreach ($this->yellow->extension->data as $key=>$value) { 3151 if (method_exists($value["object"], "onParseContentHtml")) { 3152 $output = $value["object"]->onParseContentHtml($this, $this->parserData); 3153 if (!is_null($output)) $this->parserData = $output; 3154 } 3155 } 3156 } 3157 } else { 3158 $this->parserData = $this->getContentRaw(); 3159 $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData); 3160 } 3161 if (!$this->isExisting("description")) { 3162 $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); 3163 $this->set("description", !is_string_empty($description) ? $description : $this->get("title")); 3164 } 3165 if ($this->yellow->system->get("coreDebugMode")>=2) { 3166 echo "YellowPage::parseContent file:".$this->fileName." parser:".$this->get("parser")."<br />\n"; 3167 } 3168 } 3169 } 3170 3171 // Parse page content element, experimental 3172 public function parseContentElement($name, $text, $attributes, $type) { 3173 $output = null; 3174 foreach ($this->yellow->extension->data as $key=>$value) { 3175 if (method_exists($value["object"], "onParseContentElement")) { 3176 $output = $value["object"]->onParseContentElement($this, $name, $text, $attributes, $type); 3177 if (!is_null($output)) break; 3178 } 3179 } 3180 if (is_null($output)) { 3181 if ($name=="yellow" && $type=="inline" && $text=="error") { 3182 $output = $this->errorMessage; 3183 } 3184 } 3185 if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) { 3186 echo "YellowPage::parseContentElement name:$name type:$type<br />\n"; 3187 } 3188 return $output; 3189 } 3190 3191 // Parse page 3192 public function parsePage() { 3193 $this->parsePageLayout($this->get("layout")); 3194 if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); 3195 if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); 3196 if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); 3197 $theme = $this->yellow->lookup->normaliseName($this->get("theme")); 3198 if (!is_file($this->yellow->system->get("coreThemeDirectory").$theme.".css") && 3199 !in_array($theme, $this->yellow->toolbox->enumerate("theme"))) { 3200 $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); 3201 } 3202 if (!$this->yellow->language->isExisting($this->get("language"))) { 3203 $this->error(500, "Language '".$this->get("language")."' does not exist!"); 3204 } 3205 if (!is_object($this->parser)) { 3206 $this->error(500, "Parser '".$this->get("parser")."' does not exist!"); 3207 } 3208 if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) { 3209 $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!"); 3210 } 3211 if ($this->yellow->lookup->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { 3212 $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); 3213 $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); 3214 $this->status(301, $location); 3215 } 3216 if ($this->yellow->lookup->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { 3217 $this->error(404); 3218 } 3219 if ($this->isExisting("pageClean")) $this->outputData = null; 3220 foreach ($this->yellow->extension->data as $key=>$value) { 3221 if (method_exists($value["object"], "onParsePageOutput")) { 3222 $output = $value["object"]->onParsePageOutput($this, $this->outputData); 3223 if (!is_null($output)) $this->outputData = $output; 3224 } 3225 } 3226 } 3227 3228 // Parse page layout 3229 public function parsePageLayout($name) { 3230 $this->outputData = null; 3231 foreach ($this->yellow->extension->data as $key=>$value) { 3232 if (method_exists($value["object"], "onParsePageLayout")) { 3233 $value["object"]->onParsePageLayout($this, $name); 3234 } 3235 } 3236 if (is_null($this->outputData)) { 3237 ob_start(); 3238 $this->includeLayout($name); 3239 $this->outputData = ob_get_contents(); 3240 ob_end_clean(); 3241 } 3242 } 3243 3244 // Include page layout 3245 public function includeLayout($name) { 3246 $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; 3247 $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). 3248 $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; 3249 if (is_file($fileNameLayoutTheme)) { 3250 if ($this->yellow->system->get("coreDebugMode")>=2) { 3251 echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br />\n"; 3252 } 3253 $this->setLastModified(filemtime($fileNameLayoutTheme)); 3254 require($fileNameLayoutTheme); 3255 } elseif (is_file($fileNameLayoutNormal)) { 3256 if ($this->yellow->system->get("coreDebugMode")>=2) { 3257 echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br />\n"; 3258 } 3259 $this->setLastModified(filemtime($fileNameLayoutNormal)); 3260 require($fileNameLayoutNormal); 3261 } else { 3262 $this->error(500, "Layout '$name' does not exist!"); 3263 echo "Layout error<br />\n"; 3264 } 3265 } 3266 3267 // Set page setting 3268 public function set($key, $value) { 3269 $this->metaData[$key] = $value; 3270 } 3271 3272 // Return page setting 3273 public function get($key) { 3274 return $this->isExisting($key) ? $this->metaData[$key] : ""; 3275 } 3276 3277 // Return page setting, HTML encoded 3278 public function getHtml($key) { 3279 return htmlspecialchars($this->get($key)); 3280 } 3281 3282 // Return page setting as language specific date 3283 public function getDate($key, $format = "") { 3284 if (!is_string_empty($format)) { 3285 $format = $this->yellow->language->getText($format); 3286 } else { 3287 $format = $this->yellow->language->getText("coreDateFormatMedium"); 3288 } 3289 return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); 3290 } 3291 3292 // Return page setting as language specific date, HTML encoded 3293 public function getDateHtml($key, $format = "") { 3294 return htmlspecialchars($this->getDate($key, $format)); 3295 } 3296 3297 // Return page setting as language specific date, relative to today 3298 public function getDateRelative($key, $format = "", $daysLimit = 30) { 3299 if (!is_string_empty($format)) { 3300 $format = $this->yellow->language->getText($format); 3301 } else { 3302 $format = $this->yellow->language->getText("coreDateFormatMedium"); 3303 } 3304 return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); 3305 } 3306 3307 // Return page setting as language specific date, relative to today, HTML encoded 3308 public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { 3309 return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); 3310 } 3311 3312 // Return page setting as date 3313 public function getDateFormatted($key, $format) { 3314 return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); 3315 } 3316 3317 // Return page setting as date, HTML encoded 3318 public function getDateFormattedHtml($key, $format) { 3319 return htmlspecialchars($this->getDateFormatted($key, $format)); 3320 } 3321 3322 // Return page content data, raw format 3323 public function getContentRaw() { 3324 $this->parseMetaUpdate(); 3325 return substrb($this->rawData, $this->metaDataOffsetBytes); 3326 } 3327 3328 // Return page content data, HTML encoded or raw format 3329 public function getContentHtml() { 3330 $this->parseContent(); 3331 return $this->parserData; 3332 } 3333 3334 // Return page extra data, HTML encoded 3335 public function getExtraHtml($name) { 3336 $output = ""; 3337 foreach ($this->yellow->extension->data as $key=>$value) { 3338 if (method_exists($value["object"], "onParsePageExtra")) { 3339 $outputExtension = $value["object"]->onParsePageExtra($this, $name); 3340 if (!is_null($outputExtension)) $output .= $outputExtension; 3341 } 3342 } 3343 if ($name=="header") { 3344 $assetLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreAssetLocation"); 3345 $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; 3346 if (is_file($fileNameTheme)) { 3347 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".css"; 3348 $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$fileLocation\" />\n"; 3349 } 3350 $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; 3351 if (is_file($fileNameScript)) { 3352 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".js"; 3353 $output .= "<script type=\"text/javascript\" src=\"$fileLocation\"></script>\n"; 3354 } 3355 $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; 3356 if (is_file($fileNameFavicon)) { 3357 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".png"; 3358 $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$fileLocation\" />\n"; 3359 } 3360 } 3361 return $output; 3362 } 3363 3364 // Return parent page, null if none 3365 public function getParent() { 3366 $parentLocation = $this->yellow->content->getParentLocation($this->location); 3367 return $this->yellow->content->find($parentLocation); 3368 } 3369 3370 // Return top-level parent page, null if none 3371 public function getParentTop($homeFallback = false) { 3372 $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); 3373 if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { 3374 $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); 3375 } 3376 return $this->yellow->content->find($parentTopLocation); 3377 } 3378 3379 // Return page collection with pages on the same level 3380 public function getSiblings($showInvisible = false) { 3381 $parentLocation = $this->yellow->content->getParentLocation($this->location); 3382 return $this->yellow->content->getChildren($parentLocation, $showInvisible); 3383 } 3384 3385 // Return page collection with child pages 3386 public function getChildren($showInvisible = false) { 3387 return $this->yellow->content->getChildren($this->location, $showInvisible); 3388 } 3389 3390 // Return page collection with child pages recursively 3391 public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { 3392 return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); 3393 } 3394 3395 // Set page collection with additional pages 3396 public function setPages($key, $pages) { 3397 $this->pageCollections[$key] = $pages; 3398 } 3399 3400 // Return page collection with additional pages 3401 public function getPages($key) { 3402 return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow); 3403 } 3404 3405 // Set shared page 3406 public function setPage($key, $page) { 3407 $this->sharedPages[$key] = $page; 3408 } 3409 3410 // Return shared page 3411 public function getPage($key) { 3412 return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow); 3413 } 3414 3415 // Return page URL 3416 public function getUrl($canonicalUrl = false) { 3417 if ($canonicalUrl) { 3418 $scheme = $this->yellow->system->get("coreServerScheme"); 3419 $address = $this->yellow->system->get("coreServerAddress"); 3420 $location = $this->yellow->system->get("coreServerBase").$this->location; 3421 } else { 3422 $scheme = $this->scheme; 3423 $address = $this->address; 3424 $location = $this->base.$this->location; 3425 } 3426 return "$scheme://$address$location"; 3427 } 3428 3429 // Return page base 3430 public function getBase($multiLanguage = false) { 3431 return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; 3432 } 3433 3434 // Return page location 3435 public function getLocation($absoluteLocation = false) { 3436 return $absoluteLocation ? $this->base.$this->location : $this->location; 3437 } 3438 3439 // Set page request argument 3440 public function setRequest($key, $value) { 3441 $_REQUEST[$key] = $value; 3442 } 3443 3444 // Return page request argument 3445 public function getRequest($key) { 3446 return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; 3447 } 3448 3449 // Return page request argument, HTML encoded 3450 public function getRequestHtml($key) { 3451 return htmlspecialchars($this->getRequest($key)); 3452 } 3453 3454 // Set page response header 3455 public function setHeader($key, $value) { 3456 $this->headerData[$key] = $value; 3457 } 3458 3459 // Return page response header 3460 public function getHeader($key) { 3461 return $this->isHeader($key) ? $this->headerData[$key] : ""; 3462 } 3463 3464 // Set page response output 3465 public function setOutput($output) { 3466 $this->outputData = $output; 3467 } 3468 3469 // Return page modification date, Unix time or HTTP format 3470 public function getModified($httpFormat = false) { 3471 $modified = strtotime($this->get("modified")); 3472 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; 3473 } 3474 3475 // Set last modification date, Unix time 3476 public function setLastModified($modified) { 3477 $this->lastModified = max($this->lastModified, $modified); 3478 } 3479 3480 // Return last modification date, Unix time or HTTP format 3481 public function getLastModified($httpFormat = false) { 3482 $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(), 3483 $this->yellow->language->getModified(), $this->yellow->extension->getModified()); 3484 foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified()); 3485 foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified()); 3486 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; 3487 } 3488 3489 // Return raw data for error page 3490 public function getRawDataError() { 3491 $statusCode = $this->statusCode; 3492 $fileNameError = $this->getFileNameError(); 3493 $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")); 3494 if (is_file($fileNameError)) { 3495 $rawData = $this->yellow->toolbox->readFile($fileNameError); 3496 } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) { 3497 $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n"; 3498 $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError); 3499 } else { 3500 $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; 3501 $rawData .= "Layout:error\n---\n".$this->errorMessage; 3502 } 3503 return $rawData; 3504 } 3505 3506 // Return file name for error page 3507 public function getFileNameError() { 3508 $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/"; 3509 $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile"); 3510 return str_replace("(.*)", $this->statusCode, $fileNameError); 3511 } 3512 3513 // Return page status code, number or HTTP format 3514 public function getStatusCode($httpFormat = false) { 3515 $statusCode = $this->statusCode; 3516 if ($httpFormat) { 3517 $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); 3518 if (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage; 3519 } 3520 return $statusCode; 3521 } 3522 3523 // Respond with status code, no page content 3524 public function status($statusCode, $location = "") { 3525 if ($statusCode>0 && !$this->isExisting("pageClean")) { 3526 $this->statusCode = $statusCode; 3527 $this->lastModified = 0; 3528 $this->headerData = array(); 3529 if (!is_string_empty($location)) { 3530 $this->setHeader("Location", $location); 3531 $this->setHeader("Cache-Control", "no-cache, no-store"); 3532 } 3533 $this->set("pageClean", (string)$statusCode); 3534 } 3535 } 3536 3537 // Respond with error page 3538 public function error($statusCode, $errorMessage = "") { 3539 if ($statusCode>=400 && is_string_empty($this->errorMessage)) { 3540 $this->statusCode = $statusCode; 3541 $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage; 3542 } 3543 } 3544 3545 // Check if page is available 3546 public function isAvailable() { 3547 return $this->available; 3548 } 3549 3550 // Check if page is visible 3551 public function isVisible() { 3552 return $this->visible; 3553 } 3554 3555 // Check if page is within current HTTP request 3556 public function isActive() { 3557 return $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); 3558 } 3559 3560 // Check if page is cacheable 3561 public function isCacheable() { 3562 return $this->cacheable; 3563 } 3564 3565 // Check if page with error 3566 public function isError() { 3567 return $this->statusCode>=400; 3568 } 3569 3570 // Check if page setting exists 3571 public function isExisting($key) { 3572 return isset($this->metaData[$key]); 3573 } 3574 3575 // Check if request argument exists 3576 public function isRequest($key) { 3577 return isset($_REQUEST[$key]); 3578 } 3579 3580 // Check if response header exists 3581 public function isHeader($key) { 3582 return isset($this->headerData[$key]); 3583 } 3584 3585 // Check if shared page exists 3586 public function isPage($key) { 3587 return isset($this->sharedPages[$key]); 3588 } 3589 } 3590 3591 class YellowPageCollection extends ArrayObject { 3592 public $yellow; // access to API 3593 public $filterValue; // current page filter value 3594 public $paginationNumber; // current page number in pagination 3595 public $paginationCount; // highest page number in pagination 3596 3597 public function __construct($yellow) { 3598 parent::__construct(array()); 3599 $this->yellow = $yellow; 3600 } 3601 3602 // Append page to end of page collection 3603 #[\ReturnTypeWillChange] 3604 public function append($page) { 3605 parent::append($page); 3606 } 3607 3608 // Prepend page to start of page collection 3609 #[\ReturnTypeWillChange] 3610 public function prepend($page) { 3611 $array = $this->getArrayCopy(); 3612 array_unshift($array, $page); 3613 $this->exchangeArray($array); 3614 } 3615 3616 // Remove page from page collection 3617 public function remove($page): YellowPageCollection { 3618 $array = array(); 3619 $location = $page->location; 3620 foreach ($this->getArrayCopy() as $page) { 3621 if ($page->location!=$location) array_push($array, $page); 3622 } 3623 $this->exchangeArray($array); 3624 return $this; 3625 } 3626 3627 // Filter page collection by page setting 3628 public function filter($key, $value, $exactMatch = true): YellowPageCollection { 3629 $array = array(); 3630 $value = str_replace(" ", "-", strtoloweru($value)); 3631 $valueLength = strlenu($value); 3632 $this->filterValue = ""; 3633 foreach ($this->getArrayCopy() as $page) { 3634 if ($page->isExisting($key)) { 3635 foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { 3636 $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; 3637 if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { 3638 if (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); 3639 array_push($array, $page); 3640 break; 3641 } 3642 } 3643 } 3644 } 3645 $this->exchangeArray($array); 3646 return $this; 3647 } 3648 3649 // Filter page collection by location or file 3650 public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection { 3651 $array = array(); 3652 $this->filterValue = $regex; 3653 foreach ($this->getArrayCopy() as $page) { 3654 if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page); 3655 } 3656 $this->exchangeArray($array); 3657 return $this; 3658 } 3659 3660 // Sort page collection by settings similarity 3661 public function similar($page): YellowPageCollection { 3662 $location = $page->location; 3663 $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); 3664 $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); 3665 if (!is_array_empty($tokens)) { 3666 $array = array(); 3667 foreach ($this->getArrayCopy() as $page) { 3668 $sortScore = 0; 3669 foreach ($tokens as $token) { 3670 if (stristr($page->get("title"), $token)) $sortScore += 50; 3671 if (stristr($page->get("tag"), $token)) $sortScore += 5; 3672 if (stristr($page->get("author"), $token)) $sortScore += 2; 3673 } 3674 if ($page->location!=$location) { 3675 $page->set("sortScore", $sortScore); 3676 array_push($array, $page); 3677 } 3678 } 3679 $this->exchangeArray($array); 3680 $this->sort("modified", false)->sort("sortScore", false); 3681 } 3682 return $this; 3683 } 3684 3685 // Sort page collection by page setting 3686 public function sort($key, $ascendingOrder = true): YellowPageCollection { 3687 $array = $this->getArrayCopy(); 3688 $sortIndex = 0; 3689 $sortKeys = array(); 3690 foreach ($array as $page) { 3691 $sortKeys[$page->location] = $page->get($key)." ".++$sortIndex; 3692 } 3693 $callback = function ($a, $b) use ($sortKeys, $ascendingOrder) { 3694 return $ascendingOrder ? 3695 strnatcasecmp($sortKeys[$a->location], $sortKeys[$b->location]) : 3696 strnatcasecmp($sortKeys[$b->location], $sortKeys[$a->location]); 3697 }; 3698 usort($array, $callback); 3699 $this->exchangeArray($array); 3700 return $this; 3701 } 3702 3703 // Group page collection by page setting, return array with multiple collections 3704 public function group($key, $ascendingOrder = true, $format = ""): array { 3705 $array = array(); 3706 $groupByInitial = $format=="initial"; 3707 $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial"; 3708 foreach ($this->getIterator() as $page) { 3709 if ($page->isExisting($key)) { 3710 foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) { 3711 if ($groupByInitial) { 3712 $group = strtoupperu(substru($group, 0, 1)); 3713 } elseif ($groupByDate) { 3714 $group = $this->yellow->language->getDateFormatted(strtotime($group), $format); 3715 } 3716 if (!is_string_empty($group)) { 3717 if (!isset($array[$group])) { 3718 $groupSearch = strtoloweru($group); 3719 foreach (array_keys($array) as $groupFound) { 3720 if (strtoloweru($groupFound)==$groupSearch) { 3721 $group = $groupFound; 3722 break; 3723 } 3724 } 3725 if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow); 3726 } 3727 $array[$group]->append($page); 3728 } 3729 } 3730 } 3731 } 3732 $callbackString = function ($a, $b) use ($ascendingOrder) { 3733 return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a); 3734 }; 3735 $callbackCollection = function ($a, $b) use ($ascendingOrder) { 3736 return $ascendingOrder ? count($a)-count($b) : count($b)-count($a); 3737 }; 3738 if ($format!="count") { 3739 uksort($array, $callbackString); 3740 } else { 3741 uasort($array, $callbackCollection); 3742 } 3743 return $array; 3744 } 3745 3746 // Calculate union, merge page collection 3747 public function merge($input): YellowPageCollection { 3748 $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); 3749 return $this; 3750 } 3751 3752 // Calculate intersection, remove pages that are not present in another page collection 3753 public function intersect($input): YellowPageCollection { 3754 $callback = function ($a, $b) { 3755 return strcmp($a->location, $b->location); 3756 }; 3757 $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); 3758 return $this; 3759 } 3760 3761 // Calculate difference, remove pages that are present in another page collection 3762 public function diff($input): YellowPageCollection { 3763 $callback = function ($a, $b) { 3764 return strcmp($a->location, $b->location); 3765 }; 3766 $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); 3767 return $this; 3768 } 3769 3770 // Limit the number of pages in page collection 3771 public function limit($pagesMax): YellowPageCollection { 3772 $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); 3773 return $this; 3774 } 3775 3776 // Reverse page collection 3777 public function reverse(): YellowPageCollection { 3778 $this->exchangeArray(array_reverse($this->getArrayCopy())); 3779 return $this; 3780 } 3781 3782 // Randomize page collection 3783 public function shuffle(): YellowPageCollection { 3784 $array = $this->getArrayCopy(); 3785 shuffle($array); 3786 $this->exchangeArray($array); 3787 return $this; 3788 } 3789 3790 // Paginate page collection 3791 public function paginate($limit): YellowPageCollection { 3792 if (!$this->isPagination() && $limit!=0) { 3793 $this->paginationNumber = 1; 3794 $this->paginationCount = ceil($this->count() / $limit); 3795 if ($this->yellow->page->isRequest("page")) { 3796 $this->paginationNumber = intval($this->yellow->page->getRequest("page")); 3797 } 3798 if ($this->paginationNumber<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; 3799 if ($this->paginationNumber) { 3800 $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit)); 3801 } else { 3802 $this->yellow->page->error(404); 3803 } 3804 } 3805 return $this; 3806 } 3807 3808 // Return current page number in pagination 3809 public function getPaginationNumber() { 3810 return $this->paginationNumber; 3811 } 3812 3813 // Return highest page number in pagination 3814 public function getPaginationCount() { 3815 return $this->paginationCount; 3816 } 3817 3818 // Return location for a page in pagination 3819 public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { 3820 $location = $locationArguments = ""; 3821 if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { 3822 $location = $this->yellow->page->getLocation($absoluteLocation); 3823 $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); 3824 } 3825 return $location.$locationArguments; 3826 } 3827 3828 // Return location for previous page in pagination 3829 public function getPaginationPrevious($absoluteLocation = true) { 3830 $pageNumber = $this->paginationNumber-1; 3831 return $this->getPaginationLocation($absoluteLocation, $pageNumber); 3832 } 3833 3834 // Return location for next page in pagination 3835 public function getPaginationNext($absoluteLocation = true) { 3836 $pageNumber = $this->paginationNumber+1; 3837 return $this->getPaginationLocation($absoluteLocation, $pageNumber); 3838 } 3839 3840 // Return current page number in collection 3841 public function getPageNumber($page) { 3842 $pageNumber = 0; 3843 foreach ($this->getIterator() as $key=>$value) { 3844 if ($page->getLocation()==$value->getLocation()) { 3845 $pageNumber = $key+1; 3846 break; 3847 } 3848 } 3849 return $pageNumber; 3850 } 3851 3852 // Return page in collection, null if none 3853 public function getPage($pageNumber = 1) { 3854 return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; 3855 } 3856 3857 // Return previous page in collection, null if none 3858 public function getPagePrevious($page) { 3859 $pageNumber = $this->getPageNumber($page)-1; 3860 return $this->getPage($pageNumber); 3861 } 3862 3863 // Return next page in collection, null if none 3864 public function getPageNext($page) { 3865 $pageNumber = $this->getPageNumber($page)+1; 3866 return $this->getPage($pageNumber); 3867 } 3868 3869 // Return current page filter 3870 public function getFilter() { 3871 return $this->filterValue; 3872 } 3873 3874 // Return page collection modification date, Unix time or HTTP format 3875 public function getModified($httpFormat = false) { 3876 $modified = 0; 3877 foreach ($this->getIterator() as $page) { 3878 $modified = max($modified, $page->getModified()); 3879 } 3880 return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; 3881 } 3882 3883 // Check if there is a pagination 3884 public function isPagination() { 3885 return $this->paginationCount>1; 3886 } 3887 3888 // Check if page collection is empty 3889 public function isEmpty() { 3890 return empty($this->getArrayCopy()); 3891 } 3892 } 3893 3894 class YellowArray extends ArrayObject { 3895 public function __construct($array = []) { 3896 parent::__construct($array); 3897 } 3898 3899 // Set array element 3900 public function set($key, $value) { 3901 $this->offsetSet($key, $value); 3902 } 3903 3904 // Return array element 3905 public function get($key) { 3906 return $this->offsetExists($key) ? $this->offsetGet($key) : ""; 3907 } 3908 3909 // Check if array element exists 3910 public function isExisting($key) { 3911 return $this->offsetExists($key); 3912 } 3913 3914 // Return array element 3915 #[\ReturnTypeWillChange] 3916 public function offsetGet($key) { 3917 if (is_string($key)) $key = lcfirst($key); 3918 return parent::offsetGet($key); 3919 } 3920 3921 // Set array element 3922 #[\ReturnTypeWillChange] 3923 public function offsetSet($key, $value) { 3924 if (is_string($key)) $key = lcfirst($key); 3925 parent::offsetSet($key, $value); 3926 } 3927 3928 // Remove array element 3929 #[\ReturnTypeWillChange] 3930 public function offsetUnset($key) { 3931 if (is_string($key)) $key = lcfirst($key); 3932 parent::offsetUnset($key); 3933 } 3934 3935 // Check if array element exists 3936 #[\ReturnTypeWillChange] 3937 public function offsetExists($key) { 3938 if (is_string($key)) $key = lcfirst($key); 3939 return parent::offsetExists($key); 3940 } 3941 3942 // Check if array is empty 3943 public function isEmpty() { 3944 return empty($this->getArrayCopy()); 3945 } 3946 } 3947 3948 // Make string lowercase, UTF-8 compatible 3949 function strtoloweru() { 3950 return call_user_func_array("mb_strtolower", func_get_args()); 3951 } 3952 3953 // Make string uppercase, UTF-8 compatible 3954 function strtoupperu() { 3955 return call_user_func_array("mb_strtoupper", func_get_args()); 3956 } 3957 3958 // Return string length, UTF-8 characters 3959 function strlenu() { 3960 return call_user_func_array("mb_strlen", func_get_args()); 3961 } 3962 3963 // Return string length, bytes 3964 function strlenb() { 3965 return call_user_func_array("strlen", func_get_args()); 3966 } 3967 3968 // Return string position of first match, UTF-8 characters 3969 function strposu() { 3970 return call_user_func_array("mb_strpos", func_get_args()); 3971 } 3972 3973 // Return string position of first match, bytes 3974 function strposb() { 3975 return call_user_func_array("strpos", func_get_args()); 3976 } 3977 3978 // Return string position of last match, UTF-8 characters 3979 function strrposu() { 3980 return call_user_func_array("mb_strrpos", func_get_args()); 3981 } 3982 3983 // Return string position of last match, bytes 3984 function strrposb() { 3985 return call_user_func_array("strrpos", func_get_args()); 3986 } 3987 3988 // Return part of a string, UTF-8 characters 3989 function substru() { 3990 return call_user_func_array("mb_substr", func_get_args()); 3991 } 3992 3993 // Return part of a string, bytes 3994 function substrb() { 3995 return call_user_func_array("substr", func_get_args()); 3996 } 3997 3998 // Check if string is empty 3999 function is_string_empty($string) { 4000 return is_null($string) || $string===""; 4001 } 4002 4003 // Check if array is empty 4004 function is_array_empty($array) { 4005 return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy())); 4006 }