Index: admin/blog_theme.php
===================================================================
--- admin/blog_theme.php	(revision 2166)
+++ admin/blog_theme.php	(revision 2182)
@@ -15,9 +15,32 @@
 dcPage::check('admin');
 
-# Loading themes
+# --------------------------------------------------
+# @todo Add settings to Dotclear update features
+if ($core->blog->settings->system->repository_theme_url === null) {
+	$core->blog->settings->system->put(
+		'repository_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
+$core->themes->loadModules($core->blog->themes_path, null);
+
+# -- Repository helper --
+$repository = new dcRepository(
+	$core->themes, 
+	$core->blog->settings->system->repository_theme_url
+);
+$repository->check();
+
+# -- Page helper --
+$list = new adminThemesList(
+	$core, 
+	$core->blog->themes_path,
+	false
+);
+
+# -- Theme screenshot --
 if (!empty($_GET['shot']) && $core->themes->moduleExists($_GET['shot']))
 {
@@ -43,321 +66,167 @@
 }
 
-$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();
-	http::redirect('blog_theme.php?upd=1');
-}
-
-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?del=1');
-	}
-	catch (Exception $e)
-	{
+# -- Display module configuration page --
+if ($list->setConfigurationFile($core->themes, $core->blog->settings->system->theme)) {
+
+	# Get content before page headers
+	include $list->getConfigurationFile();
+
+	# Gather content
+	$list->setConfigurationContent();
+
+	# 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') => 'blog_theme.php',
+				'<span class="page-title">'.__('Theme configuration').'</span>' => ''
+			))
+	);
+
+	if (!empty($_GET['done'])){
+		dcPage::success(__('Theme successfully configured.'));
+	}
+
+	# Display previously gathered content
+	$list->getConfigurationContent();
+
+	dcPage::close();
+
+	# Stop reading code here
+	return;
+}
+
+# -- Execute actions --
+if (!empty($_POST) && empty($_REQUEST['conf']) && $core->auth->isSuperAdmin() && $list->isPathWritable()) {
+	try {
+		$list->executeAction('themes', $core->themes, $repository);
+	}
+	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);
-		http::redirect('blog_theme.php?added='.$ret_code);
-	}
-	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(
+# -- Page header --
+dcPage::open(__('Themes management'),
+//	(!$conf_file ? 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.'));
-	}
-	
-	if (!empty($_GET['added'])) {
-		dcPage::success(($_GET['added'] == 2 ? __('Theme has been successfully upgraded') : __('Theme has been successfully installed.')));
-	}
-	
-	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>'.
+# -- Succes messages --
+if (!empty($_GET['msg'])) {
+	$list->displayMessage($_GET['msg']);
+}
+
+# -- Display modules lists --
+if ($core->auth->isSuperAdmin() && $list->isPathWritable()) {
+
+	# Updated modules from repo
+	$modules = $repository->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 %2$s.', 'There are %s themes to update available from %s.', count($modules)),
+			count($modules),
+			'<a href="http://dotaddict.org/dc2/themes">Dotaddict</a>'
+		).'</p>';
+
+		$list
+			->newList('theme-update')
+			->setModules($modules)
+			->setPageTab('update')
+			->displayModulesList(
+				/*cols */		array('sshot', 'name', 'desc', 'author', 'version', 'current_version', 'parent'),
+				/* actions */	array('update')
+			);
+
+		echo
+		'</div>';
+	}
+}
+
+# List all active plugins
+echo
+'<div class="multi-part" id="themes" title="'.__('Installed themes').'">';
+
+$modules = $core->themes->getModules();
+if (!empty($modules)) {
+
+	echo
+	'<h3>'.__('Activated themes').'</h3>'.
+	'<p>'.__('Manage installed themes from this list.').'</p>';
+
+	$list
+		->newList('theme-activate')
+		->setModules($modules)
+		->setPageTab('themes')
+		->displayModulesList(
+			/* cols */		array('sshot', 'name', 'config', 'desc', 'author', 'version', 'parent'),
+			/* actions */	array('deactivate', 'delete')
+		);
+}
+
+echo 
+'</div>';
+
+if ($core->auth->isSuperAdmin() && $list->isPathWritable()) {
+
+	# New modules from repo
+	$search = $list->getSearchQuery();
+	$modules = $search ? $repository->search($search) : $repository->get();
+
+	echo
+	'<div class="multi-part" id="new" title="'.__('Add themes from Dotaddict').'">'.
+	'<h3>'.__('Add themes from Dotaddict repository').'</h3>';
+
+	$list
+		->newList('theme-new')
+		->setModules($modules)
+		->setPageTab('new')
+		->displaySearchForm()
+		->displayNavMenu()
+		->displayModulesList(
+			/* cols */		array('expander', 'sshot', 'name', 'config', 'desc', 'author', 'version', 'parent'),
+			/* 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>';
-	
-	# 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>';
-	}
+
+	# Add a new plugin
+	echo
+	'<div class="multi-part" id="addtheme" title="'.__('Install or upgrade manually').'">';
+
+	echo '<p>'.__('You can install themes by uploading or downloading zip files.').'</p>';
+	
+	$list->displayManualForm();
+
+	echo
+	'</div>';
 }
 
Index: admin/index.php
===================================================================
--- admin/index.php	(revision 2166)
+++ admin/index.php	(revision 2175)
@@ -123,4 +123,25 @@
 	}
 	$__dashboard_icons['prefs'] = new ArrayObject(array(__('My preferences'),'preferences.php','images/menu/user-pref-b.png'));
+}
+
+# Check plugins and themes update from repository
+function dc_check_repository_update($mod, $url, $img, $icon)
+{
+	$repo = new dcRepository($mod, $url);
+	$repo->check();
+	$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_repository_update($core->plugins, $core->blog->settings->system->repository_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_repository_update($themes, $core->blog->settings->system->repository_theme_url, 'blog-theme', $__dashboard_icons['blog_theme']);
 }
 
Index: admin/js/_plugins.js
===================================================================
--- admin/js/_plugins.js	(revision 0)
+++ admin/js/_plugins.js	(revision 2171)
@@ -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 dl = document.createElement('ul');
+                                dl.className = "mod-more";
+                                
+                                if (author) {
+                                        $(dl).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>';
+                                        }
+                                        $(dl).append($('<li>'+dotclear.msg.module_help+' '+dd+'</li>'));
+                                }
+
+                                $(td).append($(box).addClass('two-boxes').append(dl));
+                                
+                                var section = $(rsp).find('section').text();
+                                var tags = $(rsp).find('tags').text();
+                                
+								var boxb = document.createElement('div');
+                                var dlb = document.createElement('ul');
+                                dlb.className = "mod-more";
+                                
+                                if (section) {
+                                        $(dlb).append($('<li class="module-section">'+dotclear.msg.module_section+' '+section+'</li>'));
+                                }
+                                if (tags) {
+                                        $(dlb).append($('<li class="module-tags">'+dotclear.msg.module_tags+' '+tags+'</li>'));
+                                }
+                                $(td).append($(boxb).addClass('two-boxes').append(dlb));
+                        } 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 tr.line'));
+        });
+        $('table.modules.expandable tr.line').each(function() {
+                dotclear.moduleExpander(this);
+        });
 });
Index: admin/plugins.php
===================================================================
--- admin/plugins.php	(revision 2166)
+++ admin/plugins.php	(revision 2182)
@@ -13,183 +13,115 @@
 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);
-			}
-			
-			http::redirect('plugins.php?removed=1');
-		}
-		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);
-			
-			http::redirect('plugins.php?added='.$ret_code);
-		}
-		catch (Exception $e)
-		{
-			$core->error->add($e->getMessage());
-			$default_tab = 'addplugin';
-		}
-	}
-}
-
-# Plugin install
-$plugins_install = $core->plugins->installModules();
-
-/* DISPLAY Main page
--------------------------------------------------------- */
+dcPage::check('admin');
+
+# --------------------------------------------------
+# @todo Add settings to Dotclear update features
+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->repository_plugin_url === null) {
+	$core->blog->settings->system->put(
+		'repository_plugin_url', 'http://update.dotaddict.org/dc2/plugins.xml', 'string', 'Plugins XML feed location', true, true
+	);
+}
+# --------------------------------------------------
+
+# -- Repository helper --
+$repository = new dcRepository(
+	$core->plugins, 
+	$core->blog->settings->system->repository_plugin_url
+);
+$repository->check();
+
+# -- Page helper --
+$list = new adminModulesList(
+	$core, 
+	DC_PLUGINS_ROOT,
+	$core->blog->settings->system->plugins_allow_multi_install
+);
+
+$list::setDistributedModules(array(
+	'aboutConfig',
+	'akismet',
+	'antispam',
+	'attachments',
+	'blogroll',
+	'blowupConfig',
+	'daInstaller',
+	'fairTrackbacks',
+	'importExport',
+	'maintenance',
+	'pages',
+	'pings',
+	'simpleMenu',
+	'tags',
+	'themeEditor',
+	'userPref',
+	'widgets'
+));
+
+# -- Display module configuration page --
+if ($list->setConfigurationFile($core->plugins)) {
+
+	# Get content before page headers
+	include $list->getConfigurationFile();
+
+	# Gather content
+	$list->setConfigurationContent();
+
+	# Display page
+	dcPage::open(__('Plugins management'),
+
+		# --BEHAVIOR-- pluginsToolsHeaders
+		$core->callBehavior('pluginsToolsHeaders', $core, true),
+
+		dcPage::breadcrumb(
+			array(
+				html::escapeHTML($core->blog->name) => '',
+				'<a href="'.$list->getPageURL().'">'.__('Plugins management').'</a>' => '',
+				'<span class="page-title">'.__('Plugin configuration').'</span>' => ''
+			))
+	);
+
+	# Message
+	if (!empty($_GET['done'])){
+		dcPage::success(__('Plugin successfully configured.'));
+	}
+
+	# Display previously gathered content
+	$list->getConfigurationContent();
+
+	dcPage::close();
+
+	# Stop reading code here
+	return;
+}
+
+# -- Execute actions --
+if (!empty($_POST) && empty($_REQUEST['conf']) && $core->auth->isSuperAdmin() && $list->isPathWritable()) {
+	try {
+		$list->executeAction('plugins', $core->plugins, $repository);
+	}
+	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(
@@ -199,195 +131,153 @@
 );
 
-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>';
+# -- Succes messages --
+if (!empty($_GET['msg'])) {
+	$list->displayMessage($_GET['msg']);
+}
+
+# -- 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>';
+		echo 
+		'<li>'.$k.' ('.$v.')</li>';
+	}
+	echo 
+	'</ul></div>';
+}
+
+# -- Display modules lists --
+if ($core->auth->isSuperAdmin() && $list->isPathWritable()) {
+
+	# Updated modules from repo
+	$modules = $repository->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 %2$s.', 'There are %s plugins to update available from %s.', count($modules)),
+			count($modules),
+			'<a href="http://dotaddict.org/dc2/plugins">Dotaddict</a>'
+		).'</p>';
+
+		$list
+			->newList('plugin-update')
+			->setModules($modules)
+			->setPageTab('update')
+			->displayModulesList(
+				/*cols */		array('icon', 'name', 'version', 'current_version', 'desc'),
+				/* actions */	array('update')
+			);
+
+		echo
+		'</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
-'<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').'">';
+
+$modules = $core->plugins->getModules();
+if (!empty($modules)) {
+
 	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>';
-	}
-	echo
-	'</table></div>';
-}
-
-$p_disabled = $core->plugins->getDisabledModules();
-uksort($p_disabled,create_function('$a,$b','return strcasecmp($a,$b);'));
-if (!empty($p_disabled))
-{
+	'<p>'.__('Manage installed plugins from this list.').'</p>';
+
+	$list
+		->newList('plugin-activate')
+		->setModules($modules)
+		->setPageTab('plugins')
+		->displayModulesList(
+			/* cols */		array('expander', 'icon', 'name', 'config', 'version', 'desc', 'distrib'),
+			/* actions */	array('deactivate', 'delete')
+		);
+}
+
+# Deactivated modules
+$modules = $core->plugins->getDisabledModules();
+if (!empty($modules)) {
+
 	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)
