Index: admin/blog_theme.php
===================================================================
--- admin/blog_theme.php	(revision 2191)
+++ admin/blog_theme.php	(revision 2241)
@@ -15,362 +15,231 @@
 dcPage::check('admin');
 
-# Loading themes
+# -- "First time" settings setup --
+if ($core->blog->settings->system->store_theme_url === null) {
+	$core->blog->settings->system->put(
+		'store_theme_url', 'http://update.dotaddict.org/dc2/themes.xml', 'string', 'Themes XML feed location', true, true
+	);
+}
+
+# -- Loading themes --
 $core->themes = new dcThemes($core);
-$core->themes->loadModules($core->blog->themes_path,null);
-
-# Theme screenshot
-if (!empty($_GET['shot']) && $core->themes->moduleExists($_GET['shot']))
-{
-	if (empty($_GET['src'])) {
-		$f = $core->blog->themes_path.'/'.$_GET['shot'].'/screenshot.jpg';
-	} else {
-		$f = $core->blog->themes_path.'/'.$_GET['shot'].'/'.path::clean($_GET['src']);
-	}
-	
-	$f = path::real($f);
-	
+$core->themes->loadModules($core->blog->themes_path, null);
+
+# -- Page helper --
+$list = new adminThemesList(
+	$core->themes, 
+	$core->blog->themes_path,
+	$core->blog->settings->system->store_theme_url
+);
+$list::$distributed_modules = array(
+	'blueSilence',
+	'blowupConfig',
+	'customCSS',
+	'default',
+	'ductile'
+);
+
+# -- Theme screenshot --
+if (!empty($_GET['shot']) && $list->modules->moduleExists($_GET['shot'])) {
+
+	$f= path::real(empty($_GET['src']) ?
+		$core->blog->themes_path.'/'.$_GET['shot'].'/screenshot.jpg' :
+		$core->blog->themes_path.'/'.$_GET['shot'].'/'.path::clean($_GET['src'])
+	);
+
 	if (!file_exists($f)) {
 		$f = dirname(__FILE__).'/images/noscreenshot.png';
 	}
-	
-	http::cache(array_merge(array($f),get_included_files()));
-	
+
+	http::cache(array_merge(array($f), get_included_files()));
+
 	header('Content-Type: '.files::getMimeType($f));
 	header('Content-Length: '.filesize($f));
 	readfile($f);
-	
+
 	exit;
 }
 
-$can_install = $core->auth->isSuperAdmin();
-$is_writable = is_dir($core->blog->themes_path) && is_writable($core->blog->themes_path);
-$default_tab = 'themes-list';
-
-# Selecting theme
-if (!empty($_POST['theme']) && !empty($_POST['select']) && empty($_REQUEST['conf']))
-{
-	$core->blog->settings->addNamespace('system');
-	$core->blog->settings->system->put('theme',$_POST['theme']);
-	$core->blog->triggerBlog();
-	$theme = $core->themes->getModules($_POST['theme']);
-	dcPage::addSuccessNotice(sprintf(
-		__('Current theme has been successfully changed to "%s".'),
-		html::escapeHTML($theme['name']))
+# -- Display module configuration page --
+if ($list->setConfiguration($core->blog->settings->system->theme)) {
+
+	# Get content before page headers
+	include $list->includeConfiguration();
+
+	# Gather content
+	$list->getConfiguration();
+
+	# Display page
+	dcPage::open(__('Blog appearance'),
+		dcPage::jsPageTabs().
+		dcPage::jsColorPicker().
+
+		# --BEHAVIOR-- themesToolsHeaders
+		$core->callBehavior('themesToolsHeaders', $core, true),
+
+		dcPage::breadcrumb(
+			array(
+				html::escapeHTML($core->blog->name) => '',
+				__('Blog appearance') => $list->getURL('',false),
+				'<span class="page-title">'.__('Theme configuration').'</span>' => ''
+			))
 	);
 
-	http::redirect('blog_theme.php');
-}
-
-if ($can_install && !empty($_POST['theme']) && !empty($_POST['remove']) && empty($_REQUEST['conf']))
-{
-	try
-	{
-		if ($_POST['theme'] == 'default') {
-			throw new Exception(__('You can\'t remove default theme.'));
-		}
-		
-		if (!$core->themes->moduleExists($_POST['theme'])) {
-			throw new Exception(__('Theme does not exist.'));
-		}
-		
-		$theme = $core->themes->getModules($_POST['theme']);
-		
-		# --BEHAVIOR-- themeBeforeDelete
-		$core->callBehavior('themeBeforeDelete',$theme);
-		
-		$core->themes->deleteModule($_POST['theme']);
-		
-		# --BEHAVIOR-- themeAfterDelete
-		$core->callBehavior('themeAfterDelete',$theme);
-		
-		http::redirect('blog_theme.php');
-	}
-	catch (Exception $e)
-	{
-		$core->error->add($e->getMessage());
-	}
-}
-
-# Theme upload
-if ($can_install && $is_writable && ((!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file'])) ||
-	(!empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url']))))
-{
-	try
-	{
-		if (empty($_POST['your_pwd']) || !$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 = $core->blog->themes_path.'/'.$_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 = $core->blog->themes_path.'/'.basename($url);
-			
-			try
-			{
-				$client = netHttp::initClient($url,$path);
-				$client->setUserAgent('Dotclear - http://www.dotclear.org/');
-				$client->useGzip(false);
-				$client->setPersistReferers(false);
-				$client->setOutput($dest);
-				$client->get($path);
-			}
-			catch( Exception $e)
-			{
-				throw new Exception(__('An error occurred while downloading the file.'));
-			}
-			
-			unset($client);
-		}
-		
-		$ret_code = dcModules::installPackage($dest,$core->themes);
-		if ($ret_code == 2) {
-			dcPage::addSuccessNotice(__('Theme has been successfully upgraded.'));
-		} else {
-			dcPage::addSuccessNotice(__('Theme has been successfully installed.'));
-		}
-		http::redirect('blog_theme.php');
-	}
-	catch (Exception $e)
-	{
-		$core->error->add($e->getMessage());
-		$default_tab = 'add-theme';
-	}
-}
-
-$theme_conf_mode = false;
-if (!empty($_REQUEST['conf']))
-{
-	$theme_conf_file = path::real($core->blog->themes_path.'/'.$core->blog->settings->system->theme).'/_config.php';
-	if (file_exists($theme_conf_file)) {
-		$theme_conf_mode = true;
-	}
-}
-
-function display_theme_details($id,$details,$current)
-{
-	global $core;
-	
-	$screenshot = 'images/noscreenshot.png';
-	if (file_exists($core->blog->themes_path.'/'.$id.'/screenshot.jpg')) {
-		$screenshot = 'blog_theme.php?shot='.rawurlencode($id);
-	}
-	
-	$radio_id = 'theme_'.html::escapeHTML($id);
-	if (preg_match('#^http(s)?://#',$core->blog->settings->system->themes_url)) {
-		$theme_url = http::concatURL($core->blog->settings->system->themes_url,'/'.$id);
-	} else {
-		$theme_url = http::concatURL($core->blog->url,$core->blog->settings->system->themes_url.'/'.$id);
-	}
-	$has_conf = file_exists(path::real($core->blog->themes_path.'/'.$id).'/_config.php');
-	$has_css = file_exists(path::real($core->blog->themes_path.'/'.$id).'/style.css');
-	$parent = $core->themes->moduleInfo($id,'parent');
-	$has_parent = (boolean)$parent;
-	if ($has_parent) {
-		$is_parent_present = $core->themes->moduleExists($parent);
-	}
-	
-	$res =
-	'<div class="theme-details'.($current ? ' current-theme' : '').'">'.
-	'<div class="theme-shot"><img src="'.$screenshot.'" alt="" /></div>'.
-	'<div class="theme-info">'.
-		'<h4>'.form::radio(array('theme',$radio_id),html::escapeHTML($id),$current,'','',($has_parent && !$is_parent_present)).' '.
-		'<label class="classic" for="'.$radio_id.'">'.
-		html::escapeHTML($details['name']).'</label></h4>'.
-		'<p><span class="theme-desc">'.html::escapeHTML($details['desc']).'</span> '.
-		'<span class="theme-author">'.sprintf(__('by %s'),html::escapeHTML($details['author'])).'</span> '.
-		'<span class="theme-version">'.sprintf(__('version %s'),html::escapeHTML($details['version'])).'</span> ';
-		if ($has_parent) {
-			if ($is_parent_present) {
-				$res .= '<span class="theme-parent-ok">'.sprintf(__('(built on "%s")'),html::escapeHTML($parent)).'</span> ';
-			} else {
-				$res .= '<span class="theme-parent-missing">'.sprintf(__('(requires "%s")'),html::escapeHTML($parent)).'</span> ';
-			}
-		}
-		if ($has_css) {
-			$res .= '<span class="theme-css"><a href="'.$theme_url.'/style.css">'.__('Stylesheet').'</a></span>';
-		}
-		$res .= '</p>';
-	$res .=
-	'</div>'.
-	'<div class="theme-actions">';
-		if ($current && $has_conf) {
-			$res .= '<p><a href="blog_theme.php?conf=1" class="button">'.__('Configure theme').'</a></p>';
-		}
-		if ($current) {
-			# --BEHAVIOR-- adminCurrentThemeDetails
-			$res .= $core->callBehavior('adminCurrentThemeDetails',$core,$id,$details);
-		}
-	$res .=
-	'</div>'.
-	'</div>';
-	
-	return $res;
-}
-
-if (!$theme_conf_mode)
-{
-	$breadcrumb = dcPage::breadcrumb(
+	# Display previously gathered content
+	$list->displayConfiguration();
+
+	dcPage::close();
+
+	# Stop reading code here
+	return;
+}
+
+# -- Execute actions --
+try {
+	$list->doActions('themes');
+}
+catch (Exception $e) {
+	$core->error->add($e->getMessage());
+}
+
+# -- Page header --
+dcPage::open(__('Themes management'),
+	dcPage::jsLoad('js/_blog_theme.js').
+	dcPage::jsPageTabs().
+	dcPage::jsColorPicker().
+
+	# --BEHAVIOR-- themesToolsHeaders
+	$core->callBehavior('themesToolsHeaders', $core, false),
+
+	dcPage::breadcrumb(
 		array(
 			html::escapeHTML($core->blog->name) => '',
-			__('Blog appearance') => ''
-		));
-} else {
-	$breadcrumb = dcPage::breadcrumb(
-		array(
-			html::escapeHTML($core->blog->name) => '',
-			__('Blog appearance') => 'blog_theme.php',
-			__('Theme configuration') => ''
-		));
-}
-
-dcPage::open(__('Blog appearance'),
-	(!$theme_conf_mode ? dcPage::jsLoad('js/_blog_theme.js') : '').
-	dcPage::jsPageTabs($default_tab).
-	dcPage::jsColorPicker(),
-	$breadcrumb
+			'<span class="page-title">'.__('Blog appearance').'</span>' => ''
+		))
 );
 
-if (!$theme_conf_mode)
-{
-	if (!empty($_GET['upd'])) {
-		dcPage::success(__('Theme has been successfully changed.'));
+# -- Display modules lists --
+if ($core->auth->isSuperAdmin() && $list->isWritablePath()) {
+
+	# Updated modules from repo
+	$modules = $list->store->get(true);
+	if (!empty($modules)) {
+		echo 
+		'<div class="multi-part" id="update" title="'.html::escapeHTML(__('Update themes')).'">'.
+		'<h3>'.html::escapeHTML(__('Update themes')).'</h3>'.
+		'<p>'.sprintf(
+			__('There is one theme to update available from repository.', 'There are %s themes to update available from repository.', count($modules)),
+			count($modules)
+		).'</p>';
+
+		$list
+			->setList('theme-update')
+			->setTab('themes')
+			->setModules($modules)
+			->displayModules(
+				/*cols */		array('sshot', 'name', 'desc', 'author', 'version', 'current_version', 'parent'),
+				/* actions */	array('update', 'delete')
+			);
+
+		echo
+		'<p class="info vertical-separator">'.sprintf(
+			__("Visit %s repository, the resources center for Dotclear."),
+			'<a href="http://dotaddict.org/dc2/themes">Dotaddict</a>'
+			).
+		'</p>'.
+
+		'</div>';
 	}
-	
-	if (!empty($_GET['added'])) {
-		dcPage::success(($_GET['added'] == 2 ? __('Theme has been successfully upgraded') : __('Theme has been successfully installed.')));
+}
+
+# Activated modules
+$modules = $list->modules->getModules();
+if (!empty($modules)) {
+
+	echo
+	'<div class="multi-part" id="themes" title="'.__('Installed themes').'">'.
+	'<h3>'.__('Installed themes').'</h3>'.
+	'<p>'.__('You can configure and manage installed themes from this list.').'</p>';
+
+	$list
+		->setList('theme-activate')
+		->setTab('themes')
+		->setModules($modules)
+		->displayModules(
+			/* cols */		array('sshot', 'distrib', 'name', 'config', 'desc', 'author', 'version', 'parent'),
+			/* actions */	array('select', 'behavior', 'deactivate', 'delete')
+		);
+
+	echo 
+	'</div>';
+}
+
+# Deactivated modules
+$modules = $list->modules->getDisabledModules();
+if (!empty($modules)) {
+
+	echo
+	'<div class="multi-part" id="deactivate" title="'.__('Deactivated themes').'">'.
+	'<h3>'.__('Deactivated themes').'</h3>'.
+	'<p>'.__('Deactivated themes are installed but not usable. You can activate them from here.').'</p>';
+
+	$list
+		->setList('theme-deactivate')
+		->setTab('themes')
+		->setModules($modules)
+		->displayModules(
+			/* cols */		array('name', 'distrib'),
+			/* actions */	array('activate', 'delete')
+		);
+
+	echo 
+	'</div>';
+}
+
+if ($core->auth->isSuperAdmin() && $list->isWritablePath()) {
+
+	# New modules from repo
+	$search = $list->getSearch();
+	$modules = $search ? $list->store->search($search) : $list->store->get();
+
+	if (!empty($search) || !empty($modules)) {
+		echo
+		'<div class="multi-part" id="new" title="'.__('Add themes').'">'.
+		'<h3>'.__('Add themes from repository').'</h3>'.
+		'<p>'.__('Search and install themes directly from repository.').'</p>';
+
+		$list
+			->setList('theme-new')
+			->setTab('new')
+			->setModules($modules)
+			->displaySearch()
+			->displayIndex()
+			->displayModules(
+				/* cols */		array('expander', 'sshot', 'name', 'score', 'config', 'desc', 'author', 'version', 'parent', 'details', 'support'),
+				/* actions */	array('install'),
+				/* nav limit */	true
+			);
+
+		echo
+		'<p class="info vertical-separator">'.sprintf(
+			__("Visit %s repository, the resources center for Dotclear."),
+			'<a href="http://dotaddict.org/dc2/themes">Dotaddict</a>'
+			).
+		'</p>'.
+
+		'</div>';
 	}
-	
-	if (!empty($_GET['del'])) {
-		dcPage::success(__('Theme has been successfully deleted.'));
-	}
-	
-	# Themes list
-	echo '<div class="multi-part" id="themes-list" title="'.__('Themes').'">'.
-	'<h3>'.__('Available themes in your installation').'</h3>';
-	
-	$themes = $core->themes->getModules();
-	if (isset($themes[$core->blog->settings->system->theme])) {
-		echo '<p>'.sprintf(__('You are currently using <strong>%s</strong>'),$themes[$core->blog->settings->system->theme]['name']).'.</p>';
-	}
-	
-	echo
-	'<form action="blog_theme.php" method="post" id="themes-form">'.
-	'<div id="themes">';
-	
-	if (isset($themes[$core->blog->settings->system->theme])) {
-		echo display_theme_details($core->blog->settings->system->theme,$themes[$core->blog->settings->system->theme],true);
-	}
-	
-	foreach ($themes as $k => $v)
-	{
-		if ($core->blog->settings->system->theme == $k) { // Current theme
-			continue;
-		}
-		echo display_theme_details($k,$v,false);
-	}
-	
-	echo '</div>';
-	
-	echo
-	'<div id="themes-actions">'.
-	
-	'<p>'.$core->formNonce().'<input type="submit" name="select" value="'.__('Use selected theme').'" /> ';	
-	if ($can_install) {
-		echo ' <input type="submit" class="delete" name="remove" value="'.__('Delete selected theme').'" />';
-	}
-	echo '</p>'.
-	
-	'</div>'.
-	'</form>'.
+
+	# Add a new plugin
+	echo
+	'<div class="multi-part" id="addtheme" title="'.__('Install or upgrade manually').'">'.
+	'<h3>'.__('Add themes from a package').'</h3>'.
+	'<p>'.__('You can install themes by uploading or downloading zip files.').'</p>';
+
+	$list->displayManualForm();
+
+	echo
 	'</div>';
-	
-	# Add a new theme
-	if ($can_install)
-	{
-		echo
-		'<div class="multi-part clear" id="add-theme" title="'.__('Install or upgrade a theme').'">'.
-		'<h3>'.__('Add themes to your installation').'</h3>'.
-		'<p class="form-note info">'.sprintf(__('You can find additional themes for your blog on %s.'),
-		'<a href="http://themes.dotaddict.org/galerie-dc2/">Dotaddict</a>').'</p>';
-		
-		if ($is_writable)
-		{
-			echo '<p>'.__('You can also install themes by uploading or downloading zip files.').'</p>';
-			
-			# 'Upload theme' form
-			echo
-			'<form method="post" action="blog_theme.php" 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> '.__('Theme zip file:').'</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 theme').'" />'.
-			$core->formNonce().'</p>'.
-			'</form>';
-			
-			# 'Fetch theme' form
-			echo
-			'<form method="post" action="blog_theme.php" 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> '.__('Theme 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 theme').'" />'.
-			$core->formNonce().'</p>'.
-			'</form>';
-		}
-		else
-		{
-			echo
-			'<p class="static-msg">'.
-			__('To enable this function, please give write access to your themes directory.').
-			'</p>';
-		}
-		echo '</div>';
-	}
-}
-else
-{
-	$theme_name = $core->themes->moduleInfo($core->blog->settings->system->theme,'name');
-	$core->themes->loadModuleL10Nresources($core->blog->settings->system->theme,$_lang);
-
-	echo
-	'<p><a class="back" href="blog_theme.php">'.__('Back to Blog appearance').'</a></p>';
-	
-	try
-	{
-		# Let theme configuration set their own form(s) if required
-		$standalone_config = (boolean) $core->themes->moduleInfo($core->blog->settings->system->theme,'standalone_config');
-
-		if (!$standalone_config)
-			echo '<form id="theme_config" action="blog_theme.php?conf=1" method="post" enctype="multipart/form-data">';
-
-		include $theme_conf_file;
-
-		if (!$standalone_config)
-			echo
-			'<p class="clear"><input type="submit" value="'.__('Save').'" />'.
-			$core->formNonce().'</p>'.
-			'</form>';
-
-	}
-	catch (Exception $e)
-	{
-		echo '<div class="error"><p>'.$e->getMessage().'</p></div>';
-	}
 }
 
 dcPage::close();
-?>
Index: admin/index.php
===================================================================
--- admin/index.php	(revision 2239)
+++ admin/index.php	(revision 2240)
@@ -76,4 +76,24 @@
 $favs = $core->favs->getUserFavorites();
 $core->favs->appendDashboardIcons($__dashboard_icons);
+
+# Check plugins and themes update from repository
+function dc_check_store_update($mod, $url, $img, $icon)
+{
+	$repo = new dcStore($mod, $url);
+	$upd = $repo->get(true);
+	if (!empty($upd)) {
+		$icon[0] .= '<br />'.sprintf(__('An update is available', '%s updates are available.', count($upd)),count($upd));
+		$icon[1] .= '#update';
+		$icon[2] = 'images/menu/'.$img.'-b-update.png';
+	}
+}
+if (isset($__dashboard_icons['plugins'])) {
+	dc_check_store_update($core->plugins, $core->blog->settings->system->store_plugin_url, 'plugins', $__dashboard_icons['plugins']);
+}
+if (isset($__dashboard_icons['blog_theme'])) {
+	$themes = new dcThemes($core);
+	$themes->loadModules($core->blog->themes_path, null);
+	dc_check_store_update($themes, $core->blog->settings->system->store_theme_url, 'blog-theme', $__dashboard_icons['blog_theme']);
+}
 
 # Latest news for dashboard
Index: admin/js/_blog_theme.js
===================================================================
--- admin/js/_blog_theme.js	(revision 2101)
+++ admin/js/_blog_theme.js	(revision 2195)
@@ -1,71 +1,13 @@
 $(function() {
-	$('#themes-actions').hide();
-	var submit_s = $('#themes-actions input[name=select]');
-	var submit_r = $('#themes-actions input[name=remove]');
-	
-	var details = $('#themes div.theme-details');
-	$('div.theme-actions',details).hide();
-	$('input:radio',details).hide();
-	$('div.theme-info span, div.theme-info a',details).hide();
-	details.removeClass('theme-details').addClass('theme-details-js');
-	
-	var themes_wrapper = $('<div id="themes-wrapper"></div>');
-	var theme_box = $('<div id="theme-box"><div</div>');
-	$('#themes').wrap(themes_wrapper).before(theme_box);
-	
-	details.each(function() {
-		var box = this;
-		var a = $(document.createElement('a'));
-		a.attr('href','#');
-		a.attr('title',$('>div h3>label',this).text());
-		$(box).wrap(a);
-		$(box).parent().click(function(event) {
-			update_box(box);
-			event.preventDefault();
-			return false;
-		});
+	$('.module-sshot').not('.current-theme .module-sshot').each(function(){
+		var bar = $('<div>').addClass('bloc-toggler');
+		$(this).after(
+		$(bar).toggleWithLegend($(this).parent().children('.toggle-bloc'),{
+			img_on_src: dotclear.img_plus_theme_src,
+			img_on_alt: dotclear.img_plus_theme_alt,
+			img_off_src: dotclear.img_minus_theme_src,
+			img_off_alt: dotclear.img_minus_theme_alt, 
+			legend_click: true
+		}));
 	});
-	
-	function update_box(e) {
-		theme_box.empty();
-		var img = $('div.theme-shot',e).clone();
-		var info = $('div.theme-info',e).clone();
-		
-		if ($(e).hasClass('current-theme')) {
-			var actions = $('div.theme-actions',e).clone();
-			actions.show();
-		} else {
-			var actions = $('<div class="theme-actions"></div>');
-			if (submit_s.length > 0  && !$('input:radio',info).attr('disabled')) {
-				var select = $('<a href="#" class="button">' + dotclear.msg.use_this_theme + '</a>');
-				select.addClass('select-button').click(function(){
-					submit_s.click();
-					return false;
-				});
-				actions.append(select).append('&nbsp;&nbsp;');
-			}
-			if (submit_r.length > 0 && $('input:radio',info).attr('id') != 'theme_default') {
-				var remove = $('<a href="#" class="button delete">' + dotclear.msg.remove_this_theme + '</a>');
-				remove.click(function() {
-					var t_name = $(this).parents('#theme-box').find('div.theme-info h3:first').text();
-					t_name = $.trim(t_name);
-					if (window.confirm(dotclear.msg.confirm_delete_theme.replace('%s',t_name))) {
-						submit_r.click();
-					}
-					return false;
-				});
-				actions.append(remove);
-			}
-		}
-		
-		$('input:radio',info).remove();
-		$('span, a',info).show();
-		
-		theme_box.append(img).append(info).append(actions);
-		details.removeClass('theme-selected');
-		$(e).addClass('theme-selected');
-		$('input:radio',e).prop('checked',true);
-	}
-	
-	update_box(details[0]);
 });
Index: admin/js/_plugins.js
===================================================================
--- admin/js/_plugins.js	(revision 0)
+++ admin/js/_plugins.js	(revision 2222)
@@ -1,6 +1,158 @@
+dotclear.moduleExpander = function(line) {
+        var td = line.firstChild;
+        
+        var img = document.createElement('img');
+        img.src = dotclear.img_plus_src;
+        img.alt = dotclear.img_plus_alt;
+        img.className = 'expand';
+        $(img).css('cursor','pointer');
+        img.line = line;
+        img.onclick = function() { dotclear.viewModuleContent(this,this.line); };
+        
+        td.insertBefore(img,td.firstChild);
+};
+
+dotclear.modulesExpander = function(line,lines) {
+        var td = line.firstChild;
+
+        var img = document.createElement('img');
+        img.src = dotclear.img_plus_src;
+        img.alt = dotclear.img_plus_alt;
+        img.className = 'expand';
+        $(img).css('cursor','pointer');
+        img.lines = lines;
+        img.onclick = function() { dotclear.viewModulesContent(this,this.lines); };
+
+        td.insertBefore(img,td.firstChild);
+};
+
+dotclear.viewModulesContent = function(img,lines) {
+        
+        action = 'toggle';
+
+        if (img.alt == dotclear.img_plus_alt) {
+                img.src = dotclear.img_minus_src;
+                img.alt = dotclear.img_minus_alt;
+                action = 'open';
+        } else {
+                img.src = dotclear.img_plus_src;
+                img.alt = dotclear.img_plus_alt;
+                action = 'close';
+        }
+        
+        lines.each(function() {
+                var td = this.firstChild;
+                dotclear.viewModuleContent(td.firstChild,td.firstChild.line,action);
+        });
+};
+
+dotclear.viewModuleContent = function(img,line,action) {
+
+        var action = action || 'toggle';
+		var cols = $('td',$(line)).length
+        var sp = line.id.split('_m_');
+		var listId=sp[0];
+		var moduleId= sp[1];
+
+        var tr = document.getElementById('pe'+moduleId);
+        
+		if ( !tr && ( action == 'toggle' || action == 'open' ) ) {
+                tr = document.createElement('tr');
+                tr.id = 'pe'+moduleId;
+
+                var td = document.createElement('td');
+                td.colSpan = cols;
+                td.className = 'expand';
+                tr.appendChild(td);
+                
+                img.src = dotclear.img_minus_src;
+                img.alt = dotclear.img_minus_alt;
+                
+                // Get post content
+                $.get('services.php',{f:'getModuleById', id: moduleId, list: listId},function(data) {
+                        var rsp = $(data).children('rsp')[0];
+                        
+                        if (rsp.attributes[0].value == 'ok') {
+                                var author = $(rsp).find('author').text();
+                                var details = $(rsp).find('details').text();
+                                var support = $(rsp).find('support').text();
+								var box = document.createElement('div');
+                                var bloc = document.createElement('ul');
+                                bloc.className = "mod-more";
+                                
+                                if (author) {
+                                        $(bloc).append($('<li class="module-author">'+dotclear.msg.module_author+' '+author+'</li>'));
+                                }
+                                if (details) {
+                                        var dd = '';
+                                        dd += '<a class="module-details" href="'+details+'">'+dotclear.msg.module_details+'</a>';
+                                        if (support) {
+                                                dd += ' - ';
+                                                dd += '<a class="module-support" href="'+support+'">'+dotclear.msg.module_support+'</a>';
+                                        }
+                                        $(bloc).append($('<li>'+dotclear.msg.module_help+' '+dd+'</li>'));
+                                }
+
+                                $(td).append($(box).addClass('two-boxes').append(bloc));
+                                
+                                var section = $(rsp).find('section').text();
+                                var tags = $(rsp).find('tags').text();
+                                
+								var boxb = document.createElement('div');
+                                var blocb = document.createElement('ul');
+                                blocb.className = "mod-more";
+                                
+                                if (section) {
+                                        $(blocb).append($('<li class="module-section">'+dotclear.msg.module_section+' '+section+'</li>'));
+                                }
+                                if (tags) {
+                                        $(blocb).append($('<li class="module-tags">'+dotclear.msg.module_tags+' '+tags+'</li>'));
+                                }
+                                $(td).append($(boxb).addClass('two-boxes').append(blocb));
+                        } else {
+                                alert($(rsp).find('message').text());
+                        }
+                });
+                
+                $(line).addClass('expand');
+                line.parentNode.insertBefore(tr,line.nextSibling);
+        }
+        else if (tr && tr.style.display == 'none' && ( action == 'toggle' || action == 'open' ) )
+        {
+                $(tr).css('display', 'table-row');
+                $(line).addClass('expand');
+                img.src = dotclear.img_minus_src;
+                img.alt = dotclear.img_minus_alt;
+        }
+        else if (tr && tr.style.display != 'none' && ( action == 'toggle' || action == 'close' ) )
+        {
+                $(tr).css('display', 'none');
+                $(line).removeClass('expand');
+                img.src = dotclear.img_plus_src;
+                img.alt = dotclear.img_plus_alt;
+        }
+        
+        parentTable = $(line).parents('table');
+        if( parentTable.find('tr.expand').length == parentTable.find('tr.line').length ) {
+                img = parentTable.find('tr:not(.line) th:first img');
+                img.attr('src',dotclear.img_minus_src);
+                img.attr('alt',dotclear.img_minus_alt);
+        }
+        
+        if( parentTable.find('tr.expand').length == 0 ) {
+                img = parentTable.find('tr:not(.line) th:first img');
+                img.attr('src',dotclear.img_plus_src);
+                img.attr('alt',dotclear.img_plus_alt);
+        }
+        
+};
+
+
 $(function() {
-	$('table.plugins form input[type=submit][name=delete]').click(function() {
-		var p_name = $('input[name=plugin_id]',$(this).parent()).val();
-		return window.confirm(dotclear.msg.confirm_delete_plugin.replace('%s',p_name));
-	});
+        $('table.modules.expandable tr:not(.line)').each(function() {
+                dotclear.modulesExpander(this,$('table.modules.expandable tr.line'));
+        });
+        $('table.modules.expandable tr.line').each(function() {
+                dotclear.moduleExpander(this);
+        });
 });
Index: admin/plugins.php
===================================================================
--- admin/plugins.php	(revision 2256)
+++ admin/plugins.php	(revision 2267)
@@ -13,188 +13,100 @@
 require dirname(__FILE__).'/../inc/admin/prepend.php';
 
-dcPage::checkSuper();
-
-$default_tab = !empty($_REQUEST['tab']) ? html::escapeHTML($_REQUEST['tab']) : 'plugins';
-
-$p_paths = explode(PATH_SEPARATOR, DC_PLUGINS_ROOT);
-$p_path = array_pop($p_paths);
-unset($p_paths);
-
-$is_writable = false;
-if (is_dir($p_path) && is_writeable($p_path)) {
-	$is_writable = true;
-	$p_path_pat = preg_quote($p_path,'!');
-}
-
-$plugin_id = !empty($_POST['plugin_id']) ? $_POST['plugin_id'] : null;
-
-if ($is_writable)
-{
-	# Delete plugin
-	if ($plugin_id && !empty($_POST['delete']))
-	{
-		try
-		{
-			if (empty($_POST['deactivated']))
-			{
-				if (!$core->plugins->moduleExists($plugin_id)) {
-					throw new Exception(__('No such plugin.'));
-				}
-				
-				$plugin = $core->plugins->getModules($plugin_id);
-				$plugin['id'] = $plugin_id;
-				
-				if (!preg_match('!^'.$p_path_pat.'!', $plugin['root'])) {
-					throw new Exception(__('You don\'t have permissions to delete this plugin.'));
-				}
-				
-				# --BEHAVIOR-- pluginBeforeDelete
-				$core->callBehavior('pluginsBeforeDelete', $plugin);
-				
-				$core->plugins->deleteModule($plugin_id);
-				
-				# --BEHAVIOR-- pluginAfterDelete
-				$core->callBehavior('pluginsAfterDelete', $plugin);
-			}
-			else
-			{
-				$core->plugins->deleteModule($plugin_id,true);
-			}
-
-			dcPage::addSuccessNotice(__('Plugin has been successfully deleted.'));
-			http::redirect('plugins.php');
-		}
-		catch (Exception $e)
-		{
-			$core->error->add($e->getMessage());
-		}
-	}
-	# Deactivate plugin
-	elseif ($plugin_id && !empty($_POST['deactivate']))
-	{
-		try
-		{
-			if (!$core->plugins->moduleExists($plugin_id)) {
-				throw new Exception(__('No such plugin.'));
-			}
-			
-			$plugin = $core->plugins->getModules($plugin_id);
-			$plugin['id'] = $plugin_id;
-			
-			if (!$plugin['root_writable']) {
-				throw new Exception(__('You don\'t have permissions to deactivate this plugin.'));
-			}
-			
-			# --BEHAVIOR-- pluginBeforeDeactivate
-			$core->callBehavior('pluginsBeforeDeactivate', $plugin);
-				
-			$core->plugins->deactivateModule($plugin_id);
-
-			# --BEHAVIOR-- pluginAfterDeactivate
-			$core->callBehavior('pluginsAfterDeactivate', $plugin);
-				
-			http::redirect('plugins.php');
-		}
-		catch (Exception $e)
-		{
-			$core->error->add($e->getMessage());
-		}
-	}
-	# Activate plugin
-	elseif ($plugin_id && !empty($_POST['activate']))
-	{
-		try
-		{
-			$p = $core->plugins->getDisabledModules();
-			if (!isset($p[$plugin_id])) {
-				throw new Exception(__('No such plugin.'));
-			}
-
-			# --BEHAVIOR-- pluginBeforeActivate
-			$core->callBehavior('pluginsBeforeActivate', $plugin_id);
-			
-			$core->plugins->activateModule($plugin_id);
-
-			# --BEHAVIOR-- pluginAfterActivate
-			$core->callBehavior('pluginsAfterActivate', $plugin_id);
-			
-			http::redirect('plugins.php');
-		}
-		catch (Exception $e)
-		{
-			$core->error->add($e->getMessage());
-		}
-	}
-	# Plugin upload
-	elseif ((!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file'])) ||
-		(!empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url'])))
-	{
-		try
-		{
-			if (empty($_POST['your_pwd']) || !$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 = $p_path.'/'.$_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 = $p_path.'/'.basename($url);
-				
-				try
-				{
-					$client = netHttp::initClient($url,$path);
-					$client->setUserAgent('Dotclear - http://www.dotclear.org/');
-					$client->useGzip(false);
-					$client->setPersistReferers(false);
-					$client->setOutput($dest);
-					$client->get($path);
-				}
-				catch( Exception $e)
-				{
-					throw new Exception(__('An error occurred while downloading the file.'));
-				}
-				
-				unset($client);
-			}
-
-			# --BEHAVIOR-- pluginBeforeAdd
-			$core->callBehavior('pluginsBeforeAdd', $plugin_id);
-						
-			$ret_code = $core->plugins->installPackage($dest,$core->plugins);
-
-			# --BEHAVIOR-- pluginAfterAdd
-			$core->callBehavior('pluginsAfterAdd', $plugin_id);
-			if ($ret_code == 2) {
-				dcPage::addSuccessNotice(__('Plugin has been successfully upgraded'));
-			} else {
-				dcPage::addSuccessNotice(__('Plugin has been successfully installed.'));
-			}
-			http::redirect('plugins.php');
-		}
-		catch (Exception $e)
-		{
-			$core->error->add($e->getMessage());
-			$default_tab = 'addplugin';
-		}
-	}
-}
-
-# Plugin install
-$plugins_install = $core->plugins->installModules();
-
-/* DISPLAY Main page
--------------------------------------------------------- */
+dcPage::check('admin');
+
+# -- "First time" settings setup --
+if ($core->blog->settings->system->plugins_allow_multi_install === null) {
+	$core->blog->settings->system->put(
+		'plugins_allow_multi_install', false, 'boolean', 'Allow multi-installation for plugins', true, true
+	);
+}
+if ($core->blog->settings->system->store_plugin_url === null) {
+	$core->blog->settings->system->put(
+		'store_plugin_url', 'http://update.dotaddict.org/dc2/plugins.xml', 'string', 'Plugins XML feed location', true, true
+	);
+}
+
+# -- Page helper --
+$list = new adminModulesList(
+	$core->plugins, 
+	DC_PLUGINS_ROOT, 
+	$core->blog->settings->system->store_plugin_url
+);
+
+$list::$allow_multi_install = $core->blog->settings->system->plugins_allow_multi_install;
+$list::$distributed_modules = array(
+	'aboutConfig',
+	'akismet',
+	'antispam',
+	'attachments',
+	'blogroll',
+	'blowupConfig',
+	'daInstaller',
+	'fairTrackbacks',
+	'importExport',
+	'maintenance',
+	'pages',
+	'pings',
+	'simpleMenu',
+	'tags',
+	'themeEditor',
+	'userPref',
+	'widgets'
+);
+
+# -- Display module configuration page --
+if ($list->setConfiguration()) {
+
+	# Get content before page headers
+	include $list->includeConfiguration();
+
+	# Gather content
+	$list->getConfiguration();
+
+	# Display page
+	dcPage::open(__('Plugins management'),
+
+		# --BEHAVIOR-- pluginsToolsHeaders
+		$core->callBehavior('pluginsToolsHeaders', $core, true),
+
+		dcPage::breadcrumb(
+			array(
+				html::escapeHTML($core->blog->name) => '',
+				__('Plugins management') => $list->getURL('',false),
+				'<span class="page-title">'.__('Plugin configuration').'</span>' => ''
+			))
+	);
+
+	# Display previously gathered content
+	$list->displayConfiguration();
+
+	dcPage::close();
+
+	# Stop reading code here
+	return;
+}
+
+# -- Execute actions --
+try {
+	$list->doActions('plugins');
+}
+catch (Exception $e) {
+	$core->error->add($e->getMessage());
+}
+
+# -- Plugin install --
+$plugins_install = null;
+if (!$core->error->flag()) {
+	$plugins_install = $core->plugins->installModules();
+}
+
+# -- Page header --
 dcPage::open(__('Plugins management'),
 	dcPage::jsLoad('js/_plugins.js').
-	dcPage::jsPageTabs($default_tab),
+	dcPage::jsPageTabs().
+
+	# --BEHAVIOR-- pluginsToolsHeaders
+	$core->callBehavior('pluginsToolsHeaders', $core, false),
+
 	dcPage::breadcrumb(
 		array(
@@ -204,196 +116,161 @@
 );
 
-if (!empty($_GET['removed'])) {
-	dcPage::success(__('Plugin has been successfully deleted.'));
-}
-if (!empty($_GET['added'])) {
-	dcPage::success(($_GET['added'] == 2 ? __('Plugin has been successfully upgraded') : __('Plugin has been successfully installed.')));
-}
-
-# Plugins install messages
-if (!empty($plugins_install['success']))
-{
-	echo '<div class="static-msg">'.__('Following plugins have been installed:').'<ul>';
+# -- Plugins install messages --
+if (!empty($plugins_install['success'])) {
+	echo 
+	'<div class="static-msg">'.__('Following plugins have been installed:').'<ul>';
+
 	foreach ($plugins_install['success'] as $k => $v) {
-		echo '<li>'.$k.'</li>';
-	}
-	echo '</ul></div>';
-}
-if (!empty($plugins_install['failure']))
-{
-	echo '<div class="error">'.__('Following plugins have not been installed:').'<ul>';
+		echo 
+		'<li>'.$k.'</li>';
+	}
+
+	echo 
+	'</ul></div>';
+}
+if (!empty($plugins_install['failure'])) {
+	echo 
+	'<div class="error">'.__('Following plugins have not been installed:').'<ul>';
+
 	foreach ($plugins_install['failure'] as $k => $v) {
-		echo '<li>'.$k.' ('.$v.')</li>';
-	}
-	echo '</ul></div>';
-}
-
-# List all active plugins
-echo '<p>'.__('Plugins add new functionalities to Dotclear. '.
-'Here you can activate or deactivate installed plugins.').'</p>';
-
-echo (!$core->plugins->moduleExists('daInstaller') ?
-	sprintf('<p><strong>'.__('You can find additional plugins for your blog on %s.').'</strong></p>',
-		'<a href="http://plugins.dotaddict.org/dc2/">Dotaddict</a>') :
-	sprintf('<p><strong>'.__('You can find additional plugins for your blog on %s or using the %s.').'</strong></p>',
-		'<a href="http://plugins.dotaddict.org/dc2/">Dotaddict</a>',
-		'<a href="plugin.php?p=daInstaller">'.__('DotAddict.org Installer').'</a>'));
-
-if ($is_writable) {
-	echo '<p>'.__('To install or upgrade a plugin you generally just need to upload it '.
-	'in "Install or upgrade a plugin" section.');
-} else {
-	echo '<p>'.__('To install or upgrade a plugin you just need to extract it in your plugins directory.');
-}
-echo '</p>';
+		echo 
+		'<li>'.$k.' ('.$v.')</li>';
+	}
+
+	echo 
+	'</ul></div>';
+}
+
+# -- Display modules lists --
+if ($core->auth->isSuperAdmin() && $list->isWritablePath()) {
+
+	# Updated modules from repo
+	$modules = $list->store->get(true);
+	if (!empty($modules)) {
+		echo 
+		'<div class="multi-part" id="update" title="'.html::escapeHTML(__('Update plugins')).'">'.
+		'<h3>'.html::escapeHTML(__('Update plugins')).'</h3>'.
+		'<p>'.sprintf(
+			__('There is one plugin to update available from repository.', 'There are %s plugins to update available from repository.', count($modules)),
+			count($modules)
+		).'</p>';
+
+		$list
+			->setList('plugin-update')
+			->setTab('update')
+			->setModules($modules)
+			->displayModules(
+				/*cols */		array('icon', 'name', 'version', 'current_version', 'desc'),
+				/* actions */	array('update')
+			);
+
+		echo
+		'<p class="info vertical-separator">'.sprintf(
+			__("Visit %s repository, the resources center for Dotclear."),
+			'<a href="http://dotaddict.org/dc2/plugins">Dotaddict</a>'
+			).
+		'</p>'.
+
+		'</div>';
+	}
+}
 
 echo
-'<div class="multi-part" id="plugins" title="'.__('Plugins').'">';
-
-$p_available = $core->plugins->getModules();
-uasort($p_available,create_function('$a,$b','return strcasecmp($a["name"],$b["name"]);'));
-if (!empty($p_available)) 
-{
+'<div class="multi-part" id="plugins" title="'.__('Installed plugins').'">';
+
+# Activated modules
+$modules = $list->modules->getModules();
+if (!empty($modules)) {
+	
+	echo 
+	'<h3>'.($core->auth->isSuperAdmin() ?__('Activated plugins') : __('Installed plugins')).'</h3>'.
+	'<p>'.__('You can configure and manage installed plugins from this list.').'</p>';
+
+	$list
+		->setList('plugin-activate')
+		->setTab('plugins')
+		->setModules($modules)
+		->displayModules(
+			/* cols */		array('expander', 'icon', 'name', 'config', 'version', 'desc', 'distrib'),
+			/* actions */	array('deactivate', 'delete', 'behavior')
+		);
+}
+
+# Deactivated modules
+if ($core->auth->isSuperAdmin()) {
+	$modules = $list->modules->getDisabledModules();
+	if (!empty($modules)) {
+		echo
+		'<h3>'.__('Deactivated plugins').'</h3>'.
+		'<p>'.__('Deactivated plugins are installed but not usable. You can activate them from here.').'</p>';
+
+		$list
+			->setList('plugin-deactivate')
+			->setTab('plugins')
+			->setModules($modules)
+			->displayModules(
+				/* cols */		array('icon', 'name', 'distrib'),
+				/* actions */	array('activate', 'delete')
+			);
+	}
+}
+
+echo 
+'</div>';
+
+if ($core->auth->isSuperAdmin() && $list->isWritablePath()) {
+
+	# New modules from repo
+	$search = $list->getSearch();
+	$modules = $search ? $list->store->search($search) : $list->store->get();
+
+	if (!empty($search) || !empty($modules)) {
+		echo
+		'<div class="multi-part" id="new" title="'.__('Add plugins').'">'.
+		'<h3>'.__('Add plugins from repository').'</h3>'.
+		'<p>'.__('Search and install plugins directly from repository.').'</p>';
+
+		$list
+			->setList('plugin-new')
+			->setTab('new')
+			->setModules($modules)
+			->displaySearch()
+			->displayIndex()
+			->displayModules(
+				/* cols */		array('expander', 'name', 'score', 'version', 'desc'),
+				/* actions */	array('install'),
+				/* nav limit */	true
+			);
+
+		echo
+		'<p class="info vertical-separator">'.sprintf(
+			__("Visit %s repository, the resources center for Dotclear."),
+			'<a href="http://dotaddict.org/dc2/plugins">Dotaddict</a>'
+			).
+		'</p>'.
+
+		'</div>';
+	}
+
+	# Add a new plugin
 	echo
-	'<h3>'.__('Activated plugins').'</h3>'.
-	'<div class="table-outer clear">'.
-	'<table class="plugins"><tr>'.
-	'<th>'.__('Plugin').'</th>'.
-	'<th class="nowrap">'.__('Version').'</th>'.
-	'<th class="nowrap">'.__('Details').'</th>'.
-	'<th class="nowrap">'.__('Action').'</th>'.
-	'</tr>';
-	
-	$distrib_plugins = array('aboutConfig','akismet','antispam','attachments','blogroll','blowupConfig','daInstaller',
-		'fairTrackbacks','importExport','maintenance','pages','pings','simpleMenu','tags','themeEditor','userPref','widgets');
-	$distrib_img = '<img src="images/dotclear_pw.png"'.
-		' alt="'.__('Plugin from official distribution').'" title="'.__('Plugin from official distribution').'" />';
-
-	foreach ($p_available as $k => $v)
-	{
-		$is_deletable = $is_writable && preg_match('!^'.$p_path_pat.'!',$v['root']);
-		$is_deactivable = $v['root_writable'];
-		$is_distrib = in_array($k, $distrib_plugins);
-		
-		echo
-		'<tr class="line wide">'.
-		'<td class="minimal nowrap"><strong>'.html::escapeHTML($k).'</strong></td>'.
-		'<td class="minimal">'.html::escapeHTML($v['version']).'</td>'.
-		'<td class="maximal'.($is_distrib ? ' distrib' : '').'"><strong>'.html::escapeHTML(__($v['name'])).'</strong> '.
-		'<br />'.html::escapeHTML(__($v['desc'])).($is_distrib ? ' '.$distrib_img : '').'</td>'.
-		'<td class="nowrap action">';
-		
-		if ($is_deletable || $is_deactivable)
-		{
-			echo
-			'<form action="plugins.php" method="post">'.
-			'<div>'.
-			$core->formNonce().
-			form::hidden(array('plugin_id'),html::escapeHTML($k)).
-			($is_deactivable ? '<input type="submit" name="deactivate" value="'.__('Deactivate').'" /> ' : '').
-			($is_deletable ? '<input type="submit" class="delete" name="delete" value="'.__('Delete').'" /> ' : '').
-			'</div>'.
-			'</form>';
-		}
-		
-		echo
-		'</td>'.
-		'</tr>';
-	}
+	'<div class="multi-part" id="addplugin" title="'.__('Install or upgrade manually').'">'.
+	'<h3>'.__('Add plugins from a package').'</h3>'.
+	'<p>'.__('You can install plugins by uploading or downloading zip files.').'</p>';
+
+	$list->displayManualForm();
+
 	echo
-	'</table></div>';
-}
-
-$p_disabled = $core->plugins->getDisabledModules();
-uksort($p_disabled,create_function('$a,$b','return strcasecmp($a,$b);'));
-if (!empty($p_disabled))
-{
-	echo
-	'<h3>'.__('Deactivated plugins').'</h3>'.
-	'<div class="clear table-outer">'.
-	'<table class="plugins"><tr>'.
-	'<th>'.__('Plugin').'</th>'.
-	'<th class="nowrap">'.__('Action').'</th>'.
-	'</tr>';
-	
-	foreach ($p_disabled as $k => $v)
-	{
-		$is_deletable = $is_writable && preg_match('!^'.$p_path_pat.'!',$v['root']);
-		$is_activable = $v['root_writable'];
-		
-		echo
-		'<tr class="line wide">'.
-		'<td class="maximal nowrap"><strong>'.html::escapeHTML($k).'</strong></td>'.
-		'<td class="nowrap action">';
-		
-		if ($is_deletable || $is_activable)
-		{
-			echo
-			'<form action="plugins.php" method="post">'.
-			'<div>'.
-			$core->formNonce().
-			form::hidden(array('plugin_id'),html::escapeHTML($k)).
-			form::hidden(array('deactivated'),1).
-			($is_activable ? '<input type="submit" name="activate" value="'.__('Activate').'" /> ' : '').
-			($is_deletable ? '<input type="submit" class="delete" name="delete" value="'.__('Delete').'" /> ' : '').
-			'</div>'.
-			'</form>';
-		}
-		
-		echo
-		'</td>'.
-		'</tr>';
-	}
-	echo
-	'</table></div>';
-}
-
-echo '</div>';
-
-# Add a new plugin
-echo
-'<div class="multi-part" id="addplugin" title="'.__('Install or upgrade a plugin').'">';
-
-if ($is_writable)
-{
-	echo '<p>'.__('You can install plugins by uploading or downloading zip files.').'</p>';
-	
-	# 'Upload plugin' form
-	echo
-	'<form method="post" action="plugins.php" id="uploadpkg" enctype="multipart/form-data" class="fieldset">'.
-	'<h3>'.__('Upload a zip file').'</h3>'.
-	'<p class="field"><label for="pkg_file" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Plugin zip file:').'</label> '.
-	'<input type="file" id="pkg_file" name="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 plugin').'" />'.
-	$core->formNonce().
-	'</p>'.
-	'</form>';
-	
-	# 'Fetch plugin' form
-	echo
-	'<form method="post" action="plugins.php" id="fetchpkg" class="fieldset">'.
-	'<h3>'.__('Download a zip file').'</h3>'.
-	'<p class="field"><label for="pkg_url" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Plugin 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 plugin').'" />'.
-	$core->formNonce().'</p>'.
-	'</form>';
-}
-else
-{
-	echo
-	'<p class="static-msg">'.
-	__('To enable this function, please give write access to your plugins directory.').
-	'</p>';
-}
-echo '</div>';
+	'</div>';
+}
 
 # --BEHAVIOR-- pluginsToolsTabs
-$core->callBehavior('pluginsToolsTabs',$core);
+$core->callBehavior('pluginsToolsTabs', $core);
+
+# -- Notice for super admin --
+if ($core->auth->isSuperAdmin() && !$list->isWritablePath()) {
+	echo 
+	'<p class="warning">'.__('Some functions are disabled, please give write access to your plugins directory to enable them.').'</p>';
+}
 
 dcPage::close();
-?>
Index: admin/services.php
===================================================================
--- admin/services.php	(revision 1699)
+++ admin/services.php	(revision 2227)
@@ -27,4 +27,5 @@
 $core->rest->addFunction('searchMeta',array('dcRestMethods','searchMeta'));
 $core->rest->addFunction('setSectionFold',array('dcRestMethods','setSectionFold'));
+$core->rest->addFunction('getModuleById',array('dcRestMethods','getModuleById'));
 
 $core->rest->serve();
@@ -442,6 +443,57 @@
 		return true;
 	}
-		
-	
+	
+	public static function getModuleById($core, $get, $post)
+	{
+		if (empty($get['id'])) {
+			throw new Exception('No module ID');
+		}
+		if (empty($get['list'])) {
+			throw new Exception('No list ID');
+		}
+
+		$id = $get['id'];
+		$list = $get['list'];
+		$module = array();
+
+		if ($list == 'plugin-activate') {
+			$modules = $core->plugins->getModules();
+			if (empty($modules) || !isset($modules[$id])) {
+				throw new Exception('Unknow module ID');
+			}
+			$module = $modules[$id];
+		}
+		elseif ($list == 'plugin-new') {
+			$store = new dcStore(
+				$core->plugins, 
+				$core->blog->settings->system->store_plugin_url
+			);
+			$store->check();
+
+			$modules = $store->get();
+			if (empty($modules) || !isset($modules[$id])) {
+				throw new Exception('Unknow module ID');
+			}
+			$module = $modules[$id];
+		}
+		else {
+			// behavior not implemented yet
+		}
+
+		if (empty($module)) {
+			throw new Exception('Unknow module ID');
+		}
+
+		$module = adminModulesList::sanitizeModule($id, $module);
+
+		$rsp = new xmlTag('module');
+		$rsp->id = $id;
+
+		foreach($module as $k => $v) {
+			$rsp->{$k}((string) $v);
+		}
+
+		return $rsp;
+	}
 }
 ?>
Index: admin/style/default.css
===================================================================
--- admin/style/default.css	(revision 2264)
+++ admin/style/default.css	(revision 2267)
@@ -158,5 +158,6 @@
 	-moz-box-sizing: border-box; 
 	-webkit-box-sizing: border-box; 
-	box-sizing: border-box;	
+	box-sizing: border-box;
+	text-align: left;
 	}
 .two-boxes {
@@ -1108,5 +1109,5 @@
 		}
 #help-button {
-	background: transparent url(../images/page_help.png) no-repeat 6px center;
+	background: transparent url(help-mini.png) no-repeat 6px center;
 	position: absolute;
 	top: 0;
@@ -1538,115 +1539,70 @@
 				}
 /* ------------------------------------------------------------------- blog_theme.php */
-#themes {
-	margin: 0;
-	width: 100%;
-	padding: 0;
-	}
-	.theme-details {
-		border-top: 1px solid #ccc;
-		padding: 12px;
-		display: inline-block;
-		vertical-align: top;
-		width: 284px;
-		}
-		.theme-details.current-theme {
-			background: #eef;
-			}
-		.theme-details:hover {
-			background: #f3f3f3;
-			}
-		.theme-shot img {
-			display: block;
-			border: 1px solid #ccc;
-			margin-bottom: 1.5em;
-			}
-		span.theme-desc {
-			display: block;
-			}
-		span.theme-version {
-			color: #676e78;
-			}
-		.theme-css {
-			display: block;
-			}
-	#themes-actions {
-		border-bottom: 1px solid #999;
-		margin-bottom: 3em;
-		}
-/* Themes list, JS version */
-#themes-wrapper {
-	display: table;
-	}
-	#themes-wrapper #themes {
-		display: table-cell;
-		vertical-align: top;
-		padding-left: 1em;
-		}
-	#theme-box {
-		display: table-cell;
-		vertical-align: top;
-		padding: 0;
-		width: 312px;
-		border: 1px solid #ccc;
-		border-radius: 3px;
-		}
-		#theme-box .theme-shot,
-		#theme-box .theme-info,
-		#theme-box .theme-actions {
-			background: #eef;
-			padding: 1em 16px;
-			margin: 0;
-			}
-		#theme-box .theme-shot img {
-			display: block;
-			width: 280px;
-			height: 245px;
-			border: 1px solid #ccc;
-			}
-		#theme-box h4 {
-			color: #000;
-			background: #eef;
-			}
-		#theme-box span.theme-version {
-			color: #676e78;
-			}
-		#theme-box span.theme-parent-ok {
-			color: #676e78;
-			}
-		#theme-box span.theme-parent-missing {
-			color: #c00;
-			font-weight:bold;
-			}
-		#theme-box .theme-actions {
-			border-bottom: 1px solid #ccc;
-			}
-#themes .theme-details-js {
+.box.theme {
+	margin: .5em;
+	padding: 1em 1em .5em 1em;
+	border: 1px solid #ccc;
+	}
+#theme-new .box.theme {
+	width: 280px;
+	}
+.module-name {
+	margin-bottom: .5em;
+	}
+.module-sshot img {
+	border: 1px solid #ddd;
+	padding: .5em;
+	background: #f7f7f7;
+	box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
+	}
+.module-actions {
+	margin-top: 1em;
+	}
+.bloc-toggler {
+	text-align: right;
+	}
+.bloc-toggler img {
+	opacity: .4;
+	}
+.bloc-toggler img:hover {
+	opacity: 1;
+	}
+span.module-version:before {
+	content: "- ";
+	}
+.toggle-bloc .mod-more {
+	display: block;
+	margin-left: 0;
+	}
+.box.current-theme {
+	width: 652px;
+	margin: .5em;
+	padding: 1em 0 .5em 1em;
+	background: #FCFDFD;
+	border: 1px solid #9AC123;
+	-webkit-border-radius: .5em;
+	border-radius: .5em;
+	min-height: 330px;	
+	}
+.current-theme .module-sshot img {
 	float: left;
-	width: 120px;
-	height: 150px;
-	margin: 0 12px 24px;
-	padding: 12px 12px 0;
-	text-align: center;
-	background: #f3f3f3;
-	border: 1px solid #ddd;
-	cursor: pointer;
-	border-radius: 4px;
-	}
-	#themes .theme-details-js label {
-		cursor: pointer;
-		}
-	#themes .theme-details-js.theme-selected {
-		background: #e7e7e7;
-		border: 1px solid #999;
-		}
-	#themes .theme-details-js .theme-shot img {
-		width: 120px;
-		height: 105px;
-		border: 1px solid #fff;
-		}
-	#themes a:focus div, #themes a:hover div,
-	#themes a:focus div.current-theme, #themes a:hover div.current-theme {
-		background: #bee74b;
-		}
+	margin-right: 2em;
+	margin-bottom: 1em;
+	}
+.current-theme .module-name {
+	color: #D33800;
+	margin-bottom: 1em;
+	font-size: 1.33em;
+	}
+.box.theme input {
+	margin-bottom: 1em;
+	}
+.current-actions {
+	width: auto;
+	overflow: hidden;
+	padding-top: 3em;
+	margin: 1.5em 0 2em;
+	background: transparent url(../images/minus-theme.png) no-repeat left top;
+	}
 /* ------------------------------------------------------------------ categories.php */
 #categories {
