Index: inc/admin/lib.dc.page.php
===================================================================
--- inc/admin/lib.dc.page.php	(revision 2202)
+++ inc/admin/lib.dc.page.php	(revision 2196)
@@ -108,7 +108,7 @@
 		$user_ui_hide_std_favicon = $core->auth->user_prefs->interface->hide_std_favicon;
 		if (!$user_ui_hide_std_favicon) {
-			echo 
-			'<link rel="icon" type="image/png" href="images/favicon96-login.png" />'.
-			'<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />';
+  			echo 
+  			'<link rel="icon" type="image/png" href="images/favicon96-login.png" />'.
+  			'<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />';
 		}
 		echo
@@ -176,12 +176,4 @@
 		}
 
-		// Display notices
-		echo self::notices();
-	}
-
-	public static function notices()
-	{
-		// return notices if any
-		$res = '';
 		if (isset($_SESSION['notifications'])) {
 			$types = array("success" => "success", "warning" => "warning-msg", "error" => "error");
@@ -190,5 +182,5 @@
 				if (isset($notifications[$type])) {
 					foreach ($notifications[$type] as $n) {
-						$res .= self::getNotification($n,$class);
+						echo self::getNotification($n,$class);
 					}
 				}
@@ -196,8 +188,7 @@
 			unset($_SESSION['notifications']);
 		}
-		return $res;
-	}
-
-	public static function addNotice($type,$message)
+	}
+
+	public static function AddNotice($type,$message)
 	{
 		$notification = isset($_SESSION['notifications']) ? $_SESSION['notifications'] : array();
@@ -272,5 +263,5 @@
 		'<div id="footer">'.
 		'<a href="http://dotclear.org/" title="'.$text.'"><img src="style/dc_logos/w-dotclear90.png" alt="'.$text.'" /></a></div>'."\n".
- 		'<!-- '."\n".
+		'<!-- '."\n".
 '                  .'."\n".
 '               ,;:\'`\'::'."\n".
@@ -310,8 +301,8 @@
 
 		self::jsLoadIE7().
-		'	<link rel="stylesheet" href="style/default.css" type="text/css" media="screen" />'."\n";
+		'  	<link rel="stylesheet" href="style/default.css" type="text/css" media="screen" />'."\n";
 		if (l10n::getTextDirection($GLOBALS['_lang']) == 'rtl') {
 			echo
-			'	<link rel="stylesheet" href="style/default-rtl.css" type="text/css" media="screen" />'."\n";
+			'  	<link rel="stylesheet" href="style/default-rtl.css" type="text/css" media="screen" />'."\n";
 		}
 
@@ -580,4 +571,9 @@
 		self::jsVar('dotclear.img_menu_on','images/menu_on.png').
 		self::jsVar('dotclear.img_menu_off','images/menu_off.png').
+		
+		self::jsVar('dotclear.img_plus_theme_src','images/plus-theme.png').
+		self::jsVar('dotclear.img_plus_theme_alt',__('uncover')).
+		self::jsVar('dotclear.img_minus_theme_src','images/minus-theme.png').
+		self::jsVar('dotclear.img_minus_theme_alt',__('hide')).
 
 		self::jsVar('dotclear.msg.help',
@@ -609,6 +605,4 @@
 		self::jsVar('dotclear.msg.confirm_delete_posts',
 			__("Are you sure you want to delete selected entries (%s)?")).
-		self::jsVar('dotclear.msg.confirm_delete_medias',
-			__("Are you sure you want to delete selected medias (%d)?")).
 		self::jsVar('dotclear.msg.confirm_delete_categories',
 			__("Are you sure you want to delete selected categories (%s)?")).
@@ -665,4 +659,17 @@
 		self::jsVar('dotclear.msg.load_enhanced_uploader',
 			__('Loading enhanced uploader, please wait.')).
+
+		self::jsVar('dotclear.msg.module_author',
+			__('Author:')).
+		self::jsVar('dotclear.msg.module_details',
+			__('Details')).
+		self::jsVar('dotclear.msg.module_support',
+			__('Support')).
+		self::jsVar('dotclear.msg.module_help',
+			__('Help:')).
+		self::jsVar('dotclear.msg.module_section',
+			__('Section:')).
+		self::jsVar('dotclear.msg.module_tags',
+			__('Tags:')).
 			"\n//]]>\n".
 		"</script>\n";
@@ -725,215 +732,215 @@
 	}
 
-	public static function jsModal()
-	{
-		return
-		'<link rel="stylesheet" type="text/css" href="style/modal/modal.css" />'."\n".
-		self::jsLoad('js/jquery/jquery.modal.js').
-		'<script type="text/javascript">'."\n".
-		"//<![CDATA[\n".
-		self::jsVar('$.modal.prototype.params.loader_img','style/modal/loader.gif').
-		self::jsVar('$.modal.prototype.params.close_img','style/modal/close.png').
-		"\n//]]>\n".
-		"</script>\n";
-	}
-
-	public static function jsColorPicker()
-	{
-		return
-		'<link rel="stylesheet" type="text/css" href="style/farbtastic/farbtastic.css" />'."\n".
-		self::jsLoad('js/jquery/jquery.farbtastic.js').
-		self::jsLoad('js/color-picker.js');
-	}
-
-	public static function jsDatePicker()
-	{
-		return
-		'<link rel="stylesheet" type="text/css" href="style/date-picker.css" />'."\n".
-		self::jsLoad('js/date-picker.js').
-		'<script type="text/javascript">'."\n".
-		"//<![CDATA[\n".
-
-		"datePicker.prototype.months[0] = '".html::escapeJS(__('January'))."'; ".
-		"datePicker.prototype.months[1] = '".html::escapeJS(__('February'))."'; ".
-		"datePicker.prototype.months[2] = '".html::escapeJS(__('March'))."'; ".
-		"datePicker.prototype.months[3] = '".html::escapeJS(__('April'))."'; ".
-		"datePicker.prototype.months[4] = '".html::escapeJS(__('May'))."'; ".
-		"datePicker.prototype.months[5] = '".html::escapeJS(__('June'))."'; ".
-		"datePicker.prototype.months[6] = '".html::escapeJS(__('July'))."'; ".
-		"datePicker.prototype.months[7] = '".html::escapeJS(__('August'))."'; ".
-		"datePicker.prototype.months[8] = '".html::escapeJS(__('September'))."'; ".
-		"datePicker.prototype.months[9] = '".html::escapeJS(__('October'))."'; ".
-		"datePicker.prototype.months[10] = '".html::escapeJS(__('November'))."'; ".
-		"datePicker.prototype.months[11] = '".html::escapeJS(__('December'))."'; ".
-
-		"datePicker.prototype.days[0] = '".html::escapeJS(__('Monday'))."'; ".
-		"datePicker.prototype.days[1] = '".html::escapeJS(__('Tuesday'))."'; ".
-		"datePicker.prototype.days[2] = '".html::escapeJS(__('Wednesday'))."'; ".
-		"datePicker.prototype.days[3] = '".html::escapeJS(__('Thursday'))."'; ".
-		"datePicker.prototype.days[4] = '".html::escapeJS(__('Friday'))."'; ".
-		"datePicker.prototype.days[5] = '".html::escapeJS(__('Saturday'))."'; ".
-		"datePicker.prototype.days[6] = '".html::escapeJS(__('Sunday'))."'; ".
-
-		"datePicker.prototype.img_src = 'images/date-picker.png'; ".
-
-		"datePicker.prototype.close_msg = '".html::escapeJS(__('close'))."'; ".
-		"datePicker.prototype.now_msg = '".html::escapeJS(__('now'))."'; ".
-
-		"\n//]]>\n".
-		"</script>\n";
-	}
-
-	public static function jsToolBar()
-	{
-		$res =
-		'<link rel="stylesheet" type="text/css" href="style/jsToolBar/jsToolBar.css" />'.
-		'<script type="text/javascript" src="js/jsToolBar/jsToolBar.js"></script>';
-
-		if (isset($GLOBALS['core']->auth) && $GLOBALS['core']->auth->getOption('enable_wysiwyg')) {
-			$res .= '<script type="text/javascript" src="js/jsToolBar/jsToolBar.wysiwyg.js"></script>';
-		}
-
-		$res .=
-		'<script type="text/javascript" src="js/jsToolBar/jsToolBar.dotclear.js"></script>'.
-		'<script type="text/javascript">'."\n".
-		"//<![CDATA[\n".
-		"jsToolBar.prototype.dialog_url = 'popup.php'; ".
-		"jsToolBar.prototype.iframe_css = '".
-		'body{'.
-		'font: 12px "DejaVu Sans","Lucida Grande","Lucida Sans Unicode",Arial,sans-serif;'.
-		'color : #000;'.
-		'background: #f9f9f9;'.
-		'margin: 0;'.
-		'padding : 2px;'.
-		'border: none;'.
-		(l10n::getTextDirection($GLOBALS['_lang']) == 'rtl' ? 'direction:rtl;' : '').
-		'}'.
-		'pre, code, kbd, samp {'.
-		'font-family:"Courier New",Courier,monospace;'.
-		'font-size : 1.1em;'.
-		'}'.
-		'code {'.
-		'color : #666;'.
-		'font-weight : bold;'.
-		'}'.
-		'body > p:first-child {'.
-		'margin-top: 0;'.
-		'}'.
-		"'; ".
-		"jsToolBar.prototype.base_url = '".html::escapeJS($GLOBALS['core']->blog->host)."'; ".
-		"jsToolBar.prototype.switcher_visual_title = '".html::escapeJS(__('visual'))."'; ".
-		"jsToolBar.prototype.switcher_source_title = '".html::escapeJS(__('source'))."'; ".
-		"jsToolBar.prototype.legend_msg = '".
-		html::escapeJS(__('You can use the following shortcuts to format your text.'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.none = '".html::escapeJS(__('-- none --'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.nonebis = '".html::escapeJS(__('-- block format --'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.p = '".html::escapeJS(__('Paragraph'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h1 = '".html::escapeJS(__('Level 1 header'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h2 = '".html::escapeJS(__('Level 2 header'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h3 = '".html::escapeJS(__('Level 3 header'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h4 = '".html::escapeJS(__('Level 4 header'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h5 = '".html::escapeJS(__('Level 5 header'))."'; ".
-		"jsToolBar.prototype.elements.blocks.options.h6 = '".html::escapeJS(__('Level 6 header'))."'; ".
-		"jsToolBar.prototype.elements.strong.title = '".html::escapeJS(__('Strong emphasis'))."'; ".
-		"jsToolBar.prototype.elements.em.title = '".html::escapeJS(__('Emphasis'))."'; ".
-		"jsToolBar.prototype.elements.ins.title = '".html::escapeJS(__('Inserted'))."'; ".
-		"jsToolBar.prototype.elements.del.title = '".html::escapeJS(__('Deleted'))."'; ".
-		"jsToolBar.prototype.elements.quote.title = '".html::escapeJS(__('Inline quote'))."'; ".
-		"jsToolBar.prototype.elements.code.title = '".html::escapeJS(__('Code'))."'; ".
-		"jsToolBar.prototype.elements.br.title = '".html::escapeJS(__('Line break'))."'; ".
-		"jsToolBar.prototype.elements.blockquote.title = '".html::escapeJS(__('Blockquote'))."'; ".
-		"jsToolBar.prototype.elements.pre.title = '".html::escapeJS(__('Preformated text'))."'; ".
-		"jsToolBar.prototype.elements.ul.title = '".html::escapeJS(__('Unordered list'))."'; ".
-		"jsToolBar.prototype.elements.ol.title = '".html::escapeJS(__('Ordered list'))."'; ".
-
-		"jsToolBar.prototype.elements.link.title = '".html::escapeJS(__('Link'))."'; ".
-		"jsToolBar.prototype.elements.link.href_prompt = '".html::escapeJS(__('URL?'))."'; ".
-		"jsToolBar.prototype.elements.link.hreflang_prompt = '".html::escapeJS(__('Language?'))."'; ".
-
-		"jsToolBar.prototype.elements.img.title = '".html::escapeJS(__('External image'))."'; ".
-		"jsToolBar.prototype.elements.img.src_prompt = '".html::escapeJS(__('URL?'))."'; ".
-
-		"jsToolBar.prototype.elements.img_select.title = '".html::escapeJS(__('Media chooser'))."'; ".
-		"jsToolBar.prototype.elements.post_link.title = '".html::escapeJS(__('Link to an entry'))."'; ";
-
-		if (!$GLOBALS['core']->auth->check('media,media_admin',$GLOBALS['core']->blog->id)) {
-			$res .= "jsToolBar.prototype.elements.img_select.disabled = true;\n";
-		}
-
-		$res .=
-		"\n//]]>\n".
-		"</script>\n";
-
-		return $res;
-	}
-
-	public static function jsUpload($params=array(),$base_url=null)
-	{
-		if (!$base_url) {
-			$base_url = path::clean(dirname(preg_replace('/(\?.*$)?/','',$_SERVER['REQUEST_URI']))).'/';
-		}
-
-		$params = array_merge($params,array(
-			'sess_id='.session_id(),
-			'sess_uid='.$_SESSION['sess_browser_uid'],
-			'xd_check='.$GLOBALS['core']->getNonce()
+public static function jsModal()
+{
+	return
+	'<link rel="stylesheet" type="text/css" href="style/modal/modal.css" />'."\n".
+	self::jsLoad('js/jquery/jquery.modal.js').
+	'<script type="text/javascript">'."\n".
+	"//<![CDATA[\n".
+	self::jsVar('$.modal.prototype.params.loader_img','style/modal/loader.gif').
+	self::jsVar('$.modal.prototype.params.close_img','style/modal/close.png').
+	"\n//]]>\n".
+	"</script>\n";
+}
+
+public static function jsColorPicker()
+{
+	return
+	'<link rel="stylesheet" type="text/css" href="style/farbtastic/farbtastic.css" />'."\n".
+	self::jsLoad('js/jquery/jquery.farbtastic.js').
+	self::jsLoad('js/color-picker.js');
+}
+
+public static function jsDatePicker()
+{
+	return
+	'<link rel="stylesheet" type="text/css" href="style/date-picker.css" />'."\n".
+	self::jsLoad('js/date-picker.js').
+	'<script type="text/javascript">'."\n".
+	"//<![CDATA[\n".
+
+	"datePicker.prototype.months[0] = '".html::escapeJS(__('January'))."'; ".
+	"datePicker.prototype.months[1] = '".html::escapeJS(__('February'))."'; ".
+	"datePicker.prototype.months[2] = '".html::escapeJS(__('March'))."'; ".
+	"datePicker.prototype.months[3] = '".html::escapeJS(__('April'))."'; ".
+	"datePicker.prototype.months[4] = '".html::escapeJS(__('May'))."'; ".
+	"datePicker.prototype.months[5] = '".html::escapeJS(__('June'))."'; ".
+	"datePicker.prototype.months[6] = '".html::escapeJS(__('July'))."'; ".
+	"datePicker.prototype.months[7] = '".html::escapeJS(__('August'))."'; ".
+	"datePicker.prototype.months[8] = '".html::escapeJS(__('September'))."'; ".
+	"datePicker.prototype.months[9] = '".html::escapeJS(__('October'))."'; ".
+	"datePicker.prototype.months[10] = '".html::escapeJS(__('November'))."'; ".
+	"datePicker.prototype.months[11] = '".html::escapeJS(__('December'))."'; ".
+
+	"datePicker.prototype.days[0] = '".html::escapeJS(__('Monday'))."'; ".
+	"datePicker.prototype.days[1] = '".html::escapeJS(__('Tuesday'))."'; ".
+	"datePicker.prototype.days[2] = '".html::escapeJS(__('Wednesday'))."'; ".
+	"datePicker.prototype.days[3] = '".html::escapeJS(__('Thursday'))."'; ".
+	"datePicker.prototype.days[4] = '".html::escapeJS(__('Friday'))."'; ".
+	"datePicker.prototype.days[5] = '".html::escapeJS(__('Saturday'))."'; ".
+	"datePicker.prototype.days[6] = '".html::escapeJS(__('Sunday'))."'; ".
+
+	"datePicker.prototype.img_src = 'images/date-picker.png'; ".
+
+	"datePicker.prototype.close_msg = '".html::escapeJS(__('close'))."'; ".
+	"datePicker.prototype.now_msg = '".html::escapeJS(__('now'))."'; ".
+
+	"\n//]]>\n".
+	"</script>\n";
+}
+
+public static function jsToolBar()
+{
+	$res =
+	'<link rel="stylesheet" type="text/css" href="style/jsToolBar/jsToolBar.css" />'.
+	'<script type="text/javascript" src="js/jsToolBar/jsToolBar.js"></script>';
+
+	if (isset($GLOBALS['core']->auth) && $GLOBALS['core']->auth->getOption('enable_wysiwyg')) {
+		$res .= '<script type="text/javascript" src="js/jsToolBar/jsToolBar.wysiwyg.js"></script>';
+	}
+
+	$res .=
+	'<script type="text/javascript" src="js/jsToolBar/jsToolBar.dotclear.js"></script>'.
+	'<script type="text/javascript">'."\n".
+	"//<![CDATA[\n".
+	"jsToolBar.prototype.dialog_url = 'popup.php'; ".
+	"jsToolBar.prototype.iframe_css = '".
+	'body{'.
+	'font: 12px "DejaVu Sans","Lucida Grande","Lucida Sans Unicode",Arial,sans-serif;'.
+	'color : #000;'.
+	'background: #f9f9f9;'.
+	'margin: 0;'.
+	'padding : 2px;'.
+	'border: none;'.
+	(l10n::getTextDirection($GLOBALS['_lang']) == 'rtl' ? 'direction:rtl;' : '').
+	'}'.
+	'pre, code, kbd, samp {'.
+	'font-family:"Courier New",Courier,monospace;'.
+	'font-size : 1.1em;'.
+	'}'.
+	'code {'.
+	'color : #666;'.
+	'font-weight : bold;'.
+	'}'.
+	'body > p:first-child {'.
+	'margin-top: 0;'.
+	'}'.
+	"'; ".
+	"jsToolBar.prototype.base_url = '".html::escapeJS($GLOBALS['core']->blog->host)."'; ".
+	"jsToolBar.prototype.switcher_visual_title = '".html::escapeJS(__('visual'))."'; ".
+	"jsToolBar.prototype.switcher_source_title = '".html::escapeJS(__('source'))."'; ".
+	"jsToolBar.prototype.legend_msg = '".
+	html::escapeJS(__('You can use the following shortcuts to format your text.'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.none = '".html::escapeJS(__('-- none --'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.nonebis = '".html::escapeJS(__('-- block format --'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.p = '".html::escapeJS(__('Paragraph'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h1 = '".html::escapeJS(__('Level 1 header'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h2 = '".html::escapeJS(__('Level 2 header'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h3 = '".html::escapeJS(__('Level 3 header'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h4 = '".html::escapeJS(__('Level 4 header'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h5 = '".html::escapeJS(__('Level 5 header'))."'; ".
+	"jsToolBar.prototype.elements.blocks.options.h6 = '".html::escapeJS(__('Level 6 header'))."'; ".
+	"jsToolBar.prototype.elements.strong.title = '".html::escapeJS(__('Strong emphasis'))."'; ".
+	"jsToolBar.prototype.elements.em.title = '".html::escapeJS(__('Emphasis'))."'; ".
+	"jsToolBar.prototype.elements.ins.title = '".html::escapeJS(__('Inserted'))."'; ".
+	"jsToolBar.prototype.elements.del.title = '".html::escapeJS(__('Deleted'))."'; ".
+	"jsToolBar.prototype.elements.quote.title = '".html::escapeJS(__('Inline quote'))."'; ".
+	"jsToolBar.prototype.elements.code.title = '".html::escapeJS(__('Code'))."'; ".
+	"jsToolBar.prototype.elements.br.title = '".html::escapeJS(__('Line break'))."'; ".
+	"jsToolBar.prototype.elements.blockquote.title = '".html::escapeJS(__('Blockquote'))."'; ".
+	"jsToolBar.prototype.elements.pre.title = '".html::escapeJS(__('Preformated text'))."'; ".
+	"jsToolBar.prototype.elements.ul.title = '".html::escapeJS(__('Unordered list'))."'; ".
+	"jsToolBar.prototype.elements.ol.title = '".html::escapeJS(__('Ordered list'))."'; ".
+
+	"jsToolBar.prototype.elements.link.title = '".html::escapeJS(__('Link'))."'; ".
+	"jsToolBar.prototype.elements.link.href_prompt = '".html::escapeJS(__('URL?'))."'; ".
+	"jsToolBar.prototype.elements.link.hreflang_prompt = '".html::escapeJS(__('Language?'))."'; ".
+
+	"jsToolBar.prototype.elements.img.title = '".html::escapeJS(__('External image'))."'; ".
+	"jsToolBar.prototype.elements.img.src_prompt = '".html::escapeJS(__('URL?'))."'; ".
+
+	"jsToolBar.prototype.elements.img_select.title = '".html::escapeJS(__('Media chooser'))."'; ".
+	"jsToolBar.prototype.elements.post_link.title = '".html::escapeJS(__('Link to an entry'))."'; ";
+
+	if (!$GLOBALS['core']->auth->check('media,media_admin',$GLOBALS['core']->blog->id)) {
+		$res .= "jsToolBar.prototype.elements.img_select.disabled = true;\n";
+	}
+
+	$res .=
+	"\n//]]>\n".
+	"</script>\n";
+
+	return $res;
+}
+
+public static function jsUpload($params=array(),$base_url=null)
+{
+	if (!$base_url) {
+		$base_url = path::clean(dirname(preg_replace('/(\?.*$)?/','',$_SERVER['REQUEST_URI']))).'/';
+	}
+
+	$params = array_merge($params,array(
+		'sess_id='.session_id(),
+		'sess_uid='.$_SESSION['sess_browser_uid'],
+		'xd_check='.$GLOBALS['core']->getNonce()
 		));
 
-		return
-		'<script type="text/javascript">'."\n".
-		"//<![CDATA[\n".
-		"dotclear.jsUpload = {};\n".
-		"dotclear.jsUpload.msg = {};\n".
-		self::jsVar('dotclear.msg.enhanced_uploader_activate',__('Temporarily activate enhanced uploader')).
-		self::jsVar('dotclear.msg.enhanced_uploader_disable',__('Temporarily disable enhanced uploader')).
-		self::jsVar('dotclear.jsUpload.msg.limit_exceeded',__('Limit exceeded.')).
-		self::jsVar('dotclear.jsUpload.msg.size_limit_exceeded',__('File size exceeds allowed limit.')).
-		self::jsVar('dotclear.jsUpload.msg.canceled',__('Canceled.')).
-		self::jsVar('dotclear.jsUpload.msg.http_error',__('HTTP Error:')).
-		self::jsVar('dotclear.jsUpload.msg.error',__('Error:')).
-		self::jsVar('dotclear.jsUpload.msg.choose_file',__('Choose file')).
-		self::jsVar('dotclear.jsUpload.msg.choose_files',__('Choose files')).
-		self::jsVar('dotclear.jsUpload.msg.cancel',__('Cancel')).
-		self::jsVar('dotclear.jsUpload.msg.clean',__('Clean')).
-		self::jsVar('dotclear.jsUpload.msg.upload',__('Upload')).
-		self::jsVar('dotclear.jsUpload.msg.send',__('Send')).
-		self::jsVar('dotclear.jsUpload.msg.file_successfully_uploaded',__('File successfully uploaded.')).
-		self::jsVar('dotclear.jsUpload.msg.no_file_in_queue',__('No file in queue.')).
-		self::jsVar('dotclear.jsUpload.msg.file_in_queue',__('1 file in queue.')).
-		self::jsVar('dotclear.jsUpload.msg.files_in_queue',__('%d files in queue.')).
-		self::jsVar('dotclear.jsUpload.msg.queue_error',__('Queue error:')).
-		self::jsVar('dotclear.jsUpload.base_url',$base_url).
-		"\n//]]>\n".
-		"</script>\n".
-
-		self::jsLoad('js/jsUpload/vendor/jquery.ui.widget.js').
-		self::jsLoad('js/jsUpload/tmpl.js').
-		self::jsLoad('js/jsUpload/template-upload.js').
-		self::jsLoad('js/jsUpload/template-download.js').
-		self::jsLoad('js/jsUpload/load-image.js').
-		self::jsLoad('js/jsUpload/jquery.iframe-transport.js').
-		self::jsLoad('js/jsUpload/jquery.fileupload.js').
-		self::jsLoad('js/jsUpload/jquery.fileupload-process.js').
-		self::jsLoad('js/jsUpload/jquery.fileupload-resize.js').
-		self::jsLoad('js/jsUpload/jquery.fileupload-ui.js');
-	}
-
-	public static function jsToolMan()
-	{
-		return
-		'<script type="text/javascript" src="js/tool-man/core.js"></script>'.
-		'<script type="text/javascript" src="js/tool-man/events.js"></script>'.
-		'<script type="text/javascript" src="js/tool-man/css.js"></script>'.
-		'<script type="text/javascript" src="js/tool-man/coordinates.js"></script>'.
-		'<script type="text/javascript" src="js/tool-man/drag.js"></script>'.
-		'<script type="text/javascript" src="js/tool-man/dragsort.js"></script>'.
-		'<script type="text/javascript" src="js/dragsort-tablerows.js"></script>';
-	}
-
-	public static function jsMetaEditor()
-	{
-		return
-		'<script type="text/javascript" src="js/meta-editor.js"></script>';
-	}
+	return
+	'<script type="text/javascript">'."\n".
+	"//<![CDATA[\n".
+	"dotclear.jsUpload = {};\n".
+	"dotclear.jsUpload.msg = {};\n".
+	self::jsVar('dotclear.msg.enhanced_uploader_activate',__('Temporarily activate enhanced uploader')).
+	self::jsVar('dotclear.msg.enhanced_uploader_disable',__('Temporarily disable enhanced uploader')).
+	self::jsVar('dotclear.jsUpload.msg.limit_exceeded',__('Limit exceeded.')).
+	self::jsVar('dotclear.jsUpload.msg.size_limit_exceeded',__('File size exceeds allowed limit.')).
+	self::jsVar('dotclear.jsUpload.msg.canceled',__('Canceled.')).
+	self::jsVar('dotclear.jsUpload.msg.http_error',__('HTTP Error:')).
+	self::jsVar('dotclear.jsUpload.msg.error',__('Error:')).
+	self::jsVar('dotclear.jsUpload.msg.choose_file',__('Choose file')).
+	self::jsVar('dotclear.jsUpload.msg.choose_files',__('Choose files')).
+	self::jsVar('dotclear.jsUpload.msg.cancel',__('Cancel')).
+	self::jsVar('dotclear.jsUpload.msg.clean',__('Clean')).
+	self::jsVar('dotclear.jsUpload.msg.upload',__('Upload')).
+	self::jsVar('dotclear.jsUpload.msg.send',__('Send')).
+	self::jsVar('dotclear.jsUpload.msg.file_successfully_uploaded',__('File successfully uploaded.')).
+	self::jsVar('dotclear.jsUpload.msg.no_file_in_queue',__('No file in queue.')).
+	self::jsVar('dotclear.jsUpload.msg.file_in_queue',__('1 file in queue.')).
+	self::jsVar('dotclear.jsUpload.msg.files_in_queue',__('%d files in queue.')).
+	self::jsVar('dotclear.jsUpload.msg.queue_error',__('Queue error:')).
+	self::jsVar('dotclear.jsUpload.base_url',$base_url).
+	"\n//]]>\n".
+	"</script>\n".
+
+	self::jsLoad('js/jsUpload/vendor/jquery.ui.widget.js').
+	self::jsLoad('js/jsUpload/tmpl.js').
+	self::jsLoad('js/jsUpload/template-upload.js').
+	self::jsLoad('js/jsUpload/template-download.js').
+	self::jsLoad('js/jsUpload/load-image.js').
+	self::jsLoad('js/jsUpload/jquery.iframe-transport.js').
+	self::jsLoad('js/jsUpload/jquery.fileupload.js').
+	self::jsLoad('js/jsUpload/jquery.fileupload-process.js').
+	self::jsLoad('js/jsUpload/jquery.fileupload-resize.js').
+	self::jsLoad('js/jsUpload/jquery.fileupload-ui.js');
+}
+
+public static function jsToolMan()
+{
+	return
+	'<script type="text/javascript" src="js/tool-man/core.js"></script>'.
+	'<script type="text/javascript" src="js/tool-man/events.js"></script>'.
+	'<script type="text/javascript" src="js/tool-man/css.js"></script>'.
+	'<script type="text/javascript" src="js/tool-man/coordinates.js"></script>'.
+	'<script type="text/javascript" src="js/tool-man/drag.js"></script>'.
+	'<script type="text/javascript" src="js/tool-man/dragsort.js"></script>'.
+	'<script type="text/javascript" src="js/dragsort-tablerows.js"></script>';
+}
+
+public static function jsMetaEditor()
+{
+	return
+	'<script type="text/javascript" src="js/meta-editor.js"></script>';
+}
 }
 ?>
Index: inc/admin/lib.moduleslist.php
===================================================================
--- inc/admin/lib.moduleslist.php	(revision 2195)
+++ inc/admin/lib.moduleslist.php	(revision 2195)
@@ -0,0 +1,1021 @@
+<?php
+
+class adminModulesList
+{
+	public $core;
+	public $modules;
+
+	public static  $allow_multi_install;
+	public static $distributed_modules = array();
+
+	protected $list_id = 'unknow';
+
+	protected $config_module = '';
+	protected $config_file = '';
+	protected $config_content = '';
+
+	protected $path = false;
+	protected $path_writable = false;
+	protected $path_pattern = false;
+
+	protected $page_url = 'plugins.php';
+	protected $page_qs = '?';
+	protected $page_tab = '';
+
+	public static $nav_indexes = 'abcdefghijklmnopqrstuvwxyz0123456789';
+	protected $nav_list = array();
+	protected $nav_special = 'other';
+
+	protected $sort_field = 'sname';
+	protected $sort_asc = true;
+
+	public function __construct($core, $root, $allow_multi_install=false)
+	{
+		$this->core = $core;
+		self::$allow_multi_install = (boolean) $allow_multi_install;
+		$this->setPathInfo($root);
+		$this->setNavSpecial(__('other'));
+
+		$this->init();
+	}
+
+	protected function init()
+	{
+		return null;
+	}
+
+	public function newList($list_id)
+	{
+		$this->modules = array();
+		$this->page_tab = '';
+		$this->list_id = $list_id;
+
+		return $this;
+	}
+
+	protected function setPathInfo($root)
+	{
+		$paths = explode(PATH_SEPARATOR, $root);
+		$path = array_pop($paths);
+		unset($paths);
+
+		$this->path = $path;
+		if (is_dir($path) && is_writeable($path)) {
+			$this->path_writable = true;
+			$this->path_pattern = preg_quote($path,'!');
+		}
+
+		return $this;
+	}
+
+	public function getPath()
+	{
+		return $this->path;
+	}
+
+	public function isPathWritable()
+	{
+		return $this->path_writable;
+	}
+
+	public function isPathDeletable($root)
+	{
+		return $this->path_writable 
+			&& preg_match('!^'.$this->path_pattern.'!', $root) 
+			&& $this->core->auth->isSuperAdmin();
+	}
+
+	public function setPageURL($url)
+	{
+		$this->page_qs = strpos('?', $url) ? '&' : '?';
+		$this->page_url = $url;
+
+		return $this;
+	}
+
+	public function getPageURL($queries='', $with_tab=true)
+	{
+		return $this->page_url.
+			(!empty($queries) ? $this->page_qs : '').
+			(is_array($queries) ? http_build_query($queries) : $queries).
+			($with_tab && !empty($this->page_tab) ? '#'.$this->page_tab : '');
+	}
+
+	public function setPageTab($tab)
+	{
+		$this->page_tab = $tab;
+
+		return $this;
+	}
+
+	public function getPageTab()
+	{
+		return $this->page_tab;
+	}
+
+	public function getSearchQuery()
+	{
+		$query = !empty($_REQUEST['m_search']) ? trim($_REQUEST['m_search']) : null;
+		return strlen($query) > 1 ? $query : null;
+	}
+
+	public function displaySearchForm()
+	{
+		$query = $this->getSearchQuery();
+
+		if (empty($this->modules) && $query === null) {
+			return $this;
+		}
+
+		echo 
+		'<form action="'.$this->getPageURL().'" method="get" class="fieldset">'.
+		'<p><label for="m_search" class="classic">'.__('Search in repository:').'&nbsp;</label><br />'.
+		form::field(array('m_search','m_search'), 30, 255, html::escapeHTML($query)).
+		'<input type="submit" value="'.__('Search').'" /> ';
+		if ($query) { echo ' <a href="'.$this->getPageURL().'" class="button">'.__('Reset search').'</a>'; }
+		echo '</p>'.
+		'</form>';
+
+		if ($query) {
+			echo 
+			'<p class="message">'.sprintf(
+				__('Found %d result for search "%s":', 'Found %d results for search "%s":', count($this->modules)), 
+				count($this->modules), html::escapeHTML($query)
+				).
+			'</p>';
+		}
+		return $this;
+	}
+
+	public function setNavSpecial($str)
+	{
+		$this->nav_special = (string) $str;
+		$this->nav_list = array_merge(str_split(self::$nav_indexes), array($this->nav_special));
+
+		return $this;
+	}
+
+	public function getNavQuery()
+	{
+		return isset($_REQUEST['m_nav']) && in_array($_REQUEST['m_nav'], $this->nav_list) ? $_REQUEST['m_nav'] : $this->nav_list[0];
+	}
+
+	public function displayNavMenu()
+	{
+		if (empty($this->modules) || $this->getSearchQuery() !== null) {
+			return $this;
+		}
+
+		# Fetch modules required field
+		$indexes = array();
+		foreach ($this->modules as $id => $module) {
+			if (!isset($module[$this->sort_field])) {
+				continue;
+			}
+			$char = substr($module[$this->sort_field], 0, 1);
+			if (!in_array($char, $this->nav_list)) {
+				$char = $this->nav_special;
+			}
+			if (!isset($indexes[$char])) {
+				$indexes[$char] = 0;
+			}
+			$indexes[$char]++;
+		}
+
+		$buttons = array();
+		foreach($this->nav_list as $char) {
+			# Selected letter
+			if ($this->getNavQuery() == $char) {
+				$buttons[] = '<li class="active" title="'.__('current selection').'"><strong> '.$char.' </strong></li>';
+			}
+			# Letter having modules
+			elseif (!empty($indexes[$char])) {
+				$title = sprintf(__('%d module', '%d modules', $indexes[$char]), $indexes[$char]);
+				$buttons[] = '<li class="btn" title="'.$title.'"><a href="'.$this->getPageURL('m_nav='.$char).'" title="'.$title.'"> '.$char.' </a></li>';
+			}
+			# Letter without modules
+			else {
+				$buttons[] = '<li class="btn no-link" title="'.__('no module').'"> '.$char.' </li>';
+			}
+		}
+		# Parse navigation menu
+		echo '<div class="pager">'.__('Browse index:').' <ul>'.implode('',$buttons).'</ul></div>';
+
+		return $this;
+	}
+
+	public function setSortField($field, $asc=true)
+	{
+		$this->sort_field = $field;
+		$this->sort_asc = (boolean) $asc;
+
+		return $this;
+	}
+
+	public function getSortQuery()
+	{
+		return !empty($_REQUEST['m_sort']) ? $_REQUEST['m_sort'] : $this->sort_field;
+	}
+
+	public function displaySortForm()
+	{
+		//not yet implemented
+	}
+
+	public function displayMessage($action)
+	{
+		switch($action) {
+			case 'activate': 
+				$str = __('Module successfully activated.'); break;
+			case 'deactivate': 
+				$str = __('Module successfully deactivated.'); break;
+			case 'delete': 
+				$str = __('Module successfully deleted.'); break;
+			case 'install': 
+				$str = __('Module successfully installed.'); break;
+			case 'update': 
+				$str = __('Module successfully updated.'); break;
+			default:
+				$str = ''; break;
+		}
+		if (!empty($str)) {
+			dcPage::success($str);
+		}
+	}
+
+	public function setModules($modules)
+	{
+		$this->modules = array();
+		if (!empty($modules) && is_array($modules)) {
+			foreach($modules as $id => $module) {
+				$this->modules[$id] = self::parseModuleInfo($id, $module);
+			}
+		}
+		return $this;
+	}
+
+	public function getModules()
+	{
+		return $this->modules;
+	}
+
+	public static function parseModuleInfo($id, $module)
+	{
+		$label = empty($module['label']) ? $id : $module['label'];
+		$name = __(empty($module['name']) ? $label : $module['name']);
+		
+		return array_merge(
+			# Default values
+			array(
+				'desc' 				=> '',
+				'author' 			=> '',
+				'version' 			=> 0,
+				'current_version' 	=> 0,
+				'root' 				=> '',
+				'root_writable' 	=> false,
+				'permissions' 		=> null,
+				'parent' 			=> null,
+				'priority' 			=> 1000,
+				'standalone_config' => false,
+				'support' 			=> '',
+				'section' 			=> '',
+				'tags' 				=> '',
+				'details' 			=> '',
+				'sshot' 			=> ''
+			),
+			# Module's values
+			$module,
+			# Clean up values
+			array(
+				'id' 				=> $id,
+				'sid' 				=> self::sanitizeString($id),
+				'label' 			=> $label,
+				'name' 				=> $name,
+				'sname' 			=> self::sanitizeString($name)
+			)
+		);
+	}
+
+	public static function setDistributedModules($modules)
+	{
+		self::$distributed_modules = $modules;
+	}
+
+	public static function isDistributedModule($module)
+	{
+		$distributed_modules = self::$distributed_modules;
+
+		return is_array($distributed_modules) && in_array($module, $distributed_modules);
+	}
+
+	public static function sortModules($modules, $field, $asc=true)
+	{
+		$sorter = array();
+		foreach($modules as $id => $module) {
+			$sorter[$id] = isset($module[$field]) ? $module[$field] : $field;
+		}
+		array_multisort($sorter, $asc ? SORT_ASC : SORT_DESC, $modules);
+
+		return $modules;
+	}
+
+	public function displayModulesList($cols=array('name', 'config', 'version', 'desc'), $actions=array(), $nav_limit=false)
+	{
+		echo 
+		'<div class="table-outer">'.
+		'<table id="'.html::escapeHTML($this->list_id).'" class="modules'.(in_array('expander', $cols) ? ' expandable' : '').'">'.
+		'<caption class="hidden">'.html::escapeHTML(__('Modules list')).'</caption><tr>';
+
+		if (in_array('name', $cols)) {
+			echo 
+			'<th class="first nowrap"'.(in_array('icon', $cols) ? ' colspan="2"' : '').'>'.__('Name').'</th>';
+		}
+
+		if (in_array('version', $cols)) {
+			echo 
+			'<th class="nowrap count" scope="col">'.__('Version').'</th>';
+		}
+
+		if (in_array('current_version', $cols)) {
+			echo 
+			'<th class="nowrap count" scope="col">'.__('Current version').'</th>';
+		}
+
+		if (in_array('desc', $cols)) {
+			echo 
+			'<th class="nowrap" scope="col">'.__('Details').'</th>';
+		}
+
+		if (in_array('distrib', $cols)) {
+			echo '<th'.(in_array('desc', $cols) ? '' : ' class="maximal"').'></th>';
+		}
+
+		if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+			echo 
+			'<th class="minimal nowrap">'.__('Action').'</th>';
+		}
+
+		echo 
+		'</tr>';
+
+		$sort_field = $this->getSortQuery();
+
+		# Sort modules by id
+		$modules = $this->getSearchQuery() === null ?
+			self::sortModules($this->modules, $sort_field, $this->sort_asc) :
+			$this->modules;
+
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearchQuery() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getNavQuery() != $char) {
+					continue;
+				}
+			}
+
+			echo 
+			'<tr class="line" id="'.html::escapeHTML($this->list_id).'_m_'.html::escapeHTML($id).'">';
+
+			if (in_array('icon', $cols)) {
+				echo 
+				'<td class="module-icon nowrap">'.sprintf(
+					'<img alt="%1$s" title="%1$s" src="%2$s" />', 
+					html::escapeHTML($id), file_exists($module['root'].'/icon.png') ? 'index.php?pf='.$id.'/icon.png' : 'images/module.png'
+				).'</td>';
+			}
+
+			# Link to config file
+			$config = in_array('config', $cols) && !empty($module['root']) && file_exists(path::real($module['root'].'/_config.php'));
+
+			echo 
+			'<td class="module-name nowrap" scope="row">'.($config ? 
+				'<a href="'.$this->getPageURL('module='.$id.'&conf=1').'" title"'.sprintf(__('Configure module "%s"'), html::escapeHTML($module['name'])).'">'.html::escapeHTML($module['name']).'</a>' : 
+				html::escapeHTML($module['name'])
+			).'</td>';
+
+			if (in_array('version', $cols)) {
+				echo 
+				'<td class="module-version nowrap count">'.html::escapeHTML($module['version']).'</td>';
+			}
+
+			if (in_array('current_version', $cols)) {
+				echo 
+				'<td class="module-current-version nowrap count">'.html::escapeHTML($module['current_version']).'</td>';
+			}
+
+			if (in_array('desc', $cols)) {
+				echo 
+				'<td class="module-desc maximal">'.html::escapeHTML($module['desc']).'</td>';
+			}
+
+			if (in_array('distrib', $cols)) {
+				echo 
+				'<td class="module-distrib">'.(self::isDistributedModule($id) ? 
+					'<img src="images/dotclear_pw.png" alt="'.
+					__('Module from official distribution').'" title="'.
+					__('module from official distribution').'" />' 
+				: '').'</td>';
+			}
+
+			if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+				echo 
+				'<td class="module-actions nowrap">';
+
+				$this->displayLineActions($id, $module, $actions);
+
+				echo
+				'</td>';
+			}
+
+			echo 
+			'</tr>';
+
+			$count++;
+		}
+		echo 
+		'</table></div>';
+
+		if(!$count && $this->getSearchQuery() === null) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+	}
+
+	protected function displayLineActions($id, $module, $actions, $echo=true)
+	{
+		$submits = array();
+
+		# Update (from repository)
+		if (in_array('update', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="update" value="'.__('Update').'" />';
+		}
+
+		# Install (form repository)
+		if (in_array('install', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="install" value="'.__('Install').'" />';
+		}
+		
+		$tmp = $this->displayOtherLineAction($id, $module, $actions);
+		if (!empty($tmp) && is_array($tmp)) {
+			$submits = array_merge($submits, $tmp);
+		}
+
+		# Activate
+		if (in_array('deactivate', $actions) && $module['root_writable']) {
+			$submits[] = '<input type="submit" name="deactivate" value="'.__('Deactivate').'" class="reset" />';
+		}
+
+		# Deactivate
+		if (in_array('activate', $actions) && $module['root_writable']) {
+			$submits[] = '<input type="submit" name="activate" value="'.__('Activate').'" />';
+		}
+
+		# Delete
+		if (in_array('delete', $actions) && $this->isPathDeletable($module['root'])) {
+			$submits[] = '<input type="submit" class="delete" name="delete" value="'.__('Delete').'" />';
+		}
+
+		# Parse form
+		if (!empty($submits)) {
+			$res = 
+			'<form action="'.$this->getPageURL().'" method="post">'.
+			'<div>'.
+			$this->core->formNonce().
+			form::hidden(array('module'), html::escapeHTML($id)).
+			form::hidden(array('tab'), $this->page_tab).
+			implode(' ', $submits).
+			'</div>'.
+			'</form>';
+
+			if (!$echo) {
+				return $res;
+			}
+			echo $res;
+		}
+	}
+
+	protected function displayOtherLineAction($id, $module, $actions)
+	{
+		return null;
+	}
+
+	public function executeAction($prefix, dcModules $modules, dcRepository $repository)
+	{
+		if (!$this->core->auth->isSuperAdmin() || !$this->isPathWritable()) {
+			return null;
+		}
+
+		# List actions
+		if (!empty($_POST['module'])) {
+
+			$id = $_POST['module'];
+
+			if (!empty($_POST['activate'])) {
+
+				$enabled = $modules->getDisabledModules();
+				if (!isset($enabled[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				# --BEHAVIOR-- moduleBeforeActivate
+				$this->core->callBehavior($prefix.'BeforeActivate', $id);
+
+				$modules->activateModule($id);
+
+				# --BEHAVIOR-- moduleAfterActivate
+				$this->core->callBehavior($prefix.'AfterActivate', $id);
+
+				http::redirect($this->getPageURL('msg=activate'));
+			}
+
+			elseif (!empty($_POST['deactivate'])) {
+
+				if (!$modules->moduleExists($id)) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $modules->getModules($id);
+				$module['id'] = $id;
+
+				if (!$module['root_writable']) {
+					throw new Exception(__('You don\'t have permissions to deactivate this module.'));
+				}
+
+				# --BEHAVIOR-- moduleBeforeDeactivate
+				$this->core->callBehavior($prefix.'BeforeDeactivate', $module);
+
+				$modules->deactivateModule($id);
+
+				# --BEHAVIOR-- moduleAfterDeactivate
+				$this->core->callBehavior($prefix.'AfterDeactivate', $module);
+
+				http::redirect($this->getPageURL('msg=deactivate'));
+			}
+
+			elseif (!empty($_POST['delete'])) {
+
+				$disabled = $modules->getDisabledModules();
+				if (!isset($disabled[$id])) {
+
+					if (!$modules->moduleExists($id)) {
+						throw new Exception(__('No such module.'));
+					}
+
+					$module = $modules->getModules($id);
+					$module['id'] = $id;
+
+					if (!$this->isPathDeletable($module['root'])) {
+						throw new Exception(__("You don't have permissions to delete this module."));
+					}
+
+					# --BEHAVIOR-- moduleBeforeDelete
+					$this->core->callBehavior($prefix.'BeforeDelete', $module);
+
+					$modules->deleteModule($id);
+
+					# --BEHAVIOR-- moduleAfterDelete
+					$this->core->callBehavior($prefix.'AfterDelete', $module);
+				}
+				else {
+					$modules->deleteModule($id, true);
+				}
+
+				http::redirect($this->getPageURL('msg=delete'));
+			}
+
+			elseif (!empty($_POST['update'])) {
+
+				$updated = $repository->get();
+				if (!isset($updated[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				if (!$modules->moduleExists($id)) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $updated[$id];
+				$module['id'] = $id;
+			
+				if (!self::$allow_multi_install) {
+					$dest = $module['root'].'/../'.basename($module['file']);
+				}
+				else {
+					$dest = $this->getPath().'/'.basename($module['file']);
+					if ($module['root'] != $dest) {
+						@file_put_contents($module['root'].'/_disabled', '');
+					}
+				}
+
+				# --BEHAVIOR-- moduleBeforeUpdate
+				$this->core->callBehavior($prefix.'BeforeUpdate', $module);
+
+				$repository->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterUpdate
+				$this->core->callBehavior($prefix.'AfterUpdate', $module);
+
+				http::redirect($this->getPageURL('msg=upadte'));
+			}
+
+			elseif (!empty($_POST['install'])) {
+
+				$updated = $repository->get();
+				if (!isset($updated[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $updated[$id];
+				$module['id'] = $id;
+
+				$dest = $this->getPath().'/'.basename($module['file']);
+
+				# --BEHAVIOR-- moduleBeforeAdd
+				$this->core->callBehavior($prefix.'BeforeAdd', $module);
+
+				$ret_code = $repository->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterAdd
+				$this->core->callBehavior($prefix.'AfterAdd', $module);
+
+				http::redirect($this->getPageURL('msg='.($ret_code == 2 ? 'update' : 'install')));
+			}
+		}
+		# Manual actions
+		elseif (!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file']) 
+			|| !empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url']))
+		{
+			if (empty($_POST['your_pwd']) || !$this->core->auth->checkPassword(crypt::hmac(DC_MASTER_KEY, $_POST['your_pwd']))) {
+				throw new Exception(__('Password verification failed'));
+			}
+
+			if (!empty($_POST['upload_pkg'])) {
+				files::uploadStatus($_FILES['pkg_file']);
+				
+				$dest = $this->getPath().'/'.$_FILES['pkg_file']['name'];
+				if (!move_uploaded_file($_FILES['pkg_file']['tmp_name'], $dest)) {
+					throw new Exception(__('Unable to move uploaded file.'));
+				}
+			}
+			else {
+				$url = urldecode($_POST['pkg_url']);
+				$dest = $this->getPath().'/'.basename($url);
+				$repository->download($url, $dest);
+			}
+
+			# --BEHAVIOR-- moduleBeforeAdd
+			$this->core->callBehavior($prefix.'BeforeAdd', null);
+
+			$ret_code = $repository->install($dest);
+
+			# --BEHAVIOR-- moduleAfterAdd
+			$this->core->callBehavior($prefix.'AfterAdd', null);
+
+			http::redirect($this->getPageURL('msg='.($ret_code == 2 ? 'update' : 'install')).'#'.$prefix);
+		}
+
+		return $this->executeOtherAction($prefix, $modules, $repository);
+	}
+
+	/**
+	 * 
+	 * Way for child class to execute their own actions 
+	 * whitout rewriting all standard actions.
+	 */
+	protected function executeOtherAction($prefix, dcModules $modules, dcRepository $repository)
+	{
+		return null;
+	}
+
+	public function displayManualForm()
+	{
+		if (!$this->core->auth->isSuperAdmin() || !$this->isPathWritable()) {
+			return null;
+		}
+
+		# 'Upload module' form
+		echo
+		'<form method="post" action="'.$this->getPageURL().'" id="uploadpkg" enctype="multipart/form-data" class="fieldset">'.
+		'<h4>'.__('Upload a zip file').'</h4>'.
+		'<p class="field"><label for="pkg_file" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Zip file path:').'</label> '.
+		'<input type="file" name="pkg_file" id="pkg_file" /></p>'.
+		'<p class="field"><label for="your_pwd1" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Your password:').'</label> '.
+		form::password(array('your_pwd','your_pwd1'),20,255).'</p>'.
+		'<p><input type="submit" name="upload_pkg" value="'.__('Upload').'" />'.
+		form::hidden(array('tab'), $this->getPageTab()).
+		$this->core->formNonce().'</p>'.
+		'</form>';
+		
+		# 'Fetch module' form
+		echo
+		'<form method="post" action="'.$this->getPageURL().'" id="fetchpkg" class="fieldset">'.
+		'<h4>'.__('Download a zip file').'</h4>'.
+		'<p class="field"><label for="pkg_url" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Zip file URL:').'</label> '.
+		form::field(array('pkg_url','pkg_url'),40,255).'</p>'.
+		'<p class="field"><label for="your_pwd2" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Your password:').'</label> '.
+		form::password(array('your_pwd','your_pwd2'),20,255).'</p>'.
+		'<p><input type="submit" name="fetch_pkg" value="'.__('Download').'" />'.
+		form::hidden(array('tab'), $this->getPageTab()).
+		$this->core->formNonce().'</p>'.
+		'</form>';
+	}
+
+	/**
+	 *
+	 * We need to get configuration content in three steps
+	 * and out of this class to keep backward compatibility.
+	 *
+	 * if ($xxx->setConfigurationFile()) {
+	 *	include $xxx->getConfigurationFile();
+	 * }
+	 * $xxx->setConfigurationContent();
+	 * ... [put here page headers and other stuff]
+	 * $xxx->getConfigurationContent();
+	 *
+	 */
+	public function setConfigurationFile(dcModules $modules, $id=null)
+	{
+		if (empty($_REQUEST['conf']) || empty($_REQUEST['module']) && !$id) {
+			return false;
+		}
+		
+		if (!empty($_REQUEST['module']) && empty($id)) {
+			$id = $_REQUEST['module'];
+		}
+
+		if (!$modules->moduleExists($id)) {
+			$core->error->add(__('Unknow module ID'));
+			return false;
+		}
+
+		$module = $modules->getModules($id);
+		$module = self::parseModuleInfo($id, $module);
+		$file = path::real($module['root'].'/_config.php');
+
+		if (!file_exists($file)) {
+			$core->error->add(__('This module has no configuration file.'));
+			return false;
+		}
+
+		$this->config_module = $module;
+		$this->config_file = $file;
+		$this->config_content = '';
+
+		if (!defined('DC_CONTEXT_MODULE')) {
+			define('DC_CONTEXT_MODULE', true);
+		}
+
+		return true;
+	}
+
+	public function getConfigurationFile()
+	{
+		if (!$this->config_file) {
+			return null;
+		}
+
+		ob_start();
+
+		return $this->config_file;
+	}
+
+	public function setConfigurationContent()
+	{
+		if ($this->config_file) {
+			$this->config_content = ob_get_contents();
+		}
+
+		ob_end_clean();
+
+		return !empty($this->file_content);
+	}
+
+	public function getConfigurationContent()
+	{
+		if (!$this->config_file) {
+			return null;
+		}
+
+		if (!$this->config_module['standalone_config']) {
+			echo
+			'<form id="module_config" action="'.$this->getPageURL('conf=1').'" method="post" enctype="multipart/form-data">'.
+			'<h3>'.sprintf(__('Configure plugin "%s"'), html::escapeHTML($this->config_module['name'])).'</h3>'.
+			'<p><a class="back" href="'.$this->getPageURL().'#plugins">'.__('Back').'</a></p>';
+		}
+
+		echo $this->config_content;
+
+		if (!$this->config_module['standalone_config']) {
+			echo
+			'<p class="clear"><input type="submit" name="save" value="'.__('Save').'" />'.
+			form::hidden('module', $this->config_module['id']).
+			$this->core->formNonce().'</p>'.
+			'</form>';
+		}
+
+		return true;
+	}
+
+	public static function sanitizeString($str)
+	{
+		return preg_replace('/[^A-Za-z0-9\@\#+_-]/', '', strtolower($str));
+	}
+}
+
+class adminThemesList extends adminModulesList
+{
+	protected $page_url = 'blog_theme.php';
+
+	public function displayModulesList($cols=array('name', 'config', 'version', 'desc'), $actions=array(), $nav_limit=false)
+	{
+		echo 
+		'<div id="'.html::escapeHTML($this->list_id).'" class="modules'.(in_array('expander', $cols) ? ' expandable' : '').' one-box">';
+
+		$sort_field = $this->getSortQuery();
+
+		# Sort modules by id
+		$modules = $this->getSearchQuery() === null ?
+			self::sortModules($this->modules, $sort_field, $this->sort_asc) :
+			$this->modules;
+
+		$res = '';
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearchQuery() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getNavQuery() != $char) {
+					continue;
+				}
+			}
+
+			$current = $this->core->blog->settings->system->theme == $id;
+
+			if (preg_match('#^http(s)?://#', $this->core->blog->settings->system->themes_url)) {
+				$theme_url = http::concatURL($this->core->blog->settings->system->themes_url, '/'.$id);
+			} else {
+				$theme_url = http::concatURL($this->core->blog->url, $this->core->blog->settings->system->themes_url.'/'.$id);
+			}
+
+			$has_conf = file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/_config.php');
+			$has_css = file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/style.css');
+			$parent = $module['parent'];
+			$has_parent = !empty($module['parent']);
+			if ($has_parent) {
+				$is_parent_present = $this->core->themes->moduleExists($parent);
+			}
+
+			$line = 
+			'<div class="box '.($current ? 'medium current-theme' : 'small theme').'">';
+
+			if (in_array('name', $cols)) {
+				$line .= 
+				'<h4 class="module-name">'.html::escapeHTML($module['name']).'</h4>';
+			}
+
+			if (in_array('sshot', $cols)) {
+				# Screenshot from url
+				if (preg_match('#^http(s)?://#', $module['sshot'])) {
+					$sshot = $module['sshot'];
+				}
+				# Screenshot from installed module
+				elseif (file_exists($this->core->blog->themes_path.'/'.$id.'/screenshot.jpg')) {
+					$sshot = $this->getPageURL('shot='.rawurlencode($id));
+				}
+				# Default screenshot
+				else {
+					$sshot = 'images/noscreenshot.png';
+				}
+
+				$line .= 
+				'<div class="module-sshot"><img src="'.$sshot.'" alt="'.__('screenshot.').'" /></div>';
+			}
+
+			$line .= 
+			'<div class="module-infos toggle-bloc">'.
+			'<p>';
+
+			if (in_array('desc', $cols)) {
+				$line .= 
+				'<span class="module-desc">'.html::escapeHTML($module['desc']).'</span> ';
+			}
+
+			if (in_array('author', $cols)) {
+				$line .= 
+				'<span class="module-author">'.sprintf(__('by %s'),html::escapeHTML($module['author'])).'</span> ';
+			}
+
+			if (in_array('version', $cols)) {
+				$line .= 
+				'<span class="module-version">'.sprintf(__('version %s'),html::escapeHTML($module['version'])).'</span> ';
+			}
+
+			if (in_array('parent', $cols) && $has_parent) {
+				if ($is_parent_present) {
+					$line .= 
+					'<span class="module-parent-ok">'.sprintf(__('(built on "%s")'),html::escapeHTML($parent)).'</span> ';
+				}
+				else {
+					$line .= 
+					'<span class="module-parent-missing">'.sprintf(__('(requires "%s")'),html::escapeHTML($parent)).'</span> ';
+				}
+			}
+
+			$line .= 
+			'</p>'.
+			'</div>';
+
+			$line .= 
+			'<div class="module-actions toggle-bloc">';
+			
+			# _GET actions
+
+			if ($current && $has_css) {
+				$line .= 
+				'<p><a href="'.$theme_url.'/style.css" class="button">'.__('View stylesheet').'</a></p>';
+			}
+			if ($current && $has_conf) {
+				$line .= 
+				'<p><a href="'.$this->getPageURL('module='.$id.'&conf=1', false).'" class="button">'.__('Configure theme').'</a></p>';
+			}
+
+			# Plugins actions
+			if ($current) {
+				# --BEHAVIOR-- adminCurrentThemeDetails
+				$line .= 
+				$this->core->callBehavior('adminCurrentThemeDetails', $this->core, $id, $module);
+			}
+
+			# _POST actions
+			if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+				$line .=
+				$this->displayLineActions($id, $module, $actions, false);
+			}
+
+			$line .= 
+			'</div>';
+
+			$line .=
+			'</div>';
+
+			$count++;
+
+			$res = $current ? $line.$res : $res.$line;
+		}
+		echo 
+		$res.
+		'</div>';
+
+		if(!$count && $this->getSearchQuery() === null) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+	}
+
+	protected function displayOtherLineAction($id, $module, $actions)
+	{
+		$submits = array();
+
+		$this->core->blog->settings->addNamespace('system');
+		if ($id == $this->core->blog->settings->system->theme) {
+			return null;
+		}
+
+		# Select theme to use on curent blog
+		if (in_array('select', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="select" value="'.__('Choose').'" />';
+		}
+
+		return $submits;
+	}
+	
+	protected function executeOtherAction($prefix, dcModules $modules, dcRepository $repository)
+	{
+		# Select theme to use on curent blog
+		if (!empty($_POST['module']) && !empty($_POST['select'])) {
+			$id = $_POST['module'];
+
+			if (!$modules->moduleExists($id)) {
+				throw new Exception(__('No such module.'));
+			}
+
+			$this->core->blog->settings->addNamespace('system');
+			$this->core->blog->settings->system->put('theme',$id);
+			$this->core->blog->triggerBlog();
+
+			http::redirect($this->getPageURL('msg=select').'#themes');
+		}
+
+		return null;
+	}
+}
Index: inc/core/class.dc.core.php
===================================================================
--- inc/core/class.dc.core.php	(revision 2198)
+++ inc/core/class.dc.core.php	(revision 1743)
@@ -1342,6 +1342,4 @@
 				array('use_smilies','boolean',false,
 				'Show smilies on entries and comments'),
-				array('inc_subcats','boolean',false,
-				'Include sub-categories in category page and category posts feed'),
 				array('wiki_comments','boolean',false,
 				'Allow commenters to use a subset of wiki syntax')
Index: inc/core/class.dc.repository.parser.php
===================================================================
--- inc/core/class.dc.repository.parser.php	(revision 2214)
+++ inc/core/class.dc.repository.parser.php	(revision 2214)
@@ -0,0 +1,105 @@
+<?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; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed parser
+
+Provides an object to parse XML feed of modules from a repository.
+*/
+class dcRepositoryParser
+{
+	/** @var	object	XML object of feed contents */
+	protected $xml;
+	/** @var	array	Array of feed contents */
+	protected $items;
+	/** @var	string	XML bloc tag */
+	protected static $bloc = 'http://dotaddict.org/da/';
+
+	/**
+	 * Constructor.
+	 *
+	 * @param	string		Feed content
+	 */
+	public function __construct($data)
+	{
+		if (!is_string($data)) {
+			throw new Exception(__('Failed to read data feed'));
+		}
+
+		$this->xml = simplexml_load_string($data);
+		$this->items = array();
+
+		if ($this->xml === false) {
+			throw new Exception(__('Wrong data feed'));
+		}
+
+		$this->_parse();
+
+		unset($data);
+		unset($this->xml);
+	}
+
+	/**
+	 * Parse XML into array
+	 */
+	protected function _parse()
+	{
+		if (empty($this->xml->module)) {
+			return;
+		}
+
+		foreach ($this->xml->module as $i) {
+			$attrs = $i->attributes();
+
+			$item = array();
+
+			# DC/DA shared markers
+			$item['id']		= (string) $attrs['id'];
+			$item['file']		= (string) $i->file;
+			$item['label']		= (string) $i->name; // deprecated
+			$item['name']		= (string) $i->name;
+			$item['version']	= (string) $i->version;
+			$item['author']	= (string) $i->author;
+			$item['desc']		= (string) $i->desc;
+
+			# DA specific markers
+			$item['dc_min'] 	= (string) $i->children(self::$bloc)->dcmin;
+			$item['details'] 	= (string) $i->children(self::$bloc)->details;
+			$item['section'] 	= (string) $i->children(self::$bloc)->section;
+			$item['support'] 	= (string) $i->children(self::$bloc)->support;
+			$item['sshot']		= (string) $i->children(self::$bloc)->sshot;
+
+			$tags = array();
+			foreach($i->children(self::$bloc)->tags as $t) {
+				$tags[] = (string) $t->tag;
+			}
+			$item['tags']		= implode(', ',$tags);
+			
+			# First filter right now. If DC_DEV is set all modules are parse
+			if (defined('DC_DEV') && DC_DEV === true || version_compare(DC_VERSION,$item['dc_min'],'>=')) {
+				$this->items[$item['id']] = $item;
+			}
+		}
+	}
+
+	/**
+	 * Get modules.
+	 *
+	 * @return	array		Modules list
+	 */
+	public function getModules()
+	{
+		return $this->items;
+	}
+}
Index: inc/core/class.dc.repository.php
===================================================================
--- inc/core/class.dc.repository.php	(revision 2214)
+++ inc/core/class.dc.repository.php	(revision 2214)
@@ -0,0 +1,260 @@
+<?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; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed reader
+
+Provides an object to parse XML feed of modules from repository.
+*/
+class dcRepository
+{
+	/** @var	object	dcCore instance */
+	public $core;
+	/** @var	object	dcModules instance */
+	public $modules;
+
+	/** @var	string	User agent used to query repository */
+	protected $user_agent = 'DotClear.org RepoBrowser/0.1';
+	/** @var	string	XML feed URL */
+	protected $xml_url;
+	/** @var	array	Array of new/update modules from repository */
+	protected $data;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param	object	$modules		dcModules instance
+	 * @param	string	$xml_url		XML feed URL
+	 */
+	public function __construct(dcModules $modules, $xml_url)
+	{
+		$this->core = $modules->core;
+		$this->modules = $modules;
+		$this->xml_url = $xml_url;
+		$this->user_agent = sprintf('Dotclear/%s)', DC_VERSION);
+	}
+
+	/**
+	 * Check repository.
+	 *
+	 * @param	boolean	$force		Force query repository
+	 * @return	boolean	True if get feed or cache
+	 */
+	public function check($force=false)
+	{
+		if (!$this->xml_url) {
+			return false;
+		}
+		if (($parser = dcRepositoryReader::quickParse($this->xml_url, DC_TPL_CACHE, $force)) === false) {
+			return false;
+		}
+
+		$raw_datas = $parser->getModules();
+
+		uasort($raw_datas, array('self','sort'));
+
+		$skipped = array_keys($this->modules->getDisabledModules());
+		foreach ($skipped as $p_id) {
+			if (isset($raw_datas[$p_id])) {
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$updates = array();
+		$current = $this->modules->getModules();
+		foreach ($current as $p_id => $p_infos) {
+			if (isset($raw_datas[$p_id])) {
+				if (self::compare($raw_datas[$p_id]['version'],$p_infos['version'],'>')) {
+					$updates[$p_id] = $raw_datas[$p_id];
+					$updates[$p_id]['root'] = $p_infos['root'];
+					$updates[$p_id]['root_writable'] = $p_infos['root_writable'];
+					$updates[$p_id]['current_version'] = $p_infos['version'];
+				}
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$this->data = array(
+			'new'	=> $raw_datas,
+			'update'	=> $updates
+		);
+
+		return true;
+	}
+
+	/**
+	 * Get a list of modules.
+	 *
+	 * @param	boolean	$update	True to get update modules, false for new ones
+	 * @return	array	List of update/new modules
+	 */
+	public function get($update=false)
+	{
+		return $this->data[$update ? 'update' : 'new'];
+	}
+
+	/**
+	 * Search a module.
+	 *
+	 * Search string is cleaned, split and compare to split:
+	 * - module id and clean id,
+	 * - module name, clean name,
+	 * - module desccription.
+	 *
+	 * Every time a part of query is find on module,
+	 * result accuracy grow. Result is sorted by acuracy.
+	 *
+	 * @param	string	$pattern	String to search
+	 * @return	array	Match modules
+	 */
+	public function search($pattern)
+	{
+		$result = array();
+
+		# Split query into small clean words
+		$patterns = explode(' ', $pattern);
+		array_walk($patterns, array('dcRepository','sanitize'));
+
+		# For each modules
+		foreach ($this->data['new'] as $id => $module) {
+
+			# Split modules infos into small clean word
+			$subjects = explode(' ', $id.' '.$module['name'].' '.$module['desc']);
+			array_walk($subjects, array('dcRepository','sanitize'));
+
+			# Check contents
+			if (!($nb = preg_match_all('/('.implode('|', $patterns).')/', implode(' ', $subjects), $_))) {
+				continue;
+			}
+
+			# Add module to result
+			if (!isset($sorter[$id])) {
+				$sorter[$id] = 0;
+				$result[$id] = $module;
+			}
+
+			# Increment matches count
+			$sorter[$id] += $nb;
+			$result[$id]['accuracy'] = $sorter[$id];
+		}
+		# Sort response by matches count
+		if (!empty($result)) {
+			array_multisort($sorter, SORT_DESC, $result);
+		}
+		return $result;
+	}
+
+	/**
+	 * Quick download and install module.
+	 *
+	 * @param	string	$url	Module package URL
+	 * @param	string	$dest	Path to install module
+	 * @return	integer		1 = installed, 2 = update
+	 */
+	public function process($url, $dest)
+	{
+		$this->download($url, $dest);
+		return $this->install($dest);
+	}
+
+	/**
+	 * Download a module.
+	 *
+	 * @param	string	$url	Module package URL
+	 * @param	string	$dest	Path to put module package
+	 */
+	public function download($url, $dest)
+	{
+		try {
+			$client = netHttp::initClient($url, $path);
+			$client->setUserAgent(self::agent());
+			$client->useGzip(false);
+			$client->setPersistReferers(false);
+			$client->setOutput($dest);
+			$client->get($path);
+			unset($client);
+		}
+		catch (Exception $e) {
+			unset($client);
+			throw new Exception(__('An error occurred while downloading the file.'));
+		}
+	}
+
+	/**
+	 * Install a previously downloaded module.
+	 *
+	 * @param	string	$path	Module package URL
+	 * @param	string	$path	Path to module package
+	 * @return	integer		1 = installed, 2 = update
+	 */
+	public function install($path)
+	{
+		return dcModules::installPackage($path, $this->modules);
+	}
+
+	/**
+	 * User Agent String.
+	 *
+	 * @param	string	$str		User agent string
+	 */
+	public function agent($str)
+	{
+		$this->user_agent = $str;
+	}
+
+	/**
+	 * Sanitize string.
+	 *
+	 * @param	string	$str		String to sanitize
+	 * @param	null	$_		Unused	param
+	 */
+	public static function sanitize(&$str, $_)
+	{
+		$str = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $str));
+	}
+
+	/**
+	 * Compare version.
+	 *
+	 * @param	string	$v1		Version
+	 * @param	string	$v2		Version
+	 * @param	string	$op		Comparison operator
+	 * @return	boolean	True is comparison is true, dude!
+	 */
+	private static function compare($v1, $v2, $op)
+	{
+		return version_compare(
+			preg_replace('!-r(\d+)$!', '-p$1', $v1), 
+			preg_replace('!-r(\d+)$!', '-p$1', $v2), 
+			$op
+		);
+	}
+
+	/** 
+	 * Sort modules list.
+	 *
+	 * @param	array	$a		A module
+	 * @param	array	$b		A module
+	 * @return	integer
+	 */
+	private static function sort($a,$b)
+	{
+		$c = strtolower($a['id']); 
+		$d = strtolower($b['id']); 
+		if ($c == $d) { 
+			return 0; 
+		} 
+		return ($c < $d) ? -1 : 1; 
+	}
+}
Index: inc/core/class.dc.repository.reader.php
===================================================================
--- inc/core/class.dc.repository.reader.php	(revision 2214)
+++ inc/core/class.dc.repository.reader.php	(revision 2214)
@@ -0,0 +1,266 @@
+<?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; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed reader
+
+Provides an object to parse XML feed of modules from repository.
+This class extends clearbricks netHttp class.
+*/
+class dcRepositoryReader extends netHttp
+{
+	/** @var	string	User agent used to query repository */
+	protected $user_agent = 'DotClear.org RepoBrowser/0.1';
+	/** @var	integer	User agent used to query repository */
+	protected $timeout = 5;
+	/** @var	array	 HTTP Cache validators */
+	protected $validators = null;
+	/** @var	string	Cache temporary directory */
+	protected $cache_dir = null;
+	/** @var	string	Cache file prefix */
+	protected $cache_file_prefix = 'dcrepo';
+	/** @var	integer	Cache TTL */
+	protected $cache_ttl = '-30 minutes';
+	/** @var	boolean	'Cache' TTL on server failed */
+	protected $cache_touch_on_fail = true;
+	/** @var	boolean	Force query server */
+	protected $force = false;
+
+	/**
+	 * Constructor.
+	 *
+	 * Bypass first argument of clearbricks netHttp constructor.
+	 */
+	public function __construct()
+	{
+		parent::__construct('');
+		$this->setUserAgent(sprintf('Dotclear/%s)', DC_VERSION));
+	}
+
+	/**
+	 * Parse modules feed.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	object	dcRepository instance
+	 */
+	public function parse($url)
+	{
+		$this->validators = array();
+
+		if ($this->cache_dir) {
+			return $this->withCache($url);
+		}
+		elseif (!$this->getModulesXML($url) || $this->getStatus() != '200') {
+			return false;
+		}
+
+		return new dcRepositoryParser($this->getContent());
+	}
+
+	/**
+	 * Quick parse modules feed.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @param	string	$cache_dir	Cache directoy or null for no cache
+	 * @param	boolean	$force		Force query repository
+	 * @return	object	Self instance
+	 */
+	public static function quickParse($url, $cache_dir=null, $force=false)
+	{
+		$parser = new self();
+		if ($cache_dir) {
+			$parser->setCacheDir($cache_dir);
+		}
+		if ($force) {
+			$parser->setForce($force);
+		}
+
+		return $parser->parse($url);
+	}
+
+	/**
+	 * Set cache directory.
+	 *
+	 * @param	string	$dir		Cache directory
+	 * @return	boolean	True if cache dierctory is useable
+	 */
+	public function setCacheDir($dir)
+	{
+		$this->cache_dir = null;
+
+		if (!empty($dir) && is_dir($dir) && is_writeable($dir)) {
+			$this->cache_dir = $dir;
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set cache TTL.
+	 *
+	 * @param	string	$str		Cache TTL
+	 */
+	public function setCacheTTL($str)
+	{
+		$str = trim($str);
+
+		if (!empty($str)) {
+			$this->cache_ttl = substr($str, 0, 1) == '-' ? $str : '-'.$str;
+		}
+	}
+
+	/**
+	 * Set force query reposiory.
+	 *
+	 * @param	boolean	$force	True to force query
+	 */
+	public function setForce($force)
+	{
+		$this->force = $force;
+	}
+
+	/**
+	 * Get repository XML feed URL content.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	string	Feed content
+	 */
+	protected function getModulesXML($url)
+	{
+		if (!self::readURL($url, $ssl, $host, $port, $path, $user, $pass)) {
+			return false;
+		}
+		$this->setHost($host, $port);
+		$this->useSSL($ssl);
+		$this->setAuthorization($user, $pass);
+
+		return $this->get($path);
+	}
+
+	/**
+	 * Get repository modules list using cache.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	array	Feed content or False on fail
+	 */
+	protected function withCache($url)
+	{
+		$url_md5 = md5($url);
+		$cached_file = sprintf('%s/%s/%s/%s/%s.ser',
+			$this->cache_dir,
+			$this->cache_file_prefix,
+			substr($url_md5,0,2),
+			substr($url_md5,2,2),
+			$url_md5
+		);
+
+		$may_use_cached = false;
+
+		# Use cache file ?
+		if (@file_exists($cached_file) && !$this->force) {
+			$may_use_cached = true;
+			$ts = @filemtime($cached_file);
+			if ($ts > strtotime($this->cache_ttl)) {
+				# Direct cache
+				return unserialize(file_get_contents($cached_file));
+			}
+			$this->setValidator('IfModifiedSince', $ts);
+		}
+
+		# Query repository
+		if (!$this->getModulesXML($url)) {
+			if ($may_use_cached) {
+				# Touch cache TTL even if query failed ?
+				if ($this->cache_touch_on_fail) {
+					@files::touch($cached_file);
+				}
+				# Connection failed - fetched from cache
+				return unserialize(file_get_contents($cached_file));
+			}
+			return false;
+		}
+
+		# Parse response
+		switch ($this->getStatus())
+		{
+			# Not modified, use cache
+			case '304':
+				@files::touch($cached_file);
+				return unserialize(file_get_contents($cached_file));
+			# Ok, parse feed
+			case '200':
+				if ($modules = new dcRepositoryParser($this->getContent())) {
+					try {
+						files::makeDir(dirname($cached_file), true);
+					}
+					catch (Exception $e) {
+						return $modules;
+					}
+
+					if (($fp = @fopen($cached_file, 'wb'))) {
+						fwrite($fp, serialize($modules));
+						fclose($fp);
+						files::inheritChmod($cached_file);
+					}
+					return $modules;
+				}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Prepare query.
+	 *
+	 * @return	array	Query headers
+	 */
+	protected function buildRequest()
+	{
+		$headers = parent::buildRequest();
+
+		# Cache validators
+		if (!empty($this->validators)) {
+			if (isset($this->validators['IfModifiedSince'])) {
+				$headers[] = 'If-Modified-Since: '.$this->validators['IfModifiedSince'];
+			}
+			if (isset($this->validators['IfNoneMatch'])) {
+				if (is_array($this->validators['IfNoneMatch'])) {
+					$etags = implode(',', $this->validators['IfNoneMatch']);
+				}
+				else {
+					$etags = $this->validators['IfNoneMatch'];
+				}
+				$headers[] = '';
+			}
+		}
+
+		return $headers;
+	}
+
+	/**
+	 * Tweak query cache validator.
+	 *
+	 * @param	string	$key		Validator key
+	 * @param	string	$value		Validator value
+	 */
+	private function setValidator($key, $value)
+	{
+		if ($key == 'IfModifiedSince') {
+			$value = gmdate('D, d M Y H:i:s', $value).' GMT';
+		}
+
+		$this->validators[$key] = $value;
+	}
+}
Index: inc/prepend.php
===================================================================
--- inc/prepend.php	(revision 1999)
+++ inc/prepend.php	(revision 2147)
@@ -46,4 +46,7 @@
 $__autoload['dcWorkspace']			= dirname(__FILE__).'/core/class.dc.workspace.php';
 $__autoload['dcPrefs']				= dirname(__FILE__).'/core/class.dc.prefs.php';
+$__autoload['dcRepository']			= dirname(__FILE__).'/core/class.dc.repository.php';
+$__autoload['dcRepositoryReader']		= dirname(__FILE__).'/core/class.dc.repository.reader.php';
+$__autoload['dcRepositoryParser']		= dirname(__FILE__).'/core/class.dc.repository.parser.php';
 
 $__autoload['rsExtPost']				= dirname(__FILE__).'/core/class.dc.rs.extensions.php';
@@ -61,4 +64,6 @@
 $__autoload['dcPager']		= dirname(__FILE__).'/admin/lib.pager.php';
 $__autoload['dcAdminCombos']			= dirname(__FILE__).'/admin/lib.admincombos.php';
+$__autoload['adminModulesList']			= dirname(__FILE__).'/admin/lib.moduleslist.php';
+$__autoload['adminThemesList']			= dirname(__FILE__).'/admin/lib.moduleslist.php';
 
 $__autoload['dcTemplate']			= dirname(__FILE__).'/public/class.dc.template.php';
Index: inc/public/class.dc.template.php
===================================================================
--- inc/public/class.dc.template.php	(revision 2198)
+++ inc/public/class.dc.template.php	(revision 1674)
@@ -1211,5 +1211,5 @@
 				$p .=
 				'if ($_ctx->exists("categories")) { '.
-					"\$params['cat_id'] = \$_ctx->categories->cat_id.(\$core->blog->settings->system->inc_subcats?' ?sub':'');".
+					"\$params['cat_id'] = \$_ctx->categories->cat_id; ".
 				"}\n";
 			}