-{
+	'<p>'.__('Deactivated plugins are installed but not usable. You can activate them from here.').'</p>';
+
+	$list
+		->newList('plugin-deactivate')
+		->setModules($modules)
+		->setPageTab('plugins')
+		->displayModulesList(
+			/* cols */		array('icon', 'name', 'distrib'),
+			/* actions */	array('activate', 'delete')
+		);
+}
+
+echo 
+'</div>';
+
+if ($core->auth->isSuperAdmin() && $list->isPathWritable()) {
+
+	# New modules from repo
+	$search = $list->getSearchQuery();
+	$modules = $search ? $repository->search($search) : $repository->get();
+
+	echo
+	'<div class="multi-part" id="new" title="'.__('Add plugins from Dotaddict').'">'.
+	'<h3>'.__('Add plugins from Dotaddict repository').'</h3>';
+
+	$list
+		->newList('plugin-new')
+		->setModules($modules)
+		->setPageTab('new')
+		->displaySearchForm()
+		->displayNavMenu()
+		->displayModulesList(
+			/* cols */		array('expander', 'name', '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
+	'<div class="multi-part" id="addplugin" title="'.__('Install or upgrade manually').'">';
+
 	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>';
+	$list->displayManualForm();
+
+	echo
+	'</div>';
+}
 
 # --BEHAVIOR-- pluginsToolsTabs
-$core->callBehavior('pluginsToolsTabs',$core);
+$core->callBehavior('pluginsToolsTabs', $core);
+
+# -- Notice for super admin --
+if ($core->auth->isSuperAdmin() && !$list->isPathWritable()) {
+	echo 
+	'<p class="warning">'.__('Some functions are disabled, please give write access to your plugins directory to enable them.').'</p>';
+}
 
 dcPage::close();
Index: admin/post.php
===================================================================
--- admin/post.php	(revision 2181)
+++ admin/post.php	(revision 2166)
@@ -298,6 +298,6 @@
 			# --BEHAVIOR-- adminAfterPostUpdate
 			$core->callBehavior('adminAfterPostUpdate',$cur,$post_id);
-			dcPage::addSuccessNotice (sprintf('The post "%s" has been successfully updated',html::escapeHTML($cur->post_title)));
-			http::redirect('post.php?id='.$post_id);
+			
+			http::redirect('post.php?id='.$post_id.'&upd=1');
 		}
 		catch (Exception $e)
Index: admin/services.php
===================================================================
--- admin/services.php	(revision 1699)
+++ admin/services.php	(revision 2174)
@@ -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') {
+			$repository = new dcRepository(
+				$core->plugins, 
+				$core->blog->settings->system->repository_plugin_url
+			);
+			$repository->check();
+
+			$modules = $repository->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::parseModuleInfo($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 2152)
+++ admin/style/default.css	(revision 2179)
@@ -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 {
@@ -1532,115 +1533,17 @@
 				}
 /* ------------------------------------------------------------------- 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 {
-	float: left;
-	width: 120px;
-	height: 150px;
-	margin: 0 12px 24px;
-	padding: 12px 12px 0;
-	text-align: center;
-	background: #f3f3f3;
+.box.theme {
+	margin: 1em;
+	padding: 1em;
 	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;
-		}
+	}
+#theme-new .box.theme {
+	width: 280px;
+	}
+.module-sshot img {
+	border: 1px solid #ddd;
+	padding: .5em;
+	background: #fff;
+	}
 /* ------------------------------------------------------------------ categories.php */
 #categories {
@@ -1947,11 +1850,51 @@
 		}
 /* -------------------------------------------------------------------- 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(../images/page_help.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;
+	}
+.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 */
Index: inc/admin/actions/class.dcactionposts.php
===================================================================
--- inc/admin/actions/class.dcactionposts.php	(revision 2181)
+++ inc/admin/actions/class.dcactionposts.php	(revision 2169)
@@ -159,14 +159,6 @@
 		}
 		$core->blog->updPostsStatus($posts_ids,$status);
