Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/vendors/Textpattern/Skin/Skin.php - 1595 lines - 51200 bytes - Summary - Text - Print

Description: Skin

   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   * Skin
  26   *
  27   * Manage Skins and their dependencies.
  28   *
  29   * @since   4.7.0
  30   * @package Skin
  31   */
  32  
  33  namespace Textpattern\Skin;
  34  
  35  class Skin extends CommonBase implements SkinInterface
  36  {
  37      /**
  38       * Skin assets related objects.
  39       *
  40       * @var array Page, Form and CSS class objects.
  41       * @see       setAssets().
  42       */
  43  
  44      private $assets;
  45  
  46      /**
  47       * Class related main file.
  48       *
  49       * @var string Filename.
  50       * @see        getFile().
  51       */
  52  
  53      protected static $filename = 'manifest.json';
  54  
  55      /**
  56       * {@inheritdoc}
  57       */
  58  
  59      protected static $extension = 'json';
  60  
  61      /**
  62       * Importable skins.
  63       *
  64       * @var array Associative array of skin names and their infos from JSON files
  65       * @see       setUploaded(), getUploaded().
  66       */
  67  
  68      protected $uploaded;
  69  
  70      /**
  71       * Class related directory path.
  72       *
  73       * @var string Path.
  74       * @see        setDirPath(), getDirPath().
  75       */
  76  
  77      protected $dirPath;
  78  
  79      /**
  80       * {@inheritdoc}
  81       */
  82  
  83      public function __construct()
  84      {
  85          parent::__construct();
  86      }
  87  
  88      public function __toString()
  89      {
  90          return $this->getName();
  91      }
  92  
  93      protected function mergeResults($asset, $status)
  94      {
  95          $this->results = array_merge_recursive($this->getResults(), $asset->getResults($status));
  96  
  97          return $this;
  98      }
  99  
 100      /**
 101       * $dirPath property setter.
 102       *
 103       * @param  string $path          Path (default: get_pref('path_to_site').DS.get_pref('skin_dir')).
 104       * @return string $this->dirPath
 105       */
 106  
 107      public function setDirPath($path = null)
 108      {
 109          $path !== null or $path = get_pref('path_to_site').DS.get_pref($this->getEvent().'_dir');
 110  
 111          $this->dirPath = rtrim($path, DS);
 112          $this->uploaded = null;
 113  
 114          return $this->getDirPath();
 115      }
 116  
 117      /**
 118       * $dirPath property getter
 119       *
 120       * @return string $this->dirPath
 121       */
 122  
 123      protected function getDirPath()
 124      {
 125          $this->dirPath !== null or $this->setDirPath();
 126  
 127          return $this->dirPath;
 128      }
 129  
 130      /**
 131       * $assets property setter.
 132       *
 133       * @param array   $pages  Page names to work with;
 134       * @param array   $forms  Form names to work with;
 135       * @param array   $styles CSS names to work with.
 136       * @return object $this   The current class object (chainable)
 137       */
 138  
 139      public function setAssets($pages = null, $forms = null, $styles = null)
 140      {
 141          $assets = array(
 142              'Page' => $pages,
 143              'Form' => $forms,
 144              'Css'  => $styles,
 145          );
 146  
 147          foreach ($assets as $class => $assets) {
 148              $this->assets[] = \Txp::get('Textpattern\Skin\\'.$class, $this)->setNames($assets);
 149          }
 150  
 151          return $this;
 152      }
 153  
 154      /**
 155       * $assets property getter.
 156       *
 157       * @return array $this->$assets
 158       */
 159  
 160      protected function getAssets()
 161      {
 162          $this->assets !== null or $this->setAssets();
 163  
 164          return $this->assets;
 165      }
 166  
 167      /**
 168       * $infos and $name properties setter.
 169       *
 170       * @param  string $name        Skin name;
 171       * @param  string $title       Skin title;
 172       * @param  string $version     Skin version;
 173       * @param  string $description Skin description;
 174       * @param  string $author      Skin author;
 175       * @param  string $author_uri  Skin author URL;
 176       * @return object $this        The current class object (chainable).
 177       */
 178  
 179      public function setInfos(
 180          $name,
 181          $title = null,
 182          $version = null,
 183          $description = null,
 184          $author = null,
 185          $author_uri = null
 186      ) {
 187          $name = $this->setName($name)->getName();
 188  
 189          $title or $title = ucfirst($name);
 190  
 191          $this->infos = compact('name', 'title', 'version', 'description', 'author', 'author_uri');
 192  
 193          return $this;
 194      }
 195  
 196      /**
 197       * Get a $dir property value related subdirectory path.
 198       *
 199       * @param string  $name Directory(/skin) name (default: $this->getName()).
 200       * @return string       The Path
 201       */
 202  
 203      public function getSubdirPath($name = null)
 204      {
 205          $name !== null or $name = $this->getName();
 206  
 207          return $this->getDirPath().DS.$name;
 208      }
 209  
 210      /**
 211       * $file property getter.
 212       *
 213       * @return string self::$filename.
 214       */
 215  
 216      protected static function getFilename()
 217      {
 218          return self::$filename;
 219      }
 220  
 221      /**
 222       * Get the $file property value related path.
 223       *
 224       * @return string Path.
 225       */
 226  
 227      protected function getFilePath()
 228      {
 229          return $this->getSubdirPath().DS.self::getFilename();
 230      }
 231  
 232      /**
 233       * Get and complete the skin related file contents.
 234       *
 235       * @return array Associative array of JSON fields and their related values / fallback values.
 236       */
 237  
 238      protected function getFileContents()
 239      {
 240          $contents = json_decode(file_get_contents($this->getFilePath()), true);
 241  
 242          $contents === null or $contents = $this->parseInfos($contents);
 243  
 244          return $contents;
 245      }
 246  
 247      /**
 248       * Parse a skin related infos.
 249       *
 250       * @return array $infos Associative array of fields and their related values / fallback values.
 251       */
 252  
 253      protected function parseInfos($infos)
 254      {
 255          extract($infos);
 256  
 257          !empty($title) or $title = ucfirst($this->getName());
 258          !empty($version) or $version = gTxt('unknown');
 259          !empty($description) or $description = '';
 260          !empty($author) or $author = gTxt('unknown');
 261          !empty($author_uri) or $author_uri = '';
 262  
 263          return compact('title', 'version', 'description', 'author', 'author_uri');
 264      }
 265  
 266      /**
 267       * $sections property getter.
 268       *
 269       * @param array Section names.
 270       */
 271  
 272      protected function getSections($skin = null)
 273      {
 274          $skin = doSlash(is_string($skin) ? $skin : $this->getName());
 275          $event = $this->getEvent();
 276  
 277          return array_values(
 278              safe_column(
 279                  'name',
 280                  'txp_section',
 281                  "$event ='$skin' OR dev_{$event} ='$skin'"
 282              )
 283          );
 284      }
 285  
 286      /**
 287       * Update the txp_section table.
 288       *
 289       * @param  string $set   The SET clause (default: "skin = '".doSlash($this->getName())."'")
 290       * @param  string $where The WHERE clause (default: "skin = '".doSlash($this->getBase())."'")
 291       * @return bool          FALSE on error.
 292       */
 293  
 294      public function updateSections($set = null, $where = null, $dev = false)
 295      {
 296          $event = ($dev ? "{$dev}_" : '').$this->getEvent();
 297  
 298          $set !== null or $set = $event." = '".doSlash($this->getName())."'";
 299  
 300          if ($where === null) {
 301              $base = $this->getBase();
 302  
 303              $where = $base ? $event." = '".doSlash($base)."'" : '1 = 1';
 304          }
 305  
 306          return safe_update('txp_section', $set, $where);
 307      }
 308  
 309      /**
 310       * {@inheritdoc}
 311       */
 312  
 313      public function getEditing()
 314      {
 315          $editing = get_pref($this->getEvent().'_editing', '', true);
 316  
 317          if (!$editing) {
 318              $installed = $this->getInstalled();
 319  
 320              reset($installed);
 321  
 322              $editing = $this->setEditing(key($installed));
 323          }
 324  
 325          return $editing;
 326      }
 327  
 328      /**
 329       * {@inheritdoc}
 330       */
 331  
 332      public function setEditing($name = null)
 333      {
 334          global $prefs;
 335  
 336          $event = $this->getEvent();
 337  
 338          $name !== null or $name = $this->getName();
 339          $prefs[$event.'_editing'] = $name;
 340  
 341          set_pref($event.'_editing', $name, $event, PREF_HIDDEN, 'text_input', 0, PREF_PRIVATE);
 342  
 343          return $this->getEditing();
 344      }
 345  
 346      /**
 347       * Create a file in the $dir property value related directory.
 348       *
 349       * @param  string $pathname The file related path (default: $this->getName().DS.self::getFilename()).
 350       * @param  mixed  $contents The file related contents as as a string or
 351       *                          as an associative array for a .json file
 352       *                          (uses the $infos property related array).
 353       * @return bool             Written octets number or FALSE on error.
 354       */
 355  
 356      protected function createFile($pathname = null, $contents = null)
 357      {
 358          $pathname !== null or $pathname = $this->getName().DS.self::getFilename();
 359  
 360          if ($contents === null) {
 361              $contents = array_merge(
 362                  $this->getInfos(),
 363                  array('txp-type' => 'textpattern-theme')
 364              );
 365  
 366              unset($contents['name']);
 367          }
 368  
 369          if (pathinfo($pathname, PATHINFO_EXTENSION) === 'json') {
 370              $contents = json_encode($contents, TEXTPATTERN_JSON | JSON_PRETTY_PRINT);
 371          }
 372  
 373          return file_put_contents($this->getDirPath().DS.$pathname, $contents);
 374      }
 375  
 376      /**
 377       * $uploaded property setter.
 378       *
 379       * @return object $this The current class object (chainable).
 380       */
 381  
 382      protected function setUploaded()
 383      {
 384          $this->uploaded = array();
 385          $files = $this->getFiles(array(self::getFilename()), 1);
 386  
 387          if ($files) {
 388              foreach ($files as $file) {
 389                  $name = basename($file->getPath());
 390  
 391                  if ($name === self::sanitize($name)) {
 392                      $infos = $file->getJSONContents();
 393  
 394                      if ($infos && $infos['txp-type'] === 'textpattern-theme') {
 395                          $this->uploaded[$name] = $this->setName($name)->parseInfos($infos);
 396                      }
 397                  }
 398              }
 399          }
 400  
 401          return $this;
 402      }
 403  
 404      /**
 405       * {@inheritdoc}
 406       */
 407  
 408      public function getUploaded($expanded = true)
 409      {
 410          $this->uploaded !== null or $this->setUploaded();
 411  
 412          if (!$expanded) {
 413              $contracted = array();
 414  
 415              foreach ($this->uploaded as $name => $infos) {
 416                  $contracted[$name] = $infos['title'] . ' ('.$infos['version'].')';
 417              }
 418  
 419              return $contracted;
 420          }
 421  
 422          return $this->uploaded;
 423      }
 424  
 425      /**
 426       * $installed property merger.
 427       *
 428       * @param array $this->installed.
 429       */
 430  
 431      protected function mergeInstalled($skins)
 432      {
 433          $this->installed = array_merge($this->getInstalled(), $skins);
 434  
 435          return $this->getInstalled();
 436      }
 437  
 438      /**
 439       * $installed property remover.
 440       *
 441       * @return array $this->installed.
 442       */
 443  
 444      protected function removeInstalled($names)
 445      {
 446          $this->installed = array_diff_key(
 447              $this->getInstalled(),
 448              array_fill_keys($names, '')
 449          );
 450  
 451          return $this->getInstalled();
 452      }
 453  
 454      /**
 455       * {@inheritdoc}
 456       */
 457  
 458      protected function getTableData($criteria, $sortSQL, $offset, $limit)
 459      {
 460          $assets = array('section', 'page', 'form', 'css');
 461          $things = array('*');
 462          $table = $this->getTable();
 463  
 464          foreach ($assets as $asset) {
 465              $things[] = '(SELECT COUNT(*) '
 466                          .'FROM '.safe_pfx_j('txp_'.$asset).' '
 467                          .'WHERE txp_'.$asset.'.'.$this->getEvent().' = '.$table.'.name) '
 468                          .$asset.'_count';
 469          }
 470  
 471          $things[] = '(SELECT COUNT(*) '
 472              .'FROM '.safe_pfx_j('txp_section').' '
 473              .'WHERE dev_'.$this->getEvent().' = '.$table.'.name) '
 474              .'dev_section_count';
 475  
 476          return safe_rows_start(
 477              implode(', ', $things),
 478              $table,
 479              $criteria.' order by '.$sortSQL.' limit '.$offset.', '.$limit
 480          );
 481      }
 482  
 483      /**
 484       * Create/CreateFrom a single skin (and its related assets)
 485       * Merges results in the related property.
 486       *
 487       * @return object $this The current object (chainable).
 488       */
 489  
 490      public function create()
 491      {
 492          $event = $this->getEvent();
 493          $infos = $this->getInfos();
 494          $name = $infos['name'];
 495          $base = $this->getBase();
 496          $callbackExtra = compact('infos', 'base');
 497          $done = false;
 498  
 499          callback_event('txp.'.$event, 'create', 1, $callbackExtra);
 500  
 501          if (empty($name)) {
 502              $this->mergeResult($event.'_name_invalid', $name);
 503          } elseif ($base && !$this->isInstalled($base)) {
 504              $this->mergeResult($event.'_unknown', $base);
 505          } elseif ($this->isInstalled()) {
 506              $this->mergeResult($event.'_already_exists', $name);
 507          } elseif (is_dir($nameDirPath = $this->getSubdirPath())) {
 508              // Create a skin which would already have a related directory could cause conflicts.
 509              $this->mergeResult($event.'_already_exists', $nameDirPath);
 510          } elseif (!$this->createRow()) {
 511              $this->mergeResult($event.'_creation_failed', $name);
 512          } else {
 513              $this->mergeResult($event.'_created', $name, 'success');
 514  
 515              // Start working with the skin related assets.
 516              foreach ($this->getAssets() as $assetModel) {
 517                  if ($base) {
 518                      $this->setName($base);
 519                      $rows = $assetModel->getRows();
 520                      $this->setName($name);
 521                  } else {
 522                      $rows = null;
 523                  }
 524  
 525                  if (!$assetModel->createRows($rows)) {
 526                      $assetsfailed = true;
 527  
 528                      $this->mergeResult($assetModel->getEvent().'_creation_failed', $name);
 529                  }
 530              }
 531  
 532              // If the assets related process did not failed; that is a success…
 533              isset($assetsfailed) or $done = $name;
 534          }
 535  
 536          callback_event('txp.'.$event, 'create', 0, $callbackExtra + compact('done'));
 537  
 538          return $this; // Chainable.
 539      }
 540  
 541      /**
 542       * Update a single skin (and its related dependencies)
 543       * Merges results in the related property.
 544       *
 545       * @return object $this The current object (chainable).
 546       */
 547  
 548      public function update()
 549      {
 550          $event = $this->getEvent();
 551          $infos = $this->getInfos();
 552          $name = $infos['name'];
 553          $base = $this->getBase();
 554          $callbackExtra = compact('infos', 'base');
 555          $done = null;
 556          $ready = false;
 557  
 558          callback_event('txp.'.$event, 'update', 1, $callbackExtra);
 559  
 560          if (empty($name)) {
 561              $this->mergeResult($event.'_name_invalid', $name);
 562          } elseif (!$this->isInstalled($base)) {
 563              $this->mergeResult($event.'_unknown', $base);
 564          } elseif ($base !== $name && $this->isInstalled()) {
 565              $this->mergeResult($event.'_already_exists', $name);
 566          } elseif (is_dir($nameDirPath = $this->getSubdirPath()) && $base !== $name) {
 567              // Rename the skin with a name which would already have a related directory could cause conflicts.
 568              $this->mergeResult($event.'_already_exists', $nameDirPath);
 569          } elseif (!$this->updateRow()) {
 570              $this->mergeResult($event.'_update_failed', $base);
 571              $locked = $base;
 572          } else {
 573              $this->mergeResult($event.'_updated', $name, 'success');
 574              $ready = true;
 575              $locked = $base;
 576              $baseDirPath = $this->getSubdirPath($base);
 577  
 578              // Rename the skin related directory to allow new updates from files.
 579              if (is_dir($baseDirPath) && !@rename($baseDirPath, $nameDirPath)) {
 580                  $this->mergeResult('path_renaming_failed', $base, 'warning');
 581              } else {
 582                  $locked = $name;
 583              }
 584          }
 585  
 586          if ($ready) {
 587              // Update skin related sections.
 588              if ($sections = $this->getSections($base)) {
 589                  $updated = $this->updateSections();
 590                  $updated = $this->updateSections(null, null, 'dev') && $updated;
 591                  $updated or $this->mergeResult($event.'_related_sections_update_failed', array($base => $sections));
 592              }
 593  
 594              // update the skin_editing pref if needed.
 595              $this->getEditing() !== $base or $this->setEditing();
 596  
 597              // Start working with the skin related assets.
 598              $assetUpdateSet = $event." = '".doSlash($this->getName())."'";
 599              $assetUpdateWhere = $event." = '".doSlash($this->getBase())."'";
 600  
 601              foreach ($this->getAssets() as $assetModel) {
 602                  if (!$assetModel->updateRow($assetUpdateSet, $assetUpdateWhere)) {
 603                      $assetsFailed = true;
 604                      $this->mergeResult($assetModel->getEvent().'_update_failed', $base);
 605                  }
 606              }
 607  
 608              // If the assets related process did not failed; that is a success…
 609              isset($assetsFailed) or $done = $name;
 610          }
 611  
 612          callback_event('txp.'.$event, 'update', 0, $callbackExtra + compact('done'));
 613  
 614          return $this; // Chainable
 615      }
 616  
 617      /**
 618       * Duplicate multiple skins (and their related $assets)
 619       * Merges results in the related property.
 620       *
 621       * @return object $this The current object (chainable).
 622       */
 623  
 624      public function duplicate()
 625      {
 626          $event = $this->getEvent();
 627          $names = $this->getNames();
 628          $callbackExtra = compact('names');
 629          $ready = $done = array();
 630  
 631          callback_event('txp.'.$event, 'duplicate', 1, $callbackExtra);
 632  
 633          foreach ($names as $name) {
 634              $nameDirPath = $this->setName($name)->getSubdirPath();
 635              $copy = $name.'_copy';
 636  
 637              if (!$this->isInstalled()) {
 638                  $this->mergeResult($event.'_unknown', $name);
 639              } elseif ($this->isInstalled($copy)) {
 640                  $this->mergeResult($event.'_already_exists', $copy);
 641              } elseif (is_dir($copyPath = $this->getSubdirPath($copy))) {
 642                  $this->mergeResult($event.'_already_exists', $copyPath);
 643              } else {
 644                  $ready[] = $name;
 645              }
 646          }
 647  
 648          if ($ready) {
 649              $rows = $this->getRows(
 650                  "*",
 651                  "name IN ('".implode("', '", array_map('doSlash', $ready))."')"
 652              );
 653  
 654              if (!$rows) {
 655                  $this->mergeResult($event.'_not_found', $ready);
 656              } else {
 657                  foreach ($rows as $row) {
 658                      extract($row);
 659  
 660                      $copy = $name.'_copy';
 661                      $copyTitle = $title.' (copy)';
 662  
 663                      $this->setInfos($copy, $copyTitle, $version, $description, $author, $author_uri);
 664  
 665                      if (!$this->createRow()) {
 666                          $this->mergeResult($event.'_creation_failed', $copy);
 667                      } else {
 668                          $this->mergeResult($event.'_created', $copy, 'success');
 669                          $this->mergeInstalled(array($copy => $copyTitle));
 670  
 671                          // Start working with the skin related assets.
 672                          foreach ($this->getAssets() as $assetModel) {
 673                              $this->setName($name);
 674                              $assetString = $assetModel->getEvent();
 675                              $assetRows = $assetModel->getRows();
 676  
 677                              if (!$assetRows) {
 678                                  $deleteExtraFiles = true;
 679  
 680                                  $this->mergeResult($assetString.'_not_found', array($skin => $nameDirPath));
 681                              } elseif ($this->setName($copy) && !$assetModel->createRows($assetRows)) {
 682                                  $deleteExtraFiles = true;
 683  
 684                                  $this->mergeResult($assetString.'_creation_failed', $copy);
 685                              }
 686                          }
 687  
 688                          $this->setName($name); // Be sure to restore the right $name.
 689  
 690                          // If the assets related process did not failed; that is a success…
 691                          isset($deleteExtraFiles) or $done[] = $name;
 692                      }
 693                  }
 694              }
 695          }
 696  
 697          callback_event('txp.'.$event, 'duplicate', 0, $callbackExtra + compact('done'));
 698  
 699          return $this; // Chainable
 700      }
 701  
 702      /**
 703       * {@inheritdoc}
 704       */
 705  
 706      public function import($sync = false, $override = false)
 707      {
 708          $event = $this->getEvent();
 709          $syncPref = 'skin_delete_from_database';
 710          $sync == $this->getSyncPref($syncPref) or $this->switchSyncPref($syncPref);
 711          $names = $this->getNames();
 712          $callbackExtra = compact('names', 'sync');
 713          $done = array();
 714  
 715          callback_event('txp.'.$event, 'import', 1, $callbackExtra);
 716  
 717          foreach ($names as $name) {
 718              $this->setName($name);
 719              $this->setBase($name);
 720  
 721              $isInstalled = $this->isInstalled();
 722              $isInstalled or $sync = $override = false; // Avoid useless work.
 723  
 724              if (!$override && $isInstalled) {
 725                  $this->mergeResult($event.'_already_exists', $name);
 726              } elseif (!is_readable($filePath = $this->getFilePath())) {
 727                  $this->mergeResult('path_not_readable', $filePath);
 728              } else {
 729                  $skinInfos = array_merge(array('name' => $name), $this->getFileContents());
 730  
 731                  if (!$skinInfos) {
 732                      $this->mergeResult('invalid_json', $filePath);
 733                  } else {
 734                      extract($skinInfos);
 735  
 736                      $this->setInfos($name, $title, $version, $description, $author, $author_uri);
 737  
 738                      if (!$override && !$this->createRow()) {
 739                          $this->mergeResult($event.'_import_failed', $name);
 740                      } elseif ($override && !$this->updateRow()) {
 741                          $this->mergeResult($event.'_import_failed', $name);
 742                      } else {
 743                          $this->mergeResult($event.'_imported', $name, 'success');
 744                          $this->mergeInstalled(array($name => $title));
 745  
 746                          // Start working with the skin related assets.
 747                          foreach ($this->getAssets() as $asset) {
 748                              $asset->import($sync, $override);
 749  
 750                              if (is_array($asset->getMessage())) {
 751                                  $assetFailed = true;
 752  
 753                                  $this->mergeResults($asset, array('warning', 'error'));
 754                              }
 755                          }
 756                      }
 757  
 758                      // If the assets related process did not failed; that is a success…
 759                      isset($assetFailed) or $done[] = $name;
 760                  }
 761              }
 762          }
 763  
 764          callback_event('txp.'.$event, 'import', 0, $callbackExtra + compact('done'));
 765  
 766          return $this;
 767      }
 768  
 769      /**
 770       * {@inheritdoc}
 771       */
 772  
 773      public function export($sync = false, $override = false)
 774      {
 775          $syncPref = 'skin_delete_from_disk';
 776          $sync == $this->getSyncPref($syncPref) or $this->switchSyncPref($syncPref);
 777  
 778          $event = $this->getEvent();
 779          $names = $this->getNames();
 780          $callbackExtra = compact('names', 'sync');
 781          $ready = $done = array();
 782  
 783          callback_event('txp.'.$event, 'export', 1, $callbackExtra);
 784  
 785          foreach ($names as $name) {
 786              $this->setName($name);
 787  
 788              $nameDirPath = $this->getSubdirPath();
 789  
 790              if (!is_writable($nameDirPath)) {
 791                  $sync = false;
 792                  $override = false;
 793              }
 794  
 795              if (!self::isExportable($name)) {
 796                  $this->mergeResult($event.'_unsafe_name', $name);
 797              } elseif (!$override && is_dir($nameDirPath)) {
 798                  $this->mergeResult($event.'_already_exists', $nameDirPath);
 799              } elseif (!is_dir($nameDirPath) && !@mkdir($nameDirPath)) {
 800                  $this->mergeResult('path_not_writable', $nameDirPath);
 801              } else {
 802                  $ready[] = $name;
 803              }
 804          }
 805  
 806          if ($ready) {
 807              $rows = $this->getRows(
 808                  "*",
 809                  "name IN ('".implode("', '", array_map('doSlash', $ready))."')"
 810              );
 811  
 812              if (!$rows) {
 813                  $this->mergeResult($event.'_unknown', $names);
 814              } else {
 815                  foreach ($rows as $row) {
 816                      extract($row);
 817  
 818                      $this->setInfos($name, $title, $version, $description, $author, $author_uri);
 819  
 820                      if ($this->createFile() === false) {
 821                          $this->mergeResult($event.'_export_failed', $name);
 822                      } else {
 823                          $this->mergeResult($event.'_exported', $name, 'success');
 824  
 825                          foreach ($this->getAssets() as $asset) {
 826                              $asset->export($sync, $override);
 827  
 828                              if (is_array($asset->getMessage())) {
 829                                  $assetFailed = true;
 830  
 831                                  $this->mergeResults($asset, array('warning', 'error'));
 832                              }
 833                          }
 834  
 835                          isset($assetFailed) or $done[] = $name;
 836                      }
 837                  }
 838              }
 839          }
 840  
 841          callback_event('txp.'.$event, 'export', 0, $callbackExtra + compact('done'));
 842  
 843          return $this;
 844      }
 845  
 846      /**
 847       * Delete multiple skins (and their related $assets + directories if empty)
 848       * Merges results in the related property.
 849       *
 850       * @return object $this The current object (chainable).
 851       */
 852  
 853      public function delete($sync = false)
 854      {
 855          $syncPref = 'skin_delete_entirely';
 856          $sync == $this->getSyncPref($syncPref) or $this->switchSyncPref($syncPref);
 857  
 858          $event = $this->getEvent();
 859          $names = $this->getNames();
 860          $callbackExtra = compact('names', 'sync');
 861          $ready = $done = array();
 862  
 863          callback_event('txp.'.$event, 'delete', 1, $callbackExtra);
 864  
 865          foreach ($names as $name) {
 866              $isDir = is_dir($this->getSubdirPath($name));
 867  
 868              if (!$this->setName($name)->isInstalled()) {
 869                  $this->mergeResult($event.'_unknown', $name);
 870              } elseif ($sections = $this->getSections($name)) {
 871                  $this->mergeResult($event.'_in_use', array($name => $sections));
 872              } else {
 873                  /**
 874                   * Start working with the skin related assets.
 875                   * Done first as assets won't be accessible
 876                   * once their parent skin will be deleted.
 877                   */
 878                  $assetFailed = false;
 879  
 880                  foreach ($this->getAssets() as $assetModel) {
 881                      if (!$assetModel->deleteRows()) {
 882                          $assetFailed = true;
 883                          $this->mergeResult($assetModel->getEvent().'_deletion_failed', $name);
 884                      } elseif ($sync && $isDir) {
 885                          $notDeleted = $assetModel->deleteExtraFiles();
 886  
 887                          if ($notDeleted) {
 888                              $this->mergeResult($assetModel->getEvent().'_files_deletion_failed', array($name => $notDeleted));
 889                          }
 890                      }
 891                  }
 892  
 893                  $assetFailed or $ready[] = $name;
 894              }
 895          }
 896  
 897          if ($ready) {
 898              if ($this->deleteRows("name IN ('".implode("', '", array_map('doSlash', $ready))."')")) {
 899                  $done = $ready;
 900  
 901                  $this->removeInstalled($ready);
 902  
 903                  if (in_array($this->getEditing(), $ready)) {
 904                      $default = $this->getDefault();
 905  
 906                      !$default or $this->setEditing($default);
 907                  }
 908  
 909                  $this->mergeResult($event.'_deleted', $ready, 'success');
 910  
 911                  // Remove all skins files and directories if needed.
 912                  if ($sync) {
 913                      $notDeleted = $this->deleteFiles($ready);
 914  
 915                      !$notDeleted or $this->mergeResult($event.'_files_deletion_failed', $notDeleted);
 916                  }
 917  
 918                  update_lastmod($event.'.delete', $ready);
 919              } else {
 920                  $this->mergeResult($event.'_deletion_failed', $ready);
 921              }
 922          }
 923  
 924          callback_event('txp.'.$event, 'delete', 0, $callbackExtra + compact('done'));
 925  
 926          return $this;
 927      }
 928  
 929      /**
 930       * Delete Files from the $dir property value related directory.
 931       *
 932       * @param  string $names directory/file names.
 933       * @return bool   0 on error.
 934       */
 935  
 936      protected function deleteFiles($names = null)
 937      {
 938          $notRemoved = array();
 939  
 940          foreach ($names as $name) {
 941              if (is_dir($this->getSubdirPath($name))) {
 942                  $filePath = $this->getFilePath();
 943  
 944                  if (file_exists($filePath) && !unlink($filePath)) {
 945                      $notRemoved[$name][] = $filePath;
 946                  }
 947  
 948                  $subdirPath = $this->getSubdirPath();
 949                  $isDirEmpty = self::isDirEmpty($subdirPath);
 950  
 951                  if (!isset($notRemoved[$name]) && ($isDirEmpty && !@rmdir($subdirPath) || !$isDirEmpty)) {
 952                      $notRemoved[$name][] = $subdirPath;
 953                  }
 954              }
 955          }
 956  
 957          return $notRemoved;
 958      }
 959  
 960      /**
 961       * Control the admin tab.
 962       */
 963  
 964      public function admin()
 965      {
 966          if (!defined('txpinterface')) {
 967              die('txpinterface is undefined.');
 968          }
 969  
 970          global $event, $step;
 971  
 972          if ($event === $this->getEvent()) {
 973              require_privs($event);
 974  
 975              bouncer($step, array(
 976                  $event.'_change_pageby' => true, // Prefixed to make it work with the paginator…
 977                  'list'                  => false,
 978                  'edit'                  => false,
 979                  'save'                  => true,
 980                  'import'                => false,
 981                  'multi_edit'            => true,
 982              ));
 983  
 984              switch ($step) {
 985                  case 'save':
 986                      $infos = array_map('assert_string', psa(array(
 987                          'name',
 988                          'title',
 989                          'old_name',
 990                          'old_title',
 991                          'version',
 992                          'description',
 993                          'author',
 994                          'author_uri',
 995                          'copy',
 996                      )));
 997  
 998                      extract($infos);
 999  
1000                      if ($old_name) {
1001                          if ($copy) {
1002                              $name !== $old_name or $name .= '_copy';
1003                              $title !== $old_title or $title .= ' (copy)';
1004  
1005                              $this->setInfos($name, $title, $version, $description, $author, $author_uri)
1006                                   ->setBase($old_name)
1007                                   ->create();
1008                          } else {
1009                              $this->setInfos($name, $title, $version, $description, $author, $author_uri)
1010                                   ->setBase($old_name)
1011                                   ->update();
1012                          }
1013                      } else {
1014                          $title !== '' or $title = ucfirst($name);
1015                          $author !== '' or $author = substr(cs('txp_login_public'), 10);
1016                          $version !== '' or $version = '0.0.1';
1017  
1018                          $this->setInfos($name, $title, $version, $description, $author, $author_uri)
1019                               ->create();
1020                      }
1021                      break;
1022                  case 'multi_edit':
1023                      extract(psa(array(
1024                          'edit_method',
1025                          'selected',
1026                          'sync',
1027                      )));
1028  
1029                      if (!$selected || !is_array($selected)) {
1030                          return $this->render($step);
1031                      }
1032  
1033                      $this->setNames(ps('selected'));
1034  
1035                      switch ($edit_method) {
1036                          case 'export':
1037                              $this->export($sync, true);
1038                              break;
1039                          case 'duplicate':
1040                              $this->duplicate();
1041                              break;
1042                          case 'import':
1043                              $this->import($sync, true);
1044                              break;
1045                          case 'delete':
1046                              $this->delete($sync);
1047                              break;
1048                      }
1049                      break;
1050                  case 'edit':
1051                      break;
1052                  case 'import':
1053                      $this->setNames(array(ps('skins')))->import();
1054                      break;
1055                  case $event.'_change_pageby':
1056                      $this->getPaginator()->change();
1057                      break;
1058              }
1059  
1060              return $this->render($step);
1061          }
1062      }
1063  
1064      /**
1065       * Render (echo) the $step related admin tab.
1066       *
1067       * @param string $step
1068       */
1069  
1070      public function render($step)
1071      {
1072          $message = $this->getMessage();
1073  
1074          if ($step === 'edit') {
1075              echo $this->getEditForm($message);
1076          } else {
1077              echo $this->getList($message);
1078          }
1079      }
1080  
1081      /**
1082       * {@inheritdoc}
1083       */
1084  
1085      protected function getList($message = '')
1086      {
1087          $event = $this->getEvent();
1088          $table = $this->getTable();
1089  
1090          pagetop(gTxt('tab_'.$event), $message);
1091  
1092          extract(gpsa(array(
1093              'page',
1094              'sort',
1095              'dir',
1096              'crit',
1097              'search_method',
1098          )));
1099  
1100          if ($sort === '') {
1101              $sort = get_pref($event.'_sort_column', 'name');
1102          } else {
1103              $sortOpts = array(
1104                  'title',
1105                  'version',
1106                  'author',
1107                  'section_count',
1108                  'page_count',
1109                  'form_count',
1110                  'css_count',
1111                  'name',
1112              );
1113  
1114              in_array($sort, $sortOpts) or $sort = 'name';
1115  
1116              set_pref($event.'_sort_column', $sort, $event, PREF_HIDDEN, '', 0, PREF_PRIVATE);
1117          }
1118  
1119          if ($dir === '') {
1120              $dir = get_pref($event.'_sort_dir', 'desc');
1121          } else {
1122              $dir = ($dir == 'asc') ? 'asc' : 'desc';
1123  
1124              set_pref($event.'_sort_dir', $dir, $event, PREF_HIDDEN, '', 0, PREF_PRIVATE);
1125          }
1126  
1127          $searchOpts = array();
1128  
1129          foreach (array('name', 'title', 'description', 'author') as $option) {
1130              $searchOpts[$option] = array(
1131                  'column' => $table.'.'.$option,
1132                  'label'  => gTxt($option),
1133              );
1134          }
1135  
1136          $search = $this->getSearchFilter($searchOpts);
1137  
1138          list($criteria, $crit, $search_method) = $search->getFilter();
1139  
1140          $total = $this->countRows($criteria);
1141          $limit = $this->getPaginator()->getLimit();
1142  
1143          list($page, $offset, $numPages) = pager($total, $limit, $page);
1144  
1145          $table = \Txp::get('Textpattern\Admin\Table');
1146  
1147          return $table->render(
1148              compact('total', 'crit') + array('help' => 'skin_overview'),
1149              $this->getSearchBlock($search),
1150              $this->getCreateBlock(),
1151              $this->getContentBlock(compact('offset', 'limit', 'total', 'criteria', 'crit', 'search_method', 'page', 'sort', 'dir')),
1152              $this->getFootBlock(compact('limit', 'numPages', 'total', 'crit', 'search_method', 'page', 'sort', 'dir'))
1153          );
1154      }
1155  
1156      /**
1157       * Get the admin related search form wrapped in its div.
1158       *
1159       * @param  object $search Textpattern\Search\Filter class object.
1160       * @return HTML
1161       */
1162  
1163      protected function getSearchBlock($search)
1164      {
1165          $event = $this->getEvent();
1166  
1167          return n.tag(
1168              $search->renderForm($event, array('placeholder' => 'search_skins')),
1169              'div',
1170              array(
1171                  'class' => 'txp-layout-4col-3span',
1172                  'id'    => $event.'_control',
1173              )
1174          );
1175      }
1176  
1177      /**
1178       * Get the .txp-control-panel div.
1179       *
1180       * @return HTML div containing the 'Create' button and the import form.
1181       * @see        getImportForm(), getCreateButton().
1182       */
1183  
1184      protected function getCreateBlock()
1185      {
1186          if (has_privs($this->getEvent().'.edit')) {
1187              return tag(
1188                  $this->getCreateButton().$this->getImportForm(),
1189                  'div',
1190                  array('class' => 'txp-control-panel txp-async-update', 'id' => 'skin_control_panel')
1191              );
1192          }
1193      }
1194  
1195      /**
1196       * Get the skin import form.
1197       *
1198       * @return HTML The form or a message if no new skin directory is found.
1199       */
1200  
1201      protected function getImportForm()
1202      {
1203          $event = $this->getEvent();
1204          $dirPath = $this->getDirPath();
1205  
1206          if (is_dir($dirPath) && is_writable($dirPath)) {
1207              $new = array_diff_key($this->getUploaded(false), $this->getInstalled());
1208  
1209              if ($new) {
1210                  asort($new);
1211  
1212                  return n
1213                      .tag_start('form', array(
1214                          'id'     => $event.'_import_form',
1215                          'name'   => $event.'_import_form',
1216                          'method' => 'post',
1217                          'action' => 'index.php',
1218                      ))
1219                      .tag(gTxt('import_from_disk'), 'label', array('for' => $event.'_import'))
1220                      .popHelp($event.'_import')
1221                      .selectInput('skins', $new, '', false, false, 'skins')
1222                      .eInput($this->getEvent())
1223                      .sInput('import')
1224                      .fInput('submit', '', gTxt('import'))
1225                      .n
1226                      .tag_end('form');
1227              }
1228          } else {
1229              return n
1230                  .graf(
1231                      span(null, array('class' => 'ui-icon ui-icon-alert')).' '.
1232                      gTxt('path_not_writable', array('list' => $this->getDirPath())),
1233                      array('class' => 'alert-block warning')
1234                  );
1235          }
1236      }
1237  
1238      /**
1239       * Get the class related Admin\Paginator instance.
1240       *
1241       * @return object Admin\Paginator instance.
1242       */
1243  
1244      protected function getPaginator()
1245      {
1246          return \Txp::get('\Textpattern\Admin\Paginator', $this->getEvent(), '');
1247      }
1248  
1249      /**
1250       * Get the class related Search\Filter instance.
1251       *
1252       * @param  array  $methods Available search methods.
1253       * @return object          Search\Filter instance.
1254       */
1255  
1256      protected function getSearchFilter($methods)
1257      {
1258          return \Txp::get('Textpattern\Search\Filter', $this->getEvent(), $methods);
1259      }
1260  
1261      /**
1262       * Get the button to create a new skin.
1263       *
1264       * @return HTML Link.
1265       */
1266  
1267      protected function getCreateButton()
1268      {
1269          $event = $this->getEvent();
1270  
1271          return sLink($event, 'edit', gTxt('create_skin'), 'txp-button');
1272      }
1273  
1274      /**
1275       * Get the Admin\Table $content block.
1276       *
1277       * @param  array $data compact('offset', 'limit', 'total', 'criteria', 'crit', 'search_method', 'page', 'sort', 'dir')
1278       * @return HTML        Skin list.
1279       */
1280  
1281      protected function getContentBlock($data)
1282      {
1283          extract($data);
1284  
1285          $event = $this->getEvent();
1286          $sortSQL = $sort.' '.$dir;
1287          $switchDir = ($dir == 'desc') ? 'asc' : 'desc';
1288  
1289          if ($total < 1) {
1290              if ($crit !== '') {
1291                  $out = graf(
1292                      span(null, array('class' => 'ui-icon ui-icon-info')).' '.
1293                      gTxt('no_results_found'),
1294                      array('class' => 'alert-block information')
1295                  );
1296              } else {
1297                  $out = graf(
1298                      span(null, array('class' => 'ui-icon ui-icon-info')).' '.
1299                      gTxt('no_'.$event.'_recorded'),
1300                      array('class' => 'alert-block information')
1301                  );
1302              }
1303  
1304              return $out
1305                     .n.tag_end('div') // End of .txp-layout-1col.
1306                     .n.'</div>';      // End of .txp-layout.
1307          }
1308  
1309          $rs = $this->getTableData($criteria, $sortSQL, $offset, $limit);
1310          $numThemes = mysqli_num_rows($rs);
1311  
1312          if ($rs) {
1313              $dev_preview = has_privs('skin.edit');
1314              $out = n.tag_start('form', array(
1315                          'class'  => 'multi_edit_form',
1316                          'id'     => $event.'_form',
1317                          'name'   => 'longform',
1318                          'method' => 'post',
1319                          'action' => 'index.php',
1320                      )).
1321                      n.tag_start('div', array(
1322                          'class'      => 'txp-listtables',
1323                          'tabindex'   => 0,
1324                          'aria-label' => gTxt('list'),
1325                      )).
1326                      n.tag_start('table', array('class' => 'txp-list')).
1327                      n.tag_start('thead');
1328  
1329              $ths = hCell(
1330                  fInput('checkbox', 'select_all', 0, '', '', '', '', '', 'select_all'),
1331                  '',
1332                  ' class="txp-list-col-multi-edit" scope="col" title="'.gTxt('toggle_all_selected').'"'
1333              );
1334  
1335              $thIds = array(
1336                  'name'          => 'name',
1337                  'title'         => 'title',
1338                  'version'       => 'version',
1339                  'author'        => 'author',
1340                  'section_count' => 'tab_sections',
1341                  'page_count'    => 'tab_pages',
1342                  'form_count'    => 'tab_forms',
1343                  'css_count'     => 'tab_style',
1344              );
1345  
1346              foreach ($thIds as $thId => $thVal) {
1347                  $thClass = 'txp-list-col-'.$thId
1348                            .($thId == $sort ? ' '.$dir : '')
1349                            .($thVal !== $thId ? ' '.$event.'_detail' : '');
1350  
1351                  $ths .= column_head($thVal, $thId, $event, true, $switchDir, $crit, $search_method, $thClass);
1352              }
1353  
1354              $out .= tr($ths)
1355                  .n.tag_end('thead')
1356                  .n.tag_start('tbody');
1357  
1358              while ($a = nextRow($rs)) {
1359                  extract($a, EXTR_PREFIX_ALL, $event);
1360  
1361                  $editUrl = array(
1362                      'event'         => $event,
1363                      'step'          => 'edit',
1364                      'name'          => $skin_name,
1365                      'sort'          => $sort,
1366                      'dir'           => $dir,
1367                      'page'          => $page,
1368                      'search_method' => $search_method,
1369                      'crit'          => $crit,
1370                  );
1371  
1372                  $tdAuthor = txpspecialchars($skin_author);
1373                  empty($skin_author_uri) or $tdAuthor = href($tdAuthor, $skin_author_uri, array('rel' => 'external'));
1374  
1375                  $tds = td(fInput('checkbox', 'selected[]', $skin_name), '', 'txp-list-col-multi-edit')
1376                      .hCell(
1377                          href(txpspecialchars($skin_name), $editUrl, array('title' => gTxt('edit'))).
1378                          ($numThemes > 1 ? ' | '.href(gTxt('assign_sections'), 'index.php?event=section&step=section_select_skin&skin='.urlencode($skin_name)).
1379                          (${$event.'_section_count'} > 0 ? sp.tag(gTxt('status_in_use'), 'small', array('class' => 'alert-block alert-pill success')) :
1380                              (${$event.'_dev_section_count'} > 0 ? sp.tag(gTxt('status_in_use'), 'small', array('class' => 'alert-block alert-pill warning')) : '')
1381                          ) : ''),
1382                          '', array(
1383                              'scope' => 'row',
1384                              'class' => 'txp-list-col-name',
1385                          )
1386                      )
1387                      .td(txpspecialchars($skin_title), '', 'txp-list-col-title')
1388                      .td(txpspecialchars($skin_version), '', 'txp-list-col-version')
1389                      .td($tdAuthor, '', 'txp-list-col-author');
1390  
1391                  $countNames = array('section', 'page', 'form', 'css');
1392  
1393                  foreach ($countNames as $name) {
1394                      if (${$event.'_'.$name.'_count'} > 0) {
1395                          if ($name === 'section') {
1396                              $linkParams = array(
1397                                  'event'         => 'section',
1398                                  'search_method' => $event,
1399                                  'crit'          => '"'.$skin_name.'"',
1400                              );
1401                          } else {
1402                              $linkParams = array(
1403                                  'event' => $name,
1404                                  $event  => $skin_name,
1405                              );
1406                          }
1407  
1408                          $tdVal = href(
1409                              ${$event.'_'.$name.'_count'},
1410                              $linkParams,
1411                              array(
1412                                  'title' => gTxt(
1413                                      $event.'_count_'.$name,
1414                                      array('{num}' => ${$event.'_'.$name.'_count'})
1415                                  )
1416                              )
1417                          );
1418                      } else {
1419                          $tdVal = 0;
1420                      }
1421  
1422                      $tds .= td($tdVal, '', 'txp-list-col-'.$name.'_count');
1423                  }
1424  
1425                  $out .= tr($tds, array('id' => $this->getTable().'_'.$skin_name));
1426              }
1427  
1428              return $out
1429                     .n.tag_end('tbody')
1430                     .n.tag_end('table')
1431                     .n.tag_end('div')
1432                     .n.self::getMultiEditForm($page, $sort, $dir, $crit, $search_method)
1433                     .tInput()
1434                     .n.tag_end('form');
1435          }
1436      }
1437  
1438      /**
1439       * Get the Admin\Table $foot block.
1440       *
1441       * @param  array $data compact('limit', 'numPages', 'total', 'crit', 'search_method', 'page', 'sort', 'dir')
1442       * @return HTML        Multi-edit form, pagination and navigation form.
1443       */
1444  
1445      protected function getFootBlock($data)
1446      {
1447          extract($data);
1448  
1449          return $this->getPaginator()->render()
1450                 .nav_form($this->getEvent(), $page, $numPages, $sort, $dir, $crit, $search_method, $total, $limit);
1451      }
1452  
1453      /**
1454       * Get a multi-edit checkbox.
1455       *
1456       * @param  string $label The textpack related string to use.
1457       * @return HTML
1458       */
1459  
1460      protected function getMultiEditCheckbox($label)
1461      {
1462          return checkbox2('sync', get_pref($label, true), 0, 'sync')
1463                 .n.tag(gTxt($label), 'label', array('for' => 'sync'))
1464                 .popHelp($label);
1465      }
1466  
1467      /**
1468       * Render a multi-edit form widget.
1469       *
1470       * @param  int    $page          The current page number
1471       * @param  string $sort          The current sorting value
1472       * @param  string $dir           The current sorting direction
1473       * @param  string $crit          The current search criteria
1474       * @param  string $search_method The current search method
1475       * @return HTML
1476       */
1477  
1478      protected function getMultiEditForm($page, $sort, $dir, $crit, $search_method)
1479      {
1480          $methods = array(
1481              'import'    => array(
1482                  'label' => gTxt('update_from_disk'),
1483                  'html'  => $this->getMultiEditCheckbox('skin_delete_from_database'),
1484              ),
1485              'export'    => array(
1486                  'label' => gTxt('export_to_disk'),
1487                  'html'  => $this->getMultiEditCheckbox('skin_delete_from_disk'),
1488              ),
1489              'duplicate' => gTxt('duplicate'),
1490              'delete'    => array(
1491                  'label' => gTxt('delete'),
1492                  'html'  => $this->getMultiEditCheckbox('skin_delete_entirely'),
1493              ),
1494          );
1495  
1496          return multi_edit($methods, $this->getEvent(), 'multi_edit', $page, $sort, $dir, $crit, $search_method);
1497      }
1498  
1499      /**
1500       * Get the edit form.
1501       *
1502       * @param  mixed $message
1503       * @return HTML
1504       */
1505  
1506      protected function getEditForm($message = '')
1507      {
1508          global $step;
1509  
1510          $event = $this->getEvent();
1511  
1512          require_privs($event.'.edit');
1513  
1514          !$message or pagetop(gTxt('tab_'.$event), $message);
1515  
1516          extract(gpsa(array(
1517              'page',
1518              'sort',
1519              'dir',
1520              'crit',
1521              'search_method',
1522              'name',
1523          )));
1524  
1525          $fields = array('name', 'title', 'version', 'description', 'author', 'author_uri');
1526  
1527          if ($name) {
1528              $rs = $this->setName($name)->getRow();
1529  
1530              if (!$rs) {
1531                  return $this->main();
1532              }
1533  
1534              $caption = gTxt('edit_'.$event);
1535              $extraAction = href(
1536                  '<span class="ui-icon ui-icon-copy"></span> '.gTxt('duplicate'),
1537                  '#',
1538                  array(
1539                      'class'     => 'txp-clone',
1540                      'data-form' => $event.'_form',
1541                  )
1542              );
1543          } else {
1544              $rs = array_fill_keys($fields, '');
1545              $caption = gTxt('create_'.$event);
1546              $extraAction = '';
1547          }
1548  
1549          extract($rs, EXTR_PREFIX_ALL, $event);
1550          pagetop(gTxt('tab_'.$event));
1551  
1552          $content = hed($caption, 2);
1553  
1554          foreach ($fields as $field) {
1555              $current = ${$event.'_'.$field};
1556  
1557              if ($field === 'description') {
1558                  $input = text_area($field, 0, 0, $current, $event.'_'.$field);
1559              } elseif ($field === 'name') {
1560                  $input = fInput(
1561                      'text',
1562                      array(
1563                          'name'      => $field,
1564                          'maxlength' => '63',
1565                      ), $current, '', '', '', INPUT_REGULAR, '', $event.'_'.$field, '', true
1566                  );
1567              } elseif ($field === 'author_uri') {
1568                  $input = fInput('url', $field, $current, '', '', '', INPUT_REGULAR, '', $event.'_'.$field, '', '', 'http(s)://');
1569              } else {
1570                  $input = fInput('text', $field, $current, '', '', '', INPUT_REGULAR, '', $event.'_'.$field);
1571              }
1572  
1573              $content .= inputLabel($event.'_'.$field, $input, $event.'_'.$field, $event.'_'.$field);
1574          }
1575  
1576          $content .= pluggable_ui($event.'_ui', 'extend_detail_form', '', $rs)
1577              .graf(
1578                  $extraAction.
1579                  sLink($event, '', gTxt('cancel'), 'txp-button')
1580                  .fInput('submit', '', gTxt('save'), 'publish'),
1581                  array('class' => 'txp-edit-actions')
1582              )
1583              .eInput($event)
1584              .sInput('save')
1585              .hInput('old_name', $skin_name)
1586              .hInput('old_title', $skin_title)
1587              .hInput('search_method', $search_method)
1588              .hInput('crit', $crit)
1589              .hInput('page', $page)
1590              .hInput('sort', $sort)
1591              .hInput('dir', $dir);
1592  
1593          return form($content, '', '', 'post', 'txp-edit', '', $event.'_form');
1594      }
1595  }

title

Description

title

Description

title

Description

title

title

Body