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