Textpattern | PHP Cross Reference | Content Management Systems |
Description: Asset Base Extended by CSS, Form and Page.
1 <?php 2 3 /* 4 * Textpattern Content Management System 5 * https://textpattern.com/ 6 * 7 * Copyright (C) 2020 The Textpattern Development Team 8 * 9 * This file is part of Textpattern. 10 * 11 * Textpattern is free software; you can redistribute it and/or 12 * modify it under the terms of the GNU General Public License 13 * as published by the Free Software Foundation, version 2. 14 * 15 * Textpattern is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with Textpattern. If not, see <https://www.gnu.org/licenses/>. 22 */ 23 24 /** 25 * Asset Base 26 * 27 * Extended by CSS, Form and Page. 28 * 29 * @since 4.7.0 30 * @package Skin 31 */ 32 33 namespace Textpattern\Skin; 34 35 abstract class AssetBase extends CommonBase implements AssetInterface 36 { 37 /** 38 * The directory in the themes folder in which assets of a particular type can be found. 39 * 40 * @var string Directory name. 41 * @see setDir(), getDir(). 42 */ 43 44 protected static $dir; 45 46 /** 47 * Asset related default subdirectory to store exported files. 48 * 49 * @var string Asset subdirectory name. 50 * @see getDefaultSubdir(). 51 */ 52 53 protected static $defaultSubdir; 54 55 /** 56 * Asset related table field used as subdirectories. 57 * 58 * @var string 59 * @see getSubdirField(). 60 */ 61 62 protected static $subdirField; 63 64 /** 65 * Asset related table field(s) used as asset file contents. 66 * 67 * @var string Field name (could accept an array in the future for JSON contents) 68 * @see getFileContentsField(). 69 */ 70 71 protected static $fileContentsField; 72 73 /** 74 * Forms that Textpattern expects to exist for smooth tag operation. 75 * 76 * Asset related essential rows as an associative array of the following 77 * fields and their value: 'name', ($subdirField, ) $fileContentsField. 78 * 79 * @var array Associative array of the following fields and their value: 80 * 'name', ($subdirField, ) $fileContentsField. 81 * @see getEssential(). 82 */ 83 84 protected static $essential = array(); 85 86 /** 87 * Parent skin object. 88 * 89 * @var object skin 90 * @see __construct(). 91 */ 92 93 protected $skin; 94 95 /** 96 * Constructor. 97 */ 98 99 public function __construct(Skin $skin = null) 100 { 101 parent::__construct(); 102 103 $this->setSkin($skin); 104 } 105 106 /** 107 * {@inheritdoc} 108 */ 109 110 public function setSkin(Skin $skin = null) 111 { 112 $this->skin = $skin === null ? \Txp::get('Textpattern\Skin\Skin')->setName() : $skin; 113 114 return $this; 115 } 116 117 /** 118 * $skin property getter. 119 * 120 * @return $this->skin The asset related skin object. 121 */ 122 123 protected function getSkin() 124 { 125 return $this->skin; 126 } 127 128 /** 129 * {@inheritdoc} 130 */ 131 132 protected function getInfos($safe = false) 133 { 134 if ($safe) { 135 $infoQuery = array(); 136 137 foreach ($this->infos as $col => $value) { 138 if ($col === self::getFileContentsField()) { 139 $infoQuery[] = $col." = '".$value."'"; 140 } else { 141 $infoQuery[] = $col." = '".doSlash($value)."'"; 142 } 143 } 144 145 return implode(', ', $infoQuery); 146 } 147 148 return $this->infos; 149 } 150 151 /** 152 * $fileContentsField property getter. 153 * 154 * @return string static::$fileContentsField. 155 */ 156 157 protected static function getFileContentsField() 158 { 159 return static::$fileContentsField; 160 } 161 162 /** 163 * Get essential templates infos from the $essential property value. 164 * 165 * @param string $key $essential property key for which you want to get the value. 166 * @param string $whereKey $essential property key to check against the $valueIn value. 167 * @param array $valueIn Values to check against the $whereKey values. 168 * @return array $essential property value if $key is null, filtered infos otherwise. 169 */ 170 171 public static function getEssential( 172 $key = null, 173 $whereKey = null, 174 $valueIn = null 175 ) { 176 if ($key === null) { 177 return static::$essential; 178 } elseif ($key === '*' && $whereKey) { 179 $keyValues = array(); 180 181 foreach (static::$essential as $row) { 182 if (in_array($row[$whereKey], $valueIn)) { 183 $keyValues[] = $row; 184 } 185 } 186 } else { 187 $key !== null or $key = 'name'; 188 $keyValues = array(); 189 190 foreach (static::$essential as $row) { 191 if ($whereKey) { 192 if (in_array($row[$whereKey], $valueIn)) { 193 $keyValues[] = $row[$key]; 194 } 195 } else { 196 $keyValues[] = $row[$key]; 197 } 198 } 199 } 200 201 return $keyValues; 202 } 203 204 /** 205 * $dir property setter. 206 */ 207 208 protected static function setDir($name) 209 { 210 static::$dir = $name; 211 } 212 213 /** 214 * $dir property getter. 215 * 216 * @return string static::$dir. 217 */ 218 219 public static function getDir() 220 { 221 return static::$dir; 222 } 223 224 /** 225 * Gets the skin directory path. 226 * 227 * @return string path. 228 */ 229 230 public function getDirPath() 231 { 232 return $this->getSkin()->getSubdirPath().DS.static::getDir(); 233 } 234 235 /** 236 * $subdirField property getter. 237 */ 238 239 protected static function getSubdirField() 240 { 241 return static::$subdirField; 242 } 243 244 /** 245 * $defaultSubdir property getter. 246 */ 247 248 protected static function getDefaultSubdir() 249 { 250 return static::$defaultSubdir; 251 } 252 253 /** 254 * $defaultSubdir property getter. 255 */ 256 257 protected static function getSubdirValues() 258 { 259 return static::$subdirValues; 260 } 261 262 /** 263 * Whether a subdirectory name is valid or not. 264 * 265 * @param string $name Subdirectory name. 266 * @return string The subdirectory name if valid or the default subdirectory. 267 */ 268 269 protected static function parseSubdir($name) 270 { 271 if (in_array($name, self::getSubdirValues())) { 272 return $name; 273 } else { 274 return self::getDefaultSubdir(); 275 } 276 } 277 278 /** 279 * {@inheritdoc} 280 */ 281 282 protected function getSubdirPath($name = null) 283 { 284 $name or $name = $this->getInfos()[self::getSubdirField()]; 285 286 return $this->getDirPath().DS.$name; 287 } 288 289 /** 290 * Get the template related file path. 291 * 292 * @param string path. 293 */ 294 295 protected function getFilePath($name = null) 296 { 297 $dirPath = self::getSubdirField() ? $this->getSubdirPath($name) : $this->getDirPath(); 298 299 $name = $this->getName(); 300 $extension = pathinfo($name, PATHINFO_EXTENSION); 301 302 return $dirPath.DS.$name.(isset(static::$mimeTypes[$extension]) ? '' : '.'.self::getExtension()); 303 } 304 305 /** 306 * {@inheritdoc} 307 */ 308 309 public function getEditing() 310 { 311 $editing = get_pref('last_'.$this->getEvent().'_saved', '', true); 312 $skin = $this->getSkin()->getName(); 313 $installed = $this->getInstalled() + array($skin => array('')); 314 $installed = $installed[$skin]; 315 316 if (!$editing || !in_array($editing, $installed)) { 317 reset($installed); 318 $sliced = array_slice($installed, 0, 1); 319 $editing = array_shift($sliced); 320 321 $this->setEditing($editing); 322 } 323 324 return $editing; 325 } 326 327 /** 328 * {@inheritdoc} 329 */ 330 331 public function setEditing($name = null) 332 { 333 global $prefs; 334 335 $event = $this->getEvent(); 336 $pref = 'last_'.$event.'_saved'; 337 $name !== null or $name = $this->getName(); 338 339 return set_pref($pref, $prefs[$pref] = $name, $event, PREF_HIDDEN, 'text_input', 0, PREF_PRIVATE); 340 } 341 342 /** 343 * Set the skin_editing pref to the skin used by the default section. 344 * 345 * @return bool FALSE on error. 346 */ 347 348 protected function resetEditing() 349 { 350 return $this->setEditing(self::getDefault()); 351 } 352 353 /** 354 * {@inheritdoc} 355 */ 356 357 protected function createFile($path = null, $contents = null) 358 { 359 if ($path === null || $contents === null) { 360 $infos = $this->getInfos(); 361 } 362 363 if ($path === null) { 364 $subdirField = $this->getSubdirField(); 365 $name = $this->getName(); 366 $extension = pathinfo($name, PATHINFO_EXTENSION); 367 $file = $name.(isset(static::$mimeTypes[$extension]) ? '' : '.'.self::getExtension()); 368 369 if ($subdirField) { 370 $path = $infos[$subdirField].DS.$file; 371 } else { 372 $path = $file; 373 } 374 } 375 376 if ($contents === null) { 377 $infos = $this->getInfos(); 378 $contents = $infos[self::getFileContentsField()]; 379 } 380 381 return file_put_contents($this->getDirPath().DS.$path, $contents); 382 } 383 384 /** 385 * {@inheritdoc} 386 */ 387 388 public function createRows($rows = null) 389 { 390 $rows !== null or $rows = self::getEssential(); 391 392 $skin = $this->getSkin()->getName(); 393 $fields = array('skin', 'name'); 394 $fileContentsField = self::getFileContentsField(); 395 $subdirField = self::getSubdirField(); 396 $values = array(); 397 $update = "skin=VALUES(skin), name=VALUES(name), "; 398 399 if ($subdirField) { 400 $fields[] = $subdirField; 401 402 foreach ($rows as $row) { 403 $values[] = "('".doSlash($skin)."', " 404 ."'".doSlash($row['name'])."', " 405 ."'".doSlash($row[$subdirField])."', " 406 ."'".doSlash($row[$fileContentsField])."')"; 407 } 408 409 $update .= $subdirField."=VALUES(".$subdirField."), "; 410 } else { 411 foreach ($rows as $row) { 412 $values[] = "('".doSlash($skin)."', " 413 ."'".doSlash($row['name'])."', " 414 ."'".doSlash($row[$fileContentsField])."')"; 415 } 416 } 417 418 $fields[] = $fileContentsField; 419 $update .= $fileContentsField."=VALUES(".$fileContentsField.")"; 420 421 return safe_query( 422 "INSERT INTO ".safe_pfx($this->getTable())." (".implode(', ', $fields).") " 423 ."VALUES ".implode(', ', $values) 424 ." ON DUPLICATE KEY UPDATE ".$update 425 ); 426 } 427 428 /** 429 * Delete obsolete template rows. 430 * 431 * @return bool FALSE on error. 432 */ 433 434 protected function deleteExtraRows() 435 { 436 return $this->deleteRows( 437 "skin = '".doSlash($this->getSkin()->getName())."' AND " 438 ."name NOT IN ('".implode("', '", array_map('doSlash', $this->getNames()))."')" 439 ); 440 } 441 442 /** 443 * {@inheritdoc} 444 */ 445 446 protected function parseFiles($files) 447 { 448 $rows = $row = array(); 449 $subdirField = self::getSubdirField(); 450 $event = $this->getEvent(); 451 $extension = self::getExtension(); 452 453 $parsed = $parsedFiles = $names = array(); 454 455 if ($files) { 456 $Skin = $this->getSkin(); 457 $skin = $Skin->getName(); 458 459 foreach ($files as $file) { 460 $filename = $file->getFilename(); 461 $ext = pathinfo($filename, PATHINFO_EXTENSION); 462 $name = $ext == $extension ? pathinfo($filename, PATHINFO_FILENAME) : $filename; 463 464 if ($subdirField) { 465 $essentialSubdir = implode('', $this->getEssential($subdirField, 'name', array($name))); 466 } 467 468 if (in_array($filename, $parsedFiles)) { 469 $this->mergeResult($event.'_duplicate', array($skin => array($filename))); 470 } elseif ($subdirField && $essentialSubdir && $essentialSubdir !== basename($file->getPath())) { 471 $this->mergeResult($event.'_subdir_error', array($skin => array(basename($file->getPath()).'/'.$name))); 472 } else { 473 $names[] = $name; 474 $parsed[] = $row['name'] = $name; 475 $parsedFiles[] = $filename; 476 477 if ($subdirField) { 478 $subdir = basename($file->getPath()); 479 $subdirValid = self::parseSubdir($subdir); 480 481 if ($subdir !== $subdirValid) { 482 $this->mergeResult($event.'_subdir_invalid', array($skin => array($subdir.'/'.$name))); 483 } 484 485 $row[$subdirField] = $subdirValid; 486 } 487 488 $row[self::getFileContentsField()] = $file->getContents(); 489 490 $rows[] = $row; 491 } 492 } 493 } 494 495 $missingNames = array_diff(self::getEssential('name'), $parsed); 496 497 $this->setNames(array_merge($names, $missingNames)); 498 499 $missingRows = self::getEssential('*', 'name', $missingNames); 500 501 return array_merge($rows, $missingRows); 502 } 503 504 /** 505 * Unlink obsolete template files. 506 * 507 * @param array $not An array of template names to NOT unlink; 508 * @return array !Templates for which the unlink process FAILED!; 509 */ 510 511 public function deleteExtraFiles($nameNotIn = null) 512 { 513 $filenames = array(); 514 $extension = self::getExtension(); 515 $hasSubdir = self::getSubdirField(); 516 $notRemoved = $subdirPaths = array(); 517 518 foreach ($this->getNames() as $name) { 519 $ext = pathinfo($name, PATHINFO_EXTENSION); 520 $filenames[] = $name.(isset(static::$mimeTypes[$ext]) ? '' : '.'.$extension); 521 } 522 523 $files = $this->getFiles($filenames, $hasSubdir ? 1 : 0); 524 525 if ($files) { 526 foreach ($files as $file) { 527 $name = $file->getFilename(); 528 $ext = pathinfo($name, PATHINFO_EXTENSION); 529 isset(static::$mimeTypes[$ext]) or $name = pathinfo($name, PATHINFO_FILENAME); 530 531 $this->setName($name); 532 533 if (!$nameNotIn || !in_array($name, $nameNotIn)) { 534 unlink($file->getPathname()) or $notRemoved[] = $name; 535 536 !$hasSubdir or $subdirPaths[] = $file->getPath(); 537 } 538 } 539 } 540 541 if (!$notRemoved) { 542 if ($hasSubdir) { 543 foreach ($subdirPaths as $subdirPath) { 544 if (self::isDirEmpty($subdirPath) && !@rmdir($subdirPath)) { 545 $notRemoved[] = $subdirPath; 546 } 547 } 548 } 549 550 $dirPath = $this->getDirPath(); 551 552 if (self::isDirEmpty($dirPath) && !@rmdir($dirPath)) { 553 $notRemoved[] = $dirPath; 554 } 555 } 556 557 return $notRemoved; 558 } 559 560 /** 561 * {@inheritdoc} 562 */ 563 564 public function import($sync = false, $override = false) 565 { 566 $event = $this->getEvent(); 567 $dirPath = $this->getDirPath(); 568 $Skin = $this->getSkin(); 569 $skin = $Skin !== null ? $Skin->getName() : $this->getSkin()->getEditing(); 570 $names = $this->getNames(); 571 $callbackExtra = compact('skin', 'names', 'sync'); 572 $done = array(); 573 $dirIsReadable = is_readable($dirPath); 574 575 callback_event('txp.'.$event, 'import', 1, $callbackExtra); 576 577 if ($dirIsReadable || !$override) { 578 if ($dirIsReadable) { 579 $filenames = array(); 580 $extension = self::getExtension(); 581 582 foreach ($names as $name) { 583 $ext = pathinfo($name, PATHINFO_EXTENSION); 584 $filenames[] = $name.(isset(static::$mimeTypes[$ext]) ? '' : '.'.$extension); 585 } 586 587 $files = $this->getFiles($filenames, self::getSubdirField() ? 1 : 0); 588 589 if (!$files) { 590 $this->mergeResult($event.'_not_found', array($skin => array($dirPath))); 591 } 592 593 $rows = $this->parseFiles($files); 594 } else { 595 $this->mergeResult('path_not_readable', array($skin => array($dirPath)), 'warning'); 596 $rows = self::getEssential(); 597 } 598 599 if (!$this->createRows($rows)) { 600 $this->mergeResult($event.'_import_failed', array($skin => $names)); 601 } else { 602 $done = array_column($rows, 'name'); 603 604 $this->mergeResult($event.'_imported', array($skin => $names), 'success'); 605 } 606 607 // Drops extra rows… 608 if ($sync) { 609 if (!$this->deleteExtraRows()) { 610 $notCleaned = array_diff(array_column($this->getRows('name'), 'name'), $done); 611 $this->mergeResult($event.'_files_deletion_failed', array($skin => $notCleaned)); 612 } 613 } 614 } 615 616 callback_event('txp.'.$event, 'import', 0, $callbackExtra + compact('done')); 617 618 return $this; 619 } 620 621 /** 622 * {@inheritdoc} 623 */ 624 625 public function export($sync = false, $override = false) 626 { 627 $event = $this->getEvent(); 628 $dirPath = $this->getDirPath(); 629 $Skin = $this->getSkin(); 630 $skin = $Skin !== null ? $Skin->getName() : $this->getSkin()->getEditing(); 631 $names = $this->getNames(); 632 $callbackExtra = compact('skin', 'names', 'sync'); 633 $done = array(); 634 635 callback_event('txp.'.$event, 'export', 1, $callbackExtra); 636 637 if (!is_writable($dirPath) && !@mkdir($dirPath)) { 638 $this->mergeResult('path_not_writable', array($skin => array($dirPath))); 639 } else { 640 $rows = $this->getRows(); 641 642 if (!$rows) { 643 $this->mergeResult($event.'_not_found', $skin, 'warning'); 644 } else { 645 foreach ($rows as $row) { 646 extract($row); 647 648 if (!$this->setName($name)->isInstalled()) { 649 $this->mergeResult($event.'_unknown', array($skin => array($name))); 650 } elseif (!self::isExportable()) { 651 $this->mergeResult($event.'_name_unsafe', array($skin => array($name))); 652 } else { 653 $ready = true; 654 $subdirField = self::getSubdirField(); 655 $contentsField = self::getFileContentsField(); 656 657 if ($subdirField) { 658 $subdirPath = $this->setInfos($name, $$subdirField, $$contentsField)->getSubdirPath(); 659 660 if (!is_dir($subdirPath) && !@mkdir($subdirPath)) { 661 $this->mergeResult($event.'_not_writable', array($skin => array($name))); 662 $ready = false; 663 } 664 } else { 665 $this->setInfos($name, $$contentsField); 666 } 667 668 if ($ready) { 669 if ($this->createFile() === false) { 670 $this->mergeResult($event.'_export_failed', array($skin => array($name))); 671 } else { 672 $this->mergeResult($event.'_exported', array($skin => array($name)), 'success'); 673 674 $done[] = $name; 675 } 676 } 677 } 678 } 679 } 680 681 // Drops extra files… 682 if ($sync) { 683 $notUnlinked = $this->deleteExtraFiles($done); 684 685 if ($notUnlinked) { 686 $this->mergeResult($event.'_files_deletion_failed', array($skin => $notUnlinked)); 687 } 688 } 689 } 690 691 callback_event('txp.'.$event, 'export', 0, $callbackExtra + compact('done')); 692 693 return $this; 694 } 695 696 /** 697 * {@inheritdoc} 698 */ 699 700 public function getSelectEdit() 701 { 702 $event = $this->getEvent(); 703 $Skin = $this->getSkin(); 704 $skins = $Skin->getInstalled(); 705 706 if (count($skins) > 1) { 707 return form( 708 inputLabel( 709 'skin', 710 selectInput('skin', $skins, $Skin->getEditing(), false, 1, 'skin'), 711 'skin' 712 ) 713 .eInput($event) 714 .sInput($event.'_skin_change'), 715 '', 716 '', 717 'post' 718 ); 719 } 720 721 return; 722 } 723 724 /** 725 * Select the asset related skin to edit. 726 * Keeps track from panel to panel. 727 * 728 * @param string $skin Optional skin name. Read from GET/POST otherwise 729 * @return object $this The current class object (chainable). 730 */ 731 732 public function selectEdit($skin = null) 733 { 734 if ($skin === null) { 735 $skin = gps('skin'); 736 } 737 738 if ($skin) { 739 $Skin = $this->getSkin(); 740 $Skin->setEditing($skin); 741 $Skin->setName($skin); 742 } 743 744 $this->getEditing(); 745 746 return $this; 747 } 748 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title