Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/vendors/Textpattern/Skin/AssetBase.php - 748 lines - 21379 bytes - Summary - Text - Print

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

title

Description

title

Description

title

title

Body