Index: inc/admin/actions/class.dcaction.php
===================================================================
--- inc/admin/actions/class.dcaction.php	(revision 1903)
+++ inc/admin/actions/class.dcaction.php	(revision 1903)
@@ -0,0 +1,315 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+/**
+* dcActionsPage -- handler for action page on selected entries
+*
+*/
+abstract class dcActionsPage
+{
+	/** @var string form submit uri */
+	protected $uri;
+	/** @var dcCore dotclear core instance */
+	protected $core;
+	/** @var array action combo box */
+	protected $combo;
+	/** @var array list of defined actions (callbacks) */
+	protected $actions;
+	/** @var array selected entries (each key is the entry id, value contains the entry description) */
+	protected $entries;
+	/** @var record record that challenges ids against permissions */
+	protected $rs;
+	/** @var array redirection $_GET arguments, if any (does not contain ids by default, ids may be merged to it) */
+	protected $redir_args;
+	/** @var array list of $_POST fields used to build the redirection  */
+	protected $redirect_fields;
+	/** @var string current action, if any */
+	protected $action;
+	/** @var array list of url parameters (usually $_POST) */
+	protected $from;
+	
+	/** @var string title for checkboxes list, if displayed */
+	protected $cb_title;
+	
+    /**
+     * Class constructor
+     * 
+     * @param mixed  $core   dotclear core
+     * @param mixed  $uri   form uri
+     *
+     * @access public
+     *
+     * @return mixed Value.
+     */
+	public function __construct($core,$uri,$redirect_args=array()) {
+		$this->core = $core;
+		$this->actions = new ArrayObject();
+		$this->combo = array();
+		$this->uri = $uri;
+		$this->redir_args = $redirect_args;
+		$this->redirect_fields = array();
+		$this->action = '';
+		$this->cb_title = __('Title');
+		$this->entries = array();
+		$this->from = new ArrayObject($_POST);
+	}
+	
+    /**
+     * addAction - adds an action
+     *
+     * @param string $actions the actions names as if it was a standalone combo array.
+	 * 						  It will be merged with other actions.
+	 * 						  Can be bound to multiple values, if the same callback is to be called
+     * @param callback $callback the callback for the action.
+     *
+     * @access public
+	 *
+     * @return dcActionsPage the actions page itself, enabling to chain addAction().
+     */
+	public function addAction ($actions,$callback) {
+		foreach ($actions as $k => $a) {
+			// Check each case of combo definition
+			// Store form values in $values
+			if (is_array($a)) {
+				$values = array_values($a);
+				if (!isset($this->combo[$k])) {
+					$this->combo[$k]=array();
+				}
+				$this->combo[$k] = array_merge ($this->combo[$k],$a);
+			} elseif ($a instanceof formSelectOption) {
+				$values = $a->value;
+				$this->combo[$k] = $a->value;
+			} else {
+				$values = $a;
+				$this->combo[$k] = $a;
+			}
+			// Associate each potential value to the callback
+			foreach ($values as $v) {
+				$this->actions[$v]=$callback;
+			}
+		}
+		return $this;
+	}
+	
+    /**
+     * getCombo - returns the actions combo, useable through form::combo
+     *
+     * @access public
+	 *
+     * @return array the actions combo
+     */
+	public function getCombo() {
+		return $this->combo;
+	}
+	
+	
+    /**
+     * getIDS() - returns the list of selected entries
+     *
+     * @access public
+	 *
+     * @return array the list
+     */
+	public function getIDs() {
+		return array_keys($this->entries);
+	}
+	
+    /**
+     * getIDS() - returns the list of selected entries as HTML hidden fields string
+     *
+     * @access public
+	 *
+     * @return string the HTML code for hidden fields
+     */
+	 public function getIDsHidden() {
+		$ret = '';
+		foreach  ($this->entries as $id->$v) {
+			$ret .= form::hidden('entries[]',$id);
+		}
+		return $ret;
+	}
+	
+    /**
+     * getHiddenFields() - returns all redirection parameters as HTML hidden fields
+     *
+     * @param boolean $with_ids if true, also include ids in HTML code
+	 *
+     * @access public
+	 *
+     * @return string the HTML code for hidden fields
+     */	
+	public function getHiddenFields($with_ids = false) {
+		$ret = '';
+		foreach ($this->redir_args as $k => $v) {
+			$ret .= form::hidden(array($k),$v);
+		}
+		if ($with_ids) {
+			$ret .= $this->getIDsHidden();
+		}
+		return $ret;
+	}
+	
+	
+	/**
+     * getRS() - get record from DB Query containing requested IDs
+     *
+     * @param boolean $with_ids if true, also include ids in HTML code
+	 *
+     * @access public
+	 *
+     * @return string the HTML code for hidden fields
+     */
+	public function getRS() {
+		return $this->rs;
+	}
+	
+	/**
+     * setupRedir - setup redirection arguments
+	 *  by default, $_POST fields as defined in redirect_fields attributes
+	 *  are set into redirect_args.
+     *
+     * @param array $from input to parse fields from (usually $_POST)
+	 *
+     * @access protected
+     */
+	protected function setupRedir($from) {
+		foreach ($this->redirect_fields as $p) {
+			if (isset($from[$p])) {
+				$redir_args[$p] = $from[$p];
+			}
+		}
+	}
+
+	/**
+     * getRedirection - returns redirection URL
+     *
+     * @param array $params extra parameters to append to redirection
+	 *						must be an array : each key is the name, 
+	 *						each value is the wanted value
+     * @param boolean $with_selected_entries if true, add selected entries in url
+	 *
+     * @access public
+	 *
+     * @return string the redirection url
+     */
+	public function getRedirection($params=array(),$with_selected_entries=false) {
+		$redir_args = array_merge($params,$this->redir_args);
+		if ($with_selected_entries) {
+			$redir_args['entries'] = array_keys($this->entries);
+		}
+		return $this->uri.'?'.http_build_query($redir_args);
+	}
+
+	/**
+     * redirect - redirects to redirection page
+     *
+	 * @see getRedirection for arguments details
+	 *
+     * @access public
+     */
+	public function redirect($params=array(),$with_selected_entries=false) {
+		http::redirect($this->getRedirection($params,$with_selected_entries));
+	}	
+	
+	/**
+     * getAction - returns current action, if any
+     *
+	 * @see getRedirection for arguments details
+	 *
+     * @access public
+	 *
+     * @return string the action
+     */
+	public function getAction() {
+		return $this->action;
+	}
+
+	/**
+     * process - proceeds action handling, if any
+	 * 			this method may issue an exit() if
+	 *			an action is being processed. If it
+	 *			returns, no action has been performed
+     *
+     * @access public
+     */
+	public function process() {
+
+		$this->setupRedir($this->from);
+		$this->fetchEntries($this->from);	
+		if (isset($this->from['action'])) {
+			$this->action = $this->from['action'];
+			try {
+				$performed=false;
+				foreach ($this->actions as $k=>$v) {
+					if ($this->from['action']==$k) {
+						$performed = true;
+						call_user_func($v,$this->core,$this,$this->from);
+					}
+				}
+				if ($performed) {
+					exit;
+				}
+			} catch (Exception $e) {
+				$this->error($e);
+			}
+		}
+	}
+
+	/**
+     * getcheckboxes -returns html code for selected entries
+	 * 			as a table containing entries checkboxes
+     *
+     * @access public
+	 *
+     * @return string the html code for checkboxes
+     */
+	public function getCheckboxes() {
+		$ret = 
+			'<table class="posts-list"><tr>'.
+			'<th colspan="2">'.$this->cb_title.'</th>'.
+			'</tr>';
+		foreach ($this->entries as $id=>$title) {
+			$ret .= 
+				'<tr><td>'.
+				form::checkbox(array('entries[]'),$id,true,'','').'</td>'.
+				'<td>'.	$title.'</td></tr>';
+		}
+		$ret .= '</table>';
+		return $ret;
+	}
+	
+	/**
+     * beginPage, endPage - displays the beginning/ending of a page, if action does not redirects dirtectly
+     *
+	 * These methods are called from the actions themselves.
+	 *
+     * @param string $breadcrumb breadcrumb to display
+     * @param string $head	page header to include
+	 *
+     * @access public
+     */
+	abstract public function beginPage($breadcrumb='',$head='');
+	abstract public function endPage();
+
+	/**
+     * fetchEntries - fills-in information by requesting into db
+	 * 	this method may setup the following attributes
+	 *   * entries : list of entries (checked against permissions)
+	 *      entries ids are array keys, values contain entry description (if relevant)
+     *   * rs : record given by db request
+     * @access protected
+     */
+	 abstract protected function fetchEntries($from);
+
+}
+
Index: inc/admin/actions/class.dcactionposts.php
===================================================================
--- inc/admin/actions/class.dcactionposts.php	(revision 1903)
+++ inc/admin/actions/class.dcactionposts.php	(revision 1903)
@@ -0,0 +1,361 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+class dcPostsActionsPage extends dcActionsPage
+{
+	public function __construct($core,$uri,$redirect_args=array()) {
+		parent::__construct($core,$uri,$redirect_args);
+		$this->redirect_fields = array('user_id','cat_id','status',
+		'selected','month','lang','sortby','order','page','nb');
+		$this->loadDefaults();
+		$core->callBehavior('adminPostsActionsPage',$core,$this);
+	}
+
+	protected function loadDefaults() {
+		// We could have added a behavior here, but we want default action
+		// to be setup first
+		dcDefaultPostActions::adminPostsActionsPage($this->core,$this);
+	}
+	
+	public function beginPage($breadcrumb='',$head='') {
+		dcPage::open(
+			__('Entries'),
+			
+			dcPage::jsLoad('js/_posts_actions.js').
+			$head,
+			$breadcrumb
+		);	
+		echo '<p><a class="back" href="'.$this->getRedirection(array(),true).'">'.__('Back to entries list').'</a></p>';
+	}
+	
+	public function endPage() {
+		dcPage::close();
+	}
+	
+	public function error(Exception $e) {
+		$this->core->error->add($e->getMessage());
+		$this->beginPage(dcPage::breadcrumb(
+			array(
+				html::escapeHTML($this->core->blog->name) => '',
+				__('Entries') => 'posts.php',
+				'<span class="page-title">'.__('Entries actions').'</span>' => ''
+			))
+		);
+		$this->endPage();
+	}
+	
+	protected function fetchEntries($from) {
+		if (!empty($from['entries']))
+		{
+			$entries = $from['entries'];
+			
+			foreach ($entries as $k => $v) {
+				$entries[$k] = (integer) $v;
+			}
+			
+			$params['sql'] = 'AND P.post_id IN('.implode(',',$entries).') ';
+			
+			if (!isset($from['full_content']) || empty($from['full_content'])) {
+				$params['no_content'] = true;
+			}
+			
+			if (isset($from['post_type'])) {
+				$params['post_type'] = $from['post_type'];
+			}
+			
+			$posts = $this->core->blog->getPosts($params);
+			while ($posts->fetch())	{
+				$this->entries[$posts->post_id] = $posts->post_title;
+			}
+			$this->rs = $posts;			
+		} else {
+			$this->rs = $this->core->con->select("SELECT blog_id FROM ".$this->core->prefix."blog WHERE false");;
+		}
+	}
+}
+
+class dcDefaultPostActions 
+{
+	public static function adminPostsActionsPage($core, dcPostsActionsPage $ap) {
+		if ($core->auth->check('publish,contentadmin',$core->blog->id)) {
+			$ap->addAction(
+				array(__('Status') => array(
+					__('Publish') => 'publish',
+					__('Unpublish') => 'unpublish',
+					__('Schedule') => 'schedule',
+					__('Mark as pending') => 'pending'
+				)),
+				array('dcDefaultPostActions','doChangePostStatus')
+			);
+		}
+		$ap->addAction(
+			array(__('Mark')=> array(
+				__('Mark as selected') => 'selected',
+				__('Mark as unselected') => 'unselected'
+			)),
+			array('dcDefaultPostActions','doUpdateSelectedPost')
+		);
+		$ap->addAction(
+			array(__('Change') => array(
+				__('Change category') => 'category',
+			)),
+			array('dcDefaultPostActions','doChangePostCategory')
+		);
+		$ap->addAction(
+			array(__('Change') => array(
+				__('Change language') => 'lang',
+			)),
+			array('dcDefaultPostActions','doChangePostLang')
+		);
+		if ($core->auth->check('admin',$core->blog->id))
+		{
+			$ap->addAction(
+				array(__('Change') => array(
+					__('Change author') => 'author')),
+				array('dcDefaultPostActions','doChangePostAuthor')
+			);
+		}
+		if ($core->auth->check('delete,contentadmin',$core->blog->id)) {
+			$ap->addAction(
+				array(__('Delete') => array(
+					__('Delete') => 'delete')),
+				array('dcDefaultPostActions','doDeletePost')
+			);
+		}
+	}
+
+	public static function doChangePostStatus($core, dcPostsActionsPage $ap, $post) {
+		switch ($ap->getAction()) {
+			case 'unpublish' : $status = 0; break;
+			case 'schedule' : $status = -1; break;
+			case 'pending' : $status = -2; break;
+			default : $status = 1; break;
+		}
+		$posts_ids = $ap->getIDs();
+		if (empty($posts_ids)) {
+			throw new Exception(__('No entry selected'));
+		}
+		$core->blog->updPostsStatus($posts_ids,$status);
+		
+		$ap->redirect(array('upd' => 1),true);
+	}
+	
+	public static function doUpdateSelectedPost($core, dcPostsActionsPage $ap, $post) {
+		$posts_ids = $ap->getIDs();
+		if (empty($posts_ids)) {
+			throw new Exception(__('No entry selected'));
+		}
+		$action = $ap->getAction();
+		$core->blog->updPostsSelected($posts_ids,$action == 'selected');
+		
+		$ap->redirect(array('upd' => 1),true);
+	}
+	
+	public static function doDeletePost($core, dcPostsActionsPage $ap, $post) {
+
+		$posts_ids = $ap->getIDs();
+		if (empty($posts_ids)) {
+			throw new Exception(__('No entry selected'));
+		}
+		// Backward compatibility
+		foreach($posts_ids as $post_id)
+		{
+			# --BEHAVIOR-- adminBeforePostDelete
+			$core->callBehavior('adminBeforePostDelete',(integer) $post_id);
+		}
+		
+		# --BEHAVIOR-- adminBeforePostsDelete
+		$core->callBehavior('adminBeforePostsDelete',$posts_ids);
+		
+		$core->blog->delPosts($posts_ids);
+		
+		$ap->redirect(array('del',1),false);
+	}
+
+	public static function doChangePostCategory($core, dcPostsActionsPage $ap, $post) {
+		if (isset($post['new_cat_id'])) {
+			$posts_ids = $ap->getIDs();
+			if (empty($posts_ids)) {
+				throw new Exception(__('No entry selected'));
+			}
+			$new_cat_id = $post['new_cat_id'];
+			if (!empty($post['new_cat_title']) && $core->auth->check('categories', $core->blog->id))
+			{
+				$cur_cat = $core->con->openCursor($core->prefix.'category');
+				$cur_cat->cat_title = $post['new_cat_title'];
+				$cur_cat->cat_url = '';
+				
+				$parent_cat = !empty($post['new_cat_parent']) ? $post['new_cat_parent'] : '';
+				
+				# --BEHAVIOR-- adminBeforeCategoryCreate
+				$core->callBehavior('adminBeforeCategoryCreate', $cur_cat);
+				
+				$new_cat_id = $core->blog->addCategory($cur_cat, (integer) $parent_cat);
+				
+				# --BEHAVIOR-- adminAfterCategoryCreate
+				$core->callBehavior('adminAfterCategoryCreate', $cur_cat, $new_cat_id);
+			}
+			
+			$core->blog->updPostsCategory($posts_ids, $new_cat_id);
+			
+			$ap->redirect(array('upd'=>1),true);
+		} else {
+			$ap->beginPage(
+				dcPage::breadcrumb(
+					array(
+						html::escapeHTML($core->blog->name) => '',
+						__('Entries') => 'posts.php',
+						'<span class="page-title">'.__('Change category for entries').'</span>' => ''
+			)));
+			
+			# categories list
+			# Getting categories
+			$categories_combo = dcAdminCombos::getCategoriesCombo(
+				$core->blog->getCategories(array('post_type'=>'post'))
+			);			
+			echo
+			'<form action="posts.php" method="post">'.
+			$ap->getCheckboxes().
+			'<p><label for="new_cat_id" class="classic">'.__('Category:').'</label> '.
+			form::combo('new_cat_id',$categories_combo,'');
+			
+			if ($core->auth->check('categories', $core->blog->id)) {
+				echo 
+				'<div>'.
+				'<p id="new_cat">'.__('Create a new category for the post(s)').'</p>'.
+				'<p><label for="new_cat_title">'.__('Title:').'</label> '.
+				form::field('new_cat_title',30,255,'','').'</p>'.
+				'<p><label for="new_cat_parent">'.__('Parent:').'</label> '.
+				form::combo('new_cat_parent',$categories_combo,'','').
+				'</p>'.
+				'</div>';
+			}
+			
+			echo
+			$core->formNonce().
+			form::hidden(array('action'),'category').
+			'<input type="submit" value="'.__('Save').'" /></p>'.
+			'</form>';
+			$ap->endPage();
+
+		}
+	
+	}
+	public static function doChangePostAuthor($core, dcPostsActionsPage $ap, $post) {
+		if (isset($post['new_auth_id']) && $core->auth->check('admin',$core->blog->id)) {
+			$new_user_id = $post['new_auth_id'];
+			$posts_ids = $ap->getIDs();
+			if (empty($posts_ids)) {
+				throw new Exception(__('No entry selected'));
+			}
+			if ($core->getUser($new_user_id)->isEmpty()) {
+				throw new Exception(__('This user does not exist'));
+			}
+			
+			$cur = $core->con->openCursor($core->prefix.'post');
+			$cur->user_id = $new_user_id;
+			$cur->update('WHERE post_id '.$core->con->in($posts_ids));
+			
+			$ap->redirect(array('upd' => 1),true);
+		} else {
+			$usersList = '';
+			if ($core->auth->check('admin',$core->blog->id)) {
+				$params = array(
+					'limit' => 100,
+					'order' => 'nb_post DESC'
+					);
+				$rs = $core->getUsers($params);
+				while ($rs->fetch())
+				{
+					$usersList .= ($usersList != '' ? ',' : '').'"'.$rs->user_id.'"';
+				}
+			}
+			$ap->beginPage(
+				dcPage::breadcrumb(
+					array(
+						html::escapeHTML($core->blog->name) => '',
+						__('Entries') => 'posts.php',
+						'<span class="page-title">'.__('Change author for entries').'</span>' => '')),
+					dcPage::jsLoad('js/jquery/jquery.autocomplete.js').
+					'<script type="text/javascript">'."\n".
+					"//<![CDATA[\n".
+					'usersList = ['.$usersList.']'."\n".
+					"\n//]]>\n".
+					"</script>\n"
+			);
+
+			echo
+			'<form action="posts_actions.php" method="post">'.
+			$ap->getCheckboxes().
+			'<p><label for="new_auth_id" class="classic">'.__('New author (author ID):').'</label> '.
+			form::field('new_auth_id',20,255);
+			
+			echo
+				$core->formNonce().
+				form::hidden(array('action'),'author').
+				'<input type="submit" value="'.__('Save').'" /></p>'.
+				'</form>';
+			$ap->endPage();
+		}
+	}
+	public static function doChangePostLang($core, dcPostsActionsPage $ap, $post) {
+		$posts_ids = $ap->getIDs();
+		if (empty($posts_ids)) {
+			throw new Exception(__('No entry selected'));
+		}
+		if (isset($post['new_lang'])) {
+			$new_lang = $post['new_lang'];
+			$cur = $core->con->openCursor($core->prefix.'post');
+			$cur->post_lang = $new_lang;
+			$cur->update('WHERE post_id '.$core->con->in($posts_ids));
+			
+			$ap->redirect(array('upd' => 1),true);
+		} else {
+			$ap->beginPage(
+				dcPage::breadcrumb(
+					array(
+						html::escapeHTML($core->blog->name) => '',
+						__('Entries') => 'posts.php',
+						'<span class="page-title">'.__('Change language for entries').'</span>' => ''
+			)));
+			# lang list
+			# Languages combo
+			$rs = $core->blog->getLangs(array('order'=>'asc'));
+			$all_langs = l10n::getISOcodes(0,1);
+			$lang_combo = array('' => '', __('Most used') => array(), __('Available') => l10n::getISOcodes(1,1));
+			while ($rs->fetch()) {
+				if (isset($all_langs[$rs->post_lang])) {
+					$lang_combo[__('Most used')][$all_langs[$rs->post_lang]] = $rs->post_lang;
+					unset($lang_combo[__('Available')][$all_langs[$rs->post_lang]]);
+				} else {
+					$lang_combo[__('Most used')][$rs->post_lang] = $rs->post_lang;
+				}
+			}
+			unset($all_langs);
+			unset($rs);
+			
+			echo
+			'<form action="posts_actions.php" method="post">'.
+			$ap->getCheckboxes().
+			
+			'<p><label for="new_lang" class="classic">'.__('Entry lang:').'</label> '.
+			form::combo('new_lang',$lang_combo,'');
+			
+			echo
+				$core->formNonce().
+				form::hidden(array('action'),'lang').
+				'<input type="submit" value="'.__('Save').'" /></p>'.
+				'</form>';
+		}
+	}
+}
