.
*/
/**
* 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',
'expire_now',
'AuthorID',
'sPosted',
'LastModID',
'sLastMod',
'override_form',
'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) && $event == 'article') {
require_privs('article');
$save = gps('save');
if ($save) {
$step = 'save';
}
$publish = gps('publish');
if ($publish) {
$step = 'save';
}
if (empty($step)) {
$step = 'edit';
}
bouncer($step, array(
'create' => false,
'publish' => true,
'edit' => false,
'save' => true,
));
switch ($step) {
case 'create':
case 'edit':
article_edit();
break;
case 'publish':
case 'save':
article_save();
break;
}
}
/**
* Processes sent forms and saves new articles. Deprecated in 4.7 by article_save().
*/
function article_post()
{
article_save();
}
/**
* Processes sent forms and updates existing articles.
*/
function article_save()
{
global $txp_user, $vars, $prefs;
extract($prefs);
$incoming = array_map('assert_string', psa($vars));
$is_clone = ps('copy');
if ($is_clone) {
$incoming['ID'] = $incoming['url_title'] = '';
$incoming['Status'] = STATUS_DRAFT;
}
if ($incoming['ID']) {
$oldArticle = safe_row("Status, AuthorID, 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 && has_privs('article.edit.published')
|| $oldArticle['Status'] >= STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && has_privs('article.edit.own.published')
|| $oldArticle['Status'] < STATUS_LIVE && has_privs('article.edit')
|| $oldArticle['Status'] < STATUS_LIVE && $oldArticle['AuthorID'] === $txp_user && 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;
}
} else {
$oldArticle = array('Status' => STATUS_PENDING,
'url_title' => '',
'Title' => '',
'textile_body' => $use_textile,
'textile_excerpt' => $use_textile,
'sLastMod' => null,
'LastModID' => $txp_user,
'sPosted' => time(),
'sExpires' => null,
);
}
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));
$ID = intval($ID);
$Status = assert_int($Status);
if (!has_privs('article.publish') && $Status >= STATUS_LIVE) {
$Status = STATUS_PENDING;
}
// Comments may be on, off, or disabled.
$Annotate = (int) $Annotate;
// Set and validate article timestamp.
if ($publish_now || $reset_time) {
$whenposted = "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_ts = $oldArticle['sPosted'];
$msg = array(gTxt('invalid_postdate'), E_ERROR);
} else {
$when_ts = $ts - tz_offset($ts);
}
$whenposted = "FROM_UNIXTIME($when_ts)";
}
// Set and validate expiry timestamp.
if ($expire_now) {
$ts = time();
$expires = $ts - tz_offset($ts);
} elseif (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_expiredate'), 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 = "FROM_UNIXTIME($expires)";
} else {
$whenexpires = "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['Title'] !== $Title)
&& ($oldArticle['url_title'] === stripSpace($oldArticle['Title'], 1))
)) {
$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)) {
$set =
"Title = '$Title',
Body = '$Body',
Body_html = '$Body_html',
Excerpt = '$Excerpt',
Excerpt_html = '$Excerpt_html',
Keywords = '$Keywords',
description = '$description',
Image = '$Image',
Status = '$Status',
Posted = $whenposted,
Expires = $whenexpires,
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 : '')
.(!empty($ID) ? '' :
", AuthorID = '$user',
uid = '".md5(uniqid(rand(), true))."',
feed_time = NOW()");
if ($ID && safe_update('textpattern', $set, "ID = $ID")
|| !$ID && $rs['ID'] = $GLOBALS['ID'] = safe_insert('textpattern', $set)
) {
if ($is_clone) {
$url_title = stripSpace($Title_plain.' ('.$rs['ID'].')', 1);
safe_update(
'textpattern',
"Title = CONCAT(Title, ' (', ID, ')'),
url_title = '$url_title'",
"ID = ".$rs['ID']
);
}
if ($Status >= STATUS_LIVE) {
if ($oldArticle['Status'] < STATUS_LIVE) {
do_pings();
} else {
update_lastmod($ID ? 'article_saved' : 'article_posted', $rs);
}
}
now('posted', true);
now('expires', true);
callback_event($ID ? 'article_saved' : 'article_posted', '', 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 preview.
*
* @param string $field
*/
function article_preview($field = false)
{
global $prefs, $vars, $app_mode;
// Assume they came from post.
$view = gps('view', 'preview');
$rs = textile_main_fields(gpsa($vars));
// Preview pane
$preview = '
';
if ($view == 'preview') {
if (!$field || $field == 'body') {
$preview .= n.'
'.
n.'
'.gTxt('body').'
'.
implode('', txp_tokenize($rs['Body_html'], false, function ($tag) {
return ''.txpspecialchars($tag).'';
})).
'';
}
if ($prefs['articles_use_excerpts'] && (!$field || $field == 'excerpt')) {
$preview .= n.'
'.
n.'
'.gTxt('excerpt').'
'.
implode('', txp_tokenize($rs['Excerpt_html'], false, function ($tag) {
return ''.txpspecialchars($tag).'';
})).
'';
}
} elseif ($view == 'html') {
if (!$field || $field == 'body') {
$preview .= n.'
'.gTxt('body').'
'.
n.tag(
tag(str_replace(array(t), array(sp.sp.sp.sp), txpspecialchars($rs['Body_html'])), 'code', array(
'class' => 'language-markup',
'dir' => 'ltr',
)),
'pre', array('class' => 'body')
);
}
if ($prefs['articles_use_excerpts'] && (!$field || $field == 'excerpt')) {
$preview .= n.'
'.gTxt('excerpt').'
'.
n.tag(
tag(str_replace(array(t), array(sp.sp.sp.sp), txpspecialchars($rs['Excerpt_html'])), 'code', array(
'class' => 'language-markup',
'dir' => 'ltr',
)),
'pre', array('class' => 'excerpt')
);
}
}
$preview .= '
';// End of #pane-view.
return $preview;
}
/**
* Renders article editor form.
*
* @param string|array $message The activity message
* @param bool $concurrent Treat as a concurrent save
* @param bool $refresh_partials Whether to refresh partial contents
*/
function article_edit($message = '', $concurrent = false, $refresh_partials = false)
{
global $vars, $txp_user, $prefs, $step, $view, $app_mode;
$view = gps('view', 'text');
if ($view != 'text') {
echo article_preview(gps('preview'));
return;
}
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 initialised here)
)
*/
$partials = array(
// Hidden 'ID'.
'ID' => array(
'mode' => PARTIAL_VOLATILE_VALUE,
'selector' => 'input[name=ID]',
'cb' => 'article_partial_value',
),
// 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_STATIC,
'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',
),
// 'Author' region.
'author' => array(
'mode' => PARTIAL_VOLATILE,
'selector' => 'div.author',
'cb' => 'article_partial_author',
),
// 'Actions' region.
'actions' => array(
'mode' => PARTIAL_VOLATILE,
'selector' => '#txp-article-actions',
'cb' => 'article_partial_actions',
),
// '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',
),
// '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',
),
// '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',
),
// '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',
);
}
if ($step !== 'create') {
$step = "edit";
}
// Newly-saved article.
if (!empty($GLOBALS['ID'])) {
$ID = $GLOBALS['ID'];
} else {
$ID = $step === 'create' ? 0 : intval(gps('ID'));
}
if (!empty($ID) && !$concurrent) {
// 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'] = $rs['expire_now'] = false;
if (gps('copy') && !gps('publish')) {
$rs['ID'] = $rs['url_title'] = '';
$rs['Status'] = STATUS_DRAFT;
}
} else {
// Assume they came from post.
$store_out = array('ID' => $ID) + 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'];
}
}
$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);
if ($ID && !empty($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.
$partials = updatePartials($partials, $rs, array(PARTIAL_VOLATILE, PARTIAL_VOLATILE_VALUE));
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")';
}
if (!empty($GLOBALS['ID'])) {
$response[] = "if (typeof window.history.replaceState == 'function') {history.replaceState({}, '', '?event=article&ID=$ID')}";
}
$response = array_merge($response, updateVolatilePartials($partials));
send_script_response(join(";\n", $response));
// Bail out.
return;
}
// Get content for static partials.
$partials = updatePartials($partials, $rs, PARTIAL_STATIC);
$page_title = $ID ? $Title : gTxt('write');
pagetop($page_title, $message);
$class = array('async');
if ($Status >= STATUS_LIVE) {
$class[] = 'published';
} elseif ($ID) {
$class[] = 'saved';
}
echo n.tag_start('form', array(
'class' => $class,
'id' => 'article_form',
'name' => 'article_form',
'method' => 'post',
'action' => 'index.php',
)).
n.'';
echo hInput('ID', $ID).
eInput('article').
sInput($step).
hInput('sPosted', $sPosted).
hInput('sLastMod', $sLastMod).
hInput('AuthorID', $AuthorID).
hInput('LastModID', $LastModID);
echo n.'
'.
hed(gTxt('tab_write'), 1, array('class' => 'txp-heading'));
echo n.'
';
echo n.'
'.$partials['title']['html'],
$partials['author']['html'],
$partials['body']['html'];
if ($articles_use_excerpts) {
echo $partials['excerpt']['html'];
}
echo n.'
';
echo n.'
';
echo n.$partials['view_modes']['html'];
echo article_preview();
echo '
';// End of .txp-dialog.
echo n.'
'.// End of #main_content.
n.'
'; // End of .txp-layout-4col-3span.
// Sidebar column (only shown if in text editing view).
echo n.'
'.
n.'
';
// 'Publish/Save' button.
if (empty($ID)) {
if (has_privs('article.publish') && get_pref('default_publish_status', STATUS_LIVE) >= STATUS_LIVE) {
$push_button = fInput('submit', 'publish', gTxt('publish'), 'publish');
} else {
$push_button = fInput('submit', 'publish', gTxt('save'), 'publish');
}
echo graf($push_button, array('class' => 'txp-save'));
} elseif (
($Status >= STATUS_LIVE && has_privs('article.edit.published')) ||
($Status >= STATUS_LIVE && $AuthorID === $txp_user && has_privs('article.edit.own.published')) ||
($Status < STATUS_LIVE && has_privs('article.edit')) ||
($Status < STATUS_LIVE && $AuthorID === $txp_user && has_privs('article.edit.own'))
) {
echo graf(fInput('submit', 'save', gTxt('save'), 'publish'), array('class' => 'txp-save'));
}
echo $partials['actions']['html'].
n.'
';
echo n.'
';
// 'Override form' selection.
$form_pop = $allow_form_override ? form_pop($override_form, 'override-form', $rs['Section']) : '';
$html_override = $form_pop
? pluggable_ui('article_ui', 'override',
inputLabel(
'override-form',
$form_pop,
'override_default_form',
array('override_form', 'instructions_override_form'),
array('class' => 'txp-form-field override-form')
),
$rs)
: '';
// 'Sort and display' section.
echo pluggable_ui(
'article_ui',
'sort_display',
wrapRegion('txp-write-sort-group', $partials['status']['html'].$partials['section']['html'].$html_override, '', gTxt('sort_display')),
$rs
);
echo graf(
href(' '.gTxt('expand_all'), '#', array(
'class' => 'txp-expand-all',
'aria-controls' => 'supporting_content',
)).
href(' '.gTxt('collapse_all'), '#', array(
'class' => 'txp-collapse-all',
'aria-controls' => 'supporting_content',
)), array('class' => 'txp-actions')
);
// 'Date and time' collapsible section.
if (empty($ID)) {
// Timestamp.
// Avoiding modified date to disappear.
if (!empty($store_out['year'])) {
$persist_timestamp = safe_strtotime(
$store_out['year'].'-'.$store_out['month'].'-'.$store_out['day'].' '.
$store_out['hour'].':'.$store_out['minute'].':'.$store_out['second']
);
} else {
$persist_timestamp = time();
}
$posted_block = tag(pluggable_ui(
'article_ui',
'timestamp',
inputLabel(
'year',
tsi('year', '%Y', $persist_timestamp, '', 'year').
' / '.
tsi('month', '%m', $persist_timestamp, '', 'month').
' / '.
tsi('day', '%d', $persist_timestamp, '', 'day'),
'publish_date',
array('publish_date', 'instructions_publish_date'),
array('class' => 'txp-form-field date posted')
).
inputLabel(
'hour',
tsi('hour', '%H', $persist_timestamp, '', 'hour').
' : '.
tsi('minute', '%M', $persist_timestamp, '', 'minute').
' : '.
tsi('second', '%S', $persist_timestamp, '', 'second'),
'publish_time',
array('', 'instructions_publish_time'),
array('class' => 'txp-form-field time posted')
).
n.tag(
checkbox('publish_now', '1', true, '', 'publish_now').
n.tag(gTxt('set_to_now'), 'label', array('for' => 'publish_now')),
'div', array('class' => 'txp-form-field posted-now')
),
array('sPosted' => $persist_timestamp) + $rs
), 'div', array('id' => 'publish-datetime-group'));
// Expires.
if (!empty($store_out['exp_year'])) {
$persist_timestamp = safe_strtotime(
$store_out['exp_year'].'-'.$store_out['exp_month'].'-'.$store_out['exp_day'].' '.
$store_out['exp_hour'].':'.$store_out['exp_minute'].':'.$store_out['second']
);
} else {
$persist_timestamp = 0;
}
$expires_block = tag(pluggable_ui(
'article_ui',
'expires',
inputLabel(
'exp_year',
tsi('exp_year', '%Y', $persist_timestamp, '', 'exp_year').
' / '.
tsi('exp_month', '%m', $persist_timestamp, '', 'exp_month').
' / '.
tsi('exp_day', '%d', $persist_timestamp, '', 'exp_day'),
'expire_date',
array('expire_date', 'instructions_expire_date'),
array('class' => 'txp-form-field date expires')
).
inputLabel(
'exp_hour',
tsi('exp_hour', '%H', $persist_timestamp, '', 'exp_hour').
' : '.
tsi('exp_minute', '%M', $persist_timestamp, '', 'exp_minute').
' : '.
tsi('exp_second', '%S', $persist_timestamp, '', 'exp_second'),
'expire_time',
array('', 'instructions_expire_time'),
array('class' => 'txp-form-field time expires')
).
n.tag(
checkbox('expire_now', '1', false, '', 'expire_now').
n.tag(gTxt('set_expire_now'), 'label', array('for' => 'expire_now')),
'div', array('class' => 'txp-form-field expire-now')
),
$rs
), 'div', array('id' => 'expires-datetime-group'));
} else {
// Timestamp.
$posted_block = $partials['posted']['html'];
// Expires.
$expires_block = $partials['expires']['html'];
}
echo wrapRegion('txp-dates-group', $posted_block.$expires_block, 'txp-dates-group-content', 'date_settings', 'article_dates');
// 'Categories' section.
$html_categories = pluggable_ui('article_ui', 'categories', $partials['categories']['html'], $rs);
echo wrapRegion('txp-categories-group', $html_categories, 'txp-categories-group-content', 'categories', 'categories');
// 'Meta' collapsible section.
// 'URL-only title' field.
$html_url_title = $partials['url_title']['html'];
// 'Description' field.
$html_description = $partials['description']['html'];
// 'Keywords' field.
$html_keywords = $partials['keywords']['html'];
echo wrapRegion('txp-meta-group', $html_url_title.$html_description.$html_keywords, 'txp-meta-group-content', 'meta', 'article_meta');
// 'Comment options' collapsible section.
echo wrapRegion('txp-comments-group', $partials['comments']['html'], 'txp-comments-group-content', 'comment_settings', 'article_comments');
// 'Article image' collapsible section.
echo wrapRegion('txp-image-group', $partials['image']['html'], 'txp-image-group-content', 'article_image', 'article_image');
// 'Custom fields' collapsible section.
echo wrapRegion('txp-custom-field-group', $partials['custom_fields']['html'], 'txp-custom-field-group-content', 'custom', 'article_custom_field');
// 'Advanced options' collapsible section.
// Unused by core, but leaving the placeholder for legacy plugin support.
$html_advanced = pluggable_ui('article_ui', 'markup', '', $rs);
if ($html_advanced) {
echo wrapRegion('txp-advanced-group', $html_advanced, 'txp-advanced-group-content', 'advanced_options', 'article_advanced');
}
// Custom menu entries.
echo pluggable_ui('article_ui', 'extend_col_1', '', $rs);
// 'Recent articles' collapsible section.
echo wrapRegion('txp-recent-group', $partials['recent_articles']['html'], 'txp-recent-group-content', 'recent_articles', 'article_recent');
echo n.'
'; // End of #supporting_content.
// Prev/next article links.
echo $partials['article_nav']['html'];
echo n.'
'; // End of .txp-layout-4col-alt.
echo //tInput().
n.'
'. // End of .txp-layout.
n.'';
}
/**
* Renders a custom field.
*
* @param int $num The custom field number
* @param string $field The label
* @param string $content The field contents
* @return string HTML form field
*/
function custField($num, $field, $content)
{
return inputLabel(
'custom-'.$num,
fInput('text', 'custom_'.$num, $content, '', '', '', INPUT_REGULAR, '', 'custom-'.$num),
txpspecialchars($field),
array('', 'instructions_custom_'.$num),
array('class' => 'txp-form-field custom-field custom-'.$num)
);
}
/**
* Gets the ID of the next or the previous article.
*
* @param string $whichway Either '<' or '>'
* @param int Unix timestamp
* @param int pivot article ID
* @return int
*/
function checkIfNeighbour($whichway, $sPosted, $ID = 0)
{
// Eventual backward compatibility.
if (empty($ID)) {
$ID = !empty($GLOBALS['ID']) ? $GLOBALS['ID'] : gps('ID');
}
$sPosted = assert_int($sPosted);
$ID = assert_int($ID);
$dir = ($whichway == 'prev') ? '<' : '>';
$ord = ($whichway == 'prev') ? "DESC" : "ASC";
$crit = callback_event('txp.article', 'neighbour.criteria', 0, compact('ID', 'whichway', 'sPosted'));
return safe_field("ID", 'textpattern',
"(Posted $dir FROM_UNIXTIME($sPosted) OR Posted = FROM_UNIXTIME($sPosted) AND ID $dir $ID) $crit ORDER BY Posted $ord, ID $ord LIMIT 1");
}
/**
* Renders an article status field.
*
* @param int $status Selected status
* @return string HTML
*/
function status_display($status = 0)
{
global $statuses;
if (!$status) {
$status = get_pref('default_publish_status', STATUS_LIVE);
has_privs('article.publish') or $status = min($status, STATUS_PENDING);
}
$disabled = has_privs('article.publish') ? false : array(STATUS_LIVE, STATUS_STICKY);
return inputLabel(
'status',
selectInput('Status', $statuses, $status, false, '', 'status', false, $disabled),
'status',
array('status', 'instructions_status'),
array('class' => 'txp-form-field status')
);
}
/**
* Renders a section field.
*
* @param string $Section The selected section
* @param string $id The HTML id
* @return string HTML <select> input
*/
function section_popup($Section, $id)
{
global $txp_sections;
$rs = $txp_sections;
unset($rs['default']);
if ($rs) {
$options = array();
foreach ($rs as $a) {
$options[$a['name']] = array('title' => $a['title'], 'data-skin' => $a['skin']);
}
return selectInput('Section', $options, $Section, false, '', $id);
}
return false;
}
/**
* Renders a category field.
*
* @param string $name The Name of the field
* @param string $val The selected option
* @param string $id The HTML id
* @return string HTML <select> input
*/
function category_popup($name, $val, $id)
{
$rs = getTree('root', 'article');
if ($rs) {
return treeSelectInput($name, $rs, $val, $id, 35);
}
return false;
}
/**
* Renders a view tab.
*
* @param string $tabevent Target view
* @param string $view The current view
* @return string HTML
*/
function tab($tabevent, $view, $tag = 'li')
{
$state = ($view == $tabevent) ? 'active' : '';
$pressed = ($view == $tabevent) ? 'true' : 'false';
if (is_array($tabevent)) {
list($tabevent, $label) = $tabevent + array(null, gTxt('text'));
} else {
$label = gTxt('view_'.$tabevent.'_short');
}
$link = href($label, '#', array(
'data-view-mode' => $tabevent ? $tabevent : false,
'aria-pressed' => $pressed,
'role' => 'button',
));
return $tag ? n.tag($link, 'li', array(
'class' => $state,
'id' => 'tab-'.$tabevent,
)) : $link;
}
/**
* Renders 'override form' field.
*
* @param string $form The selected form
* @param string $id HTML id to apply to the input control
* @param string $section The section that is currently in use
* @return string HTML <select> input
*/
function form_pop($form, $id, $section)
{
global $txp_sections;
$skinforms = array();
$form_types = get_pref('override_form_types');
$rs = safe_rows('skin, name, type', 'txp_form', "type IN (".implode(",", quote_list(do_list($form_types))).") AND name != 'default' ORDER BY type,name");
foreach ($txp_sections as $name => $row) {
$skin = $row['skin'];
if (!isset($skinforms[$skin])) {
$skinforms[$skin] = array_column(array_filter($rs, function($v) use ($skin) {
return $v['skin'] == $skin;
}), 'name');
}
}
script_js('var allForms = '.json_encode($skinforms, TEXTPATTERN_JSON), false);
$skin = isset($txp_sections[$section]['skin']) ? $txp_sections[$section]['skin'] : false;
$rs = $skin && isset($skinforms[$skin]) ? array_combine($skinforms[$skin], $skinforms[$skin]) : false;
if ($rs) {
return selectInput('override_form', $rs, $form, true, '', $id);
}
}
/**
* Checks URL title for duplicates.
*
* @param string $url_title The URL title
* @return string Localised feedback message, or an empty string
*/
function check_url_title($url_title)
{
// Check for blank or previously used identical url-titles.
if (strlen($url_title) === 0) {
return gTxt('url_title_is_blank');
} else {
$url_title_count = safe_count('textpattern', "url_title = '$url_title'");
if ($url_title_count > 1) {
return gTxt('url_title_is_multiple', array('{count}' => $url_title_count));
}
}
return '';
}
/**
* Translates a status ID to a feedback message.
*
* This message is displayed when an article is saved.
*
* @param int $Status The status
* @return string The status message
*/
function get_status_message($Status)
{
switch ($Status) {
case STATUS_PENDING:
return gTxt('article_saved_pending');
case STATUS_HIDDEN:
return gTxt('article_saved_hidden');
case STATUS_DRAFT:
return gTxt('article_saved_draft');
default:
return gTxt('article_posted');
}
}
/**
* Parses article fields using Textile.
*
* @param array $incoming
* @return array
*/
function textile_main_fields($incoming)
{
// 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($incoming[$k]);
$validator->setConstraints($hasfilter);
if (!$validator->validate()) {
$incoming[$k] = get_pref('use_textile');
}
}
$textile = new \Textpattern\Textile\Parser();
$incoming['Title_plain'] = trim($incoming['Title']);
$incoming['Title_html'] = ''; // not used
$incoming['Title'] = $textile->textileEncode($incoming['Title_plain']);
$incoming['Body_html'] = Txp::get('\Textpattern\Textfilter\Registry')->filter(
$incoming['textile_body'],
$incoming['Body'],
array('field' => 'Body', 'options' => array('lite' => false), 'data' => $incoming)
);
$incoming['Excerpt_html'] = Txp::get('\Textpattern\Textfilter\Registry')->filter(
$incoming['textile_excerpt'],
$incoming['Excerpt'],
array('field' => 'Excerpt', 'options' => array('lite' => false), 'data' => $incoming)
);
return $incoming;
}
/**
* Raises a ping callback so plugins can take action when an article is published.
*/
function do_pings()
{
global $production_status;
// Only ping for Live sites.
if ($production_status !== 'live') {
return;
}
callback_event('ping');
}
/**
* Renders the <title> element for the 'Write' page.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_html_title($rs)
{
return tag(admin_title($rs['Title']), 'title');
}
/**
* Renders article title partial.
*
* The rendered widget can be customised via the 'article_ui > title'
* pluggable UI callback event.
*
* @param array $rs Article data
*/
function article_partial_title($rs)
{
$out = inputLabel(
'title',
fInput('text', 'Title', preg_replace("/&(?![#a-z0-9]+;)/i", "&", $rs['Title']), '', '', '', INPUT_LARGE, '', 'title', false, true),
'title',
array('title', 'instructions_title'),
array('class' => 'txp-form-field title')
);
return pluggable_ui('article_ui', 'title', $out, $rs);
}
/**
* Gets article's title from the given article data set.
*
* @param array $rs Article data
* @return string
*/
function article_partial_title_value($rs)
{
return preg_replace("/&(?![#a-z0-9]+;)/i", "&", $rs['Title']);
}
/**
* Renders author partial.
*
* The rendered widget can be customised via the 'article_ui > author'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_author($rs)
{
extract($rs);
$out = n.'';
if (!empty($ID)) {
$out .= '';
$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);
if ($sPosted != $sLastMod) {
$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);
}
$out .= '';
}
$out .= '
';
return pluggable_ui('article_ui', 'author', $out, $rs);
}
/* View/Duplicate/Create new article links.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_actions($rs)
{
return graf($rs['ID']
? href(' '.gTxt('create_article'), 'index.php?event=article', array('class' => 'txp-new'))
.article_partial_article_clone($rs)
.article_partial_article_view($rs)
: null,
array(
'class' => 'txp-actions',
'id' => 'txp-article-actions',
));
}
/**
* Renders custom field partial.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_custom_field($rs, $key)
{
global $prefs;
extract($prefs);
preg_match('/custom_field_([0-9]+)/', $key, $m);
$custom_x_set = "custom_{$m[1]}_set";
$custom_x = "custom_{$m[1]}";
return ($$custom_x_set !== '' ? custField($m[1], $$custom_x_set, $rs[$custom_x]) : '');
}
/**
* Renders URL title partial.
*
* The rendered widget can be customised via the 'article_ui > url_title'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_url_title($rs)
{
$out = inputLabel(
'url-title',
fInput('text', 'url_title', article_partial_url_title_value($rs), '', '', '', INPUT_REGULAR, '', 'url-title'),
'url_title',
array('url_title', 'instructions_url_title'),
array('class' => 'txp-form-field url-title')
);
return pluggable_ui('article_ui', 'url_title', $out, $rs);
}
/**
* Gets URL title from the given article data set.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_url_title_value($rs)
{
return $rs['url_title'];
}
/**
* Renders description partial.
*
* The rendered widget can be customised via the 'article_ui > description'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_description($rs)
{
$out = inputLabel(
'description',
'',
'description',
array('description', 'instructions_description'),
array('class' => 'txp-form-field txp-form-field-textarea description')
);
return pluggable_ui('article_ui', 'description', $out, $rs);
}
/**
* Gets description from the given article data set.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_description_value($rs)
{
return $rs['description'];
}
/**
* Renders keywords partial.
*
* The rendered widget can be customised via the 'article_ui > keywords'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_keywords($rs)
{
$out = inputLabel(
'keywords',
'',
'keywords',
array('keywords', 'instructions_keywords'),
array('class' => 'txp-form-field txp-form-field-textarea keywords')
);
return pluggable_ui('article_ui', 'keywords', $out, $rs);
}
/**
* Gets keywords from the given article data set.
*
* @param array $rs Article data
* @return string
*/
function article_partial_keywords_value($rs)
{
// Separate keywords by a comma plus at least one space.
return preg_replace('/,(\S)/', ', $1', $rs['Keywords']);
}
/**
* Renders article image partial.
*
* The rendered widget can be customised via the 'article_ui > article_image'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_image($rs)
{
$default = inputLabel(
'article-image',
fInput('text', 'Image', $rs['Image'], '', '', '', INPUT_REGULAR, '', 'article-image'),
'article_image',
array('article_image', 'instructions_article_image'),
array('class' => 'txp-form-field article-image')
);
return tag(pluggable_ui('article_ui', 'article_image', $default, $rs), 'div', array('class' => 'txp-container'));
}
/**
* Renders all custom fields in one partial.
*
* The rendered widget can be customised via the 'article_ui > custom_fields'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_custom_fields($rs)
{
global $cfs;
$cf = '';
foreach ($cfs as $k => $v) {
$cf .= article_partial_custom_field($rs, "custom_field_{$k}");
}
return tag(pluggable_ui('article_ui', 'custom_fields', $cf, $rs), 'div', array('class' => 'txp-container'));
}
/**
* Renders <ol> list of recent articles.
*
* The rendered widget can be customised via the 'article_ui > recent_articles'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_recent_articles($rs)
{
$recents = safe_rows_start("Title, ID", 'textpattern', "1 = 1 ORDER BY LastMod DESC LIMIT ".(int) WRITE_RECENT_ARTICLES_COUNT);
$ra = '';
if ($recents && numRows($recents)) {
$ra = '';
while ($recent = nextRow($recents)) {
if ($recent['Title'] === '') {
$recent['Title'] = gTxt('untitled').sp.$recent['ID'];
}
$ra .= n.'- '.
href(escape_title($recent['Title']), '?event=article'.a.'step=edit'.a.'ID='.$recent['ID']).
'
';
}
$ra .= '
';
}
return tag(pluggable_ui('article_ui', 'recent_articles', $ra, $rs), 'div', array('class' => 'txp-container'));
}
/**
* Renders article 'duplicate' link.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_article_clone($rs)
{
extract($rs);
return n.href(' '.gTxt('duplicate'), '#', array(
'class' => 'txp-clone',
'id' => 'article_partial_article_clone',
));
}
/**
* Renders article 'view' link.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_article_view($rs)
{
extract($rs);
if ($Status != STATUS_LIVE and $Status != STATUS_STICKY) {
if (!has_privs('article.preview')) {
return;
}
$url = '?txpreview='.intval($ID).'.'.time(); // Article ID plus cachebuster.
} else {
include_once txpath.'/publish/taghandlers.php';
$url = permlinkurl_id($ID);
}
return n.href(' '.gTxt('view'), $url, array(
'class' => 'txp-article-view',
'id' => 'article_partial_article_view',
'rel' => 'noopener',
'target' => '_blank',
));
}
/**
* Renders article body field.
*
* The rendered widget can be customised via the 'article_ui > body'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_body($rs)
{
$textarea_options = n.href(gTxt('view_preview_short'), '#', array(
'class' => 'txp-textarea-preview',
'data-preview-link' => 'body',
));
// Article markup selection.
if (has_privs('article.set_markup')) {
// Markup help.
$help = '';
$textfilter_opts = Txp::get('\Textpattern\Textfilter\Registry')->getMap();
isset($textfilter_opts[$rs['textile_body']]) or $rs['textile_body'] = LEAVE_TEXT_UNTOUCHED;
$html_markup = array();
foreach ($textfilter_opts as $filter_key => $filter_name) {
$thisHelp = Txp::get('\Textpattern\Textfilter\Registry')->getHelp($filter_key);
$renderHelp = ($thisHelp) ? popHelp($thisHelp) : '';
$selected = (string)$filter_key === (string)$rs['textile_body'];
$html_markup[] = tag(
$filter_name, 'option', array(
'data-id' => $filter_key,
'data-help' => $renderHelp,
'selected' => $selected,
)
);
if ($selected) {
$help = $renderHelp;
}
}
// Note: not using span() for the textfilter help, because it doesn't render empty content.
$html_markup = tag(implode(n, $html_markup),
'select',
array('class' => 'jquery-ui-selectmenu'))
.tag_void('input', array(
'class' => 'textfilter-value',
'name' => 'textile_body',
'type' => 'hidden',
'value' => $rs['textile_body'],
));
$textarea_options = n.''.
''.$help.''.$textarea_options;
}
$textarea_options = ''.$textarea_options.'
';
$out = inputLabel(
'body',
'',
array('body', $textarea_options),
array('body', 'instructions_body'),
array('class' => 'txp-form-field txp-form-field-textarea body')
);
return pluggable_ui('article_ui', 'body', $out, $rs);
}
/**
* Renders article excerpt field.
*
* The rendered widget can be customised via the 'article_ui > excerpt'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_excerpt($rs)
{
$textarea_options = n.href(gTxt('view_preview_short'), '#', array(
'class' => 'txp-textarea-preview',
'data-preview-link' => 'excerpt',
));
// Excerpt markup selection.
if (has_privs('article.set_markup')) {
// Markup help.
$help = '';
$textfilter_opts = Txp::get('\Textpattern\Textfilter\Registry')->getMap();
isset($textfilter_opts[$rs['textile_excerpt']]) or $rs['textile_excerpt'] = LEAVE_TEXT_UNTOUCHED;
$html_markup = array();
foreach ($textfilter_opts as $filter_key => $filter_name) {
$thisHelp = Txp::get('\Textpattern\Textfilter\Registry')->getHelp($filter_key);
$renderHelp = ($thisHelp) ? popHelp($thisHelp) : '';
$selected = (string)$filter_key === (string)$rs['textile_excerpt'];
$html_markup[] = tag(
$filter_name, 'option', array(
'data-id' => $filter_key,
'data-help' => $renderHelp,
'selected' => $selected,
)
);
if ($selected) {
$help = $renderHelp;
}
}
// Note: not using span() for the textfilter help, because it doesn't render empty content.
$html_markup = tag(implode(n, $html_markup),
'select',
array('class' => 'jquery-ui-selectmenu'))
.tag_void('input', array(
'class' => 'textfilter-value',
'name' => 'textile_excerpt',
'type' => 'hidden',
'value' => $rs['textile_excerpt'],
));
$textarea_options = n.''.
''.$help.''.$textarea_options;
}
$textarea_options = ''.$textarea_options.'
';
$out = inputLabel(
'excerpt',
'',
array('excerpt', $textarea_options),
array('excerpt', 'instructions_excerpt'),
array('class' => 'txp-form-field txp-form-field-textarea excerpt')
);
return pluggable_ui('article_ui', 'excerpt', $out, $rs);
}
/**
* Renders list of view modes.
*
* The rendered widget can be customised via the 'article_ui > view'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_view_modes($rs)
{
global $view;
$out = n.''.
checkbox2('', false, 0, 'live-preview').
sp.tag(gTxt('live_preview'), 'label', array('for' => 'live-preview')).
n.'
'.
n.tag(tab(array('preview'), $view).tab(array('html', 'HTML'), $view), 'ul');
$out = pluggable_ui('article_ui', 'view', $out, $rs);
return n.tag($out.n, 'div', array('id' => 'view_modes'));
}
/**
* Renders next/prev links.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_article_nav($rs)
{
$out = array();
if ($rs['prev_id']) {
$out[] = prevnext_link(gTxt('prev'), 'article', 'edit', $rs['prev_id'], '', 'prev');
} else {
$out[] = span(gTxt('prev'), array(
'class' => 'navlink-disabled',
'aria-disabled' => 'true',
));
}
if ($rs['next_id']) {
$out[] = prevnext_link(gTxt('next'), 'article', 'edit', $rs['next_id'], '', 'next');
} else {
$out[] = span(gTxt('next'), array(
'class' => 'navlink-disabled',
'aria-disabled' => 'true',
));
}
return n.tag(join('', $out), 'nav', array('class' => 'nav-tertiary'));
}
/**
* Renders article status partial.
*
* The rendered widget can be customised via the 'article_ui > status'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_status($rs)
{
return n.tag(pluggable_ui('article_ui', 'status', status_display($rs['Status']), $rs), 'div', array('id' => 'txp-container-status'));
}
/**
* Renders article section partial.
*
* The rendered widget can be customised via the 'article_ui > section'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_section($rs)
{
$out = inputLabel(
'section',
section_popup($rs['Section'], 'section').
n.eLink('section', 'list', '', '', gTxt('edit'), '', '', '', 'txp-option-link'),
'section',
array('', 'instructions_section'),
array('class' => 'txp-form-field section')
);
return pluggable_ui('article_ui', 'section', $out, $rs);
}
/**
* Renders article categories partial.
*
* The rendered widget can be customised via the 'article_ui > categories'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_categories($rs)
{
$out = inputLabel(
'category-1',
category_popup('Category1', $rs['Category1'], 'category-1').
n.eLink('category', 'list', '', '', gTxt('edit'), '', '', '', 'txp-option-link'),
'category1',
array('', 'instructions_category1'),
array('class' => 'txp-form-field category category-1')
).
inputLabel(
'category-2',
category_popup('Category2', $rs['Category2'], 'category-2'),
'category2',
array('', 'instructions_category2'),
array('class' => 'txp-form-field category category-2')
);
return pluggable_ui('article_ui', 'categories', $out, $rs);
}
/**
* Renders comment options partial.
*
* The rendered widget can be customised via the 'article_ui > annotate_invite'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string|null HTML
*/
function article_partial_comments($rs)
{
global $step, $use_comments, $comments_disabled_after, $comments_default_invite, $comments_on_default;
extract($rs);
if (empty($ID)) {
// Avoid invite disappearing when previewing.
if (!empty($store_out['AnnotateInvite'])) {
$AnnotateInvite = $store_out['AnnotateInvite'];
} else {
$AnnotateInvite = $comments_default_invite;
}
$Annotate = $comments_on_default;
}
if ($use_comments == 1) {
$comments_expired = false;
if (!empty($ID) && $comments_disabled_after) {
$lifespan = $comments_disabled_after * 86400;
$time_since = time() - intval($sPosted);
if ($time_since > $lifespan) {
$comments_expired = true;
}
}
if ($comments_expired) {
$invite = graf(gTxt('expired'), array('class' => 'comment-annotate-expired'));
} else {
$invite = n.tag(
onoffRadio('Annotate', $Annotate),
'div', array('class' => 'txp-form-field comment-annotate')
).
inputLabel(
'comment-invite',
fInput('text', 'AnnotateInvite', $AnnotateInvite, '', '', '', INPUT_REGULAR, '', 'comment-invite'),
'comment_invitation',
array('', 'instructions_comment_invitation'),
array('class' => 'txp-form-field comment-invite')
);
}
return n.tag_start('div', array('id' => 'write-comments')).
pluggable_ui('article_ui', 'annotate_invite', $invite, $rs).
n.tag_end('div');
}
}
/**
* Renders timestamp partial.
*
* The rendered widget can be customised via the 'article_ui > timestamp'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_posted($rs)
{
extract($rs);
$out =
inputLabel(
'year',
tsi('year', '%Y', $sPosted, '', 'year').
' / '.
tsi('month', '%m', $sPosted, '', 'month').
' / '.
tsi('day', '%d', $sPosted, '', 'day'),
'publish_date',
array('publish_date', 'instructions_publish_date'),
array('class' => 'txp-form-field date posted')
).
inputLabel(
'hour',
tsi('hour', '%H', $sPosted, '', 'hour').
' : '.
tsi('minute', '%M', $sPosted, '', 'minute').
' : '.
tsi('second', '%S', $sPosted, '', 'second'),
'publish_time',
array('', 'instructions_publish_time'),
array('class' => 'txp-form-field time posted')
).
n.tag(
checkbox('reset_time', '1', $reset_time, '', 'reset_time').
n.tag(gTxt('reset_time'), 'label', array('for' => 'reset_time')),
'div', array('class' => 'txp-form-field reset-time')
);
return n.tag_start('div', array('id' => 'publish-datetime-group')).
pluggable_ui('article_ui', 'timestamp', $out, $rs).
n.tag_end('div');
}
/**
* Renders expiration date partial.
*
* The rendered widget can be customised via the 'article_ui > expires'
* pluggable UI callback event.
*
* @param array $rs Article data
* @return string HTML
*/
function article_partial_expires($rs)
{
extract($rs);
$out =
inputLabel(
'exp_year',
tsi('exp_year', '%Y', $sExpires, '', 'exp_year').
' / '.
tsi('exp_month', '%m', $sExpires, '', 'exp_month').
' / '.
tsi('exp_day', '%d', $sExpires, '', 'exp_day'),
'expire_date',
array('expire_date', 'instructions_expire_date'),
array('class' => 'txp-form-field date expires')
).
inputLabel(
'exp_hour',
tsi('exp_hour', '%H', $sExpires, '', 'exp_hour').
' : '.
tsi('exp_minute', '%M', $sExpires, '', 'exp_minute').
' : '.
tsi('exp_second', '%S', $sExpires, '', 'exp_second'),
'expire_time',
array('', 'instructions_expire_time'),
array('class' => 'txp-form-field time expires')
).
n.tag(
checkbox('expire_now', '1', $expire_now, '', 'expire_now').
n.tag(gTxt('set_expire_now'), 'label', array('for' => 'expire_now')),
'div', array('class' => 'txp-form-field expire-now')
).
hInput('sExpires', $sExpires);
return n.tag_start('div', array('id' => 'expires-datetime-group')).
pluggable_ui('article_ui', 'expires', $out, $rs).
n.tag_end('div');
}
/**
* Gets a partial value from the given article data set.
*
* @param array $rs Article data
* @param string $key The value to get
* @return string HTML
*/
function article_partial_value($rs, $key)
{
return($rs[$key]);
}
/**
* Validates article data.
*
* @param array $rs Article data
* @param string|array $msg Initial message
* @return string HTML
*/
function article_validate($rs, &$msg)
{
global $prefs, $step, $statuses;
if (!empty($msg)) {
return false;
}
$constraints = array(
'Status' => new ChoiceConstraint(
$rs['Status'],
array('choices' => array_keys($statuses), 'message' => 'invalid_status')
),
'Section' => new SectionConstraint($rs['Section']),
'Category1' => new CategoryConstraint(
$rs['Category1'],
array('type' => 'article')
),
'Category2' => new CategoryConstraint(
$rs['Category2'],
array('type' => 'article')
),
'textile_body' => new \Textpattern\Textfilter\Constraint(
$rs['textile_body'],
array('message' => 'invalid_textfilter_body')
),
'textile_excerpt' => new \Textpattern\Textfilter\Constraint(
$rs['textile_excerpt'],
array('message' => 'invalid_textfilter_excerpt')
),
);
if (!$prefs['articles_use_excerpts']) {
$constraints['excerpt_blank'] = new BlankConstraint(
$rs['Excerpt'],
array('message' => 'excerpt_not_blank')
);
}
if (!$prefs['use_comments']) {
$constraints['annotate_invite_blank'] = new BlankConstraint(
$rs['AnnotateInvite'],
array('message' => 'invite_not_blank')
);
$constraints['annotate_false'] = new FalseConstraint(
$rs['Annotate'],
array('message' => 'comments_are_on')
);
}
if ($prefs['allow_form_override']) {
$constraints['override_form'] = new FormConstraint(
$rs['override_form'],
array('type' => get_pref('override_form_types'))
);
} else {
$constraints['override_form'] = new BlankConstraint(
$rs['override_form'],
array('message' => 'override_form_not_blank')
);
}
callback_event_ref('article_ui', "validate_$step", 0, $rs, $constraints);
$validator = new Validator($constraints);
if ($validator->validate()) {
$msg = '';
return true;
} else {
$msg = doArray($validator->getMessages(), 'gTxt');
$msg = array(join(', ', $msg), E_ERROR);
return false;
}
}