-		dcPage::addSuccessNotice(sprintf(
-			__(
-				'%d entry has been successfully updated to status : "%s"',
-				'%d entries have been successfully updated to status : "%s"',
-				count($posts_ids)
-			),
-			count($posts_ids),
-			$core->blog->getPostStatus($status))
-		);
-		$ap->redirect(true);
+		
+		$ap->redirect(true,array('upd' => 1));
 	}
 	
@@ -178,24 +170,6 @@
 		$action = $ap->getAction();
 		$core->blog->updPostsSelected($posts_ids,$action == 'selected');
-		if ($action == 'selected') {
-			dcPage::addSuccessNotice(sprintf(
-				__(
-					'%d entry has been successfully marked as selected',
-					'%d entries have been successfully marked as selected',
-					count($posts_ids)
-				),
-				count($posts_ids))
-			);
-		} else {
-			dcPage::addSuccessNotice(sprintf(
-				__(
-					'%d entry has been successfully marked as unselected',
-					'%d entries have been successfully marked as unselected',
-					count($posts_ids)
-				),
-				count($posts_ids))
-			);
-		}
-		$ap->redirect(true);
+		
+		$ap->redirect(true,array('upd' => 1));
 	}
 	
@@ -217,14 +191,6 @@
 		
 		$core->blog->delPosts($posts_ids);
-		dcPage::addSuccessNotice(sprintf(
-			__(
-				'%d entry has been successfully deleted',
-				'%d entries have been successfully deleted',
-				count($posts_ids)
-			),
-			count($posts_ids))
-		);
-		
-		$ap->redirect(false);
+		
+		$ap->redirect(false,array('del',1));
 	}
 
@@ -241,5 +207,4 @@
 				$cur_cat->cat_title = $post['new_cat_title'];
 				$cur_cat->cat_url = '';
-				$title = $cur_cat->cat_title;
 				
 				$parent_cat = !empty($post['new_cat_parent']) ? $post['new_cat_parent'] : '';
@@ -255,16 +220,6 @@
 			
 			$core->blog->updPostsCategory($posts_ids, $new_cat_id);
-			$title = $core->blog->getCategory($new_cat_id);
-			dcPage::addSuccessNotice(sprintf(
-				__(
-					'%d entry has been successfully moved to category "%s"',
-					'%d entries have been successfully moved to category "%s"',
-					count($posts_ids)
-				),
-				count($posts_ids),
-				html::escapeHTML($title->cat_title))
-			);
-
-			$ap->redirect(true);
+			
+			$ap->redirect(true,array('upd'=>1));
 		} else {
 
@@ -282,5 +237,5 @@
 			);			
 			echo
-			'<form action="'.$ap->getURI().'" method="post">'.
+			'<form action="'.$ap->getRedirection(array(),true).'" method="post">'.
 			$ap->getCheckboxes().
 			'<p><label for="new_cat_id" class="classic">'.__('Category:').'</label> '.
@@ -324,16 +279,6 @@
 			$cur->user_id = $new_user_id;
 			$cur->update('WHERE post_id '.$core->con->in($posts_ids));