@@ -1954,11 +1910,52 @@
 		}
 /* -------------------------------------------------------------------- plugins.php */
-#plugins td.action {
+.modules td.module-actions, .modules td.module-icon {
 	vertical-align: middle;
 	}
-.distrib img {
+.modules td.module-icon img:last-child {
+	width: 16px;
+	height: 16px;
+	}
+.modules td.module-icon img.expand {
+	margin-bottom: 3px;
+	}
+.modules td.module-distrib img {
 	display: block;
 	float: right;
-	margin-top: -1em;
+	}
+.modules dt {
+	font-weight: bold;
+	}
+.modules a.module-details {
+	background: transparent url(search.png) no-repeat 2px 2px;
+	padding: 4px 4px 0 20px;
+	}
+.modules a.module-support {
+	background: transparent url(help12.png) no-repeat 2px 2px;
+	padding: 4px 4px 0 20px;
+	}
+#m_search {	
+	background: transparent url(search.png) no-repeat 4px center;
+	padding-left: 20px;
+	}
+.modules tr.expand, .modules td.expand {
+	background: #f7f7f7;
+	border-color: #bee74b;
+	}
+.modules tr.expand td:first-child {
+	font-weight: bold;
+	background: #DFE5E7;
+	}
+.modules td.expand {
+	padding: 0;
+	border-top: 1px solid #e5e5e5;
+	}
+.mod-more, .mod-more li {
+	margin: .25em 0 0 1em;
+	padding: 0;
+	list-style-type: none;
+	}
+.mod-more {
+	padding-top: .5em;
 	}
 /* ---------------------------------------------------------- post.php, page.php */
