. */ /** * Write panel. * * @package Admin\Article */ use Textpattern\Validator\BlankConstraint; use Textpattern\Validator\CategoryConstraint; use Textpattern\Validator\ChoiceConstraint; use Textpattern\Validator\FalseConstraint; use Textpattern\Validator\FormConstraint; use Textpattern\Validator\SectionConstraint; use Textpattern\Validator\Validator; if (!defined('txpinterface')) { die('txpinterface is undefined.'); } global $vars, $statuses; $vars = array( 'ID', 'Title', 'Body', 'Excerpt', 'textile_excerpt', 'Image', 'textile_body', 'Keywords', 'description', 'Status', 'Posted', 'Expires', 'Section', 'Category1', 'Category2', 'Annotate', 'AnnotateInvite', 'publish_now', 'reset_time', 'AuthorID', 'sPosted', 'LastModID', 'sLastMod', 'override_form', 'from_view', 'year', 'month', 'day', 'hour', 'minute', 'second', 'url_title', 'exp_year', 'exp_month', 'exp_day', 'exp_hour', 'exp_minute', 'exp_second', 'sExpires', ); $cfs = getCustomFields(); foreach ($cfs as $i => $cf_name) { $vars[] = "custom_$i"; } $statuses = status_list(); if (!empty($event) and $event == 'article') { require_privs('article'); $save = gps('save'); if ($save) { $step = 'save'; } $publish = gps('publish'); if ($publish) { $step = 'publish'; } if (empty($step)) { $step = 'create'; } bouncer($step, array( 'create' => false, 'publish' => true, 'edit' => false, 'save' => true, )); switch ($step) { case 'create': article_edit(); break; case 'publish': article_post(); break; case 'edit': article_edit(); break; case 'save': article_save(); break; } } /** * Processes sent forms and saves new articles. */ function article_post() { global $txp_user, $vars, $prefs; extract($prefs); $incoming = array_map('assert_string', psa($vars)); if (!has_privs('article.set_markup')) { $incoming['textile_body'] = $incoming['textile_excerpt'] = $use_textile; } $incoming = doSlash(textile_main_fields($incoming)); extract($incoming); $msg = ''; if ($Title or $Body or $Excerpt) { $is_clone = (ps('copy')); $Status = assert_int(ps('Status')); // Comments may be on, off, or disabled. $Annotate = (int) $Annotate; // Set and validate article timestamp. if ($publish_now == 1 || $reset_time == 1) { $when = "NOW()"; $when_ts = time(); } else { if (!is_numeric($year) || !is_numeric($month) || !is_numeric($day) || !is_numeric($hour) || !is_numeric($minute) || !is_numeric($second)) { $ts = false; } else { $ts = strtotime($year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second); } // Tracking the PHP meanders on how to return an error. if ($ts === false || $ts < 0) { article_edit(array(gTxt('invalid_postdate'), E_ERROR)); return; } $when_ts = $ts - tz_offset($ts); $when = "FROM_UNIXTIME($when_ts)"; } // Set and validate expiry timestamp. if (empty($exp_year)) { $expires = 0; } else { if (empty($exp_month)) { $exp_month = 1; } if (empty($exp_day)) { $exp_day = 1; } if (empty($exp_hour)) { $exp_hour = 0; } if (empty($exp_minute)) { $exp_minute = 0; } if (empty($exp_second)) { $exp_second = 0; } $ts = strtotime($exp_year.'-'.$exp_month.'-'.$exp_day.' '.$exp_hour.':'.$exp_minute.':'.$exp_second); if ($ts === false || $ts < 0) { article_edit(array(gTxt('invalid_expirydate'), E_ERROR)); return; } else { $expires = $ts - tz_offset($ts); } } if ($expires && ($expires <= $when_ts)) { article_edit(array(gTxt('article_expires_before_postdate'), E_ERROR)); return; } if ($expires) { $whenexpires = "FROM_UNIXTIME($expires)"; } else { $whenexpires = "NULL"; } $user = doSlash($txp_user); $Keywords = doSlash(trim(preg_replace('/( ?[\r\n\t,])+ ?/s', ',', preg_replace('/ +/', ' ', ps('Keywords'))), ', ')); $msg = ''; if (!has_privs('article.publish') && $Status >= STATUS_LIVE) { $Status = STATUS_PENDING; } if ($is_clone && $Status >= STATUS_LIVE) { $Status = STATUS_DRAFT; $url_title = ''; } if (empty($url_title)) { $url_title = stripSpace($Title_plain, 1); } $cfq = array(); $cfs = getCustomFields(); foreach ($cfs as $i => $cf_name) { $custom_x = "custom_{$i}"; $cfq[] = "custom_$i = '".$$custom_x."'"; } $cfq = join(', ', $cfq); $rs = compact($vars); if (article_validate($rs, $msg)) { $ok = safe_insert( 'textpattern', "Title = '$Title', Body = '$Body', Body_html = '$Body_html', Excerpt = '$Excerpt', Excerpt_html = '$Excerpt_html', Image = '$Image', Keywords = '$Keywords', description = '$description', Status = $Status, Posted = $when, Expires = $whenexpires, AuthorID = '$user', LastMod = NOW(), LastModID = '$user', Section = '$Section', Category1 = '$Category1', Category2 = '$Category2', textile_body = '$textile_body', textile_excerpt = '$textile_excerpt', Annotate = $Annotate, override_form = '$override_form', url_title = '$url_title', AnnotateInvite = '$AnnotateInvite'," .(($cfs) ? $cfq.',' : ''). "uid = '".md5(uniqid(rand(), true))."', feed_time = NOW()" ); if ($ok) { $rs['ID'] = $GLOBALS['ID'] = $ok; if ($is_clone) { safe_update( 'textpattern', "Title = CONCAT(Title, ' (', $ok, ')'), url_title = CONCAT(url_title, '-', $ok)", "ID = $ok" ); } if ($Status >= STATUS_LIVE) { do_pings(); update_lastmod('article_posted', $rs); now('posted', true); now('expires', true); } callback_event('article_posted', '', false, $rs); $s = check_url_title($url_title); $msg = array(get_status_message($Status).' '.$s, ($s ? E_WARNING : 0)); } else { unset($GLOBALS['ID']); $msg = array(gTxt('article_save_failed'), E_ERROR); } } } article_edit($msg); } /** * Processes sent forms and updates existing articles. */ function article_save() { global $txp_user, $vars, $prefs; extract($prefs); $incoming = array_map('assert_string', psa($vars)); $oldArticle = safe_row("Status, url_title, Title, textile_body, textile_excerpt, UNIX_TIMESTAMP(LastMod) AS sLastMod, LastModID, UNIX_TIMESTAMP(Posted) AS sPosted, UNIX_TIMESTAMP(Expires) AS sExpires", 'textpattern', "ID = ".(int) $incoming['ID']); if (!(($oldArticle['Status'] >= STATUS_LIVE and has_privs('article.edit.published')) or ($oldArticle['Status'] >= STATUS_LIVE and $incoming['AuthorID'] === $txp_user and has_privs('article.edit.own.published')) or ($oldArticle['Status'] < STATUS_LIVE and has_privs('article.edit')) or ($oldArticle['Status'] < STATUS_LIVE and $incoming['AuthorID'] === $txp_user and has_privs('article.edit.own')))) { // Not allowed, you silly rabbit, you shouldn't even be here. // Show default editing screen. article_edit(); return; } if ($oldArticle['sLastMod'] != $incoming['sLastMod']) { article_edit(array(gTxt('concurrent_edit_by', array('{author}' => txpspecialchars($oldArticle['LastModID']))), E_ERROR), true, true); return; } if (!has_privs('article.set_markup')) { $incoming['textile_body'] = $oldArticle['textile_body']; $incoming['textile_excerpt'] = $oldArticle['textile_excerpt']; } $incoming = textile_main_fields($incoming); extract(doSlash($incoming)); extract(array_map('assert_int', psa(array('ID', 'Status')))); // Comments may be on, off, or disabled. $Annotate = (int) $Annotate; if (!has_privs('article.publish') && $Status >= STATUS_LIVE) { $Status = STATUS_PENDING; } // Set and validate article timestamp. if ($reset_time) { $whenposted = "Posted = NOW()"; $when_ts = time(); } else { if (!is_numeric($year) || !is_numeric($month) || !is_numeric($day) || !is_numeric($hour) || !is_numeric($minute) || !is_numeric($second)) { $ts = false; } else { $ts = strtotime($year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second); } if ($ts === false || $ts < 0) { $when = $when_ts = $oldArticle['sPosted']; $msg = array(gTxt('invalid_postdate'), E_ERROR); } else { $when = $when_ts = $ts - tz_offset($ts); } $whenposted = "Posted = FROM_UNIXTIME($when)"; } // Set and validate expiry timestamp. if (empty($exp_year)) { $expires = 0; } else { if (empty($exp_month)) { $exp_month = 1; } if (empty($exp_day)) { $exp_day = 1; } if (empty($exp_hour)) { $exp_hour = 0; } if (empty($exp_minute)) { $exp_minute = 0; } if (empty($exp_second)) { $exp_second = 0; } $ts = strtotime($exp_year.'-'.$exp_month.'-'.$exp_day.' '.$exp_hour.':'.$exp_minute.':'.$exp_second); if ($ts === false || $ts < 0) { $expires = $oldArticle['sExpires']; $msg = array(gTxt('invalid_expirydate'), E_ERROR); } else { $expires = $ts - tz_offset($ts); } } if ($expires && ($expires <= $when_ts)) { $expires = $oldArticle['sExpires']; $msg = array(gTxt('article_expires_before_postdate'), E_ERROR); } if ($expires) { $whenexpires = "Expires = FROM_UNIXTIME($expires)"; } else { $whenexpires = "Expires = NULL"; } // Auto-update custom-titles according to Title, as long as unpublished and // NOT customised. if (empty($url_title) || (($oldArticle['Status'] < STATUS_LIVE) && ($oldArticle['url_title'] === $url_title) && ($oldArticle['url_title'] === stripSpace($oldArticle['Title'], 1)) && ($oldArticle['Title'] !== $Title) )) { $url_title = stripSpace($Title_plain, 1); } $Keywords = doSlash(trim(preg_replace('/( ?[\r\n\t,])+ ?/s', ',', preg_replace('/ +/', ' ', ps('Keywords'))), ', ')); $user = doSlash($txp_user); $cfq = array(); $cfs = getCustomFields(); foreach ($cfs as $i => $cf_name) { $custom_x = "custom_{$i}"; $cfq[] = "custom_$i = '".$$custom_x."'"; } $cfq = join(', ', $cfq); $rs = compact($vars); if (article_validate($rs, $msg)) { if (safe_update('textpattern', "Title = '$Title', Body = '$Body', Body_html = '$Body_html', Excerpt = '$Excerpt', Excerpt_html = '$Excerpt_html', Keywords = '$Keywords', description = '$description', Image = '$Image', Status = $Status, LastMod = NOW(), LastModID = '$user', Section = '$Section', Category1 = '$Category1', Category2 = '$Category2', Annotate = $Annotate, textile_body = '$textile_body', textile_excerpt = '$textile_excerpt', override_form = '$override_form', url_title = '$url_title', AnnotateInvite = '$AnnotateInvite'," .(($cfs) ? $cfq.',' : ''). "$whenposted, $whenexpires", "ID = $ID" )) { if ($Status >= STATUS_LIVE && $oldArticle['Status'] < STATUS_LIVE) { do_pings(); } if ($Status >= STATUS_LIVE || $oldArticle['Status'] >= STATUS_LIVE) { update_lastmod('article_saved', $rs); } now('posted', true); now('expires', true); callback_event('article_saved', '', false, $rs); if (empty($msg)) { $s = check_url_title($url_title); $msg = array(get_status_message($Status).' '.$s, $s ? E_WARNING : 0); } } else { $msg = array(gTxt('article_save_failed'), E_ERROR); } } article_edit($msg, false, true); } /** * Renders article editor form. * * @param string|array $message The activity message * @param bool $concurrent Treat as a concurrent save * @param bool $refresh_partials Whether refresh partial contents */ function article_edit($message = '', $concurrent = false, $refresh_partials = false) { global $vars, $txp_user, $prefs, $event, $view; extract($prefs); /* $partials is an array of: $key => array ( 'mode' => {PARTIAL_STATIC | PARTIAL_VOLATILE | PARTIAL_VOLATILE_VALUE}, 'selector' => $DOM_selector or array($selector, $fragment) of $DOM_selectors, 'cb' => $callback_function, 'html' => $return_value_of_callback_function (need not be intialised here) ) */ $partials = array( // HTML 'Title' field (in
). 'html_title' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => 'title', 'cb' => 'article_partial_html_title', ), // 'Text/HTML/Preview' links region. 'view_modes' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#view_modes', 'cb' => 'article_partial_view_modes', ), // 'Title' region. 'title' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.title', 'cb' => 'article_partial_title', ), // 'Title' field. 'title_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '#title', 'cb' => 'article_partial_title_value', ), // 'Body' region. 'body' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.body', 'cb' => 'article_partial_body', ), // 'Excerpt' region. 'excerpt' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.excerpt', 'cb' => 'article_partial_excerpt', ), // 'Author' region. 'author' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => 'p.author', 'cb' => 'article_partial_author', ), // 'Posted' value. 'sPosted' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '[name=sPosted]', 'cb' => 'article_partial_value', ), // 'Last modified' value. 'sLastMod' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '[name=sLastMod]', 'cb' => 'article_partial_value', ), // 'Duplicate' link. 'article_clone' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#article_partial_article_clone', 'cb' => 'article_partial_article_clone', ), // 'View' link. 'article_view' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#article_partial_article_view', 'cb' => 'article_partial_article_view', ), // 'Previous/Next' article links region. 'article_nav' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => 'nav.nav-tertiary', 'cb' => 'article_partial_article_nav', ), // 'Status' region. 'status' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#txp-container-status', 'cb' => 'article_partial_status', ), // 'Section' region. 'section' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.section', 'cb' => 'article_partial_section', ), // Categories region. 'categories' => array( 'mode' => PARTIAL_STATIC, 'selector' => '#categories_group', 'cb' => 'article_partial_categories', ), // Publish date/time region. 'posted' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#publish-datetime-group', 'cb' => 'article_partial_posted', ), // Expire date/time region. 'expires' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#expires-datetime-group', 'cb' => 'article_partial_expires', ), // Meta 'URL-only title' region. 'url_title' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.url-title', 'cb' => 'article_partial_url_title', ), // Meta 'URL-only title' field. 'url_title_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '#url-title', 'cb' => 'article_partial_url_title_value', ), // Meta 'Description' region. 'description' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.description', 'cb' => 'article_partial_description', ), // Meta 'Description' field. 'description_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '#description', 'cb' => 'article_partial_description_value', ), // Meta 'Keywords' region. 'keywords' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.keywords', 'cb' => 'article_partial_keywords', ), // Meta 'Keywords' field. 'keywords_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '#keywords', 'cb' => 'article_partial_keywords_value', ), // 'Comment options' section. 'comments' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#write-comments', 'cb' => 'article_partial_comments', ), // 'Article image' section. 'image' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => array('#txp-image-group .txp-container', '.txp-container'), 'cb' => 'article_partial_image', ), // 'Custom fields' section. 'custom_fields' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => array('#txp-custom-field-group-content .txp-container', '.txp-container'), 'cb' => 'article_partial_custom_fields', ), // 'Text formatting help' section. 'sidehelp' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => 'ul.textile', 'cb' => 'article_partial_sidehelp', ), // 'Recent articles' values. 'recent_articles' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => array('#txp-recent-group-content .txp-container', '.txp-container'), 'cb' => 'article_partial_recent_articles', ), ); // Add partials for custom fields (and their values which is redundant by // design, for plugins). global $cfs; foreach ($cfs as $k => $v) { $partials["custom_field_{$k}"] = array( 'mode' => PARTIAL_STATIC, 'selector' => "p.custom-field.custom-{$k}", 'cb' => 'article_partial_custom_field', ); $partials["custom_{$k}"] = array( 'mode' => PARTIAL_STATIC, 'selector' => "#custom-{$k}", 'cb' => 'article_partial_value', ); } extract(gpsa(array( 'view', 'from_view', 'step', ))); // Newly-saved article. if (!empty($GLOBALS['ID'])) { $ID = $GLOBALS['ID']; $step = 'edit'; } else { $ID = gps('ID'); } // Switch to 'text' view upon page load and after article post. if (!$view || gps('save') || gps('publish')) { $view = 'text'; } if (!$step) { $step = "create"; } if ($step == "edit" && $view == "text" && !empty($ID) && $from_view != 'preview' && $from_view != 'html' && !$concurrent) { $pull = true; // It's an existing article - off we go to the database. $ID = assert_int($ID); $rs = safe_row( "*, UNIX_TIMESTAMP(Posted) AS sPosted, UNIX_TIMESTAMP(Expires) AS sExpires, UNIX_TIMESTAMP(LastMod) AS sLastMod", 'textpattern', "ID = $ID" ); if (empty($rs)) { return; } $rs['reset_time'] = $rs['publish_now'] = false; } else { $pull = false; // Assume they came from post. if ($from_view == 'preview' or $from_view == 'html') { $store_out = array(); $store = json_decode(base64_decode(ps('store')), true); foreach ($vars as $var) { if (isset($store[$var])) { $store_out[$var] = $store[$var]; } } } else { $store_out = gpsa($vars); if ($concurrent) { $store_out['sLastMod'] = safe_field("UNIX_TIMESTAMP(LastMod) AS sLastMod", 'textpattern', "ID = $ID"); } if (!has_privs('article.set_markup') && !empty($ID)) { $oldArticle = safe_row("textile_body, textile_excerpt", 'textpattern', "ID = $ID"); if (!empty($oldArticle)) { $store_out['textile_body'] = $oldArticle['textile_body']; $store_out['textile_excerpt'] = $oldArticle['textile_excerpt']; } } } // Use preferred Textfilter as default and fallback. $hasfilter = new \Textpattern\Textfilter\Constraint(null); $validator = new Validator(); foreach (array('textile_body', 'textile_excerpt') as $k) { $hasfilter->setValue($store_out[$k]); $validator->setConstraints($hasfilter); if (!$validator->validate()) { $store_out[$k] = $use_textile; } } $rs = textile_main_fields($store_out); if (!empty($rs['exp_year'])) { if (empty($rs['exp_month'])) { $rs['exp_month'] = 1; } if (empty($rs['exp_day'])) { $rs['exp_day'] = 1; } if (empty($rs['exp_hour'])) { $rs['exp_hour'] = 0; } if (empty($rs['exp_minute'])) { $rs['exp_minute'] = 0; } if (empty($rs['exp_second'])) { $rs['exp_second'] = 0; } $rs['sExpires'] = safe_strtotime($rs['exp_year'].'-'.$rs['exp_month'].'-'.$rs['exp_day'].' '. $rs['exp_hour'].':'.$rs['exp_minute'].':'.$rs['exp_second']); } if (!empty($rs['year'])) { $rs['sPosted'] = safe_strtotime($rs['year'].'-'.$rs['month'].'-'.$rs['day'].' '. $rs['hour'].':'.$rs['minute'].':'.$rs['second']); } } $validator = new Validator(new SectionConstraint($rs['Section'])); if (!$validator->validate()) { $rs['Section'] = getDefaultSection(); } extract($rs); $GLOBALS['step'] = $step; if ($step != 'create' && isset($sPosted)) { // Previous record? $rs['prev_id'] = checkIfNeighbour('prev', $sPosted, $ID); // Next record? $rs['next_id'] = checkIfNeighbour('next', $sPosted, $ID); } else { $rs['prev_id'] = $rs['next_id'] = 0; } // Let plugins chime in on partials meta data. callback_event_ref('article_ui', 'partials_meta', 0, $rs, $partials); $rs['partials_meta'] = &$partials; // Get content for volatile partials. foreach ($partials as $k => $p) { if ($p['mode'] == PARTIAL_VOLATILE || $p['mode'] == PARTIAL_VOLATILE_VALUE) { $cb = $p['cb']; $partials[$k]['html'] = (is_array($cb) ? call_user_func($cb, $rs, $k) : $cb($rs, $k)); } } if ($refresh_partials) { $response[] = announce($message); $response[] = '$("#article_form [type=submit]").val(textpattern.gTxt("save"))'; if ($Status < STATUS_LIVE) { $response[] = '$("#article_form").addClass("saved").removeClass("published")'; } else { $response[] = '$("#article_form").addClass("published").removeClass("saved")'; } // Update the volatile partials. foreach ($partials as $k => $p) { // Volatile partials need a target DOM selector. if (empty($p['selector']) && $p['mode'] != PARTIAL_STATIC) { trigger_error("Empty selector for partial '$k'", E_USER_ERROR); } else { // Build response script. list($selector, $fragment) = (array)$p['selector'] + array(null, null); if (!isset($fragment)) { $fragment = $selector; } if ($p['mode'] == PARTIAL_VOLATILE) { // Volatile partials replace *all* of the existing HTML // fragment for their selector with the new one. $response[] = '$("'.$selector.'").replaceWith($("