Index: inc/core/class.dc.store.parser.php
===================================================================
--- inc/core/class.dc.store.parser.php	(revision 2222)
+++ inc/core/class.dc.store.parser.php	(revision 2222)
@@ -0,0 +1,106 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed parser
+@since 2.6
+
+Provides an object to parse XML feed of modules from a repository.
+*/
+class dcStoreParser
+{
+	/** @var	object	XML object of feed contents */
+	protected $xml;
+	/** @var	array	Array of feed contents */
+	protected $items;
+	/** @var	string	XML bloc tag */
+	protected static $bloc = 'http://dotaddict.org/da/';
+
+	/**
+	 * Constructor.
+	 *
+	 * @param	string	$data		Feed content
+	 */
+	public function __construct($data)
+	{
+		if (!is_string($data)) {
+			throw new Exception(__('Failed to read data feed'));
+		}
+
+		$this->xml = simplexml_load_string($data);
+		$this->items = array();
+
+		if ($this->xml === false) {
+			throw new Exception(__('Wrong data feed'));
+		}
+
+		$this->_parse();
+
+		unset($data);
+		unset($this->xml);
+	}
+
+	/**
+	 * Parse XML into array
+	 */
+	protected function _parse()
+	{
+		if (empty($this->xml->module)) {
+			return;
+		}
+
+		foreach ($this->xml->module as $i) {
+			$attrs = $i->attributes();
+
+			$item = array();
+
+			# DC/DA shared markers
+			$item['id']		= (string) $attrs['id'];
+			$item['file']		= (string) $i->file;
+			$item['label']		= (string) $i->name; // deprecated
+			$item['name']		= (string) $i->name;
+			$item['version']	= (string) $i->version;
+			$item['author']	= (string) $i->author;
+			$item['desc']		= (string) $i->desc;
+
+			# DA specific markers
+			$item['dc_min'] 	= (string) $i->children(self::$bloc)->dcmin;
+			$item['details'] 	= (string) $i->children(self::$bloc)->details;
+			$item['section'] 	= (string) $i->children(self::$bloc)->section;
+			$item['support'] 	= (string) $i->children(self::$bloc)->support;
+			$item['sshot']		= (string) $i->children(self::$bloc)->sshot;
+
+			$tags = array();
+			foreach($i->children(self::$bloc)->tags as $t) {
+				$tags[] = (string) $t->tag;
+			}
+			$item['tags']		= implode(', ',$tags);
+			
+			# First filter right now. If DC_DEV is set all modules are parse
+			if (defined('DC_DEV') && DC_DEV === true || version_compare(DC_VERSION,$item['dc_min'],'>=')) {
+				$this->items[$item['id']] = $item;
+			}
+		}
+	}
+
+	/**
+	 * Get modules.
+	 *
+	 * @return	array		Modules list
+	 */
+	public function getModules()
+	{
+		return $this->items;
+	}
+}
Index: inc/core/class.dc.store.php
===================================================================
--- inc/core/class.dc.store.php	(revision 2270)
+++ inc/core/class.dc.store.php	(revision 2270)
@@ -0,0 +1,287 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed reader
+@since 2.6
+
+Provides an object to parse XML feed of modules from repository.
+*/
+class dcStore
+{
+	/** @var	object	dcCore instance */
+	public $core;
+	/** @var	object	dcModules instance */
+	public $modules;
+
+	/** @var	array	Modules fields to search on and their weighting */
+	public static $weighting = array('id' => 10, 'name' => 8, 'author' => 6, 'tags' => 4, 'desc' => 2);
+
+	/** @var	string	User agent used to query repository */
+	protected $user_agent = 'DotClear.org RepoBrowser/0.1';
+	/** @var	string	XML feed URL */
+	protected $xml_url;
+	/** @var	array	Array of new/update modules from repository */
+	protected $data;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param	object	$modules		dcModules instance
+	 * @param	string	$xml_url		XML feed URL
+	 */
+	public function __construct(dcModules $modules, $xml_url)
+	{
+		$this->core = $modules->core;
+		$this->modules = $modules;
+		$this->xml_url = $xml_url;
+		$this->user_agent = sprintf('Dotclear/%s)', DC_VERSION);
+
+		$this->check();
+	}
+
+	/**
+	 * Check repository.
+	 *
+	 * @param	boolean	$force		Force query repository
+	 * @return	boolean	True if get feed or cache
+	 */
+	public function check($force=false)
+	{
+		if (!$this->xml_url) {
+			return false;
+		}
+		if (($parser = dcStoreReader::quickParse($this->xml_url, DC_TPL_CACHE, $force)) === false) {
+			return false;
+		}
+
+		$raw_datas = $parser->getModules();
+
+		uasort($raw_datas, array('self','sort'));
+
+		$skipped = array_keys($this->modules->getDisabledModules());
+		foreach ($skipped as $p_id) {
+			if (isset($raw_datas[$p_id])) {
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$updates = array();
+		$current = $this->modules->getModules();
+		foreach ($current as $p_id => $p_infos) {
+			if (isset($raw_datas[$p_id])) {
+				if (self::compare($raw_datas[$p_id]['version'],$p_infos['version'],'>')) {
+					$updates[$p_id] = $raw_datas[$p_id];
+					$updates[$p_id]['root'] = $p_infos['root'];
+					$updates[$p_id]['root_writable'] = $p_infos['root_writable'];
+					$updates[$p_id]['current_version'] = $p_infos['version'];
+				}
+				unset($raw_datas[$p_id]);
+			}
+		}
+
+		$this->data = array(
+			'new'	=> $raw_datas,
+			'update'	=> $updates
+		);
+
+		return true;
+	}
+
+	/**
+	 * Get a list of modules.
+	 *
+	 * @param	boolean	$update	True to get update modules, false for new ones
+	 * @return	array	List of update/new modules
+	 */
+	public function get($update=false)
+	{
+		return $this->data[$update ? 'update' : 'new'];
+	}
+
+	/**
+	 * Search a module.
+	 *
+	 * Search string is cleaned, split and compare to split:
+	 * - module id and clean id,
+	 * - module name, clean name,
+	 * - module desccription.
+	 *
+	 * Every time a part of query is find on module,
+	 * result accuracy grow. Result is sorted by accuracy.
+	 *
+	 * @param	string	$pattern	String to search
+	 * @return	array	Match modules
+	 */
+	public function search($pattern)
+	{
+		$result = array();
+
+		# Split query into small clean words
+		if (!($patterns = self::patternize($pattern))) {
+			return $result;
+		}
+
+		# For each modules
+		foreach ($this->data['new'] as $id => $module) {
+			$module['id'] = $id;
+
+			# Loop through required module fields
+			foreach(self::$weighting as $field => $weight) {
+
+				# Skip fields which not exsist on module
+				if (empty($module[$field])) {
+					continue;
+				}
+
+				# Split field value into small clean word
+				if (!($subjects = self::patternize($module[$field]))) {
+					continue;
+				}
+
+				# Check contents
+				if (!($nb = preg_match_all('/('.implode('|', $patterns).')/', implode(' ', $subjects), $_))) {
+					continue;
+				}
+
+				# Add module to result
+				if (!isset($sorter[$id])) {
+					$sorter[$id] = 0;
+					$result[$id] = $module;
+				}
+
+				# Increment score by matches count * field weight
+				$sorter[$id] += $nb * $weight;
+				$result[$id]['score'] = $sorter[$id];
+			}
+		}
+		# Sort response by matches count
+		if (!empty($result)) {
+			array_multisort($sorter, SORT_DESC, $result);
+		}
+		return $result;
+	}
+
+	/**
+	 * Quick download and install module.
+	 *
+	 * @param	string	$url	Module package URL
+	 * @param	string	$dest	Path to install module
+	 * @return	integer		1 = installed, 2 = update
+	 */
+	public function process($url, $dest)
+	{
+		$this->download($url, $dest);
+		return $this->install($dest);
+	}
+
+	/**
+	 * Download a module.
+	 *
+	 * @param	string	$url	Module package URL
+	 * @param	string	$dest	Path to put module package
+	 */
+	public function download($url, $dest)
+	{
+		try {
+			$client = netHttp::initClient($url, $path);
+			$client->setUserAgent($this->user_agent);
+			$client->useGzip(false);
+			$client->setPersistReferers(false);
+			$client->setOutput($dest);
+			$client->get($path);
+			unset($client);
+		}
+		catch (Exception $e) {
+			unset($client);
+			throw new Exception(__('An error occurred while downloading the file.'));
+		}
+	}
+
+	/**
+	 * Install a previously downloaded module.
+	 *
+	 * @param	string	$path	Module package URL
+	 * @param	string	$path	Path to module package
+	 * @return	integer		1 = installed, 2 = update
+	 */
+	public function install($path)
+	{
+		return dcModules::installPackage($path, $this->modules);
+	}
+
+	/**
+	 * User Agent String.
+	 *
+	 * @param	string	$str		User agent string
+	 */
+	public function agent($str)
+	{
+		$this->user_agent = $str;
+	}
+
+	/**
+	 * Split and clean pattern.
+	 *
+	 * @param	string	$str		String to sanitize
+	 * @return	array	Array of cleaned pieces of string or false if none
+	 */
+	public static function patternize($str)
+	{
+		$arr = array();
+
+		foreach(explode(' ', $str) as $_) {
+			$_ = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $_));
+			if (strlen($_) > 2) {
+				$arr[] = $_;
+			}
+		}
+
+		return empty($arr) ? false : $arr;
+	}
+
+	/**
+	 * Compare version.
+	 *
+	 * @param	string	$v1		Version
+	 * @param	string	$v2		Version
+	 * @param	string	$op		Comparison operator
+	 * @return	boolean	True is comparison is true, dude!
+	 */
+	private static function compare($v1, $v2, $op)
+	{
+		return version_compare(
+			preg_replace('!-r(\d+)$!', '-p$1', $v1), 
+			preg_replace('!-r(\d+)$!', '-p$1', $v2), 
+			$op
+		);
+	}
+
+	/** 
+	 * Sort modules list.
+	 *
+	 * @param	array	$a		A module
+	 * @param	array	$b		A module
+	 * @return	integer
+	 */
+	private static function sort($a,$b)
+	{
+		$c = strtolower($a['id']); 
+		$d = strtolower($b['id']); 
+		if ($c == $d) { 
+			return 0; 
+		} 
+		return ($c < $d) ? -1 : 1; 
+	}
+}
Index: inc/core/class.dc.store.reader.php
===================================================================
--- inc/core/class.dc.store.reader.php	(revision 2222)
+++ inc/core/class.dc.store.reader.php	(revision 2222)
@@ -0,0 +1,267 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+/**
+@ingroup DC_CORE
+@brief Repository modules XML feed reader
+@since 2.6
+
+Provides an object to parse XML feed of modules from repository.
+This class extends clearbricks netHttp class.
+*/
+class dcStoreReader extends netHttp
+{
+	/** @var	string	User agent used to query repository */
+	protected $user_agent = 'DotClear.org RepoBrowser/0.1';
+	/** @var	integer	User agent used to query repository */
+	protected $timeout = 5;
+	/** @var	array	 HTTP Cache validators */
+	protected $validators = null;
+	/** @var	string	Cache temporary directory */
+	protected $cache_dir = null;
+	/** @var	string	Cache file prefix */
+	protected $cache_file_prefix = 'dcrepo';
+	/** @var	integer	Cache TTL */
+	protected $cache_ttl = '-30 minutes';
+	/** @var	boolean	'Cache' TTL on server failed */
+	protected $cache_touch_on_fail = true;
+	/** @var	boolean	Force query server */
+	protected $force = false;
+
+	/**
+	 * Constructor.
+	 *
+	 * Bypass first argument of clearbricks netHttp constructor.
+	 */
+	public function __construct()
+	{
+		parent::__construct('');
+		$this->setUserAgent(sprintf('Dotclear/%s)', DC_VERSION));
+	}
+
+	/**
+	 * Parse modules feed.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	object	dcStore instance
+	 */
+	public function parse($url)
+	{
+		$this->validators = array();
+
+		if ($this->cache_dir) {
+			return $this->withCache($url);
+		}
+		elseif (!$this->getModulesXML($url) || $this->getStatus() != '200') {
+			return false;
+		}
+
+		return new dcStoreParser($this->getContent());
+	}
+
+	/**
+	 * Quick parse modules feed.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @param	string	$cache_dir	Cache directoy or null for no cache
+	 * @param	boolean	$force		Force query repository
+	 * @return	object	Self instance
+	 */
+	public static function quickParse($url, $cache_dir=null, $force=false)
+	{
+		$parser = new self();
+		if ($cache_dir) {
+			$parser->setCacheDir($cache_dir);
+		}
+		if ($force) {
+			$parser->setForce($force);
+		}
+
+		return $parser->parse($url);
+	}
+
+	/**
+	 * Set cache directory.
+	 *
+	 * @param	string	$dir		Cache directory
+	 * @return	boolean	True if cache dierctory is useable
+	 */
+	public function setCacheDir($dir)
+	{
+		$this->cache_dir = null;
+
+		if (!empty($dir) && is_dir($dir) && is_writeable($dir)) {
+			$this->cache_dir = $dir;
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Set cache TTL.
+	 *
+	 * @param	string	$str		Cache TTL
+	 */
+	public function setCacheTTL($str)
+	{
+		$str = trim($str);
+
+		if (!empty($str)) {
+			$this->cache_ttl = substr($str, 0, 1) == '-' ? $str : '-'.$str;
+		}
+	}
+
+	/**
+	 * Set force query repository.
+	 *
+	 * @param	boolean	$force	True to force query
+	 */
+	public function setForce($force)
+	{
+		$this->force = $force;
+	}
+
+	/**
+	 * Get repository XML feed URL content.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	string	Feed content
+	 */
+	protected function getModulesXML($url)
+	{
+		if (!self::readURL($url, $ssl, $host, $port, $path, $user, $pass)) {
+			return false;
+		}
+		$this->setHost($host, $port);
+		$this->useSSL($ssl);
+		$this->setAuthorization($user, $pass);
+
+		return $this->get($path);
+	}
+
+	/**
+	 * Get repository modules list using cache.
+	 *
+	 * @param	string	$url		XML feed URL
+	 * @return	array	Feed content or False on fail
+	 */
+	protected function withCache($url)
+	{
+		$url_md5 = md5($url);
+		$cached_file = sprintf('%s/%s/%s/%s/%s.ser',
+			$this->cache_dir,
+			$this->cache_file_prefix,
+			substr($url_md5,0,2),
+			substr($url_md5,2,2),
+			$url_md5
+		);
+
+		$may_use_cached = false;
+
+		# Use cache file ?
+		if (@file_exists($cached_file) && !$this->force) {
+			$may_use_cached = true;
+			$ts = @filemtime($cached_file);
+			if ($ts > strtotime($this->cache_ttl)) {
+				# Direct cache
+				return unserialize(file_get_contents($cached_file));
+			}
+			$this->setValidator('IfModifiedSince', $ts);
+		}
+
+		# Query repository
+		if (!$this->getModulesXML($url)) {
+			if ($may_use_cached) {
+				# Touch cache TTL even if query failed ?
+				if ($this->cache_touch_on_fail) {
+					@files::touch($cached_file);
+				}
+				# Connection failed - fetched from cache
+				return unserialize(file_get_contents($cached_file));
+			}
+			return false;
+		}
+
+		# Parse response
+		switch ($this->getStatus())
+		{
+			# Not modified, use cache
+			case '304':
+				@files::touch($cached_file);
+				return unserialize(file_get_contents($cached_file));
+			# Ok, parse feed
+			case '200':
+				if ($modules = new dcStoreParser($this->getContent())) {
+					try {
+						files::makeDir(dirname($cached_file), true);
+					}
+					catch (Exception $e) {
+						return $modules;
+					}
+
+					if (($fp = @fopen($cached_file, 'wb'))) {
+						fwrite($fp, serialize($modules));
+						fclose($fp);
+						files::inheritChmod($cached_file);
+					}
+					return $modules;
+				}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Prepare query.
+	 *
+	 * @return	array	Query headers
+	 */
+	protected function buildRequest()
+	{
+		$headers = parent::buildRequest();
+
+		# Cache validators
+		if (!empty($this->validators)) {
+			if (isset($this->validators['IfModifiedSince'])) {
+				$headers[] = 'If-Modified-Since: '.$this->validators['IfModifiedSince'];
+			}
+			if (isset($this->validators['IfNoneMatch'])) {
+				if (is_array($this->validators['IfNoneMatch'])) {
+					$etags = implode(',', $this->validators['IfNoneMatch']);
+				}
+				else {
+					$etags = $this->validators['IfNoneMatch'];
+				}
+				$headers[] = '';
+			}
+		}
+
+		return $headers;
+	}
+
+	/**
+	 * Tweak query cache validator.
+	 *
+	 * @param	string	$key		Validator key
+	 * @param	string	$value		Validator value
+	 */
+	private function setValidator($key, $value)
+	{
+		if ($key == 'IfModifiedSince') {
+			$value = gmdate('D, d M Y H:i:s', $value).' GMT';
+		}
+
+		$this->validators[$key] = $value;
+	}
+}
