Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/include/txp_article.php - 2302 lines - 70587 bytes - Summary - Text - Print

Description: Write panel.

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

title

Description

title

Description

title

Description

title

title

Body