Textpattern | PHP Cross Reference | Content Management Systems |
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 '<' or '>' 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 <select> 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 <select> 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 <select> 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 <title> 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("/&(?![#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("/&(?![#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).' · '.safe_strftime('%d %b %Y · %X', $sPosted); 1612 1613 if ($sPosted != $sLastMod) { 1614 $out .= br.gTxt('modified_by').': '.txpspecialchars($LastModID).' · '.safe_strftime('%d %b %Y · %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 <ol> 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 = ' '; 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
Body
title
Description
Body
title
Description
Body
title
Body
title