Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/include/txp_article.php - 2205 lines - 68086 bytes - Summary - Text - Print

Description: Write panel.

   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   * Write panel.
  26   *
  27   * @package Admin\Article
  28   */
  29  
  30  use Textpattern\Validator\BlankConstraint;
  31  use Textpattern\Validator\CategoryConstraint;
  32  use Textpattern\Validator\ChoiceConstraint;
  33  use Textpattern\Validator\FalseConstraint;
  34  use Textpattern\Validator\FormConstraint;
  35  use Textpattern\Validator\SectionConstraint;
  36  use Textpattern\Validator\Validator;
  37  
  38  if (!defined('txpinterface')) {
  39      die('txpinterface is undefined.');
  40  }
  41  
  42  global $vars, $statuses;
  43  
  44  $vars = array(
  45      'ID',
  46      'Title',
  47      'Body',
  48      'Excerpt',
  49      'textile_excerpt',
  50      'Image',
  51      'textile_body',
  52      'Keywords',
  53      'description',
  54      'Status',
  55      'Posted',
  56      'Expires',
  57      'Section',
  58      'Category1',
  59      'Category2',
  60      'Annotate',
  61      'AnnotateInvite',
  62      'publish_now',
  63      'reset_time',
  64      'expire_now',
  65      'AuthorID',
  66      'sPosted',
  67      'LastModID',
  68      'sLastMod',
  69      'override_form',
  70      'year',
  71      'month',
  72      'day',
  73      'hour',
  74      'minute',
  75      'second',
  76      'url_title',
  77      'exp_year',
  78      'exp_month',
  79      'exp_day',
  80      'exp_hour',
  81      'exp_minute',
  82      'exp_second',
  83      'sExpires',
  84  );
  85  
  86  $cfs = getCustomFields();
  87  
  88  foreach ($cfs as $i => $cf_name) {
  89      $vars[] = "custom_$i";
  90  }
  91  
  92  $statuses = status_list();
  93  
  94  if (!empty($event) && $event == 'article') {
  95      require_privs('article');
  96  
  97      $save = gps('save');
  98  
  99      if ($save) {
 100          $step = 'save';
 101      }
 102  
 103      $publish = gps('publish');
 104  
 105      if ($publish) {
 106          $step = 'save';
 107      }
 108  
 109      if (empty($step)) {
 110          $step = 'edit';
 111      }
 112  
 113      bouncer($step, array(
 114          'create'         => false,
 115          'publish'        => true,
 116          'edit'           => false,
 117          'save'           => true,
 118      ));
 119  
 120      switch ($step) {
 121          case 'create':
 122          case 'edit':
 123              article_edit();
 124              break;
 125          case 'publish':
 126          case 'save':
 127              article_save();
 128              break;
 129      }
 130  }
 131  
 132  /**
 133   * Processes sent forms and saves new articles. Deprecated in 4.7 by article_save().
 134   */
 135  
 136  function article_post()
 137  {
 138      article_save();
 139  }
 140  
 141  /**
 142   * Processes sent forms and updates existing articles.
 143   */
 144  
 145  function article_save()
 146  {
 147      global $txp_user, $vars, $prefs;
 148  
 149      extract($prefs);
 150  
 151      $incoming = array_map('assert_string', psa($vars));
 152      $is_clone = ps('copy');
 153  
 154      if ($is_clone) {
 155          $incoming['ID'] = $incoming['url_title'] = '';
 156          $incoming['Status'] = STATUS_DRAFT;
 157      }
 158  
 159      if ($incoming['ID']) {
 160          $oldArticle = safe_row("Status, AuthorID, url_title, Title, textile_body, textile_excerpt,
 161              UNIX_TIMESTAMP(LastMod) AS sLastMod, LastModID,
 162              UNIX_TIMESTAMP(Posted) AS sPosted,
 163              UNIX_TIMESTAMP(Expires) AS sExpires",
 164              'textpattern', "ID = ".(int) $incoming['ID']);
 165  
 166          if (!($oldArticle['Status'] >= STATUS_LIVE && has_privs('article.edit.published')
 167              || $oldArticle['Status'] >= STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own.published')
 168              || $oldArticle['Status'] < STATUS_LIVE && has_privs('article.edit')
 169              || $oldArticle['Status'] < STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own'))) {
 170              // Not allowed, you silly rabbit, you shouldn't even be here.
 171              // Show default editing screen.
 172              article_edit();
 173  
 174              return;
 175          }
 176  
 177          if ($oldArticle['sLastMod'] != $incoming['sLastMod']) {
 178              article_edit(array(gTxt('concurrent_edit_by', array('{author}' => txpspecialchars($oldArticle['LastModID']))), E_ERROR), true, true);
 179  
 180              return;
 181          }
 182      } else {
 183          $oldArticle = array('Status' => STATUS_PENDING,
 184              'url_title'       => '',
 185              'Title'           => '',
 186              'textile_body'    => $use_textile,
 187              'textile_excerpt' => $use_textile,
 188              'sLastMod'        => null,
 189              'LastModID'       => $txp_user,
 190              'sPosted'         => time(),
 191              'sExpires'        => null,
 192          );
 193      }
 194  
 195      if (!has_privs('article.set_markup')) {
 196          $incoming['textile_body'] = $oldArticle['textile_body'];
 197          $incoming['textile_excerpt'] = $oldArticle['textile_excerpt'];
 198      }
 199  
 200      $incoming = textile_main_fields($incoming);
 201  
 202      extract(doSlash($incoming));
 203      $ID = intval($ID);
 204      $Status = assert_int($Status);
 205  
 206      if (!has_privs('article.publish') && $Status >= STATUS_LIVE) {
 207          $Status = STATUS_PENDING;
 208      }
 209  
 210      // Comments may be on, off, or disabled.
 211      $Annotate = (int) $Annotate;
 212  
 213      // Set and validate article timestamp.
 214      if ($publish_now || $reset_time) {
 215          $whenposted = "NOW()";
 216          $when_ts = time();
 217      } else {
 218          if (!is_numeric($year) || !is_numeric($month) || !is_numeric($day) || !is_numeric($hour) || !is_numeric($minute) || !is_numeric($second)) {
 219              $ts = false;
 220          } else {
 221              $ts = strtotime($year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second);
 222          }
 223  
 224          if ($ts === false || $ts < 0) {
 225              $when_ts = $oldArticle['sPosted'];
 226              $msg = array(gTxt('invalid_postdate'), E_ERROR);
 227          } else {
 228              $when_ts = $ts - tz_offset($ts);
 229          }
 230  
 231          $whenposted = "FROM_UNIXTIME($when_ts)";
 232      }
 233  
 234      // Set and validate expiry timestamp.
 235      if ($expire_now) {
 236          $ts = time();
 237          $expires = $ts - tz_offset($ts);
 238      } elseif (empty($exp_year)) {
 239          $expires = 0;
 240      } else {
 241          if (empty($exp_month)) {
 242              $exp_month = 1;
 243          }
 244  
 245          if (empty($exp_day)) {
 246              $exp_day = 1;
 247          }
 248  
 249          if (empty($exp_hour)) {
 250              $exp_hour = 0;
 251          }
 252  
 253          if (empty($exp_minute)) {
 254              $exp_minute = 0;
 255          }
 256  
 257          if (empty($exp_second)) {
 258              $exp_second = 0;
 259          }
 260  
 261          $ts = strtotime($exp_year.'-'.$exp_month.'-'.$exp_day.' '.$exp_hour.':'.$exp_minute.':'.$exp_second);
 262  
 263          if ($ts === false || $ts < 0) {
 264              $expires = $oldArticle['sExpires'];
 265              $msg = array(gTxt('invalid_expiredate'), E_ERROR);
 266          } else {
 267              $expires = $ts - tz_offset($ts);
 268          }
 269      }
 270  
 271      if ($expires && ($expires <= $when_ts)) {
 272          $expires = $oldArticle['sExpires'];
 273          $msg = array(gTxt('article_expires_before_postdate'), E_ERROR);
 274      }
 275  
 276      if ($expires) {
 277          $whenexpires = "FROM_UNIXTIME($expires)";
 278      } else {
 279          $whenexpires = "NULL";
 280      }
 281  
 282      // Auto-update custom-titles according to Title, as long as unpublished and
 283      // NOT customised.
 284      if (empty($url_title)
 285          || (($oldArticle['Status'] < STATUS_LIVE)
 286          && ($oldArticle['url_title'] === $url_title)
 287          && ($oldArticle['Title'] !== $Title)
 288          && ($oldArticle['url_title'] === stripSpace($oldArticle['Title'], 1))
 289      )) {
 290          $url_title = stripSpace($Title_plain, 1);
 291      }
 292  
 293      $Keywords = doSlash(trim(preg_replace('/( ?[\r\n\t,])+ ?/s', ',', preg_replace('/ +/', ' ', ps('Keywords'))), ', '));
 294      $user = doSlash($txp_user);
 295  
 296      $cfq = array();
 297      $cfs = getCustomFields();
 298  
 299      foreach ($cfs as $i => $cf_name) {
 300          $custom_x = "custom_{$i}";
 301          $cfq[] = "custom_$i = '".$$custom_x."'";
 302      }
 303  
 304      $cfq = join(', ', $cfq);
 305  
 306      $rs = compact($vars);
 307  
 308      if (article_validate($rs, $msg)) {
 309          $set =
 310             "Title           = '$Title',
 311              Body            = '$Body',
 312              Body_html       = '$Body_html',
 313              Excerpt         = '$Excerpt',
 314              Excerpt_html    = '$Excerpt_html',
 315              Keywords        = '$Keywords',
 316              description     = '$description',
 317              Image           = '$Image',
 318              Status          = '$Status',
 319              Posted          =  $whenposted,
 320              Expires         =  $whenexpires,
 321              LastMod         =  NOW(),
 322              LastModID       = '$user',
 323              Section         = '$Section',
 324              Category1       = '$Category1',
 325              Category2       = '$Category2',
 326              Annotate        =  $Annotate,
 327              textile_body    = '$textile_body',
 328              textile_excerpt = '$textile_excerpt',
 329              override_form   = '$override_form',
 330              url_title       = '$url_title',
 331              AnnotateInvite  = '$AnnotateInvite'"
 332              .(($cfs) ? ', '.$cfq : '')
 333              .(!empty($ID) ? '' :
 334              ", AuthorID        = '$user',
 335              uid             = '".md5(uniqid(rand(), true))."',
 336              feed_time       = NOW()");
 337  
 338          if ($ID && safe_update('textpattern', $set, "ID = $ID")
 339              || !$ID && $rs['ID'] = $GLOBALS['ID'] = safe_insert('textpattern', $set)
 340          ) {
 341              if ($is_clone) {
 342                  $url_title = stripSpace($Title_plain.' ('.$rs['ID'].')', 1);
 343                  safe_update(
 344                      'textpattern',
 345                      "Title = CONCAT(Title, ' (', ID, ')'),
 346                      url_title = '$url_title'",
 347                      "ID = ".$rs['ID']
 348                  );
 349              }
 350  
 351              if ($Status >= STATUS_LIVE) {
 352                  if ($oldArticle['Status'] < STATUS_LIVE) {
 353                      do_pings();
 354                  } else {
 355                      update_lastmod($ID ? 'article_saved' : 'article_posted', $rs);
 356                  }
 357              }
 358  
 359              now('posted', true);
 360              now('expires', true);
 361              callback_event($ID ? 'article_saved' : 'article_posted', '', false, $rs);
 362  
 363              if (empty($msg)) {
 364                  $s = check_url_title($url_title);
 365                  $msg = array(get_status_message($Status).' '.$s, $s ? E_WARNING : 0);
 366              }
 367          } else {
 368              $msg = array(gTxt('article_save_failed'), E_ERROR);
 369          }
 370      }
 371  
 372      article_edit($msg, false, true);
 373  }
 374  
 375  /**
 376   * Renders article preview.
 377   *
 378   * @param string $field
 379   */
 380  
 381  function article_preview($field = false)
 382  {
 383      global $prefs, $vars, $app_mode;
 384  
 385      // Assume they came from post.
 386      $view = gps('view', 'preview');
 387      $rs = textile_main_fields(gpsa($vars));
 388  
 389      // Preview pane
 390      $preview = '<div id="pane-view" class="'.txpspecialchars($view).'">';
 391  
 392      if ($view == 'preview') {
 393          if (!$field || $field == 'body') {
 394              $preview .= n.'<div class="body">'.
 395                  n.'<h2>'.gTxt('body').'</h2>'.
 396                  implode('', txp_tokenize($rs['Body_html'], false, function ($tag) {
 397                      return '<span class="disabled">'.txpspecialchars($tag).'</span>';
 398                  })).
 399              '</div>';
 400          }
 401  
 402          if ($prefs['articles_use_excerpts'] && (!$field || $field == 'excerpt')) {
 403              $preview .= n.'<div class="excerpt">'.
 404                  n.'<h2>'.gTxt('excerpt').'</h2>'.
 405                  implode('', txp_tokenize($rs['Excerpt_html'], false, function ($tag) {
 406                      return '<span class="disabled">'.txpspecialchars($tag).'</span>';
 407                  })).
 408                  '</div>';
 409          }
 410      } elseif ($view == 'html') {
 411          if (!$field || $field == 'body') {
 412              $preview .= n.'<h2>'.gTxt('body').'</h2>'.
 413              n.tag(
 414                  tag(str_replace(array(t), array(sp.sp.sp.sp), txpspecialchars($rs['Body_html'])), 'code', array(
 415                      'class' => 'language-markup',
 416                      'dir'   => 'ltr',
 417                  )),
 418                  'pre', array('class' => 'body')
 419              );
 420          }
 421  
 422          if ($prefs['articles_use_excerpts'] && (!$field || $field == 'excerpt')) {
 423              $preview .= n.'<h2>'.gTxt('excerpt').'</h2>'.
 424                  n.tag(
 425                      tag(str_replace(array(t), array(sp.sp.sp.sp), txpspecialchars($rs['Excerpt_html'])), 'code', array(
 426                          'class' => 'language-markup',
 427                          'dir'   => 'ltr',
 428                      )),
 429                      'pre', array('class' => 'excerpt')
 430                  );
 431          }
 432      }
 433  
 434      $preview .= '</div>';// End of #pane-view.
 435  
 436      return $preview;
 437  }
 438  
 439  /**
 440   * Renders article editor form.
 441   *
 442   * @param string|array $message          The activity message
 443   * @param bool         $concurrent       Treat as a concurrent save
 444   * @param bool         $refresh_partials Whether to refresh partial contents
 445   */
 446  
 447  function article_edit($message = '', $concurrent = false, $refresh_partials = false)
 448  {
 449      global $vars, $txp_user, $prefs, $step, $view, $app_mode;
 450  
 451      $view = gps('view', 'text');
 452  
 453      if ($view != 'text') {
 454          echo article_preview(gps('preview'));
 455  
 456          return;
 457      }
 458  
 459      extract($prefs);
 460  
 461      /*
 462      $partials is an array of:
 463      $key => array (
 464          'mode' => {PARTIAL_STATIC | PARTIAL_VOLATILE | PARTIAL_VOLATILE_VALUE},
 465          'selector' => $DOM_selector or array($selector, $fragment) of $DOM_selectors,
 466           'cb' => $callback_function,
 467           'html' => $return_value_of_callback_function (need not be initialised here)
 468      )
 469      */
 470      $partials = array(
 471          // Hidden 'ID'.
 472          'ID'   => array(
 473              'mode'     => PARTIAL_VOLATILE_VALUE,
 474              'selector' => 'input[name=ID]',
 475              'cb'       => 'article_partial_value',
 476          ),
 477          // HTML 'Title' field (in <head>).
 478          'html_title'   => array(
 479              'mode'     => PARTIAL_VOLATILE,
 480              'selector' => 'title',
 481              'cb'       => 'article_partial_html_title',
 482          ),
 483          // 'Text/HTML/Preview' links region.
 484          'view_modes' => array(
 485              'mode'     => PARTIAL_STATIC,
 486              'selector' => '#view_modes',
 487              'cb'       => 'article_partial_view_modes',
 488          ),
 489          // 'Title' region.
 490          'title' => array(
 491              'mode'     => PARTIAL_STATIC,
 492              'selector' => 'div.title',
 493              'cb'       => 'article_partial_title',
 494          ),
 495          // 'Title' field.
 496          'title_value'  => array(
 497              'mode'     => PARTIAL_VOLATILE_VALUE,
 498              'selector' => '#title',
 499              'cb'       => 'article_partial_title_value',
 500          ),
 501          // 'Author' region.
 502          'author' => array(
 503              'mode'     => PARTIAL_VOLATILE,
 504              'selector' => 'div.author',
 505              'cb'       => 'article_partial_author',
 506          ),
 507          // 'Actions' region.
 508          'actions' => array(
 509              'mode'     => PARTIAL_VOLATILE,
 510              'selector' => '#txp-article-actions',
 511              'cb'       => 'article_partial_actions',
 512          ),
 513          // 'Body' region.
 514          'body' => array(
 515              'mode'     => PARTIAL_STATIC,
 516              'selector' => 'div.body',
 517              'cb'       => 'article_partial_body',
 518          ),
 519          // 'Excerpt' region.
 520          'excerpt' => array(
 521              'mode'     => PARTIAL_STATIC,
 522              'selector' => 'div.excerpt',
 523              'cb'       => 'article_partial_excerpt',
 524          ),
 525          // 'Posted' value.
 526          'sPosted' => array(
 527              'mode'     => PARTIAL_VOLATILE_VALUE,
 528              'selector' => '[name=sPosted]',
 529              'cb'       => 'article_partial_value',
 530          ),
 531          // 'Last modified' value.
 532          'sLastMod' => array(
 533              'mode'     => PARTIAL_VOLATILE_VALUE,
 534              'selector' => '[name=sLastMod]',
 535              'cb'       => 'article_partial_value',
 536          ),
 537          // 'Previous/Next' article links region.
 538          'article_nav' => array(
 539              'mode'     => PARTIAL_VOLATILE,
 540              'selector' => 'nav.nav-tertiary',
 541              'cb'       => 'article_partial_article_nav',
 542          ),
 543          // 'Status' region.
 544          'status' => array(
 545              'mode'     => PARTIAL_VOLATILE,
 546              'selector' => '#txp-container-status',
 547              'cb'       => 'article_partial_status',
 548          ),
 549          // 'Section' region.
 550          'section' => array(
 551              'mode'     => PARTIAL_STATIC,
 552              'selector' => 'div.section',
 553              'cb'       => 'article_partial_section',
 554          ),
 555          // Categories region.
 556          'categories' => array(
 557              'mode'     => PARTIAL_STATIC,
 558              'selector' => '#categories_group',
 559              'cb'       => 'article_partial_categories',
 560          ),
 561          // Publish date/time region.
 562          'posted' => array(
 563              'mode'     => PARTIAL_VOLATILE,
 564              'selector' => '#publish-datetime-group',
 565              'cb'       => 'article_partial_posted',
 566          ),
 567          // Expire date/time region.
 568          'expires' => array(
 569              'mode'     => PARTIAL_VOLATILE,
 570              'selector' => '#expires-datetime-group',
 571              'cb'       => 'article_partial_expires',
 572          ),
 573          // Meta 'URL-only title' region.
 574          'url_title' => array(
 575              'mode'     => PARTIAL_STATIC,
 576              'selector' => 'div.url-title',
 577              'cb'       => 'article_partial_url_title',
 578          ),
 579          // Meta 'URL-only title' field.
 580          'url_title_value' => array(
 581              'mode'     => PARTIAL_VOLATILE_VALUE,
 582              'selector' => '#url-title',
 583              'cb'       => 'article_partial_url_title_value',
 584          ),
 585          // Meta 'Description' region.
 586          'description' => array(
 587              'mode'     => PARTIAL_STATIC,
 588              'selector' => 'div.description',
 589              'cb'       => 'article_partial_description',
 590          ),
 591          // Meta 'Description' field.
 592          'description_value'  => array(
 593              'mode'     => PARTIAL_VOLATILE_VALUE,
 594              'selector' => '#description',
 595              'cb'       => 'article_partial_description_value',
 596          ),
 597          // Meta 'Keywords' region.
 598          'keywords' => array(
 599              'mode'     => PARTIAL_STATIC,
 600              'selector' => 'div.keywords',
 601              'cb'       => 'article_partial_keywords',
 602          ),
 603          // Meta 'Keywords' field.
 604          'keywords_value'  => array(
 605              'mode'     => PARTIAL_VOLATILE_VALUE,
 606              'selector' => '#keywords',
 607              'cb'       => 'article_partial_keywords_value',
 608          ),
 609          // 'Comment options' section.
 610          'comments' => array(
 611              'mode'     => PARTIAL_VOLATILE,
 612              'selector' => '#write-comments',
 613              'cb'       => 'article_partial_comments',
 614          ),
 615          // 'Article image' section.
 616          'image' => array(
 617              'mode'     => PARTIAL_VOLATILE,
 618              'selector' => array('#txp-image-group .txp-container', '.txp-container'),
 619              'cb'       => 'article_partial_image',
 620          ),
 621          // 'Custom fields' section.
 622          'custom_fields' => array(
 623              'mode'     => PARTIAL_VOLATILE,
 624              'selector' => array('#txp-custom-field-group-content .txp-container', '.txp-container'),
 625              'cb'       => 'article_partial_custom_fields',
 626          ),
 627          // 'Recent articles' values.
 628          'recent_articles' => array(
 629              'mode'     => PARTIAL_VOLATILE,
 630              'selector' => array('#txp-recent-group-content .txp-container', '.txp-container'),
 631              'cb'       => 'article_partial_recent_articles',
 632          ),
 633      );
 634  
 635      // Add partials for custom fields (and their values which is redundant by
 636      // design, for plugins).
 637      global $cfs;
 638  
 639      foreach ($cfs as $k => $v) {
 640          $partials["custom_field_{$k}"] = array(
 641              'mode'     => PARTIAL_STATIC,
 642              'selector' => "p.custom-field.custom-{$k}",
 643              'cb'       => 'article_partial_custom_field',
 644          );
 645          $partials["custom_{$k}"] = array(
 646              'mode'     => PARTIAL_STATIC,
 647              'selector' => "#custom-{$k}",
 648              'cb'       => 'article_partial_value',
 649          );
 650      }
 651  
 652      if ($step !== 'create') {
 653          $step = "edit";
 654      }
 655  
 656      // Newly-saved article.
 657      if (!empty($GLOBALS['ID'])) {
 658          $ID = $GLOBALS['ID'];
 659      } else {
 660          $ID = $step === 'create' ? 0 : intval(gps('ID'));
 661      }
 662  
 663      if (!empty($ID) && !$concurrent) {
 664          // It's an existing article - off we go to the database.
 665          $ID = assert_int($ID);
 666  
 667          $rs = safe_row(
 668              "*, UNIX_TIMESTAMP(Posted) AS sPosted,
 669              UNIX_TIMESTAMP(Expires) AS sExpires,
 670              UNIX_TIMESTAMP(LastMod) AS sLastMod",
 671              'textpattern',
 672              "ID = $ID"
 673          );
 674  
 675          if (empty($rs)) {
 676              return;
 677          }
 678  
 679          $rs['reset_time'] = $rs['publish_now'] = $rs['expire_now'] = false;
 680  
 681          if (gps('copy') && !gps('publish')) {
 682              $rs['ID'] = $rs['url_title'] = '';
 683              $rs['Status'] = STATUS_DRAFT;
 684          }
 685      } else {
 686          // Assume they came from post.
 687          $store_out = array('ID' => $ID) + gpsa($vars);
 688  
 689          if ($concurrent) {
 690              $store_out['sLastMod'] = safe_field("UNIX_TIMESTAMP(LastMod) AS sLastMod", 'textpattern', "ID = $ID");
 691          }
 692  
 693          if (!has_privs('article.set_markup') && !empty($ID)) {
 694              $oldArticle = safe_row("textile_body, textile_excerpt", 'textpattern', "ID = $ID");
 695              if (!empty($oldArticle)) {
 696                  $store_out['textile_body'] = $oldArticle['textile_body'];
 697                  $store_out['textile_excerpt'] = $oldArticle['textile_excerpt'];
 698              }
 699          }
 700  
 701          $rs = textile_main_fields($store_out);
 702  
 703          if (!empty($rs['exp_year'])) {
 704              if (empty($rs['exp_month'])) {
 705                  $rs['exp_month'] = 1;
 706              }
 707  
 708              if (empty($rs['exp_day'])) {
 709                  $rs['exp_day'] = 1;
 710              }
 711  
 712              if (empty($rs['exp_hour'])) {
 713                  $rs['exp_hour'] = 0;
 714              }
 715  
 716              if (empty($rs['exp_minute'])) {
 717                  $rs['exp_minute'] = 0;
 718              }
 719  
 720              if (empty($rs['exp_second'])) {
 721                  $rs['exp_second'] = 0;
 722              }
 723  
 724              $rs['sExpires'] = safe_strtotime($rs['exp_year'].'-'.$rs['exp_month'].'-'.$rs['exp_day'].' '.
 725                  $rs['exp_hour'].':'.$rs['exp_minute'].':'.$rs['exp_second']);
 726          }
 727  
 728          if (!empty($rs['year'])) {
 729              $rs['sPosted'] = safe_strtotime($rs['year'].'-'.$rs['month'].'-'.$rs['day'].' '.
 730                  $rs['hour'].':'.$rs['minute'].':'.$rs['second']);
 731          }
 732      }
 733  
 734      $validator = new Validator(new SectionConstraint($rs['Section']));
 735      if (!$validator->validate()) {
 736          $rs['Section'] = getDefaultSection();
 737      }
 738  
 739      extract($rs);
 740  
 741      if ($ID && !empty($sPosted)) {
 742          // Previous record?
 743          $rs['prev_id'] = checkIfNeighbour('prev', $sPosted, $ID);
 744  
 745          // Next record?
 746          $rs['next_id'] = checkIfNeighbour('next', $sPosted, $ID);
 747      } else {
 748          $rs['prev_id'] = $rs['next_id'] = 0;
 749      }
 750  
 751      // Let plugins chime in on partials meta data.
 752      callback_event_ref('article_ui', 'partials_meta', 0, $rs, $partials);
 753      $rs['partials_meta'] = &$partials;
 754  
 755      // Get content for volatile partials.
 756      $partials = updatePartials($partials, $rs, array(PARTIAL_VOLATILE, PARTIAL_VOLATILE_VALUE));
 757  
 758      if ($refresh_partials) {
 759          $response[] = announce($message);
 760          $response[] = '$("#article_form [type=submit]").val(textpattern.gTxt("save"))';
 761  
 762          if ($Status < STATUS_LIVE) {
 763              $response[] = '$("#article_form").addClass("saved").removeClass("published")';
 764          } else {
 765              $response[] = '$("#article_form").addClass("published").removeClass("saved")';
 766          }
 767  
 768          if (!empty($GLOBALS['ID'])) {
 769              $response[] = "if (typeof window.history.replaceState == 'function') {history.replaceState({}, '', '?event=article&ID=$ID')}";
 770          }
 771  
 772          $response = array_merge($response, updateVolatilePartials($partials));
 773          send_script_response(join(";\n", $response));
 774  
 775          // Bail out.
 776          return;
 777      }
 778  
 779      // Get content for static partials.
 780      $partials = updatePartials($partials, $rs, PARTIAL_STATIC);
 781  
 782      $page_title = $ID ? $Title : gTxt('write');
 783      pagetop($page_title, $message);
 784  
 785      $class = array('async');
 786  
 787      if ($Status >= STATUS_LIVE) {
 788          $class[] = 'published';
 789      } elseif ($ID) {
 790          $class[] = 'saved';
 791      }
 792  
 793      echo n.tag_start('form', array(
 794              'class'  => $class,
 795              'id'     => 'article_form',
 796              'name'   => 'article_form',
 797              'method' => 'post',
 798              'action' => 'index.php',
 799          )).
 800          n.'<div class="txp-layout">';
 801  
 802      echo hInput('ID', $ID).
 803          eInput('article').
 804          sInput($step).
 805          hInput('sPosted', $sPosted).
 806          hInput('sLastMod', $sLastMod).
 807          hInput('AuthorID', $AuthorID).
 808          hInput('LastModID', $LastModID);
 809  
 810          echo n.'<div class="txp-layout-4col-3span">'.
 811          hed(gTxt('tab_write'), 1, array('class' => 'txp-heading'));
 812  
 813      echo n.'<div role="region" id="main_content">';
 814  
 815      echo n.'<div class="text" id="pane-text">'.$partials['title']['html'],
 816          $partials['author']['html'],
 817          $partials['body']['html'];
 818      if ($articles_use_excerpts) {
 819          echo $partials['excerpt']['html'];
 820      }
 821      echo n.'</div>';
 822  
 823      echo n.'<div class="txp-dialog">';
 824      echo n.$partials['view_modes']['html'];
 825      echo article_preview();
 826      echo '</div>';// End of .txp-dialog.
 827  
 828      echo n.'</div>'.// End of #main_content.
 829          n.'</div>'; // End of .txp-layout-4col-3span.
 830  
 831      // Sidebar column (only shown if in text editing view).
 832      echo n.'<div class="txp-layout-4col-alt">'.
 833          n.'<div class="txp-save-zone">';
 834  
 835      // 'Publish/Save' button.
 836      if (empty($ID)) {
 837          if (has_privs('article.publish') && get_pref('default_publish_status', STATUS_LIVE) >= STATUS_LIVE) {
 838              $push_button = fInput('submit', 'publish', gTxt('publish'), 'publish');
 839          } else {
 840              $push_button = fInput('submit', 'publish', gTxt('save'), 'publish');
 841          }
 842  
 843          echo graf($push_button, array('class' => 'txp-save'));
 844      } elseif (
 845          ($Status >= STATUS_LIVE && has_privs('article.edit.published')) ||
 846          ($Status >= STATUS_LIVE && $AuthorID === $txp_user && has_privs('article.edit.own.published')) ||
 847          ($Status < STATUS_LIVE && has_privs('article.edit')) ||
 848          ($Status < STATUS_LIVE && $AuthorID === $txp_user && has_privs('article.edit.own'))
 849      ) {
 850          echo graf(fInput('submit', 'save', gTxt('save'), 'publish'), array('class' => 'txp-save'));
 851      }
 852  
 853      echo $partials['actions']['html'].
 854          n.'</div>';
 855  
 856      echo n.'<div role="region" id="supporting_content">';
 857  
 858      // 'Override form' selection.
 859      $form_pop = $allow_form_override ? form_pop($override_form, 'override-form', $rs['Section']) : '';
 860      $html_override = $form_pop
 861          ? pluggable_ui('article_ui', 'override',
 862              inputLabel(
 863                  'override-form',
 864                  $form_pop,
 865                  'override_default_form',
 866                  array('override_form', 'instructions_override_form'),
 867                  array('class' => 'txp-form-field override-form')
 868              ),
 869              $rs)
 870          : '';
 871  
 872      // 'Sort and display' section.
 873      echo pluggable_ui(
 874          'article_ui',
 875          'sort_display',
 876          wrapRegion('txp-write-sort-group', $partials['status']['html'].$partials['section']['html'].$html_override, '', gTxt('sort_display')),
 877          $rs
 878      );
 879  
 880      echo graf(
 881          href('<span class="ui-icon ui-icon-arrowthickstop-1-s"></span> '.gTxt('expand_all'), '#', array(
 882              'class'         => 'txp-expand-all',
 883              'aria-controls' => 'supporting_content',
 884          )).
 885          href('<span class="ui-icon ui-icon-arrowthickstop-1-n"></span> '.gTxt('collapse_all'), '#', array(
 886              'class'         => 'txp-collapse-all',
 887              'aria-controls' => 'supporting_content',
 888          )), array('class' => 'txp-actions')
 889      );
 890  
 891      // 'Date and time' collapsible section.
 892      if (empty($ID)) {
 893          // Timestamp.
 894          // Avoiding modified date to disappear.
 895  
 896          if (!empty($store_out['year'])) {
 897              $persist_timestamp = safe_strtotime(
 898                  $store_out['year'].'-'.$store_out['month'].'-'.$store_out['day'].' '.
 899                  $store_out['hour'].':'.$store_out['minute'].':'.$store_out['second']
 900              );
 901          } else {
 902              $persist_timestamp = time();
 903          }
 904  
 905          $posted_block = tag(pluggable_ui(
 906              'article_ui',
 907              'timestamp',
 908              inputLabel(
 909                  'year',
 910                  tsi('year', '%Y', $persist_timestamp, '', 'year').
 911                  ' <span role="separator">/</span> '.
 912                  tsi('month', '%m', $persist_timestamp, '', 'month').
 913                  ' <span role="separator">/</span> '.
 914                  tsi('day', '%d', $persist_timestamp, '', 'day'),
 915                  'publish_date',
 916                  array('publish_date', 'instructions_publish_date'),
 917                  array('class' => 'txp-form-field date posted')
 918              ).
 919              inputLabel(
 920                  'hour',
 921                  tsi('hour', '%H', $persist_timestamp, '', 'hour').
 922                  ' <span role="separator">:</span> '.
 923                  tsi('minute', '%M', $persist_timestamp, '', 'minute').
 924                  ' <span role="separator">:</span> '.
 925                  tsi('second', '%S', $persist_timestamp, '', 'second'),
 926                  'publish_time',
 927                  array('', 'instructions_publish_time'),
 928                  array('class' => 'txp-form-field time posted')
 929              ).
 930              n.tag(
 931                  checkbox('publish_now', '1', true, '', 'publish_now').
 932                  n.tag(gTxt('set_to_now'), 'label', array('for' => 'publish_now')),
 933                  'div', array('class' => 'txp-form-field posted-now')
 934              ),
 935              array('sPosted' => $persist_timestamp) + $rs
 936          ), 'div', array('id' => 'publish-datetime-group'));
 937  
 938          // Expires.
 939          if (!empty($store_out['exp_year'])) {
 940              $persist_timestamp = safe_strtotime(
 941                  $store_out['exp_year'].'-'.$store_out['exp_month'].'-'.$store_out['exp_day'].' '.
 942                  $store_out['exp_hour'].':'.$store_out['exp_minute'].':'.$store_out['second']
 943              );
 944          } else {
 945              $persist_timestamp = 0;
 946          }
 947  
 948          $expires_block = tag(pluggable_ui(
 949              'article_ui',
 950              'expires',
 951              inputLabel(
 952                  'exp_year',
 953                  tsi('exp_year', '%Y', $persist_timestamp, '', 'exp_year').
 954                  ' <span role="separator">/</span> '.
 955                  tsi('exp_month', '%m', $persist_timestamp, '', 'exp_month').
 956                  ' <span role="separator">/</span> '.
 957                  tsi('exp_day', '%d', $persist_timestamp, '', 'exp_day'),
 958                  'expire_date',
 959                  array('expire_date', 'instructions_expire_date'),
 960                  array('class' => 'txp-form-field date expires')
 961              ).
 962              inputLabel(
 963                  'exp_hour',
 964                  tsi('exp_hour', '%H', $persist_timestamp, '', 'exp_hour').
 965                  ' <span role="separator">:</span> '.
 966                  tsi('exp_minute', '%M', $persist_timestamp, '', 'exp_minute').
 967                  ' <span role="separator">:</span> '.
 968                  tsi('exp_second', '%S', $persist_timestamp, '', 'exp_second'),
 969                  'expire_time',
 970                  array('', 'instructions_expire_time'),
 971                  array('class' => 'txp-form-field time expires')
 972              ).
 973              n.tag(
 974                  checkbox('expire_now', '1', false, '', 'expire_now').
 975                  n.tag(gTxt('set_expire_now'), 'label', array('for' => 'expire_now')),
 976                  'div', array('class' => 'txp-form-field expire-now')
 977              ),
 978              $rs
 979          ), 'div', array('id' => 'expires-datetime-group'));
 980      } else {
 981          // Timestamp.
 982          $posted_block = $partials['posted']['html'];
 983  
 984          // Expires.
 985          $expires_block = $partials['expires']['html'];
 986      }
 987  
 988      echo wrapRegion('txp-dates-group', $posted_block.$expires_block, 'txp-dates-group-content', 'date_settings', 'article_dates');
 989  
 990      // 'Categories' section.
 991      $html_categories = pluggable_ui('article_ui', 'categories', $partials['categories']['html'], $rs);
 992      echo wrapRegion('txp-categories-group', $html_categories, 'txp-categories-group-content', 'categories', 'categories');
 993  
 994      // 'Meta' collapsible section.
 995  
 996      // 'URL-only title' field.
 997      $html_url_title = $partials['url_title']['html'];
 998  
 999      // 'Description' field.
1000      $html_description = $partials['description']['html'];
1001  
1002      // 'Keywords' field.
1003      $html_keywords = $partials['keywords']['html'];
1004  
1005      echo wrapRegion('txp-meta-group', $html_url_title.$html_description.$html_keywords, 'txp-meta-group-content', 'meta', 'article_meta');
1006  
1007      // 'Comment options' collapsible section.
1008      echo wrapRegion('txp-comments-group', $partials['comments']['html'], 'txp-comments-group-content', 'comment_settings', 'article_comments');
1009  
1010      // 'Article image' collapsible section.
1011      echo wrapRegion('txp-image-group', $partials['image']['html'], 'txp-image-group-content', 'article_image', 'article_image');
1012  
1013      // 'Custom fields' collapsible section.
1014      echo wrapRegion('txp-custom-field-group', $partials['custom_fields']['html'], 'txp-custom-field-group-content', 'custom', 'article_custom_field');
1015  
1016      // 'Advanced options' collapsible section.
1017      // Unused by core, but leaving the placeholder for legacy plugin support.
1018      $html_advanced = pluggable_ui('article_ui', 'markup', '', $rs);
1019  
1020      if ($html_advanced) {
1021          echo wrapRegion('txp-advanced-group', $html_advanced, 'txp-advanced-group-content', 'advanced_options', 'article_advanced');
1022      }
1023  
1024      // Custom menu entries.
1025      echo pluggable_ui('article_ui', 'extend_col_1', '', $rs);
1026  
1027      // 'Recent articles' collapsible section.
1028      echo wrapRegion('txp-recent-group', $partials['recent_articles']['html'], 'txp-recent-group-content', 'recent_articles', 'article_recent');
1029  
1030      echo n.'</div>'; // End of #supporting_content.
1031  
1032      // Prev/next article links.
1033      echo $partials['article_nav']['html'];
1034  
1035      echo n.'</div>'; // End of .txp-layout-4col-alt.
1036  
1037      echo //tInput().
1038          n.'</div>'. // End of .txp-layout.
1039          n.'</form>';
1040  }
1041  
1042  /**
1043   * Renders a custom field.
1044   *
1045   * @param  int    $num     The custom field number
1046   * @param  string $field   The label
1047   * @param  string $content The field contents
1048   * @return string HTML form field
1049   */
1050  
1051  function custField($num, $field, $content)
1052  {
1053      return inputLabel(
1054          'custom-'.$num,
1055          fInput('text', 'custom_'.$num, $content, '', '', '', INPUT_REGULAR, '', 'custom-'.$num),
1056          txpspecialchars($field),
1057          array('', 'instructions_custom_'.$num),
1058          array('class' => 'txp-form-field custom-field custom-'.$num)
1059      );
1060  }
1061  
1062  /**
1063   * Gets the ID of the next or the previous article.
1064   *
1065   * @param  string $whichway Either '&lt;' or '&gt;'
1066   * @param  int    Unix timestamp
1067   * @param  int    pivot article ID
1068   * @return int
1069   */
1070  
1071  function checkIfNeighbour($whichway, $sPosted, $ID = 0)
1072  {
1073      // Eventual backward compatibility.
1074      if (empty($ID)) {
1075          $ID = !empty($GLOBALS['ID']) ? $GLOBALS['ID'] : gps('ID');
1076      }
1077      $sPosted = assert_int($sPosted);
1078      $ID = assert_int($ID);
1079      $dir = ($whichway == 'prev') ? '<' : '>';
1080      $ord = ($whichway == 'prev') ? "DESC" : "ASC";
1081      $crit = callback_event('txp.article', 'neighbour.criteria', 0, compact('ID', 'whichway', 'sPosted'));
1082  
1083      return safe_field("ID", 'textpattern',
1084          "(Posted $dir FROM_UNIXTIME($sPosted) OR Posted = FROM_UNIXTIME($sPosted) AND ID $dir $ID) $crit ORDER BY Posted $ord, ID $ord LIMIT 1");
1085  }
1086  
1087  /**
1088   * Renders an article status field.
1089   *
1090   * @param  int $status Selected status
1091   * @return string HTML
1092   */
1093  
1094  function status_display($status = 0)
1095  {
1096      global $statuses;
1097  
1098      if (!$status) {
1099          $status = get_pref('default_publish_status', STATUS_LIVE);
1100          has_privs('article.publish') or $status = min($status, STATUS_PENDING);
1101      }
1102  
1103      $disabled = has_privs('article.publish') ? false : array(STATUS_LIVE, STATUS_STICKY);
1104  
1105      return inputLabel(
1106          'status',
1107          selectInput('Status', $statuses, $status, false, '', 'status', false, $disabled),
1108          'status',
1109          array('status', 'instructions_status'),
1110          array('class' => 'txp-form-field status')
1111      );
1112  }
1113  
1114  /**
1115   * Renders a section field.
1116   *
1117   * @param  string $Section The selected section
1118   * @param  string $id      The HTML id
1119   * @return string HTML &lt;select&gt; input
1120   */
1121  
1122  function section_popup($Section, $id)
1123  {
1124      global $txp_sections;
1125  
1126      $rs = $txp_sections;
1127      unset($rs['default']);
1128  
1129      if ($rs) {
1130          $options = array();
1131  
1132          foreach ($rs as $a) {
1133              $options[$a['name']] = array('title' => $a['title'], 'data-skin' => $a['skin']);
1134          }
1135  
1136          return selectInput('Section', $options, $Section, false, '', $id);
1137      }
1138  
1139      return false;
1140  }
1141  
1142  /**
1143   * Renders a category field.
1144   *
1145   * @param  string $name The Name of the field
1146   * @param  string $val  The selected option
1147   * @param  string $id   The HTML id
1148   * @return string HTML &lt;select&gt; input
1149   */
1150  
1151  function category_popup($name, $val, $id)
1152  {
1153      $rs = getTree('root', 'article');
1154  
1155      if ($rs) {
1156          return treeSelectInput($name, $rs, $val, $id, 35);
1157      }
1158  
1159      return false;
1160  }
1161  
1162  /**
1163   * Renders a view tab.
1164   *
1165   * @param  string $tabevent Target view
1166   * @param  string $view     The current view
1167   * @return string HTML
1168   */
1169  
1170  function tab($tabevent, $view, $tag = 'li')
1171  {
1172      $state = ($view == $tabevent) ? 'active' : '';
1173      $pressed = ($view == $tabevent) ? 'true' : 'false';
1174      
1175      if (is_array($tabevent)) {
1176          list($tabevent, $label) = $tabevent + array(null, gTxt('text'));
1177      } else {
1178          $label = gTxt('view_'.$tabevent.'_short');
1179      }
1180  
1181      $link = href($label, '#', array(
1182          'data-view-mode' => $tabevent ? $tabevent : false,
1183          'aria-pressed'   => $pressed,
1184          'role'           => 'button',
1185      ));
1186  
1187      return $tag ? n.tag($link, 'li', array(
1188          'class' => $state,
1189          'id'    => 'tab-'.$tabevent,
1190      )) : $link;
1191  }
1192  
1193  /**
1194   * Renders 'override form' field.
1195   *
1196   * @param  string $form    The selected form
1197   * @param  string $id      HTML id to apply to the input control
1198   * @param  string $section The section that is currently in use
1199   * @return string HTML &lt;select&gt; input
1200   */
1201  
1202  function form_pop($form, $id, $section)
1203  {
1204      global $txp_sections;
1205  
1206      $skinforms = array();
1207      $form_types = get_pref('override_form_types');
1208  
1209      $rs = safe_rows('skin, name, type', 'txp_form', "type IN (".implode(",", quote_list(do_list($form_types))).") AND name != 'default' ORDER BY type,name");
1210  
1211      foreach ($txp_sections as $name => $row) {
1212          $skin = $row['skin'];
1213  
1214          if (!isset($skinforms[$skin])) {
1215              $skinforms[$skin] = array_column(array_filter($rs, function($v) use ($skin) {
1216                  return $v['skin'] == $skin;
1217              }), 'name');
1218          }
1219      }
1220  
1221      script_js('var allForms = '.json_encode($skinforms, TEXTPATTERN_JSON), false);
1222  
1223      $skin = isset($txp_sections[$section]['skin']) ? $txp_sections[$section]['skin'] : false;
1224      $rs = $skin && isset($skinforms[$skin]) ? array_combine($skinforms[$skin], $skinforms[$skin]) : false;
1225  
1226      if ($rs) {
1227          return selectInput('override_form', $rs, $form, true, '', $id);
1228      }
1229  }
1230  
1231  /**
1232   * Checks URL title for duplicates.
1233   *
1234   * @param  string $url_title The URL title
1235   * @return string Localised feedback message, or an empty string
1236   */
1237  
1238  function check_url_title($url_title)
1239  {
1240      // Check for blank or previously used identical url-titles.
1241      if (strlen($url_title) === 0) {
1242          return gTxt('url_title_is_blank');
1243      } else {
1244          $url_title_count = safe_count('textpattern', "url_title = '$url_title'");
1245  
1246          if ($url_title_count > 1) {
1247              return gTxt('url_title_is_multiple', array('{count}' => $url_title_count));
1248          }
1249      }
1250  
1251      return '';
1252  }
1253  
1254  /**
1255   * Translates a status ID to a feedback message.
1256   *
1257   * This message is displayed when an article is saved.
1258   *
1259   * @param  int $Status The status
1260   * @return string The status message
1261   */
1262  
1263  function get_status_message($Status)
1264  {
1265      switch ($Status) {
1266          case STATUS_PENDING:
1267              return gTxt('article_saved_pending');
1268          case STATUS_HIDDEN:
1269              return gTxt('article_saved_hidden');
1270          case STATUS_DRAFT:
1271              return gTxt('article_saved_draft');
1272          default:
1273              return gTxt('article_posted');
1274      }
1275  }
1276  
1277  /**
1278   * Parses article fields using Textile.
1279   *
1280   * @param  array $incoming
1281   * @return array
1282   */
1283  
1284  function textile_main_fields($incoming)
1285  {
1286      // Use preferred Textfilter as default and fallback.
1287      $hasfilter = new \Textpattern\Textfilter\Constraint(null);
1288      $validator = new Validator();
1289  
1290      foreach (array('textile_body', 'textile_excerpt') as $k) {
1291          $hasfilter->setValue($incoming[$k]);
1292          $validator->setConstraints($hasfilter);
1293          if (!$validator->validate()) {
1294              $incoming[$k] = get_pref('use_textile');
1295          }
1296      }
1297  
1298      $textile = new \Textpattern\Textile\Parser();
1299  
1300      $incoming['Title_plain'] = trim($incoming['Title']);
1301      $incoming['Title_html'] = ''; // not used
1302      $incoming['Title'] = $textile->textileEncode($incoming['Title_plain']);
1303  
1304      $incoming['Body_html'] = Txp::get('\Textpattern\Textfilter\Registry')->filter(
1305          $incoming['textile_body'],
1306          $incoming['Body'],
1307          array('field' => 'Body', 'options' => array('lite' => false), 'data' => $incoming)
1308      );
1309  
1310      $incoming['Excerpt_html'] = Txp::get('\Textpattern\Textfilter\Registry')->filter(
1311          $incoming['textile_excerpt'],
1312          $incoming['Excerpt'],
1313          array('field' => 'Excerpt', 'options' => array('lite' => false), 'data' => $incoming)
1314      );
1315  
1316      return $incoming;
1317  }
1318  
1319  /**
1320   * Raises a ping callback so plugins can take action when an article is published.
1321   */
1322  
1323  function do_pings()
1324  {
1325      global $production_status;
1326  
1327      // Only ping for Live sites.
1328      if ($production_status !== 'live') {
1329          return;
1330      }
1331  
1332      callback_event('ping');
1333  }
1334  
1335  /**
1336   * Renders the &lt;title&gt; element for the 'Write' page.
1337   *
1338   * @param  array $rs Article data
1339   * @return string HTML
1340   */
1341  
1342  function article_partial_html_title($rs)
1343  {
1344      return tag(admin_title($rs['Title']), 'title');
1345  }
1346  
1347  /**
1348   * Renders article title partial.
1349   *
1350   * The rendered widget can be customised via the 'article_ui > title'
1351   * pluggable UI callback event.
1352   *
1353   * @param array $rs Article data
1354   */
1355  
1356  function article_partial_title($rs)
1357  {
1358      $out = inputLabel(
1359          'title',
1360          fInput('text', 'Title', preg_replace("/&amp;(?![#a-z0-9]+;)/i", "&", $rs['Title']), '', '', '', INPUT_LARGE, '', 'title', false, true),
1361          'title',
1362          array('title', 'instructions_title'),
1363          array('class' => 'txp-form-field title')
1364      );
1365  
1366      return pluggable_ui('article_ui', 'title', $out, $rs);
1367  }
1368  
1369  /**
1370   * Gets article's title from the given article data set.
1371   *
1372   * @param  array $rs Article data
1373   * @return string
1374   */
1375  
1376  function article_partial_title_value($rs)
1377  {
1378      return preg_replace("/&amp;(?![#a-z0-9]+;)/i", "&", $rs['Title']);
1379  }
1380  
1381  /**
1382   * Renders author partial.
1383   *
1384   * The rendered widget can be customised via the 'article_ui > author'
1385   * pluggable UI callback event.
1386   *
1387   * @param  array $rs Article data
1388   * @return string HTML
1389   */
1390  
1391  function article_partial_author($rs)
1392  {
1393      extract($rs);
1394  
1395      $out = n.'<div class="author">';
1396  
1397      if (!empty($ID)) {
1398          $out .= '<small>';
1399          $out .= gTxt('id').' '.txpspecialchars($ID).sp.span('&#183;', array('role' => 'separator')).sp.gTxt('posted_by').' '.txpspecialchars($AuthorID).sp.span('&#183;', array('role' => 'separator')).sp.safe_strftime('%d %b %Y %X', $sPosted);
1400  
1401          if ($sPosted != $sLastMod) {
1402              $out .= sp.span('&#124;', array('role' => 'separator')).sp.gTxt('modified_by').' '.txpspecialchars($LastModID).sp.span('&#183;', array('role' => 'separator')).sp.safe_strftime('%d %b %Y %X', $sLastMod);
1403          }
1404  
1405          $out .= '</small>';
1406      }
1407  
1408      $out .= '</div>';
1409  
1410      return pluggable_ui('article_ui', 'author', $out, $rs);
1411  }
1412  
1413  /* View/Duplicate/Create new article links.
1414   *
1415   * @param  array $rs Article data
1416   * @return string HTML
1417   */
1418  
1419  function article_partial_actions($rs)
1420  {
1421      return graf($rs['ID']
1422          ? href('<span class="ui-icon ui-extra-icon-new-document"></span> '.gTxt('create_article'), 'index.php?event=article', array('class' => 'txp-new'))
1423          .article_partial_article_clone($rs)
1424          .article_partial_article_view($rs)
1425          : null,
1426          array(
1427              'class' => 'txp-actions',
1428              'id'    => 'txp-article-actions',
1429          ));
1430  }
1431  
1432  /**
1433   * Renders custom field partial.
1434   *
1435   * @param  array $rs Article data
1436   * @return string HTML
1437   */
1438  
1439  function article_partial_custom_field($rs, $key)
1440  {
1441      global $prefs;
1442      extract($prefs);
1443  
1444      preg_match('/custom_field_([0-9]+)/', $key, $m);
1445      $custom_x_set = "custom_{$m[1]}_set";
1446      $custom_x = "custom_{$m[1]}";
1447  
1448      return ($$custom_x_set !== '' ? custField($m[1], $$custom_x_set, $rs[$custom_x]) : '');
1449  }
1450  
1451  /**
1452   * Renders URL title partial.
1453   *
1454   * The rendered widget can be customised via the 'article_ui > url_title'
1455   * pluggable UI callback event.
1456   *
1457   * @param  array $rs Article data
1458   * @return string HTML
1459   */
1460  
1461  function article_partial_url_title($rs)
1462  {
1463      $out = inputLabel(
1464          'url-title',
1465          fInput('text', 'url_title', article_partial_url_title_value($rs), '', '', '', INPUT_REGULAR, '', 'url-title'),
1466          'url_title',
1467          array('url_title', 'instructions_url_title'),
1468          array('class' => 'txp-form-field url-title')
1469      );
1470  
1471      return pluggable_ui('article_ui', 'url_title', $out, $rs);
1472  }
1473  
1474  /**
1475   * Gets URL title from the given article data set.
1476   *
1477   * @param  array $rs Article data
1478   * @return string HTML
1479   */
1480  
1481  function article_partial_url_title_value($rs)
1482  {
1483      return $rs['url_title'];
1484  }
1485  
1486  /**
1487   * Renders description partial.
1488   *
1489   * The rendered widget can be customised via the 'article_ui > description'
1490   * pluggable UI callback event.
1491   *
1492   * @param  array $rs Article data
1493   * @return string HTML
1494   */
1495  
1496  function article_partial_description($rs)
1497  {
1498      $out = inputLabel(
1499          'description',
1500          '<textarea id="description" name="description" cols="'.INPUT_MEDIUM.'" rows="'.TEXTAREA_HEIGHT_SMALL.'">'.txpspecialchars(article_partial_description_value($rs)).'</textarea>',
1501          'description',
1502          array('description', 'instructions_description'),
1503          array('class' => 'txp-form-field txp-form-field-textarea description')
1504      );
1505  
1506      return pluggable_ui('article_ui', 'description', $out, $rs);
1507  }
1508  
1509  /**
1510   * Gets description from the given article data set.
1511   *
1512   * @param  array $rs Article data
1513   * @return string HTML
1514   */
1515  
1516  function article_partial_description_value($rs)
1517  {
1518      return $rs['description'];
1519  }
1520  
1521  /**
1522   * Renders keywords partial.
1523   *
1524   * The rendered widget can be customised via the 'article_ui > keywords'
1525   * pluggable UI callback event.
1526   *
1527   * @param  array $rs Article data
1528   * @return string HTML
1529   */
1530  
1531  function article_partial_keywords($rs)
1532  {
1533      $out = inputLabel(
1534          'keywords',
1535          '<textarea id="keywords" name="Keywords" cols="'.INPUT_MEDIUM.'" rows="'.TEXTAREA_HEIGHT_SMALL.'">'.txpspecialchars(article_partial_keywords_value($rs)).'</textarea>',
1536          'keywords',
1537          array('keywords', 'instructions_keywords'),
1538          array('class' => 'txp-form-field txp-form-field-textarea keywords')
1539      );
1540  
1541      return pluggable_ui('article_ui', 'keywords', $out, $rs);
1542  }
1543  
1544  /**
1545   * Gets keywords from the given article data set.
1546   *
1547   * @param  array $rs Article data
1548   * @return string
1549   */
1550  
1551  function article_partial_keywords_value($rs)
1552  {
1553      // Separate keywords by a comma plus at least one space.
1554      return preg_replace('/,(\S)/', ', $1', $rs['Keywords']);
1555  }
1556  
1557  /**
1558   * Renders article image partial.
1559   *
1560   * The rendered widget can be customised via the 'article_ui > article_image'
1561   * pluggable UI callback event.
1562   *
1563   * @param  array $rs Article data
1564   * @return string HTML
1565   */
1566  
1567  function article_partial_image($rs)
1568  {
1569      $default = inputLabel(
1570          'article-image',
1571          fInput('text', 'Image', $rs['Image'], '', '', '', INPUT_REGULAR, '', 'article-image'),
1572          'article_image',
1573          array('article_image', 'instructions_article_image'),
1574          array('class' => 'txp-form-field article-image')
1575      );
1576  
1577      return tag(pluggable_ui('article_ui', 'article_image', $default, $rs), 'div', array('class' => 'txp-container'));
1578  }
1579  
1580  /**
1581   * Renders all custom fields in one partial.
1582   *
1583   * The rendered widget can be customised via the 'article_ui > custom_fields'
1584   * pluggable UI callback event.
1585   *
1586   * @param  array $rs Article data
1587   * @return string HTML
1588   */
1589  
1590  function article_partial_custom_fields($rs)
1591  {
1592      global $cfs;
1593      $cf = '';
1594  
1595      foreach ($cfs as $k => $v) {
1596          $cf .= article_partial_custom_field($rs, "custom_field_{$k}");
1597      }
1598  
1599      return tag(pluggable_ui('article_ui', 'custom_fields', $cf, $rs), 'div', array('class' => 'txp-container'));
1600  }
1601  
1602  /**
1603   * Renders &lt;ol&gt; list of recent articles.
1604   *
1605   * The rendered widget can be customised via the 'article_ui > recent_articles'
1606   * pluggable UI callback event.
1607   *
1608   * @param  array $rs Article data
1609   * @return string HTML
1610   */
1611  
1612  function article_partial_recent_articles($rs)
1613  {
1614      $recents = safe_rows_start("Title, ID", 'textpattern', "1 = 1 ORDER BY LastMod DESC LIMIT ".(int) WRITE_RECENT_ARTICLES_COUNT);
1615      $ra = '';
1616  
1617      if ($recents && numRows($recents)) {
1618          $ra = '<ol class="recent">';
1619  
1620          while ($recent = nextRow($recents)) {
1621              if ($recent['Title'] === '') {
1622                  $recent['Title'] = gTxt('untitled').sp.$recent['ID'];
1623              }
1624  
1625              $ra .= n.'<li class="recent-article">'.
1626                  href(escape_title($recent['Title']), '?event=article'.a.'step=edit'.a.'ID='.$recent['ID']).
1627                  '</li>';
1628          }
1629  
1630          $ra .= '</ol>';
1631      }
1632  
1633      return tag(pluggable_ui('article_ui', 'recent_articles', $ra, $rs), 'div', array('class' => 'txp-container'));
1634  }
1635  
1636  /**
1637   * Renders article 'duplicate' link.
1638   *
1639   * @param  array $rs Article data
1640   * @return string HTML
1641   */
1642  
1643  function article_partial_article_clone($rs)
1644  {
1645      extract($rs);
1646  
1647      return n.href('<span class="ui-icon ui-icon-copy"></span> '.gTxt('duplicate'), '#', array(
1648          'class' => 'txp-clone',
1649          'id'    => 'article_partial_article_clone',
1650      ));
1651  }
1652  
1653  /**
1654   * Renders article 'view' link.
1655   *
1656   * @param  array $rs Article data
1657   * @return string HTML
1658   */
1659  
1660  function article_partial_article_view($rs)
1661  {
1662      extract($rs);
1663  
1664      if ($Status != STATUS_LIVE and $Status != STATUS_STICKY) {
1665          if (!has_privs('article.preview')) {
1666              return;
1667          }
1668  
1669          $url = '?txpreview='.intval($ID).'.'.time(); // Article ID plus cachebuster.
1670      } else {
1671          include_once txpath.'/publish/taghandlers.php';
1672          $url = permlinkurl_id($ID);
1673      }
1674  
1675      return n.href('<span class="ui-icon ui-icon-notice"></span> '.gTxt('view'), $url, array(
1676          'class'  => 'txp-article-view',
1677          'id'     => 'article_partial_article_view',
1678          'rel'    => 'noopener',
1679          'target' => '_blank',
1680      ));
1681  }
1682  
1683  /**
1684   * Renders article body field.
1685   *
1686   * The rendered widget can be customised via the 'article_ui > body'
1687   * pluggable UI callback event.
1688   *
1689   * @param  array $rs Article data
1690   * @return string HTML
1691   */
1692  
1693  function article_partial_body($rs)
1694  {
1695      $textarea_options = n.href(gTxt('view_preview_short'), '#', array(
1696          'class'             => 'txp-textarea-preview',
1697          'data-preview-link' => 'body',
1698      ));
1699  
1700      // Article markup selection.
1701      if (has_privs('article.set_markup')) {
1702          // Markup help.
1703          $help = '';
1704          $textfilter_opts = Txp::get('\Textpattern\Textfilter\Registry')->getMap();
1705          isset($textfilter_opts[$rs['textile_body']]) or $rs['textile_body'] = LEAVE_TEXT_UNTOUCHED;
1706  
1707          $html_markup = array();
1708  
1709          foreach ($textfilter_opts as $filter_key => $filter_name) {
1710              $thisHelp = Txp::get('\Textpattern\Textfilter\Registry')->getHelp($filter_key);
1711              $renderHelp = ($thisHelp) ? popHelp($thisHelp) : '';
1712              $selected = (string)$filter_key === (string)$rs['textile_body'];
1713  
1714              $html_markup[] = tag(
1715                  $filter_name, 'option', array(
1716                      'data-id'   => $filter_key,
1717                      'data-help' => $renderHelp,
1718                      'selected'  => $selected,
1719                  )
1720              );
1721  
1722              if ($selected) {
1723                  $help = $renderHelp;
1724              }
1725          }
1726  
1727          // Note: not using span() for the textfilter help, because it doesn't render empty content.
1728          $html_markup = tag(implode(n, $html_markup),
1729              'select',
1730              array('class' => 'jquery-ui-selectmenu'))
1731              .tag_void('input', array(
1732                  'class' => 'textfilter-value',
1733                  'name'  => 'textile_body',
1734                  'type'  => 'hidden',
1735                  'value' => $rs['textile_body'],
1736              ));
1737          $textarea_options = n.'<label>'.gTxt('textfilter').n.$html_markup.'</label>'.
1738              '<span class="textfilter-help">'.$help.'</span>'.$textarea_options;
1739      }
1740  
1741      $textarea_options = '<div class="txp-textarea-options txp-textfilter-options no-ui-button">'.$textarea_options.'</div>';
1742      $out = inputLabel(
1743          'body',
1744          '<textarea id="body" name="Body" cols="'.INPUT_LARGE.'" rows="'.TEXTAREA_HEIGHT_REGULAR.'">'.txpspecialchars($rs['Body']).'</textarea>',
1745          array('body', $textarea_options),
1746          array('body', 'instructions_body'),
1747          array('class' => 'txp-form-field txp-form-field-textarea body')
1748      );
1749  
1750      return pluggable_ui('article_ui', 'body', $out, $rs);
1751  }
1752  
1753  /**
1754   * Renders article excerpt field.
1755   *
1756   * The rendered widget can be customised via the 'article_ui > excerpt'
1757   * pluggable UI callback event.
1758   *
1759   * @param  array $rs Article data
1760   * @return string HTML
1761   */
1762  
1763  function article_partial_excerpt($rs)
1764  {
1765      $textarea_options = n.href(gTxt('view_preview_short'), '#', array(
1766          'class'             => 'txp-textarea-preview',
1767          'data-preview-link' => 'excerpt',
1768      ));
1769  
1770      // Excerpt markup selection.
1771      if (has_privs('article.set_markup')) {
1772          // Markup help.
1773          $help = '';
1774          $textfilter_opts = Txp::get('\Textpattern\Textfilter\Registry')->getMap();
1775          isset($textfilter_opts[$rs['textile_excerpt']]) or $rs['textile_excerpt'] = LEAVE_TEXT_UNTOUCHED;
1776  
1777          $html_markup = array();
1778  
1779          foreach ($textfilter_opts as $filter_key => $filter_name) {
1780              $thisHelp = Txp::get('\Textpattern\Textfilter\Registry')->getHelp($filter_key);
1781              $renderHelp = ($thisHelp) ? popHelp($thisHelp) : '';
1782              $selected = (string)$filter_key === (string)$rs['textile_excerpt'];
1783  
1784              $html_markup[] = tag(
1785                  $filter_name, 'option', array(
1786                      'data-id'   => $filter_key,
1787                      'data-help' => $renderHelp,
1788                      'selected'  => $selected,
1789                  )
1790              );
1791  
1792              if ($selected) {
1793                  $help = $renderHelp;
1794              }
1795          }
1796  
1797          // Note: not using span() for the textfilter help, because it doesn't render empty content.
1798          $html_markup = tag(implode(n, $html_markup),
1799              'select',
1800              array('class' => 'jquery-ui-selectmenu'))
1801              .tag_void('input', array(
1802                  'class' => 'textfilter-value',
1803                  'name'  => 'textile_excerpt',
1804                  'type'  => 'hidden',
1805                  'value' => $rs['textile_excerpt'],
1806              ));
1807              $textarea_options = n.'<label>'.gTxt('textfilter').n.$html_markup.'</label>'.
1808                  '<span class="textfilter-help">'.$help.'</span>'.$textarea_options;
1809      }
1810  
1811      $textarea_options = '<div class="txp-textarea-options txp-textfilter-options no-ui-button">'.$textarea_options.'</div>';
1812      $out = inputLabel(
1813          'excerpt',
1814          '<textarea id="excerpt" name="Excerpt" cols="'.INPUT_LARGE.'" rows="'.TEXTAREA_HEIGHT_SMALL.'">'.txpspecialchars($rs['Excerpt']).'</textarea>',
1815          array('excerpt', $textarea_options),
1816          array('excerpt', 'instructions_excerpt'),
1817          array('class' => 'txp-form-field txp-form-field-textarea excerpt')
1818      );
1819  
1820      return pluggable_ui('article_ui', 'excerpt', $out, $rs);
1821  }
1822  
1823  /**
1824   * Renders list of view modes.
1825   *
1826   * The rendered widget can be customised via the 'article_ui > view'
1827   * pluggable UI callback event.
1828   *
1829   * @param  array $rs Article data
1830   * @return string HTML
1831   */
1832  
1833  function article_partial_view_modes($rs)
1834  {
1835      global $view;
1836  
1837      $out = n.'<div class="txp-textarea-options txp-live-preview">'.
1838          checkbox2('', false, 0, 'live-preview').
1839          sp.tag(gTxt('live_preview'), 'label', array('for' => 'live-preview')).
1840          n.'</div>'.
1841          n.tag(tab(array('preview'), $view).tab(array('html', '<bdi dir="ltr">HTML</bdi>'), $view), 'ul');
1842      $out = pluggable_ui('article_ui', 'view', $out, $rs);
1843  
1844      return n.tag($out.n, 'div', array('id' => 'view_modes'));
1845  }
1846  
1847  /**
1848   * Renders next/prev links.
1849   *
1850   * @param  array $rs Article data
1851   * @return string HTML
1852   */
1853  
1854  function article_partial_article_nav($rs)
1855  {
1856      $out = array();
1857  
1858      if ($rs['prev_id']) {
1859          $out[] = prevnext_link(gTxt('prev'), 'article', 'edit', $rs['prev_id'], '', 'prev');
1860      } else {
1861          $out[] = span(gTxt('prev'), array(
1862              'class'         => 'navlink-disabled',
1863              'aria-disabled' => 'true',
1864          ));
1865      }
1866  
1867      if ($rs['next_id']) {
1868          $out[] = prevnext_link(gTxt('next'), 'article', 'edit', $rs['next_id'], '', 'next');
1869      } else {
1870          $out[] = span(gTxt('next'), array(
1871              'class'         => 'navlink-disabled',
1872              'aria-disabled' => 'true',
1873          ));
1874      }
1875  
1876      return n.tag(join('', $out), 'nav', array('class' => 'nav-tertiary'));
1877  }
1878  
1879  /**
1880   * Renders article status partial.
1881   *
1882   * The rendered widget can be customised via the 'article_ui > status'
1883   * pluggable UI callback event.
1884   *
1885   * @param  array $rs Article data
1886   * @return string HTML
1887   */
1888  
1889  function article_partial_status($rs)
1890  {
1891      return n.tag(pluggable_ui('article_ui', 'status', status_display($rs['Status']), $rs), 'div', array('id' => 'txp-container-status'));
1892  }
1893  
1894  /**
1895   * Renders article section partial.
1896   *
1897   * The rendered widget can be customised via the 'article_ui > section'
1898   * pluggable UI callback event.
1899   *
1900   * @param  array $rs Article data
1901   * @return string HTML
1902   */
1903  
1904  function article_partial_section($rs)
1905  {
1906      $out = inputLabel(
1907          'section',
1908          section_popup($rs['Section'], 'section').
1909          n.eLink('section', 'list', '', '', gTxt('edit'), '', '', '', 'txp-option-link'),
1910          'section',
1911          array('', 'instructions_section'),
1912          array('class' => 'txp-form-field section')
1913      );
1914  
1915      return pluggable_ui('article_ui', 'section', $out, $rs);
1916  }
1917  
1918  /**
1919   * Renders article categories partial.
1920   *
1921   * The rendered widget can be customised via the 'article_ui > categories'
1922   * pluggable UI callback event.
1923   *
1924   * @param  array $rs Article data
1925   * @return string HTML
1926   */
1927  
1928  function article_partial_categories($rs)
1929  {
1930      $out = inputLabel(
1931          'category-1',
1932          category_popup('Category1', $rs['Category1'], 'category-1').
1933          n.eLink('category', 'list', '', '', gTxt('edit'), '', '', '', 'txp-option-link'),
1934          'category1',
1935          array('', 'instructions_category1'),
1936          array('class' => 'txp-form-field category category-1')
1937      ).
1938      inputLabel(
1939          'category-2',
1940          category_popup('Category2', $rs['Category2'], 'category-2'),
1941          'category2',
1942          array('', 'instructions_category2'),
1943          array('class' => 'txp-form-field category category-2')
1944      );
1945  
1946      return pluggable_ui('article_ui', 'categories', $out, $rs);
1947  }
1948  
1949  /**
1950   * Renders comment options partial.
1951   *
1952   * The rendered widget can be customised via the 'article_ui > annotate_invite'
1953   * pluggable UI callback event.
1954   *
1955   * @param  array $rs Article data
1956   * @return string|null HTML
1957   */
1958  
1959  function article_partial_comments($rs)
1960  {
1961      global $step, $use_comments, $comments_disabled_after, $comments_default_invite, $comments_on_default;
1962  
1963      extract($rs);
1964  
1965      if (empty($ID)) {
1966          // Avoid invite disappearing when previewing.
1967  
1968          if (!empty($store_out['AnnotateInvite'])) {
1969              $AnnotateInvite = $store_out['AnnotateInvite'];
1970          } else {
1971              $AnnotateInvite = $comments_default_invite;
1972          }
1973  
1974          $Annotate = $comments_on_default;
1975      }
1976  
1977      if ($use_comments == 1) {
1978          $comments_expired = false;
1979  
1980          if (!empty($ID) && $comments_disabled_after) {
1981              $lifespan = $comments_disabled_after * 86400;
1982              $time_since = time() - intval($sPosted);
1983  
1984              if ($time_since > $lifespan) {
1985                  $comments_expired = true;
1986              }
1987          }
1988  
1989          if ($comments_expired) {
1990              $invite = graf(gTxt('expired'), array('class' => 'comment-annotate-expired'));
1991          } else {
1992              $invite = n.tag(
1993                      onoffRadio('Annotate', $Annotate),
1994                      'div', array('class' => 'txp-form-field comment-annotate')
1995                  ).
1996                  inputLabel(
1997                      'comment-invite',
1998                      fInput('text', 'AnnotateInvite', $AnnotateInvite, '', '', '', INPUT_REGULAR, '', 'comment-invite'),
1999                      'comment_invitation',
2000                      array('', 'instructions_comment_invitation'),
2001                      array('class' => 'txp-form-field comment-invite')
2002                  );
2003          }
2004  
2005          return n.tag_start('div', array('id' => 'write-comments')).
2006              pluggable_ui('article_ui', 'annotate_invite', $invite, $rs).
2007              n.tag_end('div');
2008      }
2009  }
2010  
2011  /**
2012   * Renders timestamp partial.
2013   *
2014   * The rendered widget can be customised via the 'article_ui > timestamp'
2015   * pluggable UI callback event.
2016   *
2017   * @param  array $rs Article data
2018   * @return string HTML
2019   */
2020  
2021  function article_partial_posted($rs)
2022  {
2023      extract($rs);
2024  
2025      $out =
2026          inputLabel(
2027              'year',
2028              tsi('year', '%Y', $sPosted, '', 'year').
2029              ' <span role="separator">/</span> '.
2030              tsi('month', '%m', $sPosted, '', 'month').
2031              ' <span role="separator">/</span> '.
2032              tsi('day', '%d', $sPosted, '', 'day'),
2033              'publish_date',
2034              array('publish_date', 'instructions_publish_date'),
2035              array('class' => 'txp-form-field date posted')
2036          ).
2037          inputLabel(
2038              'hour',
2039              tsi('hour', '%H', $sPosted, '', 'hour').
2040              ' <span role="separator">:</span> '.
2041              tsi('minute', '%M', $sPosted, '', 'minute').
2042              ' <span role="separator">:</span> '.
2043              tsi('second', '%S', $sPosted, '', 'second'),
2044              'publish_time',
2045              array('', 'instructions_publish_time'),
2046              array('class' => 'txp-form-field time posted')
2047          ).
2048          n.tag(
2049              checkbox('reset_time', '1', $reset_time, '', 'reset_time').
2050              n.tag(gTxt('reset_time'), 'label', array('for' => 'reset_time')),
2051              'div', array('class' => 'txp-form-field reset-time')
2052          );
2053  
2054      return n.tag_start('div', array('id' => 'publish-datetime-group')).
2055          pluggable_ui('article_ui', 'timestamp', $out, $rs).
2056          n.tag_end('div');
2057  }
2058  
2059  /**
2060   * Renders expiration date partial.
2061   *
2062   * The rendered widget can be customised via the 'article_ui > expires'
2063   * pluggable UI callback event.
2064   *
2065   * @param  array $rs Article data
2066   * @return string HTML
2067   */
2068  
2069  function article_partial_expires($rs)
2070  {
2071      extract($rs);
2072  
2073      $out =
2074          inputLabel(
2075              'exp_year',
2076              tsi('exp_year', '%Y', $sExpires, '', 'exp_year').
2077              ' <span role="separator">/</span> '.
2078              tsi('exp_month', '%m', $sExpires, '', 'exp_month').
2079              ' <span role="separator">/</span> '.
2080              tsi('exp_day', '%d', $sExpires, '', 'exp_day'),
2081              'expire_date',
2082              array('expire_date', 'instructions_expire_date'),
2083              array('class' => 'txp-form-field date expires')
2084          ).
2085          inputLabel(
2086              'exp_hour',
2087              tsi('exp_hour', '%H', $sExpires, '', 'exp_hour').
2088              ' <span role="separator">:</span> '.
2089              tsi('exp_minute', '%M', $sExpires, '', 'exp_minute').
2090              ' <span role="separator">:</span> '.
2091              tsi('exp_second', '%S', $sExpires, '', 'exp_second'),
2092              'expire_time',
2093              array('', 'instructions_expire_time'),
2094              array('class' => 'txp-form-field time expires')
2095          ).
2096          n.tag(
2097              checkbox('expire_now', '1', $expire_now, '', 'expire_now').
2098              n.tag(gTxt('set_expire_now'), 'label', array('for' => 'expire_now')),
2099              'div', array('class' => 'txp-form-field expire-now')
2100          ).
2101          hInput('sExpires', $sExpires);
2102  
2103      return n.tag_start('div', array('id' => 'expires-datetime-group')).
2104          pluggable_ui('article_ui', 'expires', $out, $rs).
2105          n.tag_end('div');
2106  }
2107  
2108  /**
2109   * Gets a partial value from the given article data set.
2110   *
2111   * @param  array  $rs  Article data
2112   * @param  string $key The value to get
2113   * @return string HTML
2114   */
2115  
2116  function article_partial_value($rs, $key)
2117  {
2118      return($rs[$key]);
2119  }
2120  
2121  /**
2122   * Validates article data.
2123   *
2124   * @param  array        $rs  Article data
2125   * @param  string|array $msg Initial message
2126   * @return string HTML
2127   */
2128  
2129  function article_validate($rs, &$msg)
2130  {
2131      global $prefs, $step, $statuses;
2132  
2133      if (!empty($msg)) {
2134          return false;
2135      }
2136  
2137      $constraints = array(
2138          'Status' => new ChoiceConstraint(
2139              $rs['Status'],
2140              array('choices' => array_keys($statuses), 'message' => 'invalid_status')
2141          ),
2142          'Section' => new SectionConstraint($rs['Section']),
2143          'Category1' => new CategoryConstraint(
2144              $rs['Category1'],
2145              array('type' => 'article')
2146          ),
2147          'Category2' => new CategoryConstraint(
2148              $rs['Category2'],
2149              array('type' => 'article')
2150          ),
2151          'textile_body' => new \Textpattern\Textfilter\Constraint(
2152              $rs['textile_body'],
2153              array('message' => 'invalid_textfilter_body')
2154          ),
2155          'textile_excerpt' => new \Textpattern\Textfilter\Constraint(
2156              $rs['textile_excerpt'],
2157              array('message' => 'invalid_textfilter_excerpt')
2158          ),
2159      );
2160  
2161      if (!$prefs['articles_use_excerpts']) {
2162          $constraints['excerpt_blank'] = new BlankConstraint(
2163              $rs['Excerpt'],
2164              array('message' => 'excerpt_not_blank')
2165          );
2166      }
2167  
2168      if (!$prefs['use_comments']) {
2169          $constraints['annotate_invite_blank'] = new BlankConstraint(
2170              $rs['AnnotateInvite'],
2171              array('message' => 'invite_not_blank')
2172          );
2173  
2174          $constraints['annotate_false'] = new FalseConstraint(
2175              $rs['Annotate'],
2176              array('message' => 'comments_are_on')
2177          );
2178      }
2179  
2180      if ($prefs['allow_form_override']) {
2181          $constraints['override_form'] = new FormConstraint(
2182              $rs['override_form'],
2183              array('type' => get_pref('override_form_types'))
2184          );
2185      } else {
2186          $constraints['override_form'] = new BlankConstraint(
2187              $rs['override_form'],
2188              array('message' => 'override_form_not_blank')
2189          );
2190      }
2191  
2192      callback_event_ref('article_ui', "validate_$step", 0, $rs, $constraints);
2193  
2194      $validator = new Validator($constraints);
2195      if ($validator->validate()) {
2196          $msg = '';
2197  
2198          return true;
2199      } else {
2200          $msg = doArray($validator->getMessages(), 'gTxt');
2201          $msg = array(join(', ', $msg), E_ERROR);
2202  
2203          return false;
2204      }
2205  }

title

Description

title

Description

title

Description

title

title

Body