-			dcPage::addSuccessNotice(sprintf(
-				__(
-					'%d entry has been successfully set to user "%s"',
-					'%d entries have been successfully set to user "%s"',
-					count($posts_ids)
-				),
-				count($posts_ids),
-				html::escapeHTML($new_user_id))
-			);
-
-
-			$ap->redirect(true);
+			
+			$ap->redirect(true,array('upd' => 1));
 		} else {
 			$usersList = '';
@@ -364,5 +309,5 @@
 
 			echo
-			'<form action="'.$ap->getURI().'" method="post">'.
+			'<form action="'.$ap->getRedirection(array(),true).'" method="post">'.
 			$ap->getCheckboxes().
 			'<p><label for="new_auth_id" class="classic">'.__('New author (author ID):').'</label> '.
@@ -387,14 +332,6 @@
 			$cur->post_lang = $new_lang;
 			$cur->update('WHERE post_id '.$core->con->in($posts_ids));
-			dcPage::addSuccessNotice(sprintf(
-				__(
-					'%d entry has been successfully set to language "%s"',
-					'%d entries have been successfully set to language "%s"',
-					count($posts_ids)
-				),
-				count($posts_ids),
-				html::escapeHTML(l10n::getLanguageName($new_lang)))
-			);
-			$ap->redirect(true);
+			
+			$ap->redirect(true,array('upd' => 1));
 		} else {
 			$ap->beginPage(
@@ -422,5 +359,5 @@
 			
 			echo
-			'<form action="'.$ap->getURI().'" method="post">'.
+			'<form action="'.$ap->getRedirection(array(),true).'" method="post">'.
 			$ap->getCheckboxes().
 			
Index: inc/admin/lib.dc.page.php
===================================================================
--- inc/admin/lib.dc.page.php	(revision 2181)
+++ inc/admin/lib.dc.page.php	(revision 2175)
@@ -175,38 +175,6 @@
 			'</div>';
 		}
-		
-		if (isset($_SESSION['notifications'])) {
-			$types = array("success" => "success", "warning" => "warning-msg");
-			$notifications = $_SESSION['notifications'];
-			foreach ($types as $type => $class) {
-				if (isset($notifications[$type])) {
-					foreach ($notifications[$type] as $n) {
-						echo self::getNotification($n,$class);
-					}
-				}
-			}
-			unset($_SESSION['notifications']);
-		}
-		
-	}
-	public static function AddNotice($type,$message) {
-		$notification = isset($_SESSION['notifications']) ? $_SESSION['notifications']:array();
-		$notification[$type][] = array('ts' => time(), 'text' => $message);
-		$_SESSION['notifications'] = $notification;
-	}
-	public static function addSuccessNotice($message) {
-		self::addNotice("success",$message);
-	}
-	public static function addWarningNotice($message) {
-		self::addNotice("warning",$message);
-	}
-	
-	protected static function getNotification($msg,$class) {
-		global $core;
-		$res = '<p class="'.$class.'">'.
-		dt::str(__('[%H:%M:%S] '),$msg['ts'],$core->auth->getInfo('user_tz')).' '.$msg['text'].
-		'</p>';
-		return $res;	
-	}
+	}
+
 	public static function close()
 	{
@@ -643,4 +611,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 2182)
+++ inc/admin/lib.moduleslist.php	(revision 2182)
@@ -0,0 +1,972 @@
+<?php
+
+class adminModulesList
+{
+	public $core;
+	public $modules;
+
+	public static  $allow_multi_install;
+	public static $distributed_modules = array();
+
+	protected $list_id = 'unknow';
+
+	protected $config_module = '';
+	protected $config_file = '';
+	protected $config_content = '';
+
+	protected $path = false;
+	protected $path_writable = false;
+	protected $path_pattern = false;
+
+	protected $page_url = 'plugins.php';
+	protected $page_qs = '?';
+	protected $page_tab = '';
+
+	public static $nav_indexes = 'abcdefghijklmnopqrstuvwxyz0123456789';
+	protected $nav_list = array();
+	protected $nav_special = 'other';
+
+	protected $sort_field = 'sname';
+	protected $sort_asc = true;
+
+	public function __construct($core, $root, $allow_multi_install=false)
+	{
+		$this->core = $core;
+		self::$allow_multi_install = (boolean) $allow_multi_install;
+		$this->setPathInfo($root);
+		$this->setNavSpecial(__('other'));
+
+		$this->init();
+	}
+
+	protected function init()
+	{
+		return null;
+	}
+
+	public function newList($list_id)
+	{
+		$this->modules = array();
+		$this->page_tab = '';
+		$this->list_id = $list_id;
+
+		return $this;
+	}
+
+	protected function setPathInfo($root)
+	{
+		$paths = explode(PATH_SEPARATOR, $root);
+		$path = array_pop($paths);
+		unset($paths);
+
+		$this->path = $path;
+		if (is_dir($path) && is_writeable($path)) {
+			$this->path_writable = true;
+			$this->path_pattern = preg_quote($path,'!');
+		}
+
+		return $this;
+	}
+
+	public function getPath()
+	{
+		return $this->path;
+	}
+
+	public function isPathWritable()
+	{
+		return $this->path_writable;
+	}
+
+	public function isPathDeletable($root)
+	{
+		return $this->path_writable 
+			&& preg_match('!^'.$this->path_pattern.'!', $root) 
+			&& $this->core->auth->isSuperAdmin();
+	}
+
+	public function setPageURL($url)
+	{
+		$this->page_qs = strpos('?', $url) ? '&' : '?';
+		$this->page_url = $url;
+
+		return $this;
+	}
+
+	public function getPageURL($queries='', $with_tab=true)
+	{
+		return $this->page_url.
+			(!empty($queries) ? $this->page_qs : '').
+			(is_array($queries) ? http_build_query($queries) : $queries).
+			($with_tab && !empty($this->page_tab) ? '#'.$this->page_tab : '');
+	}
+
+	public function setPageTab($tab)
+	{
+		$this->page_tab = $tab;
+
+		return $this;
+	}
+
+	public function getPageTab()
+	{
+		return $this->page_tab;
+	}
+
+	public function getSearchQuery()
+	{
+		$query = !empty($_REQUEST['m_search']) ? trim($_REQUEST['m_search']) : null;
+		return strlen($query) > 1 ? $query : null;
+	}
+
+	public function displaySearchForm()
+	{
+		$query = $this->getSearchQuery();
+
+		if (empty($this->modules) && $query === null) {
+			return $this;
+		}
+
+		echo 
+		'<form action="'.$this->getPageURL().'" method="get" class="fieldset">'.
+		'<p><label for="m_search" class="classic">'.__('Search in repository:').'&nbsp;</label><br />'.
+		form::field(array('m_search','m_search'), 30, 255, html::escapeHTML($query)).
+		'<input type="submit" value="'.__('Search').'" /> ';
+		if ($query) { echo ' <a href="'.$this->getPageURL().'" class="button">'.__('Reset search').'</a>'; }
+		echo '</p>'.
+		'</form>';
+
+		if ($query) {
+			echo 
+			'<p class="message">'.sprintf(
+				__('Found %d result for search "%s":', 'Found %d results for search "%s":', count($this->modules)), 
+				count($this->modules), html::escapeHTML($query)
+				).
+			'</p>';
+		}
+		return $this;
+	}
+
+	public function setNavSpecial($str)
+	{
+		$this->nav_special = (string) $str;
+		$this->nav_list = array_merge(str_split(self::$nav_indexes), array($this->nav_special));
+
+		return $this;
+	}
+
+	public function getNavQuery()
+	{
+		return isset($_REQUEST['m_nav']) && in_array($_REQUEST['m_nav'], $this->nav_list) ? $_REQUEST['m_nav'] : $this->nav_list[0];
+	}
+
+	public function displayNavMenu()
+	{
+		if (empty($this->modules) || $this->getSearchQuery() !== null) {
+			return $this;
+		}
+
+		# Fetch modules required field
+		$indexes = array();
+		foreach ($this->modules as $id => $module) {
+			if (!isset($module[$this->sort_field])) {
+				continue;
+			}
+			$char = substr($module[$this->sort_field], 0, 1);
+			if (!in_array($char, $this->nav_list)) {
+				$char = $this->nav_special;
+			}
+			if (!isset($indexes[$char])) {
+				$indexes[$char] = 0;
+			}
+			$indexes[$char]++;
+		}
+
+		$buttons = array();
+		foreach($this->nav_list as $char) {
+			# Selected letter
+			if ($this->getNavQuery() == $char) {
+				$buttons[] = '<li class="active" title="'.__('current selection').'"><strong> '.$char.' </strong></li>';
+			}
+			# Letter having modules
+			elseif (!empty($indexes[$char])) {
+				$title = sprintf(__('%d module', '%d modules', $indexes[$char]), $indexes[$char]);
+				$buttons[] = '<li class="btn" title="'.$title.'"><a href="'.$this->getPageURL('m_nav='.$char).'" title="'.$title.'"> '.$char.' </a></li>';
+			}
+			# Letter without modules
+			else {
+				$buttons[] = '<li class="btn no-link" title="'.__('no module').'"> '.$char.' </li>';
+			}
+		}
+		# Parse navigation menu
+		echo '<div class="pager">'.__('Browse index:').' <ul>'.implode('',$buttons).'</ul></div>';
+
+		return $this;
+	}
+
+	public function setSortField($field, $asc=true)
+	{
+		$this->sort_field = $field;
+		$this->sort_asc = (boolean) $asc;
+
+		return $this;
+	}
+
+	public function getSortQuery()
+	{
+		return !empty($_REQUEST['m_sort']) ? $_REQUEST['m_sort'] : $this->sort_field;
+	}
+
+	public function displaySortForm()
+	{
+		//not yet implemented
+	}
+
+	public function displayMessage($action)
+	{
+		switch($action) {
+			case 'activate': 
+				$str = __('Module successfully activated.'); break;
+			case 'deactivate': 
+				$str = __('Module successfully deactivated.'); break;
+			case 'delete': 
+				$str = __('Module successfully deleted.'); break;
+			case 'install': 
+				$str = __('Module successfully installed.'); break;
+			case 'update': 
+				$str = __('Module successfully updated.'); break;
+			default:
+				$str = ''; break;
+		}
+		if (!empty($str)) {
+			dcPage::success($str);
+		}
+	}
+
+	public function setModules($modules)
+	{
+		$this->modules = array();
+		if (!empty($modules) && is_array($modules)) {
+			foreach($modules as $id => $module) {
+				$this->modules[$id] = self::parseModuleInfo($id, $module);
+			}
+		}
+		return $this;
+	}
+
+	public function getModules()
+	{
+		return $this->modules;
+	}
+
+	public static function parseModuleInfo($id, $module)
+	{
+		$label = empty($module['label']) ? $id : $module['label'];
+		$name = __(empty($module['name']) ? $label : $module['name']);
+		
+		return array_merge(
+			# Default values
+			array(
+				'desc' 				=> '',
+				'author' 			=> '',
+				'version' 			=> 0,
+				'current_version' 	=> 0,
+				'root' 				=> '',
+				'root_writable' 	=> false,
+				'permissions' 		=> null,
+				'parent' 			=> null,
+				'priority' 			=> 1000,
+				'standalone_config' => false,
+				'support' 			=> '',
+				'section' 			=> '',
+				'tags' 				=> '',
+				'details' 			=> '',
+				'sshot' 			=> ''
+			),
+			# Module's values
+			$module,
+			# Clean up values
+			array(
+				'id' 				=> $id,
+				'sid' 				=> self::sanitizeString($id),
+				'label' 			=> $label,
+				'name' 				=> $name,
+				'sname' 			=> self::sanitizeString($name)
+			)
+		);
+	}
+
+	public static function setDistributedModules($modules)
+	{
+		self::$distributed_modules = $modules;
+	}
+
+	public static function isDistributedModule($module)
+	{
+		$distributed_modules = self::$distributed_modules;
+
+		return is_array($distributed_modules) && in_array($module, $distributed_modules);
+	}
+
+	public static function sortModules($modules, $field, $asc=true)
+	{
+		$sorter = array();
+		foreach($modules as $id => $module) {
+			$sorter[$id] = isset($module[$field]) ? $module[$field] : $field;
+		}
+		array_multisort($sorter, $asc ? SORT_ASC : SORT_DESC, $modules);
+
+		return $modules;
+	}
+
+	public function displayModulesList($cols=array('name', 'config', 'version', 'desc'), $actions=array(), $nav_limit=false)
+	{
+		echo 
+		'<div class="table-outer">'.
+		'<table id="'.html::escapeHTML($this->list_id).'" class="modules'.(in_array('expander', $cols) ? ' expandable' : '').'">'.
+		'<caption class="hidden">'.html::escapeHTML(__('Modules list')).'</caption><tr>';
+
+		if (in_array('name', $cols)) {
+			echo 
+			'<th class="first nowrap"'.(in_array('icon', $cols) ? ' colspan="2"' : '').'>'.__('Name').'</th>';
+		}
+
+		if (in_array('version', $cols)) {
+			echo 
+			'<th class="nowrap count" scope="col">'.__('Version').'</th>';
+		}
+
+		if (in_array('current_version', $cols)) {
+			echo 
+			'<th class="nowrap count" scope="col">'.__('Current version').'</th>';
+		}
+
+		if (in_array('desc', $cols)) {
+			echo 
+			'<th class="nowrap" scope="col">'.__('Details').'</th>';
+		}
+
+		if (in_array('distrib', $cols)) {
+			echo '<th'.(in_array('desc', $cols) ? '' : ' class="maximal"').'></th>';
+		}
+
+		if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+			echo 
+			'<th class="minimal nowrap">'.__('Action').'</th>';
+		}
+
+		echo 
+		'</tr>';
+
+		$sort_field = $this->getSortQuery();
+
+		# Sort modules by id
+		$modules = $this->getSearchQuery() === null ?
+			self::sortModules($this->modules, $sort_field, $this->sort_asc) :
+			$this->modules;
+
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearchQuery() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getNavQuery() != $char) {
+					continue;
+				}
+			}
+
+			echo 
+			'<tr class="line" id="'.html::escapeHTML($this->list_id).'_m_'.html::escapeHTML($id).'">';
+
+			if (in_array('icon', $cols)) {
+				echo 
+				'<td class="module-icon nowrap">'.sprintf(
+					'<img alt="%1$s" title="%1$s" src="%2$s" />', 
+					html::escapeHTML($id), file_exists($module['root'].'/icon.png') ? 'index.php?pf='.$id.'/icon.png' : 'images/module.png'
+				).'</td>';
+			}
+
+			# Link to config file
+			$config = in_array('config', $cols) && !empty($module['root']) && file_exists(path::real($module['root'].'/_config.php'));
+
+			echo 
+			'<td class="module-name nowrap" scope="row">'.($config ? 
+				'<a href="'.$this->getPageURL('module='.$id.'&conf=1').'" title"'.sprintf(__('Configure module "%s"'), html::escapeHTML($module['name'])).'">'.html::escapeHTML($module['name']).'</a>' : 
+				html::escapeHTML($module['name'])
+			).'</td>';
+
+			if (in_array('version', $cols)) {
+				echo 
+				'<td class="module-version nowrap count">'.html::escapeHTML($module['version']).'</td>';
+			}
+
+			if (in_array('current_version', $cols)) {
+				echo 
+				'<td class="module-current-version nowrap count">'.html::escapeHTML($module['current_version']).'</td>';
+			}
+
+			if (in_array('desc', $cols)) {
+				echo 
+				'<td class="module-desc maximal">'.html::escapeHTML($module['desc']).'</td>';
+			}
+
+			if (in_array('distrib', $cols)) {
+				echo 
+				'<td class="module-distrib">'.(self::isDistributedModule($id) ? 
+					'<img src="images/dotclear_pw.png" alt="'.
+					__('Module from official distribution').'" title="'.
+					__('module from official distribution').'" />' 
+				: '').'</td>';
+			}
+
+			if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+				echo 
+				'<td class="module-actions nowrap">';
+
+				$this->displayLineActions($id, $module, $actions);
+
+				echo
+				'</td>';
+			}
+
+			echo 
+			'</tr>';
+
+			$count++;
+		}
+		echo 
+		'</table></div>';
+
+		if(!$count) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+	}
+
+	protected function displayLineActions($id, $module, $actions, $echo=true)
+	{
+		$submits = array();
+
+		# Activate
+		if (in_array('deactivate', $actions) && $module['root_writable']) {
+			$submits[] = '<input type="submit" name="deactivate" value="'.__('Deactivate').'" />';
+		}
+
+		# Deactivate
+		if (in_array('activate', $actions) && $module['root_writable']) {
+			$submits[] = '<input type="submit" name="activate" value="'.__('Activate').'" />';
+		}
+
+		# Delete
+		if (in_array('delete', $actions) && $this->isPathDeletable($module['root'])) {
+			$submits[] = '<input type="submit" class="delete" name="delete" value="'.__('Delete').'" />';
+		}
+
+		# Install (form repository)
+		if (in_array('install', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="install" value="'.__('Install').'" />';
+		}
+
+		# Update (from repository)
+		if (in_array('update', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="update" value="'.__('Update').'" />';
+		}
+
+		# Select (from repository)
+		if (in_array('choose', $actions) && $this->path_writable) {
+			$submits[] = '<input type="submit" name="choose" value="'.__('Choose').'" />';
+		}
+
+		# Parse form
+		if (!empty($submits)) {
+			$res = 
+			'<form action="'.$this->getPageURL().'" method="post">'.
+			'<div>'.
+			$this->core->formNonce().
+			form::hidden(array('module'), html::escapeHTML($id)).
+			form::hidden(array('tab'), $this->page_tab).
+			implode(' ', $submits).
+			'</div>'.
+			'</form>';
+
+			if (!$echo) {
+				return $res;
+			}
+			echo $res;
+		}
+	}
+
+	public function executeAction($prefix, dcModules $modules, dcRepository $repository)
+	{
+		if (!$this->core->auth->isSuperAdmin() || !$this->isPathWritable()) {
+			return null;
+		}
+
+		# List actions
+		if (!empty($_POST['module'])) {
+
+			$id = $_POST['module'];
+
+			if (!empty($_POST['activate'])) {
+
+				$enabled = $modules->getDisabledModules();
+				if (!isset($enabled[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				# --BEHAVIOR-- moduleBeforeActivate
+				$this->core->callBehavior($type.'BeforeActivate', $id);
+
+				$modules->activateModule($id);
+
+				# --BEHAVIOR-- moduleAfterActivate
+				$this->core->callBehavior($type.'AfterActivate', $id);
+
+				http::redirect($this->getPageURL('msg=activate'));
+			}
+
+			if (!empty($_POST['deactivate'])) {
+
+				if (!$modules->moduleExists($id)) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $modules->getModules($id);
+				$module['id'] = $id;
+
+				if (!$module['root_writable']) {
+					throw new Exception(__('You don\'t have permissions to deactivate this module.'));
+				}
+
+				# --BEHAVIOR-- moduleBeforeDeactivate
+				$this->core->callBehavior($prefix.'BeforeDeactivate', $module);
+
+				$modules->deactivateModule($id);
+
+				# --BEHAVIOR-- moduleAfterDeactivate
+				$this->core->callBehavior($prefix.'AfterDeactivate', $module);
+
+				http::redirect($this->getPageURL('msg=deactivate'));
+			}
+
+			if (!empty($_POST['delete'])) {
+
+				$disabled = $modules->getDisabledModules();
+				if (!isset($disabled[$id])) {
+
+					if (!$modules->moduleExists($id)) {
+						throw new Exception(__('No such module.'));
+					}
+
+					$module = $modules->getModules($id);
+					$module['id'] = $id;
+
+					if (!$this->isPathDeletable($module['root'])) {
+						throw new Exception(__("You don't have permissions to delete this module."));
+					}
+
+					# --BEHAVIOR-- moduleBeforeDelete
+					$this->core->callBehavior($prefix.'BeforeDelete', $module);
+
+					$modules->deleteModule($id);
+
+					# --BEHAVIOR-- moduleAfterDelete
+					$this->core->callBehavior($prefix.'AfterDelete', $module);
+				}
+				else {
+					$modules->deleteModule($id, true);
+				}
+
+				http::redirect($this->getPageURL('msg=delete'));
+			}
+
+			if (!empty($_POST['update'])) {
+
+				$updated = $repository->get();
+				if (!isset($updated[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				if (!$modules->moduleExists($id)) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $updated[$id];
+				$module['id'] = $id;
+			
+				if (!self::$allow_multi_install) {
+					$dest = $module['root'].'/../'.basename($module['file']);
+				}
+				else {
+					$dest = $this->getPath().'/'.basename($module['file']);
+					if ($module['root'] != $dest) {
+						@file_put_contents($module['root'].'/_disabled', '');
+					}
+				}
+
+				# --BEHAVIOR-- moduleBeforeUpdate
+				$this->core->callBehavior($type.'BeforeUpdate', $module);
+
+				$repository->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterUpdate
+				$this->core->callBehavior($type.'AfterUpdate', $module);
+
+				http::redirect($this->getPageURL('msg=upadte'));
+			}
+
+			if (!empty($_POST['install'])) {
+
+				$updated = $repository->get();
+				if (!isset($updated[$id])) {
+					throw new Exception(__('No such module.'));
+				}
+
+				$module = $updated[$id];
+				$module['id'] = $id;
+
+				$dest = $this->getPath().'/'.basename($module['file']);
+
+				# --BEHAVIOR-- moduleBeforeAdd
+				$this->core->callBehavior($type.'BeforeAdd', $module);
+
+				$ret_code = $repository->process($module['file'], $dest);
+
+				# --BEHAVIOR-- moduleAfterAdd
+				$this->core->callBehavior($type.'AfterAdd', $module);
+
+				http::redirect($this->getPageURL('msg='.($ret_code == 2 ? 'update' : 'install')));
+			}
+		}
+		# Manual actions
+		elseif (!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file']) 
+			|| !empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url']))
+		{
+			if (empty($_POST['your_pwd']) || !$this->core->auth->checkPassword(crypt::hmac(DC_MASTER_KEY, $_POST['your_pwd']))) {
+				throw new Exception(__('Password verification failed'));
+			}
+
+			if (!empty($_POST['upload_pkg'])) {
+				files::uploadStatus($_FILES['pkg_file']);
+				
+				$dest = $this->getPath().'/'.$_FILES['pkg_file']['name'];
+				if (!move_uploaded_file($_FILES['pkg_file']['tmp_name'], $dest)) {
+					throw new Exception(__('Unable to move uploaded file.'));
+				}
+			}
+			else {
+				$url = urldecode($_POST['pkg_url']);
+				$dest = $this->getPath().'/'.basename($url);
+				$repository->download($url, $dest);
+			}
+
+			# --BEHAVIOR-- moduleBeforeAdd
+			$this->core->callBehavior($prefix.'BeforeAdd', null);
+
+			$ret_code = $repository->install($dest);
+
+			# --BEHAVIOR-- moduleAfterAdd
+			$this->core->callBehavior($prefix.'AfterAdd', null);
+
+			http::redirect($this->getPageURL('msg='.($ret_code == 2 ? 'update' : 'install')).'#'.$prefix);
+		}
+		return null;
+	}
+
+	public function displayManualForm()
+	{
+		if (!$this->core->auth->isSuperAdmin() || !$this->isPathWritable()) {
+			return null;
+		}
+
+		# 'Upload module' form
+		echo
+		'<form method="post" action="'.$this->getPageURL().'" id="uploadpkg" enctype="multipart/form-data" class="fieldset">'.
+		'<h4>'.__('Upload a zip file').'</h4>'.
+		'<p class="field"><label for="pkg_file" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Zip file path:').'</label> '.
+		'<input type="file" name="pkg_file" id="pkg_file" /></p>'.
+		'<p class="field"><label for="your_pwd1" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Your password:').'</label> '.
+		form::password(array('your_pwd','your_pwd1'),20,255).'</p>'.
+		'<p><input type="submit" name="upload_pkg" value="'.__('Upload').'" />'.
+		form::hidden(array('tab'), $this->getPageTab()).
+		$this->core->formNonce().'</p>'.
+		'</form>';
+		
+		# 'Fetch module' form
+		echo
+		'<form method="post" action="'.$this->getPageURL().'" id="fetchpkg" class="fieldset">'.
+		'<h4>'.__('Download a zip file').'</h4>'.
+		'<p class="field"><label for="pkg_url" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Zip file URL:').'</label> '.
+		form::field(array('pkg_url','pkg_url'),40,255).'</p>'.
+		'<p class="field"><label for="your_pwd2" class="classic required"><abbr title="'.__('Required field').'">*</abbr> '.__('Your password:').'</label> '.
+		form::password(array('your_pwd','your_pwd2'),20,255).'</p>'.
+		'<p><input type="submit" name="fetch_pkg" value="'.__('Download').'" />'.
+		form::hidden(array('tab'), $this->getPageTab()).
+		$this->core->formNonce().'</p>'.
+		'</form>';
+	}
+
+	/**
+	 *
+	 * We need to get configuration content in three steps
+	 * and out of this class to keep backward compatibility.
+	 *
+	 * if ($xxx->setConfigurationFile()) {
+	 *	include $xxx->getConfigurationFile();
+	 * }
+	 * $xxx->setConfigurationContent();
+	 * ... [put here page headers and other stuff]
+	 * $xxx->getConfigurationContent();
+	 *
+	 */
+	public function setConfigurationFile(dcModules $modules, $id=null)
+	{
+		if (empty($_REQUEST['conf']) || empty($_REQUEST['module']) && !$id) {
+			return false;
+		}
+		
+		if (!empty($_REQUEST['module']) && empty($id)) {
+			$id = $_REQUEST['module'];
+		}
+
+		if (!$modules->moduleExists($id)) {
+			$core->error->add(__('Unknow module ID'));
+			return false;
+		}
+
+		$module = $modules->getModules($id);
+		$module = self::parseModuleInfo($id, $module);
+		$file = path::real($module['root'].'/_config.php');
+
+		if (!file_exists($file)) {
+			$core->error->add(__('This module has no configuration file.'));
+			return false;
+		}
+
+		$this->config_module = $module;
+		$this->config_file = $file;
+		$this->config_content = '';
+
+		if (!defined('DC_CONTEXT_MODULE')) {
+			define('DC_CONTEXT_MODULE', true);
+		}
+
+		return true;
+	}
+
+	public function getConfigurationFile()
+	{
+		if (!$this->config_file) {
+			return null;
+		}
+
+		ob_start();
+
+		return $this->config_file;
+	}
+
+	public function setConfigurationContent()
+	{
+		if ($this->config_file) {
+			$this->config_content = ob_get_contents();
+		}
+
+		ob_end_clean();
+
+		return !empty($this->file_content);
+	}
+
+	public function getConfigurationContent()
+	{
+		if (!$this->config_file) {
+			return null;
+		}
+
+		if (!$this->config_module['standalone_config']) {
+			echo
+			'<form id="module_config" action="'.$this->getPageURL('conf=1').'" method="post" enctype="multipart/form-data">'.
+			'<h3>'.sprintf(__('Configure plugin "%s"'), html::escapeHTML($this->config_module['name'])).'</h3>'.
+			'<p><a class="back" href="'.$this->getPageURL().'#plugins">'.__('Back').'</a></p>';
+		}
+
+		echo $this->config_content;
+
+		if (!$this->config_module['standalone_config']) {
+			echo
+			'<p class="clear"><input type="submit" name="save" value="'.__('Save').'" />'.
+			form::hidden('module', $this->config_module['id']).
+			$this->core->formNonce().'</p>'.
+			'</form>';
+		}
+
+		return true;
+	}
+
+	public static function sanitizeString($str)
+	{
+		return preg_replace('/[^A-Za-z0-9\@\#+_-]/', '', strtolower($str));
+	}
+}
+
+class adminThemesList extends adminModulesList
+{
+	protected $page_url = 'blog_theme.php';
+
+	public function displayModulesList($cols=array('name', 'config', 'version', 'desc'), $actions=array(), $nav_limit=false)
+	{
+		echo 
+		'<div id="'.html::escapeHTML($this->list_id).'" class="modules'.(in_array('expander', $cols) ? ' expandable' : '').' one-box">';
+
+		$sort_field = $this->getSortQuery();
+
+		# Sort modules by id
+		$modules = $this->getSearchQuery() === null ?
+			self::sortModules($this->modules, $sort_field, $this->sort_asc) :
+			$this->modules;
+
+		$res = '';
+		$count = 0;
+		foreach ($modules as $id => $module)
+		{
+			# Show only requested modules
+			if ($nav_limit && $this->getSearchQuery() === null) {
+				$char = substr($module[$sort_field], 0, 1);
+				if (!in_array($char, $this->nav_list)) {
+					$char = $this->nav_special;
+				}
+				if ($this->getNavQuery() != $char) {
+					continue;
+				}
+			}
+
+			$current = $this->core->blog->settings->system->theme == $id;
+
+			if (preg_match('#^http(s)?://#', $this->core->blog->settings->system->themes_url)) {
+				$theme_url = http::concatURL($this->core->blog->settings->system->themes_url, '/'.$id);
+			} else {
+				$theme_url = http::concatURL($this->core->blog->url, $this->core->blog->settings->system->themes_url.'/'.$id);
+			}
+
+			$has_conf = file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/_config.php');
+			$has_css = file_exists(path::real($this->core->blog->themes_path.'/'.$id).'/style.css');
+			$parent = $module['parent'];
+			$has_parent = !empty($module['parent']);
+			if ($has_parent) {
+				$is_parent_present = $this->core->themes->moduleExists($parent);
+			}
+
+			$line = 
+			'<div class="box small '.($current ? 'current-theme' : 'theme').'">';
+
+			if (in_array('sshot', $cols)) {
+				# Screenshot from url
+				if (preg_match('#^http(s)?://#', $module['sshot'])) {
+					$sshot = $module['sshot'];
+				}
+				# Screenshot from installed module
+				elseif (file_exists($this->core->blog->themes_path.'/'.$id.'/screenshot.jpg')) {
+					$sshot = $this->getPageURL('shot='.rawurlencode($id));
+				}
+				# Default screenshot
+				else {
+					$sshot = 'images/noscreenshot.png';
+				}
+
+				$line .= 
+				'<div class="module-sshot"><img src="'.$sshot.'" alt="'.__('screenshot.').'" /></div>';
+			}
+
+			if (in_array('name', $cols)) {
+				$line .= 
+				'<h4 class="module-name">'.html::escapeHTML($module['name']).'</h4>';
+			}
+
+			$line .= 
+			'<div class="module-infos">'.
+			'<p>';
+
+			if (in_array('desc', $cols)) {
+				$line .= 
+				'<span class="module-desc">'.html::escapeHTML($module['desc']).'</span> ';
+			}
+
+			if (in_array('author', $cols)) {
+				$line .= 
+				'<span class="module-author">'.sprintf(__('by %s'),html::escapeHTML($module['author'])).'</span> ';
+			}
+
+			if (in_array('version', $cols)) {
+				$line .= 
+				'<span class="module-version">'.sprintf(__('version %s'),html::escapeHTML($module['version'])).'</span> ';
+			}
+
+			if (in_array('parent', $cols) && $has_parent) {
+				if ($is_parent_present) {
+					$line .= 
+					'<span class="module-parent-ok">'.sprintf(__('(built on "%s")'),html::escapeHTML($parent)).'</span> ';
+				}
+				else {
+					$line .= 
+					'<span class="module-parent-missing">'.sprintf(__('(requires "%s")'),html::escapeHTML($parent)).'</span> ';
+				}
+			}
+
+			$line .= 
+			'</p>'.
+			'</div>';
+
+			$line .= 
+			'<div class="modules-actions">';
+			
+			# _GET actions
+			$line .= 
+			'<p>';
+
+			if ($current && $has_css) {
+				$line .= 
+				'<a href="'.$theme_url.'/style.css" class="button">'.__('View stylesheet').'</a> ';
+			}
+			if ($current && $has_conf) {
+				$line .= 
+				'<a href="'.$this->getPageURL('module='.$id.'&conf=1', false).'" class="button">'.__('Configure theme').'</a> ';
+			}
+			$line .= 
+			'</p>';
+
+			# Plugins actions
+			if ($current) {
+				# --BEHAVIOR-- adminCurrentThemeDetails
+				$line .= 
+				$this->core->callBehavior('adminCurrentThemeDetails', $this->core, $id, $module);
+			}
+
+			# _POST actions
+			if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
+				$line .=
+				$this->displayLineActions($id, $module, $actions, false);
+			}
+
+			$line .= 
+			'</div>';
+
+			$line .=
+			'</div>';
+
+			$count++;
+
+			$res = $current ? $line.$res : $res.$line;
+		}
+		echo 
+		$res.
+		'</div>';
+
+		if(!$count) {
+			echo 
+			'<p class="message">'.__('No module matches your search.').'</p>';
+		}
+	}
+}
Index: inc/core/class.dc.repository.parser.php
===================================================================
--- inc/core/class.dc.repository.parser.php	(revision 2146)
+++ inc/core/class.dc.repository.parser.php	(revision 2146)
@@ -0,0 +1,91 @@
+<?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 Rpeository modules feed parser
+
+Provides an object to parse feed of modules from repository.
+*/
+class dcRepositoryParser
+{
+	protected $xml;
+	protected $items;
+	protected static $bloc = 'http://dotaddict.org/da/';
+
+	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);
+	}
+
+	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 (defined('DC_DEV') && DC_DEV === true || version_compare(DC_VERSION,$item['dc_min'],'>=')) {
+				$this->items[$item['id']] = $item;
+			}
+		}
+	}
+
+	public function getModules()
+	{
+		return $this->items;
+	}
+}
Index: inc/core/class.dc.repository.php
===================================================================
--- inc/core/class.dc.repository.php	(revision 2156)
+++ inc/core/class.dc.repository.php	(revision 2156)
@@ -0,0 +1,156 @@
+<?php
+
+class dcRepository
+{
+	public $core;
+	public $modules;
+
+	protected $xml_url;
+	protected $data;
+
+	public function __construct(dcModules $modules, $xml_url)
+	{
+		$this->core = $modules->core;
+		$this->modules = $modules;
+		$this->xml_url = $xml_url;
+	}
+
+	public function check($force=false)
+	{
+		if (!$this->xml_url) {
+			return false;
+		}
+		if (($parser = dcRepositoryReader::quickParse($this->xml_url, DC_TPL_CACHE, $force)) === false) {
+			return false;
+		}
+
+		$raw_datas = $parser->getModules();
+
+		uasort($raw_datas, array('self','sort'));
+
+		$skipped = array_keys($this->modules->getDisabledModules());
+		foreach ($skipped as $p_id) {
+			if (isset($raw_datas[$p_id])) {
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$updates = array();
+		$current = $this->modules->getModules();
+		foreach ($current as $p_id => $p_infos) {
+			if (isset($raw_datas[$p_id])) {
+				if (self::compare($raw_datas[$p_id]['version'],$p_infos['version'],'>')) {
+					$updates[$p_id] = $raw_datas[$p_id];
+					$updates[$p_id]['root'] = $p_infos['root'];
+					$updates[$p_id]['root_writable'] = $p_infos['root_writable'];
+					$updates[$p_id]['current_version'] = $p_infos['version'];
+				}
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$this->data = array(
+			'new'	=> $raw_datas,
+			'update'	=> $updates
+		);
+
+		return true;
+	}
+
+	public function get($update=false)
+	{
+		return $this->data[$update ? 'update' : 'new'];
+	}
+
+	public function search($pattern)
+	{
+		$result = array();
+
+		# Split query into small clean words
+		$patterns = explode(' ', $pattern);
+		array_walk($patterns, array('dcRepository','sanitize'));
+
+		# For each modules
+		foreach ($this->data['new'] as $id => $module) {
+
+			# Split modules infos into small clean word
+			$subjects = explode(' ', $id.' '.$module['name'].' '.$module['desc']);
+			array_walk($subjects, array('dcRepository','sanitize'));
+
+			# Check contents
+			if (!($nb = preg_match_all('/('.implode('|', $patterns).')/', implode(' ', $subjects), $_))) {
+				continue;
+			}
+
+			# Add module to result
+			if (!isset($sorter[$id])) {
+				$sorter[$id] = 0;
+				$result[$id] = $module;
+			}
+
+			# Increment matches count
+			$sorter[$id] += $nb;
+			$result[$id]['accuracy'] = $sorter[$id];
+		}
+		# Sort response by matches count
+		if (!empty($result)) {
+			array_multisort($sorter, SORT_DESC, $result);
+		}
+		return $result;
+	}
+
+	public function process($url, $dest)
+	{
+		$this->download($url, $dest);
+		return $this->install($dest);
+	}
+
+	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.'));
+		}
+	}
+
+	public function install($path)
+	{
+		return dcModules::installPackage($path, $this->modules);
+	}
+
+	public static function agent()
+	{
+		return sprintf('Dotclear/%s)', DC_VERSION);
+	}
+
+	public static function sanitize(&$str, $_)
+	{
+		$str = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $str));
+	}
+
+	private static function compare($v1,$v2,$op)
+	{
+		$v1 = preg_replace('!-r(\d+)$!','-p$1',$v1);
+		$v2 = preg_replace('!-r(\d+)$!','-p$1',$v2);
+		return version_compare($v1,$v2,$op);
+	}
+
+	private static function sort($a,$b)
+	{
+		$c = strtolower($a['id']); 
+		$d = strtolower($b['id']); 
+		if ($c == $d) { 
+			return 0; 
+		} 
+		return ($c < $d) ? -1 : 1; 
+	}
+}
Index: inc/core/class.dc.repository.reader.php
===================================================================
--- inc/core/class.dc.repository.reader.php	(revision 2146)
+++ inc/core/class.dc.repository.reader.php	(revision 2146)
@@ -0,0 +1,209 @@
+<?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 feed reader
+
+Provides an object to parse feed of modules from repository.
+*/
+class dcRepositoryReader extends netHttp
+{
+	protected $user_agent 		= 'DotClear.org RepoBrowser/0.1';
+	protected $timeout 			= 5;
+	protected $validators 		= null;				///< <b>array</b>	HTTP Cache validators
+	protected $cache_dir 		= null;				///< <b>string</b>	Cache temporary directory
+	protected $cache_file_prefix 	= 'dcrepo';			///< <b>string</b>	Cache file prefix
+	protected $cache_ttl 		= '-30 minutes';		///< <b>string</b>	Cache TTL
+	protected $cache_touch_on_fail = true;
+	protected $force			= false;
+
+	public function __construct()
+	{
+		parent::__construct('');
+		$this->setUserAgent(sprintf('Dotclear/%s)', DC_VERSION));
+	}
+
+	public function parse($url)
+	{
+		$this->validators = array();
+		if ($this->cache_dir)
+		{
+			return $this->withCache($url);
+		}
+		else
+		{
+			if (!$this->getModulesXML($url)) {
+				return false;
+			}
+
+			if ($this->getStatus() != '200') {
+				return false;
+			}
+
+			return new dcRepositoryParser($this->getContent());
+		}
+	}
+
+	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);
+	}
+
+	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;
+	}
+
+	public function setCacheTTL($str)
+	{
+		$str = trim($str);
+		if (!empty($str))
+		{
+			if (substr($str, 0, 1) != '-') {
+				$str = '-'.$str;
+			}
+			$this->cache_ttl = $str;
+		}
+	}
+
+	public function setForce($force)
+	{
+		$this->force = $force;
+	}
+
+	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);
+	}
+
+	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;
+
+		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);
+		}
+
+		if (!$this->getModulesXML($url))
+		{
+			if ($may_use_cached)
+			{
+				if ($this->cache_touch_on_fail) {
+					@files::touch($cached_file);
+				}
+				# connection failed - fetched from cache
+				return unserialize(file_get_contents($cached_file));
+			}
+			return false;
+		}
+
+		switch ($this->getStatus())
+		{
+			case '304':
+				@files::touch($cached_file);
+				return unserialize(file_get_contents($cached_file));
+			case '200':
+				if ($modules = new dcRepositoryParser($this->getContent()))
+				{
+					try {
+						files::makeDir(dirname($cached_file), true);
+					} catch (Exception $e) {
+						return $modules;
+					}
+
+					if (($fp = @fopen($cached_file, 'wb')))
+					{
+						fwrite($fp, serialize($modules));
+						fclose($fp);
+						files::inheritChmod($cached_file);
+					}
+					return $modules;
+				}
+		}
+
+		return false;
+	}
+
+	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;
+	}
+
+	private function setValidator($key, $value)
+	{
+		if ($key == 'IfModifiedSince') {
+			$value = gmdate('D, d M Y H:i:s', $value).' GMT';
+		}
+
+		$this->validators[$key] = $value;
+	}
+}
Index: inc/prepend.php
===================================================================
--- inc/prepend.php	(revision 1999)
+++ inc/prepend.php	(revision 2147)
@@ -46,4 +46,7 @@
 $__autoload['dcWorkspace']			= dirname(__FILE__).'/core/class.dc.workspace.php';
 $__autoload['dcPrefs']				= dirname(__FILE__).'/core/class.dc.prefs.php';