@@ -2383,7 +2380,10 @@
 	}
 .debug {
- 	background: #FF6;
+	background: #ff6;
 	padding: 3px 0.5em 2px;
- 	}
+	}
+input[type=submit].delete.debug, a.delete.debug {
+	background: #ff6;
+	}
 /* ---------------------------------------------- Couleurs ajoutées via javascript	
 /* color-picker.js */
Index: inc/admin/lib.dc.page.php
===================================================================
--- inc/admin/lib.dc.page.php	(revision 2225)
+++ inc/admin/lib.dc.page.php	(revision 2240)
@@ -580,4 +580,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',
@@ -667,4 +672,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";
Index: inc/admin/lib.moduleslist.php
===================================================================
--- inc/admin/lib.moduleslist.php	(revision 2261)
+++ inc/admin/lib.moduleslist.php	(revision 2261)
@@ -0,0 +1,1338 @@
+<?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_ADMIN_CONTEXT')) { return; }
+
+/**
+ * @ingroup DC_CORE
+ * @brief Helper for admin list of modules.
+ * @since 2.6
+
+ * Provides an object to parse XML feed of modules from a repository.
+ */
+class adminModulesList
+{
+	public $core;		/**< @var	object	dcCore instance */
+	public $modules;	/**< @var	object	dcModules instance */
+	public $store;		/**< @var	object	dcStore instance */
+
+	public static $allow_multi_install = false;		/**< @var	boolean	Work with multiple root directories */
+	public static $distributed_modules = array();	/**< @var	array	List of modules distributed with Dotclear */
+
+	protected $list_id = 'unknow';	/**< @var	string	Current list ID */
+	protected $data = array();		/**< @var	array	Current modules */
+
+	protected $config_module = '';	/**< @var	string	Module ID to configure */
+	protected $config_file = '';	/**< @var	string	Module path to configure */
+	protected $config_content = '';	/**< @var	string	Module configuration page content */
+
+	protected $path = false;			/**< @var	string	Modules root directory */
+	protected $path_writable = false;	/**< @var	boolean	Indicate if modules root directory is writable */
+	protected $path_pattern = false;	/**< @var	string	Directory pattern to work on */
+
+	protected $page_url = 'plugins.php';	/**< @var	string	Page URL */
+	protected $page_qs = '?';				/**< @var	string	Page query string */
+	protected $page_tab = '';				/**< @var	string	Page tab */
+	protected $page_redir = '';				/**< @var	string	Page redirection */
+
+	public static $nav_indexes = 'abcdefghijklmnopqrstuvwxyz0123456789'; /**< @var	string	Index list */
+	protected $nav_list = array();		/**< @var	array	Index list with special index */
+	protected $nav_special = 'other';	/**< @var	string	Text for other special index */
+
+	protected $sort_field = 'sname';	/**< @var	string	Field used to sort modules */
+	protected $sort_asc = true;			/**< @var	boolean	Sort order asc */
+
+	/**
+	 * Constructor.
+	 *
+	 * Note that this creates dcStore instance.
+	 *
+	 * @param	object	$modules		dcModules instance
+	 * @param	string	$modules_root	Modules root directories
+	 * @param	string	$xml_url		URL of modules feed from repository
+	 */
+	public function __construct(dcModules $modules, $modules_root, $xml_url)
+	{
+		$this->core = $modules->core;
+		$this->modules = $modules;
+		$this->store = new dcStore($modules, $xml_url);
+
+		$this->setPath($modules_root);
+		$this->setIndex(__('other'));
+	}
+
+	/**
+	 * Begin a new list.
+	 *
+	 * @param	string	$id		New list ID
+	 * @return	adminModulesList self instance
+	 */
+	public function setList($id)
+	{
+		$this->data = array();
+		$this->page_tab = '';
+		$this->list_id = $id;
+
+		return $this;
+	}
+
+	/**
+	 * Get list ID.
+	 *
+	 * @return	List ID
+	 */
+	public function getList()
+	{
+		return $this->list_id;
+	}
+
+	/// @name Modules root directory methods
+	//@{
+	/**
+	 * Set path info.
+	 *
+	 * @param	string	$root		Modules root directories
+	 * @return	adminModulesList self instance
+	 */
+	protected function setPath($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;
+	}
+
+	/**
+	 * Get modules root directory.
+	 *
+	 * @return	Path to work on
+	 */
+	public function getPath()
+	{
+		return $this->path;
+	}
+
+	/**
+	 * Check if modules root directory is writable.
+	 *
+	 * @return	True if directory is writable
+	 */
+	public function isWritablePath()
+	{
+		return $this->path_writable;
+	}
+
+	/**
+	 * Check if root directory of a module is deletable.
+	 *
+	 * @param	string	$root		Module root directory
+	 * @return	True if directory is delatable
+	 */
+	public function isDeletablePath($root)
+	{
+		return $this->path_writable 
+			&& (preg_match('!^'.$this->path_pattern.'!', $root) || defined('DC_DEV') && DC_DEV) 
+			&& $this->core->auth->isSuperAdmin();
+	}
+	//@}
+
+	/// @name Page methods
+	//@{
+	/**
+	 * Set page base URL.
+	 *
+	 * @param	string	$url		Page base URL
+	 * @return	adminModulesList self instance
+	 */
+	public function setURL($url)
+	{
+		$this->page_qs = strpos('?', $url) ? '&' : '?';
+		$this->page_url = $url;
+
+		return $this;
+	}
+
+	/**
+	 * Get page URL.
+	 *
+	 * @param	string|array	$queries	Additionnal query string
+	 * @param	booleany	$with_tab		Add current tab to URL end
+	 * @return	Clean page URL
+	 */
+	public function getURL($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 : '');
+	}
+
+	/**
+	 * Set page tab.
+	 *
+	 * @param	string	$tab		Page tab
+	 * @return	adminModulesList self instance
+	 */
+	public function setTab($tab)
+	{
+		$this->page_tab = $tab;
+
+		return $this;
+	}
+
+	/**
+	 * Get page tab.
+	 *
+	 * @return	Page tab
+	 */
+	public function getTab()
+	{
+		return $this->page_tab;
+	}
+
+	/**
+	 * Set page redirection.
+	 *
+	 * @param	string	$default		Default redirection
+	 * @return	adminModulesList self instance
+	 */
+	public function setRedir($default='')
+	{
+		$this->page_redir = empty($_REQUEST['redir']) ? $default : $_REQUEST['redir'];
+
+		return $this;
+	}
+
+	/**
+	 * Get page redirection.
+	 *
+	 * @return	Page redirection
+	 */
+	public function getRedir()
+	{
+		return empty($this->page_redir) ? $this->getURL() : $this->page_redir;
+	}
+	//@}
+
+	/// @name Search methods
+	//@{
+	/**
+	 * Get search query.
+	 *
+	 * @return	Search query
+	 */
+	public function getSearch()
+	{
+		$query = !empty($_REQUEST['m_search']) ? trim($_REQUEST['m_search']) : null;
+		return strlen($query) > 2 ? $query : null;
+	}
+
+	/**
+	 * Display searh form.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function displaySearch()
+	{
+		$query = $this->getSearch();
+
+		if (empty($this->data) && $query === null) {
+			return $this;
+		}
+
+		echo 
+		'<form action="'.$this->getURL().'" 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->getURL().'" 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->data)), 
+				count($this->data), html::escapeHTML($query)
+				).
+			'</p>';
+		}
+		return $this;
+	}
+	//@}
+
+	/// @name Navigation menu methods
+	//@{
+	/**
+	 * Set navigation special index.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function setIndex($str)
+	{
+		$this->nav_special = (string) $str;
+		$this->nav_list = array_merge(str_split(self::$nav_indexes), array($this->nav_special));
+
+		return $this;
+	}
+
+	/**
+	 * Get index from query.
+	 *
+	 * @return	Query index or default one
+	 */
+	public function getIndex()
+	{
+		return isset($_REQUEST['m_nav']) && in_array($_REQUEST['m_nav'], $this->nav_list) ? $_REQUEST['m_nav'] : $this->nav_list[0];
+	}
+
+	/**
+	 * Display navigation by index menu.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function displayIndex()
+	{
+		if (empty($this->data) || $this->getSearch() !== null) {
+			return $this;
+		}
+
+		# Fetch modules required field
+		$indexes = array();
+		foreach ($this->data 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->getIndex() == $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->getURL('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;
+	}
+	//@}
+
+	/// @name Sort methods
+	//@{
+	/**
+	 * Set default sort field.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function setSort($field, $asc=true)
+	{
+		$this->sort_field = $field;
+		$this->sort_asc = (boolean) $asc;
+
+		return $this;
+	}
+
+	/**
+	 * Get sort field from query.
+	 *
+	 * @return	Query sort field or default one
+	 */
+	public function getSort()
+	{
+		return !empty($_REQUEST['m_sort']) ? $_REQUEST['m_sort'] : $this->sort_field;
+	}
+
+	/**
+	 * Display sort field form.
+	 *
+	 * @note	This method is not implemented yet
+	 * @return	adminModulesList self instance
+	 */
+	public function displaySort()
+	{
+		//
+
+		return $this;
+	}
+	//@}
+
+	/// @name Modules methods
+	//@{
+	/**
+	 * Set modules and sanitize them.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function setModules($modules)
+	{
+		$this->data = array();
+		if (!empty($modules) && is_array($modules)) {
+			foreach($modules as $id => $module) {
+				$this->data[$id] = self::sanitizeModule($id, $module);
+			}
+		}
+		return $this;
+	}
+
+	/**
+	 * Get modules currently set.
+	 *
+	 * @return	Array of modules
+	 */
+	public function getModules()
+	{
+		return $this->data;
+	}
+
+	/**
+	 * Sanitize a module.
+	 *
+	 * This clean infos of a module by adding default keys 
+	 * and clean some of them, sanitize module can safely 
+	 * be used in lists.
+	 *
+	 * @return	Array of the module informations
+	 */
+	public static function sanitizeModule($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' 			=> '',
+				'score'				=> 0
+			),
+			# Module's values
+			$module,
+			# Clean up values
+			array(
+				'id' 				=> $id,
+				'sid' 				=> self::sanitizeString($id),
+				'label' 			=> $label,
+				'name' 				=> $name,
+				'sname' 			=> self::sanitizeString($name)
+			)
+		);
+	}
+
+	/**
+	 * Check if a module is part of the distribution.
+	 *
+	 * @param	string	$id		Module root directory
+	 * @return	True if module is part of the distribution
+	 */
+	public static function isDistributedModule($id)
+	{
+		$distributed_modules = self::$distributed_modules;
+
+		return is_array($distributed_modules) && in_array($id, $distributed_modules);
+	}
+
+	/**
+	 * Sort modules list by specific field.
+	 *
+	 * @param	string	$module		Array of modules
+	 * @param	string	$field		Field to sort from
+	 * @param	bollean	$asc		Sort asc if true, else decs
+	 * @return	Array of sorted 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;
+	}
+
+	/**
+	 * Display list of modules.
+	 *
+	 * @param	array	$cols		List of colones (module field) to display
+	 * @param	array	$actions	List of predefined actions to show on form
+	 * @param	boolean	$nav_limit	Limit list to previously selected index
+	 * @return	adminModulesList self instance
+	 */
+	public function displayModules($cols=array('name', '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('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
+			echo 
+			'<th class="nowrap">'.__('Score').'</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->getSort();
+
+		# Sort modules by $sort_field (default sname)
+		$modules = $this->getSearch() === null ?
+			self::sortModules($this->data, $sort_field, $this->sort_asc) :
+			$this->data;
+
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearch() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getIndex() != $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->getURL('module='.$id.'&conf=1').'" title"'.sprintf(__('Configure module "%s"'), html::escapeHTML($module['name'])).'">'.html::escapeHTML($module['name']).'</a>' : 
+				html::escapeHTML($module['name'])
+			).'</td>';
+
+			# Display score only for debug purpose
+			if (in_array('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
+				echo 
+				'<td class="module-version nowrap count"><span class="debug">'.$module['score'].'</span></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()) {
+				$buttons = $this->getActions($id, $module, $actions);
+
+				echo 
+				'<td class="module-actions nowrap">'.
+
+				'<form action="'.$this->getURL().'" method="post">'.
+				'<div>'.
+				$this->core->formNonce().
+				form::hidden(array('module'), html::escapeHTML($id)).
+
+				implode(' ', $buttons).
+
+				'</div>'.
+				'</form>'.
+
+				'</td>';
+			}
+
+			echo 
+			'</tr>';
+
+			$count++;
+		}
+		echo 
+		'</table></div>';
+
+		if(!$count && $this->getSearch() === null) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Get action buttons to add to modules list.
+	 *
+	 * @param	string	$id			Module ID
+	 * @param	array	$module		Module info
+	 * @param	array	$actions	Actions keys
+	 * @return	Array of actions buttons
+	 */
+	protected function getActions($id, $module, $actions)
+	{
+		$submits = array();
+
+		# Use loop to keep requested order
+		foreach($actions as $action) {
+			switch($action) {
+
+				# Deactivate
+				case 'activate': if ($module['root_writable']) {
+					$submits[] = 
+					'<input type="submit" name="activate" value="'.__('Activate').'" />';
+				} break;
+
+				# Activate
+				case 'deactivate': if ($module['root_writable']) {
+					$submits[] = 
+					'<input type="submit" name="deactivate" value="'.__('Deactivate').'" class="reset" />';
+				} break;
+
+				# Delete
+				case 'delete': if ($this->isDeletablePath($module['root'])) {
+					$dev = !preg_match('!^'.$this->path_pattern.'!', $module['root']) && defined('DC_DEV') && DC_DEV ? ' debug' : '';
+					$submits[] = 
+					'<input type="submit" class="delete '.$dev.'" name="delete" value="'.__('Delete').'" />';
+				} break;
+
+				# Install (from store)
+				case 'install': if ($this->path_writable) {
+					$submits[] = 
+					'<input type="submit" name="install" value="'.__('Install').'" />';
+				} break;
+
+				# Update (from store)
+				case 'update': if ($this->path_writable) {
+					$submits[] = 
+					'<input type="submit" name="update" value="'.__('Update').'" />';
+				} break;
+
+				# Behavior
+				case 'behavior':
+
+					# --BEHAVIOR-- adminModulesListGetActions
+					$tmp = $this->core->callBehavior('adminModulesListGetActions', $this, $id, $module);
+
+					if (!empty($tmp)) {
+						$submits[] = $tmp;
+					}
+				break;
+			}
+		}
+
+
+		return $submits;
+	}
+
+	/**
+	 * Execute POST action.
+	 *
+	 * @note	Set a notice on success through dcPage::addSuccessNotice
+	 * @throw	Exception	Module not find or command failed
+	 * @param	string	$prefix		Prefix used on behaviors
+	 * @return	Null
+	 */
+	public function doActions($prefix)
+	{
+		if (empty($_POST) || !empty($_REQUEST['conf']) 
+		|| !$this->core->auth->isSuperAdmin() || !$this->isWritablePath()) {
+			return null;
+		}
+
+		# List actions
+		if (!empty($_POST['module'])) {
+
+			$id = $_POST['module'];
+
+			if (!empty($_POST['activate'])) {
+
+				$enabled = $this->modules->getDisabledModules();
+				if (!isset($enabled[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				# --BEHAVIOR-- moduleBeforeActivate
+				$this->core->callBehavior($prefix.'BeforeActivate', $id);
+
+				$this->modules->activateModule($id);
+
+				# --BEHAVIOR-- moduleAfterActivate
+				$this->core->callBehavior($prefix.'AfterActivate', $id);
+
+				dcPage::addSuccessNotice(__('Module has been successfully activated.'));
+				http::redirect($this->getURL());
+			}
+
+			elseif (!empty($_POST['deactivate'])) {
+
+				if (!$this->modules->moduleExists($id)) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $this->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);
+
+				$this->modules->deactivateModule($id);
+
+				# --BEHAVIOR-- moduleAfterDeactivate
+				$this->core->callBehavior($prefix.'AfterDeactivate', $module);
+
+				dcPage::addSuccessNotice(__('Module has been successfully deactivated.'));
+				http::redirect($this->getURL());
+			}
+
+			elseif (!empty($_POST['delete'])) {
+
+				$disabled = $this->modules->getDisabledModules();
+				if (!isset($disabled[$id])) {
+
+					if (!$this->modules->moduleExists($id)) {
+						throw new Exception(__('No such module.'));
+					}
+
+					$module = $this->modules->getModules($id);
+					$module['id'] = $id;
+
+					if (!$this->isDeletablePath($module['root'])) {
+						throw new Exception(__("You don't have permissions to delete this module."));
+					}
+
+					# --BEHAVIOR-- moduleBeforeDelete
+					$this->core->callBehavior($prefix.'BeforeDelete', $module);
+
+					$this->modules->deleteModule($id);
+
+					# --BEHAVIOR-- moduleAfterDelete
+					$this->core->callBehavior($prefix.'AfterDelete', $module);
+				}
+				else {
+					$this->modules->deleteModule($id, true);
+				}
+
+				dcPage::addSuccessNotice(__('Module has been successfully deleted.'));
+				http::redirect($this->getURL());
+			}
+
+			elseif (!empty($_POST['install'])) {
+
+				$updated = $this->store->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 = $this->store->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterAdd
+				$this->core->callBehavior($prefix.'AfterAdd', $module);
+
+				dcPage::addSuccessNotice($ret_code == 2 ?
+					__('Module has been successfully updated.') :
+					__('Module has been successfully installed.')
+				);
+				http::redirect($this->getURL());
+			}
+
+			elseif (!empty($_POST['update'])) {
+
+				$updated = $this->store->get();
+				if (!isset($updated[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				if (!$this->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);
+
+				$this->store->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterUpdate
+				$this->core->callBehavior($prefix.'AfterUpdate', $module);
+
+				dcPage::addSuccessNotice(__('Module has been successfully updated.'));
+				http::redirect($this->getURL());
+			}
+			else {
+
+				# --BEHAVIOR-- adminModulesListDoActions
+				$this->core->callBehavior('adminModulesListDoActions', $this, $id, $prefix);
+
+			}
+		}
+		# 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);
+				$this->store->download($url, $dest);
+			}
+
+			# --BEHAVIOR-- moduleBeforeAdd
+			$this->core->callBehavior($prefix.'BeforeAdd', null);
+
+			$ret_code = $this->store->install($dest);
+
+			# --BEHAVIOR-- moduleAfterAdd
+			$this->core->callBehavior($prefix.'AfterAdd', null);
+
+			dcPage::addSuccessNotice($ret_code == 2 ?
+				__('Module has been successfully updated.') :
+				__('Module has been successfully installed.')
+			);
+			http::redirect($this->getURL().'#'.$prefix);
+		}
+
+		return null;
+	}
+
+	/**
+	 * Display tab for manual installation.
+	 *
+	 * @return	adminModulesList self instance
+	 */
+	public function displayManualForm()
+	{
+		if (!$this->core->auth->isSuperAdmin() || !$this->isWritablePath()) {
+			return null;
+		}
+
+		# 'Upload module' form
+		echo
+		'<form method="post" action="'.$this->getURL().'" 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').'" />'.
+		$this->core->formNonce().'</p>'.
+		'</form>';
+
+		# 'Fetch module' form
+		echo
+		'<form method="post" action="'.$this->getURL().'" 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').'" />'.
+		$this->core->formNonce().'</p>'.
+		'</form>';
+
+		return $this;
+	}
+	//@}
+
+	/// @name Module configuration methods
+	//@{
+	/**
+	 * Prepare module configuration.
+	 *
+	 * We need to get configuration content in three steps
+	 * and out of this class to keep backward compatibility.
+	 *
+	 * if ($xxx->setConfiguration()) {
+	 *	include $xxx->includeConfiguration();
+	 * }
+	 * $xxx->getConfiguration();
+	 * ... [put here page headers and other stuff]
+	 * $xxx->displayConfiguration();
+	 *
+	 * @param	string	$id		Module to work on or it gather through REQUEST
+	 * @return	True if config set
+	 */
+	public function setConfiguration($id=null)
+	{
+		if (empty($_REQUEST['conf']) || empty($_REQUEST['module']) && !$id) {
+			return false;
+		}
+		
+		if (!empty($_REQUEST['module']) && empty($id)) {
+			$id = $_REQUEST['module'];
+		}
+
+		if (!$this->modules->moduleExists($id)) {
+			$core->error->add(__('Unknow module ID'));
+			return false;
+		}
+
+		$module = $this->modules->getModules($id);
+		$module = self::sanitizeModule($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;
+	}
+
+	/**
+	 * Get path of module configuration file.
+	 *
+	 * @note Required previously set file info
+	 * @return Full path of config file or null
+	 */
+	public function includeConfiguration()
+	{
+		if (!$this->config_file) {
+			return null;
+		}
+		$this->setRedir($this->getURL().'#plugins');
+
+		ob_start();
+
+		return $this->config_file;
+	}
+
+	/**
+	 * Gather module configuration file content.
+	 *
+	 * @note Required previously file inclusion
+	 * @return True if content has been captured
+	 */
+	public function getConfiguration()
+	{
+		if ($this->config_file) {
+			$this->config_content = ob_get_contents();
+		}
+
+		ob_end_clean();
+
+		return !empty($this->file_content);
+	}
+
+	/**
+	 * Display module configuration form.
+	 *
+	 * @note Required previously gathered content
+	 * @return	adminModulesList self instance
+	 */
+	public function displayConfiguration()
+	{
+		if ($this->config_file) {
+
+			if (!$this->config_module['standalone_config']) {
+				echo
+				'<form id="module_config" action="'.$this->getURL('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->getRedir().'">'.__('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']).
+				form::hidden('redir', $this->getRedir()).
+				$this->core->formNonce().'</p>'.
+				'</form>';
+			}
+		}
+
+		return $this;
+	}
+	//@}
+
+	/**
+	 * Helper to sanitize a string.
+	 *
+	 * Used for search or id.
+	 *
+	 * @param	string	$str		String to sanitize
+	 * @return	Sanitized string
+	 */
+	public static function sanitizeString($str)
+	{
+		return preg_replace('/[^A-Za-z0-9\@\#+_-]/', '', strtolower($str));
+	}
+}
+
+/**
+ * @ingroup DC_CORE
+ * @brief Helper to manage list of themes.
+ * @since 2.6
+ */
+class adminThemesList extends adminModulesList
+{
+	protected $page_url = 'blog_theme.php';
+
+	public function displayModules($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->getSort();
+
+		# Sort modules by id
+		$modules = $this->getSearch() === null ?
+			self::sortModules($this->data, $sort_field, $this->sort_asc) :
+			$this->data;
+
+		$res = '';
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearch() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getIndex() != $char) {
+					continue;
+				}
+			}
+
+			$current = $this->core->blog->settings->system->theme == $id && $this->modules->moduleExists($id);
+			$distrib = self::isDistributedModule($id) ? ' dc-box' : '';
+
+			$line = 
+			'<div class="box '.($current ? 'medium current-theme' : 'small theme').$distrib.'">';
+
+			if (in_array('name', $cols)) {
+				$line .= 
+				'<h4 class="module-name">'.html::escapeHTML($module['name']).'</h4>';
+			}
+
+			# Display score only for debug purpose
+			if (in_array('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
+				$line .= 
+				'<p class="module-score debug">'.sprintf(__('Score: %s'), $module['score']).'</p>';
+			}
+
+			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->getURL('shot='.rawurlencode($id));
+				}
+				# Default screenshot
+				else {
+					$sshot = 'images/noscreenshot.png';
+				}
+
+				$line .= 
+				'<div class="module-sshot"><img src="'.$sshot.'" alt="'.
+				sprintf(__('%s screenshot.'), html::escapeHTML($module['name'])).'" /></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('current_version', $cols)) {
+				$line .= 
+				'<span class="module-current-version">'.sprintf(__('(current version %s)'),html::escapeHTML($module['current_version'])).'</span> ';
+			}
+
+			if (in_array('parent', $cols) && !empty($module['parent'])) {
+				if ($this->modules->moduleExists($module['parent'])) {
+					$line .= 
+					'<span class="module-parent-ok">'.sprintf(__('(built on "%s")'),html::escapeHTML($module['parent'])).'</span> ';
+				}
+				else {
+					$line .= 
+					'<span class="module-parent-missing">'.sprintf(__('(requires "%s")'),html::escapeHTML($module['parent'])).'</span> ';
+				}
+			}
+
+			$has_details = in_array('details', $cols) && !empty($module['details']);
+			$has_support = in_array('support', $cols) && !empty($module['support']);
+			if ($has_details || $has_support) {
+				$line .=
+				'<span class="mod-more">';
+
+				if ($has_details) {
+					$line .= 
+					'<a class="module-details" href="'.$module['details'].'">'.__('Details').'</a>';
+				}
+
+				if ($has_support) {
+					$line .= 
+					' - <a class="module-support" href="'.$module['support'].'">'.__('Support').'</a>';
+				}
+
+				$line .=
+				'</span>';
+			}
+
+			$line .= 
+			'</p>'.
+			'</div>';
+
+			$line .= 
+			'<div class="module-actions toggle-bloc">';
+			
+			# Plugins actions
+			if ($current) {
+
+				# _GET actions
+				if (file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/style.css')) {
+					$theme_url = preg_match('#^http(s)?://#', $this->core->blog->settings->system->themes_url) ?
+						http::concatURL($this->core->blog->settings->system->themes_url, '/'.$id) :
+						http::concatURL($this->core->blog->url, $this->core->blog->settings->system->themes_url.'/'.$id);
+					$line .= 
+					'<p><a href="'.$theme_url.'/style.css">'.__('View stylesheet').'</a></p>';
+				}
+
+				$line .= '<div class="current-actions">';
+
+				if (file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/_config.php')) {
+					$line .= 
+					'<p><a href="'.$this->getURL('module='.$id.'&conf=1', false).'" class="button">'.__('Configure theme').'</a></p>';
+				}
+
+				# --BEHAVIOR-- adminCurrentThemeDetails
+				$line .= 
+				$this->core->callBehavior('adminCurrentThemeDetails', $this->core, $id, $module);
+
+				$line .= '</div>';
+			}
+
+			# _POST actions
+			if (!empty($actions)) {
+				$line .=
+				'<form action="'.$this->getURL().'" method="post">'.
+				'<div>'.
+				$this->core->formNonce().
+				form::hidden(array('module'), html::escapeHTML($id)).
+
+				implode(' ', $this->getActions($id, $module, $actions)).
+ 
+				'</div>'.
+				'</form>';
+			}
+
+			$line .= 
+			'</div>';
+
+			$line .=
+			'</div>';
+
+			$count++;
+
+			$res = $current ? $line.$res : $res.$line;
+		}
+		echo 
+		$res.
+		'</div>';
+
+		if(!$count && $this->getSearch() === null) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+	}
+
+	protected function getActions($id, $module, $actions)
+	{
+		$submits = array();
+
+		$this->core->blog->settings->addNamespace('system');
+		if ($id != $this->core->blog->settings->system->theme) {
+
+			# Select theme to use on curent blog
+			if (in_array('select', $actions) && $this->path_writable) {
+				$submits[] = 
+				'<input type="submit" name="select" value="'.__('Use this one').'" />';
+			}
+		}
+
+		return array_merge(
+			$submits,
+			parent::getActions($id, $module, $actions)
+		);
+	}
+
+	public function doActions($prefix)
+	{
+		if (!empty($_POST) && empty($_REQUEST['conf']) && $this->isWritablePath()) {
+
+			# Select theme to use on curent blog
+			if (!empty($_POST['module']) && !empty($_POST['select'])) {
+				$id = $_POST['module'];
+
+				if (!$this->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();
+
+				dcPage::addSuccessNotice(__('Module has been successfully selected.'));
+				http::redirect($this->getURL().'#themes');
+			}
+		}
+
+		return parent::doActions($prefix);
+	}
+}
Index: inc/core/class.dc.store.parser.php
===================================================================
--- inc/core/class.dc.store.parser.php	(revision 2222)
+++ inc/core/class.dc.store.parser.php	(revision 2222)
@@ -0,0 +1,106 @@
+<?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
+@since 2.6
+
+Provides an object to parse XML feed of modules from a repository.
+*/
+class dcStoreParser
+{
+	/** @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	$data		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.store.php
===================================================================
--- inc/core/class.dc.store.php	(revision 2222)
+++ inc/core/class.dc.store.php	(revision 2222)
@@ -0,0 +1,287 @@
+<?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
+@since 2.6
+
+Provides an object to parse XML feed of modules from repository.
+*/
+class dcStore
+{
+	/** @var	object	dcCore instance */
+	public $core;
+	/** @var	object	dcModules instance */
+	public $modules;
+
+	/** @var	array	Modules fields to search on and their weighting */
+	public static $weighting = array('id' => 10, 'name' => 8, 'author' => 6, 'tags' => 4, 'desc' => 2);
+
+	/** @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);
+
+		$this->check();
+	}
+
+	/**
+	 * 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 = dcStoreReader::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 accuracy.
+	 *
+	 * @param	string	$pattern	String to search
+	 * @return	array	Match modules
+	 */
+	public function search($pattern)
+	{
+		$result = array();
+
+		# Split query into small clean words
+		if (!($patterns = self::patternize($pattern))) {
+			return $result;
+		}
+
+		# For each modules
+		foreach ($this->data['new'] as $id => $module) {
+			$module['id'] = $id;
+
+			# Loop through required module fields
+			foreach(self::$weighting as $field => $weight) {
+
+				# Skip fields which not exsist on module
+				if (empty($module[$field])) {
+					continue;
+				}
+
+				# Split field value into small clean word
+				if (!($subjects = self::patternize($module[$field]))) {
+					continue;
+				}
+
+				# 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 score by matches count * field weight
+				$sorter[$id] += $nb * $weight;
+				$result[$id]['score'] = $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;
+	}
+
+	/**
+	 * Split and clean pattern.
+	 *
+	 * @param	string	$str		String to sanitize
+	 * @return	array	Array of cleaned pieces of string or false if none
+	 */
+	public static function patternize($str)
+	{
+		$arr = array();
+
+		foreach(explode(' ', $str) as $_) {
+			$_ = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $_));
+			if (strlen($_) > 2) {
+				$arr[] = $_;
+			}
+		}
+
+		return empty($arr) ? false : $arr;
+	}
+
+	/**
+	 * 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.store.reader.php
===================================================================
--- inc/core/class.dc.store.reader.php	(revision 2222)
+++ inc/core/class.dc.store.reader.php	(revision 2222)
@@ -0,0 +1,267 @@
+<?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
+@since 2.6
+
+Provides an object to parse XML feed of modules from repository.
+This class extends clearbricks netHttp class.
+*/
+class dcStoreReader 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	dcStore 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 dcStoreParser($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 repository.
+	 *
+	 * @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 dcStoreParser($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 2236)
+++ inc/prepend.php	(revision 2240)
@@ -46,4 +46,7 @@
 $__autoload['dcWorkspace']			= dirname(__FILE__).'/core/class.dc.workspace.php';
 $__autoload['dcPrefs']				= dirname(__FILE__).'/core/class.dc.prefs.php';
+$__autoload['dcStore']			= dirname(__FILE__).'/core/class.dc.store.php';
+$__autoload['dcStoreReader']		= dirname(__FILE__).'/core/class.dc.store.reader.php';
+$__autoload['dcStoreParser']		= dirname(__FILE__).'/core/class.dc.store.parser.php';
 $__autoload['dcFavorites']			= dirname(__FILE__).'/admin/class.dc.favorites.php';
 
@@ -62,4 +65,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: plugins/maintenance/_admin.php
===================================================================
--- plugins/maintenance/_admin.php	(revision 2177)
+++ plugins/maintenance/_admin.php	(revision 2196)
@@ -29,4 +29,5 @@
 $core->addBehavior('adminAfterDashboardOptionsUpdate',	array('dcMaintenanceAdmin',	'adminAfterDashboardOptionsUpdate'));
 $core->addBehavior('adminPageHelpBlock',	array('dcMaintenanceAdmin',	'adminPageHelpBlock'));
+$core->addBehavior('pluginsToolsHeaders',	array('dcMaintenanceAdmin',	'pluginsToolsHeaders'));
 
 /**
@@ -223,5 +224,4 @@
 		$core->auth->user_prefs->maintenance->put('dashboard_item', !empty($_POST['maintenance_dashboard_item']), 'boolean');
 	}
-
 
 	/**
@@ -294,3 +294,17 @@
 		}
 	}
+
+	/**
+	 * Add javascript for plugin configuration.
+	 *
+	 * @param	$core	<b>dcCore</b>	dcCore instance
+	 * @param	$module	<b>mixed</b>	Module ID or false if none
+	 * @return	<b>string</b>	Header code for js inclusion
+	 */
+	public static function pluginsToolsHeaders($core, $module)
+	{
+		if ($module == 'maintenance') {
+			return dcPage::jsLoad('index.php?pf=maintenance/js/settings.js');
+		}
+	}
 }
Index: plugins/maintenance/_config.php
===================================================================
--- plugins/maintenance/_config.php	(revision 2227)
+++ plugins/maintenance/_config.php	(revision 2227)
@@ -0,0 +1,100 @@
+<?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_CONTEXT_MODULE')) { return; }
+
+$core->blog->settings->addNamespace('maintenance');
+$maintenance = new dcMaintenance($core);
+$tasks = $maintenance->getTasks();
+
+$combo_ts = array(
+	__('Never') 			=> 0,
+	__('Every week') 		=> 604800,
+	__('Every two weeks') 	=> 1209600,
+	__('Every month') 		=> 2592000,
+	__('Every two months') 	=> 5184000
+);
+
+if (!empty($_POST['save'])) {
+
+	try {
+		$core->blog->settings->maintenance->put(
+			'plugin_message', 
+			!empty($_POST['settings_plugin_message']), 
+			'boolean', 
+			'Display alert message of late tasks on plugin page', 
+			true, 
+			true
+		);
+
+		foreach($tasks as $t) {
+			if (!empty($_POST['settings_recall_type']) && $_POST['settings_recall_type'] == 'all') {
+				$ts = $_POST['settings_recall_time'];
+			}
+			else {
+				$ts = empty($_POST['settings_ts_'.$t->id()]) ? 0 : $_POST['settings_ts_'.$t->id()];
+			}
+			$core->blog->settings->maintenance->put(
+				'ts_'.$t->id(), 
+				abs((integer) $ts), 
+				'integer', 
+				sprintf('Recall time for task %s', $t->id()), 
+				true, 
+				$t->blog()
+			);
+		}
+
+		dcPage::addSuccessNotice(__('Maintenance plugin has been successfully configured.'));
+		http::redirect($list->getURL('module=maintenance&conf=1'));
+	}
+	catch(Exception $e) {
+		$core->error->add($e->getMessage());
+	}
+}
+
+echo 
+'<p>'.__('Set up reminders for maintenance tasks.').'</p>'.
+
+'<h4 class="pretty-title">'.__('Activation').'</h4>'.
+'<p><label for="settings_plugin_message" class="classic">'.
+form::checkbox('settings_plugin_message', 1, $core->blog->settings->maintenance->plugin_message).
+__('Display alert messages on late tasks').'</label></p>'.
+
+'<p class="info">'.sprintf(
+	__('You can place list of late tasks on your %s.'),
+	'<a href="preferences.php#user-favorites">'.__('Dashboard').'</a>'
+).'</p>'.
+
+'<h4 class="pretty-title vertical-separator">'.__('Frequency').'</h4>'.
+
+'<p class="vertical-separator">'.form::radio(array('settings_recall_type', 'settings_recall_all'), 'all').' '.
+'<label class="classic" for="settings_recall_all">'.
+'<strong>'.__('Use one recall time for all tasks').'</strong></label>'.
+
+'<p class="field wide vertical-separator"><label for="settings_recall_time">'.__('Recall time for all tasks:').'</label>'.
+form::combo('settings_recall_time', $combo_ts, 'seperate', 'recall-for-all').
+'</p>'.
+
+'<p class="vertical-separator">'.form::radio(array('settings_recall_type', 'settings_recall_separate'), 'separate', 1).' '.
+'<label class="classic" for="settings_recall_separate">'.
+'<strong>'.__('Use one recall time per task').'</strong></label>';
+
+foreach($tasks as $t)
+{
+	echo
+	'<div class="two-boxes">'.
+
+	'<p class="field wide"><label for="settings_ts_'.$t->id().'">'.$t->task().'</label>'.
+	form::combo('settings_ts_'.$t->id(), $combo_ts, $t->ts(), 'recall-per-task').
+	'</p>'.
+
+	'</div>';
+}
Index: plugins/maintenance/index.php
===================================================================
--- plugins/maintenance/index.php	(revision 2166)
+++ plugins/maintenance/index.php	(revision 2175)
@@ -27,42 +27,4 @@
 $code = empty($_POST['code']) ? null : (integer) $_POST['code'];
 $tab = empty($_REQUEST['tab']) ? '' : $_REQUEST['tab'];
-
-// Save settings
-
-if (!empty($_POST['settings'])) {
-
-	try {
-		$core->blog->settings->maintenance->put(
-			'plugin_message', 
-			!empty($_POST['settings_plugin_message']), 
-			'boolean', 
-			'Display alert message of late tasks on plugin page', 
-			true, 
-			true
-		);
-
-		foreach($tasks as $t) {
-			if (!empty($_POST['settings_recall_type']) && $_POST['settings_recall_type'] == 'all') {
-				$ts = $_POST['settings_recall_time'];
-			}
-			else {
-				$ts = empty($_POST['settings_ts_'.$t->id()]) ? 0 : $_POST['settings_ts_'.$t->id()];
-			}
-			$core->blog->settings->maintenance->put(
-				'ts_'.$t->id(), 
-				abs((integer) $ts), 
-				'integer', 
-				sprintf('Recall time for task %s', $t->id()), 
-				true, 
-				$t->blog()
-			);
-		}
-		
-		http::redirect($p_url.'&done=1&tab='.$tab.'#'.$tab);
-	}
-	catch(Exception $e) {
-		$core->error->add($e->getMessage());
-	}
-}
 
 // Get task object
@@ -287,55 +249,4 @@
 		'</div>';
 	}
-
-	// Settings
-
-	echo 
-	'<div id="settings" class="multi-part" title="'.__('Alert settings').'">'.
-	'<h3>'.__('Alert settings').'</h3>'.
-	'<form action="'.$p_url.'" method="post">'.
-
-	'<h4 class="pretty-title">'.__('Activation').'</h4>'.
-	'<p><label for="settings_plugin_message" class="classic">'.
-	form::checkbox('settings_plugin_message', 1, $core->blog->settings->maintenance->plugin_message).
-	__('Display alert messages on late tasks').'</label></p>'.
-
-	'<p class="info">'.sprintf(
-		__('You can place list of late tasks on your %s.'),
-		'<a href="preferences.php#user-favorites">'.__('Dashboard').'</a>'
-	).'</p>'.
-
-	'<h4 class="pretty-title vertical-separator">'.__('Frequency').'</h4>'.
-
-	'<p class="vertical-separator">'.form::radio(array('settings_recall_type', 'settings_recall_all'), 'all').' '.
-	'<label class="classic" for="settings_recall_all">'.
-	'<strong>'.__('Use one recall time for all tasks').'</strong></label>'.
-
-	'<p class="field wide vertical-separator"><label for="settings_recall_time">'.__('Recall time for all tasks:').'</label>'.
-	form::combo('settings_recall_time', $combo_ts, 'seperate', 'recall-for-all').
-	'</p>'.
-
-	'<p class="vertical-separator">'.form::radio(array('settings_recall_type', 'settings_recall_separate'), 'separate', 1).' '.
-	'<label class="classic" for="settings_recall_separate">'.
-	'<strong>'.__('Use one recall time per task').'</strong></label>';
-
-	foreach($tasks as $t)
-	{
-		echo
-		'<div class="two-boxes">'.
-
-		'<p class="field wide"><label for="settings_ts_'.$t->id().'">'.$t->task().'</label>'.
-		form::combo('settings_ts_'.$t->id(), $combo_ts, $t->ts(), 'recall-per-task').
-		'</p>'.
-
-		'</div>';
-	}
-
-	echo 
-	'<p class="field wide"><input type="submit" value="'.__('Save this settings').'" /> '.
-	form::hidden(array('tab'), 'settings').
-	form::hidden(array('settings'), 1).
-	$core->formNonce().'</p>'.
-	'</form>'.
-	'</div>';
 }
 
