. */ /** * Articles panel. * * @package Admin\List */ use Textpattern\Validator\CategoryConstraint; use Textpattern\Validator\SectionConstraint; use Textpattern\Validator\Validator; use Textpattern\Search\Filter; if (!defined('txpinterface')) { die('txpinterface is undefined.'); } if ($event == 'list') { global $statuses, $all_cats, $all_authors, $all_sections; require_privs('article'); $statuses = status_list(); $all_cats = getTree('root', 'article'); $all_authors = the_privileged('article.edit.own'); $all_sections = safe_column("name", 'txp_section', "name != 'default'"); $available_steps = array( 'list_list' => false, 'list_change_pageby' => true, 'list_multi_edit' => true, ); if ($step && bouncer($step, $available_steps)) { $step(); } else { list_list(); } } /** * The main panel listing all articles. * * @param string|array $message The activity message * @param string $post Not used */ function list_list($message = '', $post = '') { global $statuses, $use_comments, $comments_disabled_after, $step, $txp_user, $article_list_pageby, $event; pagetop(gTxt('tab_list'), $message); extract(gpsa(array( 'page', 'sort', 'dir', 'crit', 'search_method', ))); if ($sort === '') { $sort = get_pref('article_sort_column', 'posted'); } else { if (!in_array($sort, array('id', 'title', 'expires', 'section', 'category1', 'category2', 'status', 'author', 'comments', 'lastmod'))) { $sort = 'posted'; } set_pref('article_sort_column', $sort, 'list', 2, '', 0, PREF_PRIVATE); } if ($dir === '') { $dir = get_pref('article_sort_dir', 'desc'); } else { $dir = ($dir == 'asc') ? "asc" : "desc"; set_pref('article_sort_dir', $dir, 'list', 2, '', 0, PREF_PRIVATE); } $sesutats = array_flip($statuses); switch ($sort) { case 'id': $sort_sql = "textpattern.ID $dir"; break; case 'title': $sort_sql = "textpattern.Title $dir, textpattern.Posted DESC"; break; case 'expires': $sort_sql = "textpattern.Expires $dir"; break; case 'section': $sort_sql = "section.title $dir, textpattern.Posted DESC"; break; case 'category1': $sort_sql = "category1.title $dir, textpattern.Posted DESC"; break; case 'category2': $sort_sql = "category2.title $dir, textpattern.Posted DESC"; break; case 'status': $sort_sql = "textpattern.Status $dir, textpattern.Posted DESC"; break; case 'author': $sort_sql = "user.RealName $dir, textpattern.Posted DESC"; break; case 'comments': $sort_sql = "textpattern.comments_count $dir, textpattern.Posted DESC"; break; case 'lastmod': $sort_sql = "textpattern.LastMod $dir, textpattern.Posted DESC"; break; default: $sort = 'posted'; $sort_sql = "textpattern.Posted $dir"; break; } $switch_dir = ($dir == 'desc') ? 'asc' : 'desc'; $search = new Filter($event, array( 'id' => array( 'column' => 'textpattern.ID', 'label' => gTxt('ID'), 'type' => 'integer', ), 'title_body_excerpt' => array( 'column' => array('textpattern.Title', 'textpattern.Body', 'textpattern.Excerpt'), 'label' => gTxt('title_body_excerpt'), ), 'section' => array( 'column' => array('textpattern.Section', 'section.title'), 'label' => gTxt('section'), ), 'keywords' => array( 'column' => 'textpattern.Keywords', 'label' => gTxt('keywords'), 'type' => 'find_in_set', ), 'categories' => array( 'column' => array('textpattern.Category1', 'textpattern.Category2', 'category1.title', 'category2.title'), 'label' => gTxt('categories'), ), 'status' => array( 'column' => array('textpattern.Status'), 'label' => gTxt('status'), 'type' => 'boolean', ), 'author' => array( 'column' => array('textpattern.AuthorID', 'user.RealName'), 'label' => gTxt('author'), ), 'article_image' => array( 'column' => array('textpattern.Image'), 'label' => gTxt('article_image'), 'type' => 'integer', ), 'posted' => array( 'column' => array('textpattern.Posted'), 'label' => gTxt('posted'), 'options' => array('case_sensitive' => true), ), 'lastmod' => array( 'column' => array('textpattern.LastMod'), 'label' => gTxt('article_modified'), 'options' => array('case_sensitive' => true), ), ) ); $search->setAliases('status', $statuses); list($criteria, $crit, $search_method) = $search->getFilter(array( 'id' => array('can_list' => true), 'article_image' => array('can_list' => true), 'title_body_excerpt' => array('always_like' => true), )); $search_render_options = array( 'placeholder' => 'search_articles', ); $sql_from = safe_pfx('textpattern')." textpattern LEFT JOIN ".safe_pfx('txp_category')." category1 ON category1.name = textpattern.Category1 AND category1.type = 'article' LEFT JOIN ".safe_pfx('txp_category')." category2 ON category2.name = textpattern.Category2 AND category2.type = 'article' LEFT JOIN ".safe_pfx('txp_section')." section ON section.name = textpattern.Section LEFT JOIN ".safe_pfx('txp_users')." user ON user.name = textpattern.AuthorID"; if ($criteria === 1) { $total = safe_count('textpattern', $criteria); } else { $total = getThing("SELECT COUNT(*) FROM $sql_from WHERE $criteria"); } echo n.'
'. n.tag( hed(gTxt('tab_list'), 1, array('class' => 'txp-heading')), 'div', array('class' => 'txp-layout-4col-alt') ); $searchBlock = n.tag( $search->renderForm('list', $search_render_options), 'div', array( 'class' => 'txp-layout-4col-3span', 'id' => $event.'_control', ) ); $createBlock = array(); if (has_privs('article.edit')) { $createBlock[] = n.tag( sLink('article', '', gTxt('add_new_article'), 'txp-button'), 'div', array('class' => 'txp-control-panel') ); } $contentBlockStart = n.tag_start('div', array( 'class' => 'txp-layout-1col', 'id' => $event.'_container', )); $createBlock = implode(n, $createBlock); if ($total < 1) { if ($criteria != 1) { echo $searchBlock. $contentBlockStart. $createBlock. graf( span(null, array('class' => 'ui-icon ui-icon-info')).' '. gTxt('no_results_found'), array('class' => 'alert-block information') ); } else { echo $contentBlockStart. $createBlock. graf( span(null, array('class' => 'ui-icon ui-icon-info')).' '. gTxt('no_articles_recorded'), array('class' => 'alert-block information') ); } echo n.tag_end('div'). // End of .txp-layout-1col. n.'
'; // End of .txp-layout. return; } $limit = max($article_list_pageby, 15); list($page, $offset, $numPages) = pager($total, $limit, $page); echo $searchBlock.$contentBlockStart.$createBlock; $rs = safe_query( "SELECT textpattern.ID, textpattern.Title, textpattern.url_title, textpattern.Section, textpattern.Category1, textpattern.Category2, textpattern.Status, textpattern.Annotate, textpattern.AuthorID, UNIX_TIMESTAMP(textpattern.Posted) AS posted, UNIX_TIMESTAMP(textpattern.LastMod) AS lastmod, UNIX_TIMESTAMP(textpattern.Expires) AS expires, category1.title AS category1_title, category2.title AS category2_title, section.title AS section_title, user.RealName AS RealName, (SELECT COUNT(*) FROM ".safe_pfx('txp_discuss')." WHERE parentid = textpattern.ID) AS total_comments FROM $sql_from WHERE $criteria ORDER BY $sort_sql LIMIT $offset, $limit" ); if ($rs) { $show_authors = !has_single_author('textpattern', 'AuthorID'); echo n.tag( toggle_box('articles_detail'), 'div', array('class' => 'txp-list-options')). n.tag_start('form', array( 'class' => 'multi_edit_form', 'id' => 'articles_form', 'name' => 'longform', 'method' => 'post', 'action' => 'index.php', )). n.tag_start('div', array('class' => 'txp-listtables')). n.tag_start('table', array('class' => 'txp-list')). n.tag_start('thead'). tr( hCell( fInput('checkbox', 'select_all', 0, '', '', '', '', '', 'select_all'), '', ' class="txp-list-col-multi-edit" scope="col" title="'.gTxt('toggle_all_selected').'"' ). column_head( 'ID', 'id', 'list', true, $switch_dir, $crit, $search_method, (('id' == $sort) ? "$dir " : '').'txp-list-col-id' ). column_head( 'title', 'title', 'list', true, $switch_dir, $crit, $search_method, (('title' == $sort) ? "$dir " : '').'txp-list-col-title' ). column_head( 'posted', 'posted', 'list', true, $switch_dir, $crit, $search_method, (('posted' == $sort) ? "$dir " : '').'txp-list-col-created date' ). column_head( 'article_modified', 'lastmod', 'list', true, $switch_dir, $crit, $search_method, (('lastmod' == $sort) ? "$dir " : '').'txp-list-col-lastmod date articles_detail' ). column_head( 'expires', 'expires', 'list', true, $switch_dir, $crit, $search_method, (('expires' == $sort) ? "$dir " : '').'txp-list-col-expires date articles_detail' ). column_head( 'section', 'section', 'list', true, $switch_dir, $crit, $search_method, (('section' == $sort) ? "$dir " : '').'txp-list-col-section' ). column_head( 'category1', 'category1', 'list', true, $switch_dir, $crit, $search_method, (('category1' == $sort) ? "$dir " : '').'txp-list-col-category1 category articles_detail' ). column_head( 'category2', 'category2', 'list', true, $switch_dir, $crit, $search_method, (('category2' == $sort) ? "$dir " : '').'txp-list-col-category2 category articles_detail' ). column_head( 'status', 'status', 'list', true, $switch_dir, $crit, $search_method, (('status' == $sort) ? "$dir " : '').'txp-list-col-status' ). ( $show_authors ? column_head('author', 'author', 'list', true, $switch_dir, $crit, $search_method, (('author' == $sort) ? "$dir " : '').'txp-list-col-author name') : '' ). ( $use_comments == 1 ? column_head('comments', 'comments', 'list', true, $switch_dir, $crit, $search_method, (('comments' == $sort) ? "$dir " : '').'txp-list-col-comments articles_detail') : '' ) ). n.tag_end('thead'); include_once txpath.'/publish/taghandlers.php'; echo n.tag_start('tbody'); $validator = new Validator(); while ($a = nextRow($rs)) { extract($a); if ($Title === '') { $Title = ''.eLink('article', 'edit', 'ID', $ID, gTxt('untitled')).''; } else { $Title = eLink('article', 'edit', 'ID', $ID, $Title); } // Valid section and categories? $validator->setConstraints(array(new SectionConstraint($Section))); $vs = $validator->validate() ? '' : ' error'; $validator->setConstraints(array(new CategoryConstraint($Category1, array('type' => 'article')))); $vc[1] = $validator->validate() ? '' : ' error'; $validator->setConstraints(array(new CategoryConstraint($Category2, array('type' => 'article')))); $vc[2] = $validator->validate() ? '' : ' error'; $Category1 = ($Category1) ? span(txpspecialchars($category1_title), array('title' => $Category1)) : ''; $Category2 = ($Category2) ? span(txpspecialchars($category2_title), array('title' => $Category2)) : ''; if ($Status != STATUS_LIVE and $Status != STATUS_STICKY) { $view_url = '?txpreview='.intval($ID).'.'.time(); } else { $view_url = permlinkurl($a); } if (isset($statuses[$Status])) { $Status = $statuses[$Status]; } $comments = '('.$total_comments.')'; if ($total_comments) { $comments = href($comments, array( 'event' => 'discuss', 'step' => 'list', 'search_method' => 'parent', 'crit' => $ID, ), array('title' => gTxt('manage'))); } $comment_status = ($Annotate) ? gTxt('on') : gTxt('off'); if ($comments_disabled_after) { $lifespan = $comments_disabled_after * 86400; $time_since = time() - $posted; if ($time_since > $lifespan) { $comment_status = gTxt('expired'); } } $comments = tag($comment_status, 'span', array('class' => 'comments-status')).' '. tag($comments, 'span', array('class' => 'comments-manage')); echo tr( td( ( ( ($a['Status'] >= STATUS_LIVE and has_privs('article.edit.published')) or ($a['Status'] >= STATUS_LIVE and $AuthorID === $txp_user and has_privs('article.edit.own.published')) or ($a['Status'] < STATUS_LIVE and has_privs('article.edit')) or ($a['Status'] < STATUS_LIVE and $AuthorID === $txp_user and has_privs('article.edit.own')) ) ? fInput('checkbox', 'selected[]', $ID, 'checkbox') : '' ), '', 'txp-list-col-multi-edit' ). hCell( eLink('article', 'edit', 'ID', $ID, $ID). span( sp.span('|', array('role' => 'separator')). sp.href(gTxt('view'), $view_url), array('class' => 'txp-option-link articles_detail') ), '', array( 'class' => 'txp-list-col-id', 'scope' => 'row', ) ). td( $Title, '', 'txp-list-col-title' ). td( gTime($posted), '', 'txp-list-col-created date'.($posted < time() ? '' : ' unpublished') ). td( gTime($lastmod), '', 'txp-list-col-lastmod date articles_detail'.($posted === $lastmod ? ' not-modified' : '') ). td( ($expires ? gTime($expires) : ''), '', 'txp-list-col-expires date articles_detail' ). td( span(txpspecialchars($section_title), array('title' => $Section)), '', 'txp-list-col-section'.$vs ). td( $Category1, '', 'txp-list-col-category1 category articles_detail'.$vc[1] ). td( $Category2, '', 'txp-list-col-category2 category articles_detail'.$vc[2] ). td( href($Status, $view_url, join_atts(array('title' => gTxt('view')), TEXTPATTERN_STRIP_EMPTY)), '', 'txp-list-col-status' ). ( $show_authors ? td(span(txpspecialchars($RealName), array('title' => $AuthorID)), '', 'txp-list-col-author name') : '' ). ( $use_comments ? td($comments, '', 'txp-list-col-comments articles_detail') : '' ) ); } echo n.tag_end('tbody'). n.tag_end('table'). n.tag_end('div'). // End of .txp-listtables. list_multiedit_form($page, $sort, $dir, $crit, $search_method). tInput(). n.tag_end('form'). n.tag_start('div', array( 'class' => 'txp-navigation', 'id' => $event.'_navigation', )). pageby_form('list', $article_list_pageby). nav_form('list', $page, $numPages, $sort, $dir, $crit, $search_method, $total, $limit). n.tag_end('div'); } echo n.tag_end('div'). // End of .txp-layout-1col. n.''; // End of .txp-layout. } /** * Saves pageby value for the article list. */ function list_change_pageby() { event_change_pageby('article'); list_list(); } /** * Renders a multi-edit form widget for articles. * * @param int $page The page number * @param string $sort The current sort value * @param string $dir The current sort direction * @param string $crit The current search criteria * @param string $search_method The current search method * @return string HTML */ function list_multiedit_form($page, $sort, $dir, $crit, $search_method) { global $statuses, $all_cats, $all_authors, $all_sections; if ($all_cats) { $category1 = treeSelectInput('Category1', $all_cats, ''); $category2 = treeSelectInput('Category2', $all_cats, ''); } else { $category1 = $category2 = ''; } $sections = $all_sections ? selectInput('Section', $all_sections, '', true) : ''; $comments = onoffRadio('Annotate', get_pref('comments_on_default')); $status = selectInput('Status', $statuses, '', true); $authors = $all_authors ? selectInput('AuthorID', $all_authors, '', true) : ''; $methods = array( 'changesection' => array('label' => gTxt('changesection'), 'html' => $sections), 'changecategory1' => array('label' => gTxt('changecategory1'), 'html' => $category1), 'changecategory2' => array('label' => gTxt('changecategory2'), 'html' => $category2), 'changestatus' => array('label' => gTxt('changestatus'), 'html' => $status), 'changecomments' => array('label' => gTxt('changecomments'), 'html' => $comments), 'changeauthor' => array('label' => gTxt('changeauthor'), 'html' => $authors), 'duplicate' => gTxt('duplicate'), 'delete' => gTxt('delete'), ); if (!$all_cats) { unset($methods['changecategory1'], $methods['changecategory2']); } if (has_single_author('textpattern', 'AuthorID') || !has_privs('article.edit')) { unset($methods['changeauthor']); } if (!has_privs('article.delete.own') && !has_privs('article.delete')) { unset($methods['delete']); } return multi_edit($methods, 'list', 'list_multi_edit', $page, $sort, $dir, $crit, $search_method); } /** * Processes multi-edit actions. */ function list_multi_edit() { global $txp_user, $statuses, $all_cats, $all_authors, $all_sections; extract(psa(array( 'selected', 'edit_method', ))); if (!$selected || !is_array($selected)) { return list_list(); } $selected = array_map('assert_int', $selected); // Empty entry to permit clearing the categories. $categories = array(''); foreach ($all_cats as $row) { $categories[] = $row['name']; } $allowed = array(); $field = $value = ''; switch ($edit_method) { // Delete. case 'delete': if (!has_privs('article.delete')) { if (has_privs('article.delete.own')) { $allowed = safe_column_num( "ID", 'textpattern', "ID IN (".join(',', $selected).") AND AuthorID = '".doSlash($txp_user)."'" ); } $selected = $allowed; } if ($selected && safe_delete('textpattern', "ID IN (".join(',', $selected).")")) { safe_update('txp_discuss', "visible = ".MODERATE, "parentid IN (".join(',', $selected).")"); callback_event('articles_deleted', '', 0, $selected); callback_event('multi_edited.articles', 'delete', 0, compact('selected', 'field', 'value')); update_lastmod('articles_deleted', $selected); now('posted', true); now('expires', true); return list_list(messenger('article', join(', ', $selected), 'deleted')); } return list_list(); break; // Change author. case 'changeauthor': $value = ps('AuthorID'); if (has_privs('article.edit') && in_array($value, $all_authors, true)) { $field = 'AuthorID'; } break; // Change category1. case 'changecategory1': $value = ps('Category1'); if (in_array($value, $categories, true)) { $field = 'Category1'; } break; // Change category2. case 'changecategory2': $value = ps('Category2'); if (in_array($value, $categories, true)) { $field = 'Category2'; } break; // Change comment status. case 'changecomments': $field = 'Annotate'; $value = (int) ps('Annotate'); break; // Change section. case 'changesection': $value = ps('Section'); if (in_array($value, $all_sections, true)) { $field = 'Section'; } break; // Change status. case 'changestatus': $value = (int) ps('Status'); if (array_key_exists($value, $statuses)) { $field = 'Status'; } if (!has_privs('article.publish') && $value >= STATUS_LIVE) { $value = STATUS_PENDING; } break; } $selected = safe_rows( "ID, AuthorID, Status", 'textpattern', "ID IN (".join(',', $selected).")" ); foreach ($selected as $item) { if ( ($item['Status'] >= STATUS_LIVE && has_privs('article.edit.published')) || ($item['Status'] >= STATUS_LIVE && $item['AuthorID'] === $txp_user && has_privs('article.edit.own.published')) || ($item['Status'] < STATUS_LIVE && has_privs('article.edit')) || ($item['Status'] < STATUS_LIVE && $item['AuthorID'] === $txp_user && has_privs('article.edit.own')) ) { $allowed[] = $item['ID']; } } $selected = $allowed; if ($selected) { $message = messenger('article', join(', ', $selected), 'modified'); if ($edit_method === 'duplicate') { $rs = safe_rows_start("*", 'textpattern', "ID IN (".join(',', $selected).")"); if ($rs) { while ($a = nextRow($rs)) { unset($a['ID'], $a['comments_count']); $a['uid'] = md5(uniqid(rand(), true)); $a['AuthorID'] = $txp_user; $a['LastModID'] = $txp_user; $a['Status'] = ($a['Status'] >= STATUS_LIVE) ? STATUS_DRAFT : $a['Status']; foreach ($a as $name => &$value) { if ($name == 'Expires' && !$value) { $value = "Expires = NULL"; } else { $value = "`$name` = '".doSlash($value)."'"; } } if ($id = (int) safe_insert('textpattern', join(',', $a))) { safe_update( 'textpattern', "Title = CONCAT(Title, ' (', $id, ')'), url_title = CONCAT(url_title, '-', $id), LastMod = NOW(), feed_time = NOW()", "ID = $id" ); } } } $message = gTxt('duplicated_articles', array('{id}' => join(', ', $selected))); } elseif (!$field || safe_update('textpattern', "$field = '".doSlash($value)."'", "ID IN (".join(',', $selected).")") === false) { return list_list(); } update_lastmod('articles_updated', compact('selected', 'field', 'value')); now('posted', true); now('expires', true); callback_event('multi_edited.articles', $edit_method, 0, compact('selected', 'field', 'value')); return list_list($message); } return list_list(); }