+$__autoload['dcRepository']			= dirname(__FILE__).'/core/class.dc.repository.php';
+$__autoload['dcRepositoryReader']		= dirname(__FILE__).'/core/class.dc.repository.reader.php';
+$__autoload['dcRepositoryParser']		= dirname(__FILE__).'/core/class.dc.repository.parser.php';
 
 $__autoload['rsExtPost']				= dirname(__FILE__).'/core/class.dc.rs.extensions.php';
@@ -61,4 +64,6 @@
 $__autoload['dcPager']		= dirname(__FILE__).'/admin/lib.pager.php';
 $__autoload['dcAdminCombos']			= dirname(__FILE__).'/admin/lib.admincombos.php';
+$__autoload['adminModulesList']			= dirname(__FILE__).'/admin/lib.moduleslist.php';
+$__autoload['adminThemesList']			= dirname(__FILE__).'/admin/lib.moduleslist.php';
 
 $__autoload['dcTemplate']			= dirname(__FILE__).'/public/class.dc.template.php';
Index: plugins/maintenance/_admin.php
===================================================================
--- plugins/maintenance/_admin.php	(revision 2177)
+++ plugins/maintenance/_admin.php	(revision 2163)
@@ -29,4 +29,5 @@
 $core->addBehavior('adminAfterDashboardOptionsUpdate',	array('dcMaintenanceAdmin',	'adminAfterDashboardOptionsUpdate'));
 $core->addBehavior('adminPageHelpBlock',	array('dcMaintenanceAdmin',	'adminPageHelpBlock'));
+$core->addBehavior('pluginsToolsHeaders',	array('dcMaintenanceAdmin',	'pluginsToolsHeaders'));
 
 /**
@@ -128,5 +129,4 @@
 
 		$icon[0] .= '<br />'.sprintf(__('One task to execute', '%s tasks to execute', $count), $count);
-		$icon[2] = 'index.php?pf=maintenance/icon-big-update.png';
 	}
 
@@ -223,5 +223,4 @@
 		$core->auth->user_prefs->maintenance->put('dashboard_item', !empty($_POST['maintenance_dashboard_item']), 'boolean');
 	}
-
 
 	/**
@@ -294,3 +293,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 2182)
+++ plugins/maintenance/_config.php	(revision 2182)
@@ -0,0 +1,99 @@
+<?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()
+			);
+		}
+		
+		http::redirect($list->getPageURL('module=maintenance&conf=1&done=1'));
+	}
+	catch(Exception $e) {
+		$core->error->add($e->getMessage());
+	}
+}
+
+	echo 
+	'<p>'.__('Manage alert for maintenance task.').'</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>';
 }
 
Index: plugins/tags/js/post.js
===================================================================
--- plugins/tags/js/post.js	(revision 2180)
+++ plugins/tags/js/post.js	(revision 1871)
@@ -26,5 +26,4 @@
 			delay: 1000,
 			multiple: true,
-			multipleSeparator: ", ",
 			matchSubset: false,
 			matchContains: true,
