Index: .hgsubstate
===================================================================
--- .hgsubstate	(revision 843)
+++ .hgsubstate	(revision 1102)
@@ -1,1 +1,1 @@
-4f62679e0756104bdc0b49eacaed9aba68735aa5 inc/libs/clearbricks
+789e7f2b4844160f4dc7d2795cfa1e6f4fd53a5c inc/libs/clearbricks
Index: CHANGELOG
===================================================================
--- CHANGELOG	(revision 1046)
+++ CHANGELOG	(revision 1104)
@@ -1,4 +1,5 @@
-Dotclear 2.5.0 - 201?-??-13
-===========================================================
+Dotclear 2.5.0 - 2013-02-24
+===========================================================
+* Ductile theme may now use webfont (from Google, Adobe and similar providers)
 * daInstaller plugin is now included in the official distribution
 * The media enhanced uploader may now be temporarily enabled or disabled
@@ -24,5 +25,5 @@
 * Adaptative compression rate for thumbnails generation
 * Add timestamp on admin information messages
-* Update to jQuery 1.8.2 and jQuery-UI 1.9 (custom)
+* Update to jQuery 1.8.3 and jQuery-UI 1.9.2 (custom)
 * Add default favicon.ico
 * Add default attributes settings for image,mp3,flv insertion
Index: admin/media_item.php
===================================================================
--- admin/media_item.php	(revision 1073)
+++ admin/media_item.php	(revision 1103)
@@ -60,8 +60,6 @@
 	# Prepare directories combo box
 	$dirs_combo = array();
-	foreach ($core->media->getRootDirs() as $v) {
-		if ($v->w) {
-			$dirs_combo['/'.$v->relname] = $v->relname;
-		}
+	foreach ($core->media->getDBDirs() as $v) {
+		$dirs_combo['/'.$v] = $v;
 	}
 	ksort($dirs_combo);
Index: admin/posts.php
===================================================================
--- admin/posts.php	(revision 1152)
+++ admin/posts.php	(revision 1153)
@@ -110,5 +110,7 @@
 	__('Mark as unselected') => 'unselected'
 );
-$combo_action[__('Change')] = array(__('Change category') => 'category');
+$combo_action[__('Change')] = array(
+	__('Change category') => 'category',
+	__('Change language') => 'lang');
 if ($core->auth->check('admin',$core->blog->id))
 {
Index: admin/posts_actions.php
===================================================================
--- admin/posts_actions.php	(revision 1035)
+++ admin/posts_actions.php	(revision 1102)
@@ -157,4 +157,20 @@
 		{
 			$core->error->add($e->getMessage());
+		}
+	}
+	elseif ($action == 'lang' && isset($_POST['new_lang']))
+	{
+		$new_lang = $_POST['new_lang'];
+		try
+		{
+			$cur = $core->con->openCursor($core->prefix.'post');
+			$cur->post_lang = $new_lang;
+			$cur->update('WHERE post_id '.$core->con->in($posts_ids));
+			
+			http::redirect($redir);
+		}
+		catch (Exception $e)
+		{
+			$core->error->add($e->getMessages());
 		}
 	}
@@ -257,4 +273,38 @@
 	'</form>';
 }
+elseif ($action == 'lang')
+{
+	echo '<h2 class="page-title">'.__('Change language for entries').'</h2>';
+	
+	# lang list
+	# Languages combo
+	$rs = $core->blog->getLangs(array('order'=>'asc'));
+	$all_langs = l10n::getISOcodes(0,1);
+	$lang_combo = array('' => '', __('Most used') => array(), __('Available') => l10n::getISOcodes(1,1));
+	while ($rs->fetch()) {
+		if (isset($all_langs[$rs->post_lang])) {
+			$lang_combo[__('Most used')][$all_langs[$rs->post_lang]] = $rs->post_lang;
+			unset($lang_combo[__('Available')][$all_langs[$rs->post_lang]]);
+		} else {
+			$lang_combo[__('Most used')][$rs->post_lang] = $rs->post_lang;
+		}
+	}
+	unset($all_langs);
+	unset($rs);
+	
+	echo
+	'<form action="posts_actions.php" method="post">'.
+	'<p><label for="new_lang" class="classic">'.__('Entry lang:').' '.
+	form::combo('new_lang',$lang_combo,'').
+	'</label> ';
+	
+	echo
+	$hidden_fields.
+	$core->formNonce().
+	form::hidden(array('action'),'lang').
+	'<input type="submit" value="'.__('Save').'" /></p>'.
+	'</form>';
+
+}
 elseif ($action == 'author' && $core->auth->check('admin',$core->blog->id))
 {
Index: inc/admin/default-templates/forms/form_layout.html.twig
===================================================================
--- inc/admin/default-templates/forms/form_layout.html.twig	(revision 1152)
+++ inc/admin/default-templates/forms/form_layout.html.twig	(revision 1153)
@@ -57,5 +57,5 @@
 		<label for="{{name}}" class="classic">
 	{% endif %}
-    <input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
+    <input type="checkbox" {{ block('field_attr') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
 	{% if label is not empty %}
 		{{ label }}</label>
Index: inc/core/class.dc.context.php
===================================================================
--- inc/core/class.dc.context.php	(revision 1138)
+++ inc/core/class.dc.context.php	(revision 1138)
@@ -0,0 +1,340 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2011 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 Template extension for public context
+ *
+ * This extends template environment with tools required in context.
+ * Admin context and public context should extend this class.
+ */
+class dcContext extends Twig_Extension
+{
+	protected $core;
+	protected $globals = array();
+	protected $protected_globals = array();
+	protected $memory = array();
+	protected $timestamp_messages = true;
+	
+	public function __construct($core)
+	{
+		$this->core = $core;
+		
+		# Globals editable via context
+		$this->globals = array();
+		
+		# Globals not editable via context
+		$this->protected_globals = array(
+			'messages' => array(
+				'static' => array(),
+				'lists' => array(),
+				'alert' => '',
+				'errors' => array()
+			),
+			
+			'version' 		=> DC_VERSION,
+			'vendor_name' 	=> DC_VENDOR_NAME,
+			
+			'safe_mode' 	=> isset($_SESSION['sess_safe_mode']) && $_SESSION['sess_safe_mode'],
+			'debug_mode'	=> DC_DEBUG
+		);
+	}
+	
+	/**
+	 * Prevent call crash from template on method that return this class
+	 */
+	public function __toString()
+	{
+		return '';
+	}
+	
+	/**
+	 * Test a global variable
+	 *
+	 * @param string $name Name of the variable to test
+	 * @return boolean
+	 */
+	public function __isset($name)
+	{
+		return isset($this->globals[$name]);
+	}
+	
+	/**
+	 * Add a global variable
+	 *
+	 * @param string $name Name of the variable
+	 * @param mixed $value Value of the variable
+	 */
+	public function __set($name,$value)
+	{
+/*
+		# Overload protect
+		if ($value === null && isset($this->globals[$name])) {
+			unset($this->globals[$name]);
+		}
+		elseif (!isset($this->globals[$name])) {
+			throw new Exception('Modification of overloaded globals has no effect');
+		}
+//*/
+		$this->globals[$name] = $value;
+	}
+	
+	/**
+	 * Get a global variable
+	 *
+	 * @param string $name Name of the variable
+	 * @return mixed Value of the variable or null
+	 */
+	public function __get($name)
+	{
+		return isset($this->globals[$name]) ? $this->globals[$name] : null;
+	}
+	
+	/**
+	 * Returns a list of filters to add to the existing list.
+	 *
+	 * @return array An array of filters
+	 */
+	public function getFilters()
+	{
+		return array(
+			'trans' => new Twig_Filter_Function("__", array('is_safe' => array('html')))
+		);
+	}
+	
+	/**
+	 * Returns a list of functions to add to the existing list.
+	 * 
+	 * @return array An array of functions
+	 */
+	public function getFunctions()
+	{
+		return array(
+			'__' 		=> new Twig_Function_Function("__", array('is_safe' => array('html'))),
+			'debug_info' => new Twig_Function_Method($this, 'getDebugInfo', array('is_safe' => array('html'))),
+			'memorize' => new Twig_Function_Method($this, 'setMemory', array('is_safe' => array('html'))),
+			'memorized' => new Twig_Function_Method($this, 'getMemory', array('is_safe' => array('html')))
+		);
+	}
+	
+	/**
+	 * Returns a list of global variables to add to the existing list.
+	 *
+	 * This merges overloaded variables with defined variables.
+	 *
+	 * @return array An array of global variables
+	 */
+	public function getGlobals()
+	{
+		# Keep protected globals safe
+		return array_merge($this->globals,$this->protected_globals);
+	}
+	
+	/**
+	 * Returns the name of the extension.
+	 *
+	 * @return string The extension name
+	 */
+	public function getName()
+	{
+		return 'dcContext';
+	}
+	
+	
+	/**
+	 * Add an informational message
+	 *
+	 * @param string $message A message
+	 * @return object self
+	 */
+	public function setSafeMode($safe_mode)
+	{
+		$this->protected_globals['safe_mode'] = (boolean) $safe_mode;
+		return $this;
+	}
+	
+	/**
+	 * Add an informational message
+	 *
+	 * @param string $message A message
+	 * @return object self
+	 */
+	public function addMessageStatic($message)
+	{
+		$this->protected_globals['messages']['static'][] = $this->timeStamp($message);
+		return $this;
+	}
+	
+	/**
+	 * Add a list of informational messages
+	 *
+	 * @param string $message A title
+	 * @param array $message A list of messages
+	 * @return object self
+	 */
+	public function addMessagesList($title,$messages)
+	{
+		$this->protected_globals['messages']['lists'][$title] = $this->timeStamp($messages);
+		return $this;
+	}
+	
+	/**
+	 * Set an important message
+	 *
+	 * @param string $message A message
+	 * @return object self
+	 */
+	public function setAlert($message)
+	{
+		$this->protected_globals['messages']['alert'] = $this->timeStamp($message);
+		return $this;
+	}
+	
+	/**
+	 * Add an error message
+	 *
+	 * @param string Error message
+	 * @return object self
+	 */
+	public function addError($error)
+	{
+		$this->protected_globals['messages']['errors'][] = $this->timeStamp($error);
+		return $this;
+	}
+	
+	/**
+	 * Check if there is an error message
+	 * 
+	 * @return boolean
+	 */
+	public function hasError()
+	{
+		return !empty($this->protected_globals['messages']['errors']);
+	}
+	
+	/**
+	 * Set use of timestamp on messages
+	 *
+	 * @param boolean $add Add timestamp to messages
+	 * @return object self
+	 */
+	public function setTimestamp($add=true)
+	{
+		$this->timestamp_messages;
+		return $this;
+	}
+	
+	/**
+	 * Prepend message with a timestamp
+	 *
+	 * @param string|array $message Message(s)
+	 * @return string|array Timestamp message(s)
+	 */
+	public function timeStamp($message)
+	{
+		if (!$this->timestamp_messages) {
+			return $message;
+		}
+		
+		$tz = @$this->core->auth->getInfo('user_tz');
+		if (!$tz) {
+			$tz = $this->core->blog->settings->system->blog_timezone;
+		}
+		$dt = dt::str(__('%H:%M:%S:'),null,$tz);
+		
+		if (!is_array($message)) {
+			return $dt.' '.$message;
+		} else {
+			foreach($message as $k => $v)
+			{
+				$message[$k] = $dt.' '.$v;
+			}
+			return $message;
+		}
+	}
+	
+	/**
+	 * Get an array of debug/dev infos
+	 */
+	public function getDebugInfo()
+	{
+		if (!DC_DEBUG) {
+			return array();
+		}
+		
+		$di = array(
+			'global_vars' => implode(', ',array_keys($GLOBALS)),
+			'memory' => array(
+				'usage' => memory_get_usage(),
+				'size' => files::size(memory_get_usage())
+			),
+			'xdebug' => array()
+		);
+		
+		if (function_exists('xdebug_get_profiler_filename')) {
+		
+			$url = http::getSelfURI();
+			$url .= strpos($url,'?') === false ? '?' : '&';
+			$url .= 'XDEBUG_PROFILE';
+			
+			$di['xdebug'] = array(
+				'elapse_time' => xdebug_time_index(),
+				'profiler_file' => xdebug_get_profiler_filename(),
+				'profiler_url' =>  $url
+			);
+			
+			/* xdebug configuration:
+			zend_extension = /.../xdebug.so
+			xdebug.auto_trace = On
+			xdebug.trace_format = 0
+			xdebug.trace_options = 1
+			xdebug.show_mem_delta = On
+			xdebug.profiler_enable = 0
+			xdebug.profiler_enable_trigger = 1
+			xdebug.profiler_output_dir = /tmp
+			xdebug.profiler_append = 0
+			xdebug.profiler_output_name = timestamp
+			*/
+		}
+		
+		return $di;
+	}
+	
+	/**
+	 * Add a value in a namespace memory
+	 *
+	 * This help keep variable when recalling Twig macros
+	 *
+	 * @param string $ns A namespace
+	 * @param string $str A value to memorize in this namespace
+	 */
+	public function setMemory($ns,$str)
+	{
+		if (!array_key_exists($ns,$this->memory) || !in_array($str,$this->memory[$ns])) {
+			$this->memory[$ns][] = $str;
+		}
+	}
+	
+	/**
+	 * Check if a value is previously memorized in a namespace
+	 *
+	 * @param string $ns A namespace
+	 * @param string $str A value to search in this namespace
+	 * @return array True if exists
+	 */
+	public function getMemory($ns,$str)
+	{
+		return array_key_exists($ns,$this->memory) && in_array($str,$this->memory[$ns]);
+	}
+}
+?>
Index: inc/core/class.dc.media.php
===================================================================
--- inc/core/class.dc.media.php	(revision 909)
+++ inc/core/class.dc.media.php	(revision 1103)
@@ -821,4 +821,29 @@
 		$this->callFileHandler(files::getMimeType($media_file),'remove',$f);
 	}
+
+	/**
+	* Root directories
+	*
+	* Returns an array of directory under {@link $root} directory.
+	*
+	* @uses fileItem
+	* @return array
+	*/
+	public function getDBDirs()
+	{
+		$media_dir = $this->relpwd ? $this->relpwd : '.';
+		
+		$strReq =
+		'SELECT distinct media_dir '.
+		'FROM '.$this->table.' '.
+		"WHERE media_path = '".$this->path."'";
+		$rs = $this->con->select($strReq);
+		while ($rs->fetch()) {
+			if (is_dir($this->root.'/'.$rs->media_dir))
+				$dir[] = $rs->media_dir;
+		}
+		
+		return $dir;
+	}
 	
 	/**
Index: inc/core/class.dc.template.php
===================================================================
--- inc/core/class.dc.template.php	(revision 1128)
+++ inc/core/class.dc.template.php	(revision 1128)
@@ -0,0 +1,241 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2011 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 Template environment.
+ *
+ * This class provides same Twig environment for both admin and public side. 
+ * It adds methods for backwards compatibility with old template engine.
+ *
+ * default-templates path must be added from admin|public/prepend.php with:
+ * $core->tpl->getLoader()->addPath('PATH_TO/default-templates');
+ * Selected theme path must be added with:
+ * $core->tpl->getLoader()->prependPath('PATH_TO/MY_THEME');
+ */
+class dcTemplate extends Twig_Environment
+{
+	protected $dc_blocks = array();
+	protected $dc_values = array();
+	
+	private $core;
+	
+	/**
+	 * Create template environment (Twig_Environment instance).
+	 * 
+	 * We keep same signature as the old dcTemplate class.
+	 */
+	public function __construct($cache_dir,$self_name,$core)
+	{
+		$this->core =& $core;
+		
+		# Check cache directory
+		$cache_dir = path::real($cache_dir.'/twtpl',false);
+		if (!is_dir($cache_dir)) {
+			try {
+				files::makeDir($cache_dir);
+			} catch (Exception $e) {
+				$cache_dir = false;
+			}
+		}
+		
+		# Create Twig environment
+		parent::__construct(
+			new Twig_Loader_Filesystem(dirname(__FILE__).'/../swf'),
+			array(
+				'auto_reload' => true,
+				'autoescape' => false,
+				'base_template_class' => 'Twig_Template',
+				'cache' => $cache_dir, 
+				'charset' => 'UTF-8',
+				'debug' => DC_DEBUG,
+				'optimizations' => -1,
+				'strict_variables' => 0 //DC_DEBUG // Please fix undefined variables!
+			)
+		);
+		
+		# Add extensions
+		$this->addExtension(new dcFormExtension($this));
+		$this->addExtension(new dcTabExtension($this));
+	}
+	
+	/// @name Old template engine compatibility methods
+	//@{
+	/**
+	 * Old style to add a template block.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function addBlock($name,$callback)
+	{
+		if (!is_callable($callback)) {
+			throw new Exception('No valid callback for '.$name);
+		}
+		
+		$this->dc_blocks[$name] = $callback;
+	}
+	
+	/**
+	 * Old style to add a template value.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function addValue($name,$callback)
+	{
+		if (!is_callable($callback)) {
+			throw new Exception('No valid callback for '.$name);
+		}
+		
+		$this->dc_values[$name] = $callback;
+	}
+	
+	/**
+	 * Old style to check if a template block exists.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function blockExists($name)
+	{
+		return isset($this->dc_blocks[$name]);
+	}
+	
+	
+	/**
+	 * Old style to check if a template value exists.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function valueExists($name)
+	{
+		return isset($this->dc_values[$name]);
+	}
+	
+	
+	/**
+	 * Old style to check if a template tag exists.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function tagExists($name)
+	{
+		return $this->blockExists($name) || $this->valueExists($name);
+	}
+	
+	
+	/**
+	 * Old style to get a template value callback.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function getValueCallback($name)
+	{
+		if ($this->valueExists($name))
+		{
+			return $this->dc_values[$name];
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Old style to get a template block callback.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function getBlockCallback($name)
+	{
+		if ($this->blockExists($name))
+		{
+			return $this->dc_blocks[$name];
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Old style to get list of template blocks.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function getBlocksList()
+	{
+		return array_keys($this->dc_blocks);
+	}
+	
+	/**
+	 * Old style to get list of template values.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function getValuesList()
+	{
+		return array_keys($this->dc_values);
+	}
+	
+	/**
+	 * Old style to add template path.
+	 *
+	 * Many plugins and themes use this method, so we keep it.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function setPath()
+	{
+		$path = array();
+		
+		foreach (func_get_args() as $v)
+		{
+			if (is_array($v)) {
+				$path = array_merge($path,array_values($v));
+			} else {
+				$path[] = $v;
+			}
+		}
+		
+		foreach ($path as $k => $v)
+		{
+			if (($v = path::real($v)) === false) {
+				unset($path[$k]);
+			}
+		}
+		
+		$path = array_unique($path);
+		
+		$this->getLoader()->setPaths($path);
+	}
+	
+	/**
+	 * Old style to get template path.
+	 *
+	 * Many plugins and themes use this method, so we keep it.
+	 *
+	 * @ignore
+	 * @deprecated
+	 */
+	public function getPath()
+	{
+		return $this->getLoader()->getPaths();
+	}
+	//@}
+}
+?>
Index: inc/dbschema/upgrade.php
===================================================================
--- inc/dbschema/upgrade.php	(revision 706)
+++ inc/dbschema/upgrade.php	(revision 1083)
@@ -305,4 +305,34 @@
 				$core->con->execute($strReq);
 			}
+
+			if (version_compare($version,'2.5','<='))
+			{
+				# Try to disable daInstaller plugin if it has been installed outside the default plugins directory
+				$path = explode(PATH_SEPARATOR,DC_PLUGINS_ROOT);
+				$default = path::real(dirname(__FILE__).'/../../plugins/');
+				foreach ($path as $root)
+				{
+					if (!is_dir($root) || !is_readable($root)) {
+						continue;
+					}
+					if (substr($root,-1) != '/') {
+						$root .= '/';
+					}
+					if (($p = @dir($root)) === false) {
+						continue;
+					}
+					if(path::real($root) == $default) {
+						continue;
+					}
+					if (($d = @dir($root.'daInstaller')) === false) {
+						continue;
+					}
+					$f = $root.'/daInstaller/_disabled';
+					if (!file_exists($f))
+					{
+						@file_put_contents($f,'');
+					}
+				}
+			}
 			
 			$core->setVersion('core',DC_VERSION);
Index: inc/public/lib.urlhandlers.php
===================================================================
--- inc/public/lib.urlhandlers.php	(revision 779)
+++ inc/public/lib.urlhandlers.php	(revision 1128)
@@ -86,4 +86,9 @@
 			$_ctx->nb_entry_per_page = $core->blog->settings->system->nb_post_per_page;
 		}
+		
+		// Break public template here for now 
+		// just to check if template engine is well loaded.
+		$core->tpl->display($tpl.'.twig');
+		// To be continued...
 		
 		$tpl_file = $core->tpl->getFilePath($tpl);
Index: inc/public/prepend.php
===================================================================
--- inc/public/prepend.php	(revision 921)
+++ inc/public/prepend.php	(revision 1128)
@@ -55,4 +55,11 @@
 } catch (Exception $e) {}
 
+# Add public default templates path
+$core->tpl->getLoader()->addPath(dirname(__FILE__).'/default-templates');
+# Set public context
+$_ctx = new dcPublicContext($core);
+$core->tpl->addExtension($_ctx);
+
+/*
 # Creating template context
 $_ctx = new context();
@@ -64,5 +71,5 @@
 		,640);
 }
-
+*/
 # Loading locales
 $_lang = $core->blog->settings->system->lang;
@@ -120,5 +127,5 @@
 
 # --BEHAVIOR-- publicPrepend
-$core->callBehavior('publicPrepend',$core);
+$core->callBehavior('publicPrepend',$core,$_ctx);
 
 # Prepare the HTTP cache thing
@@ -127,4 +134,14 @@
 $mod_ts[] = $core->blog->upddt;
 
+
+# Add parent theme path
+if ($__parent_theme && is_dir($core->blog->themes_path.'/'.$__parent_theme.'/tpl')) {
+	$core->tpl->getLoader()->addPath($core->blog->themes_path.'/'.$__parent_theme.'/tpl');
+}
+# Add theme path at the begining of path list
+if (is_dir($core->blog->themes_path.'/'.$__theme.'/tpl')) {
+	$core->tpl->getLoader()->prependPath($core->blog->themes_path.'/'.$__theme.'/tpl');
+}
+/*
 $__theme_tpl_path = array(
 	$core->blog->themes_path.'/'.$__theme.'/tpl'
@@ -138,5 +155,5 @@
 	dirname(__FILE__).'/default-templates',
 	$core->tpl->getPath());
-
+*/
 $core->url->mode = $core->blog->settings->system->url_scan;
 
Index: locales/fr/main.po
===================================================================
--- locales/fr/main.po	(revision 1077)
+++ locales/fr/main.po	(revision 1102)
@@ -1738,4 +1738,7 @@
 msgstr "Changer la catégorie"
 
+msgid "Change language"
+msgstr "Changer la langue"
+
 msgid "Change author"
 msgstr "Changer l'auteur"
@@ -1761,4 +1764,7 @@
 msgid "Change category for entries"
 msgstr "Changer la catégorie des billets"
+
+msgid "Change language for entries"
+msgstr "Changer la langue des billets"
 
 msgid "Change author for entries"
Index: plugins/akismet/class.dc.filter.akismet.php
===================================================================
--- plugins/akismet/class.dc.filter.akismet.php	(revision 536)
+++ plugins/akismet/class.dc.filter.akismet.php	(revision 1094)
@@ -151,5 +151,5 @@
 		
 		$res .=
-		'<p><a href="http://wordpress.com/api-keys/">'.__('Get your own API key').'</a></p>'.
+		'<p><a href="http://akismet.com/">'.__('Get your own API key').'</a></p>'.
 		'<p><input type="submit" value="'.__('Save').'" />'.
 		$this->core->formNonce().'</p>'.
Index: plugins/importExport/inc/flat/class.flat.import.php
===================================================================
--- plugins/importExport/inc/flat/class.flat.import.php	(revision 841)
+++ plugins/importExport/inc/flat/class.flat.import.php	(revision 1093)
@@ -790,6 +790,6 @@
 		$strReq = 'SELECT media_id '.
 				'FROM '.$this->prefix.'media '.
-				"WHERE media_path = '".$this->cur_media->media_path."' ".
-				"AND media_file = '".$this->cur_media->media_file."' ";
+				"WHERE media_path = '".$this->con->escape($this->cur_media->media_path)."' ".
+				"AND media_file = '".$this->con->escape($this->cur_media->media_file)."' ";
 		
 		$rs = $this->con->select($strReq);
Index: plugins/tags/_admin.php
===================================================================
--- plugins/tags/_admin.php	(revision 897)
+++ plugins/tags/_admin.php	(revision 1097)
@@ -105,5 +105,5 @@
 		$tag_url = $GLOBALS['core']->blog->url.$GLOBALS['core']->url->getURLFor('tag');
 		
-		$opts = $GLOBALS['core']->blog->url.$GLOBALS['core']->auth->getOptions();
+		$opts = $GLOBALS['core']->auth->getOptions();
 		$type = isset($opts['tag_list_format']) ? $opts['tag_list_format'] : 'more';
 		
Index: themes/ductile/_config.php
===================================================================
--- themes/ductile/_config.php	(revision 1011)
+++ themes/ductile/_config.php	(revision 1096)
@@ -44,6 +44,4 @@
 }
 
-
-
 $contexts = array(
 	'default' => __('Home (first page)'),
@@ -67,4 +65,10 @@
 	__('Impact') => 'Impact',
 	__('Monospace') => 'Monospace'
+);
+
+$webfont_apis = array(
+	__('none') => '',
+	__('javascript (Adobe)') => 'js',
+	__('stylesheet (Google)') => 'css'
 );
 
@@ -225,7 +229,14 @@
 	'subtitle_hidden' => null,
 	'logo_src' => null,
+	'preview_not_mandatory' => null,
 	// CSS
 	'body_font' => null,
+	'body_webfont_family' => null,
+	'body_webfont_url' => null,
+	'body_webfont_api' => null,
 	'alternate_font' => null,
+	'alternate_webfont_family' => null,
+	'alternate_webfont_url' => null,
+	'alternate_webfont_api' => null,
 	'blog_title_w' => null,
 	'blog_title_s' => null,
@@ -327,4 +338,5 @@
 			$ductile_user['subtitle_hidden'] = (integer) !empty($_POST['subtitle_hidden']);
 			$ductile_user['logo_src'] = $_POST['logo_src'];
+			$ductile_user['preview_not_mandatory'] = (integer) !empty($_POST['preview_not_mandatory']);
 
 			$ductile_stickers = array();
@@ -368,5 +380,12 @@
 		if ($conf_tab == 'css') {
 			$ductile_user['body_font'] = $_POST['body_font'];
+			$ductile_user['body_webfont_family'] = $_POST['body_webfont_family'];
+			$ductile_user['body_webfont_url'] = $_POST['body_webfont_url'];
+			$ductile_user['body_webfont_api'] = $_POST['body_webfont_api'];
+
 			$ductile_user['alternate_font'] = $_POST['alternate_font'];
+			$ductile_user['alternate_webfont_family'] = $_POST['alternate_webfont_family'];
+			$ductile_user['alternate_webfont_url'] = $_POST['alternate_webfont_url'];
+			$ductile_user['alternate_webfont_api'] = $_POST['alternate_webfont_api'];
 
 			$ductile_user['blog_title_w'] = (integer) !empty($_POST['blog_title_w']);
@@ -493,4 +512,9 @@
 echo '</fieldset>';
 
+echo '<fieldset><legend>'.__('Miscellaneous options').'</legend>';
+echo '<p class="field"><label for="preview_not_mandatory">'.__('Comment preview is not mandatory:').' '.
+form::checkbox('preview_not_mandatory',1,$ductile_user['preview_not_mandatory']).'</label>'.'</p>';
+echo '</fieldset>';
+
 echo '<input type="hidden" name="conf_tab" value="html">';
 echo '<p class="clear">'.form::hidden('ds_order','').'<input type="submit" value="'.__('Save').'" />'.$core->formNonce().'</p>';
@@ -507,15 +531,37 @@
 echo '<h3>'.__('General settings').'</h3>';
 
-echo '<fieldset><legend>'.__('Fonts').'</legend>'.
+echo '<fieldset><legend>'.__('Fonts').'</legend>';
+
+echo '<div class="two-cols">';
+echo '<div class="col">';
+echo
 '<p class="field"><label for="body_font">'.__('Main:').' '.
 form::combo('body_font',$fonts,$ductile_user['body_font']).'</label>'.
 (!empty($ductile_user['body_font']) ? ' '.fontDef($ductile_user['body_font']) : '').
 '</p>'.
-
+'<p class="form-note">'.__('Set main font to default to use webfont below.').'</p> '.
+'<p class="field"><label for="body_webfont_family">'.__('Webfont family:').'</label> '.
+form::field('body_webfont_family',25,255,$ductile_user['body_webfont_family']).'</p>'.
+'<p class="field"><label for="body_webfont_url">'.__('Webfont URL:').'</label> '.
+form::field('body_webfont_url',50,255,$ductile_user['body_webfont_url']).'</p>'.
+'<p class="field"><label for="body_webfont_url">'.__('Webfont API:').' '.
+form::combo('body_webfont_api',$webfont_apis,$ductile_user['body_webfont_api']).'</label>'.'</p>';
+echo '</div>';
+echo '<div class="col">';
+echo
 '<p class="field"><label for="alternate_font">'.__('Secondary:').' '.
 form::combo('alternate_font',$fonts,$ductile_user['alternate_font']).'</label>'.
 (!empty($ductile_user['alternate_font']) ? ' '.fontDef($ductile_user['alternate_font']) : '').
 '</p>'.
-'</fieldset>';
+'<p class="form-note">'.__('Set secondary font to default to use webfont below.').'</p> '.
+'<p class="field"><label for="alternate_webfont_family">'.__('Webfont family:').'</label> '.
+form::field('alternate_webfont_family',25,255,$ductile_user['alternate_webfont_family']).'</p>'.
+'<p class="field"><label for="alternate_webfont_url">'.__('Webfont URL:').'</label> '.
+form::field('alternate_webfont_url',50,255,$ductile_user['alternate_webfont_url']).'</p>'.
+'<p class="field"><label for="alternate_webfont_api">'.__('Webfont API:').' '.
+form::combo('alternate_webfont_api',$webfont_apis,$ductile_user['alternate_webfont_api']).'</label>'.'</p>';
+echo '</div>';
+echo '</div>';
+echo '</fieldset>';
 
 echo '<div class="two-cols">';
Index: themes/ductile/_public.php
===================================================================
--- themes/ductile/_public.php	(revision 1045)
+++ themes/ductile/_public.php	(revision 1096)
@@ -23,4 +23,5 @@
 $core->tpl->addValue('ductileNbEntryPerPage',array('tplDuctileTheme','ductileNbEntryPerPage'));
 $core->tpl->addValue('ductileLogoSrc',array('tplDuctileTheme','ductileLogoSrc'));
+$core->tpl->addBlock('IfPreviewIsNotMandatory',array('tplDuctileTheme','IfPreviewIsNotMandatory'));
 
 class tplDuctileTheme
@@ -175,4 +176,20 @@
 		
 		return $img_url;
+	}
+
+	public static function IfPreviewIsNotMandatory($attr,$content)
+	{
+		$s = $GLOBALS['core']->blog->settings->themes->get($GLOBALS['core']->blog->settings->system->theme.'_style');
+		if ($s !== null) {
+			$s = @unserialize($s);
+			if (is_array($s)) {
+				if (isset($s['preview_not_mandatory'])) {
+					if ($s['preview_not_mandatory']) {
+						return $content;
+					}
+				}
+			}
+		}
+		return '';
 	}
 
@@ -250,4 +267,73 @@
 			$core->blog->settings->system->themes_url.'/'.$core->blog->settings->system->theme.
 			'/ductile.js"></script>'."\n";
+
+		echo self::ductileWebfontHelper();
+	}
+
+	public static function ductileWebfontHelper()
+	{
+		$s = $GLOBALS['core']->blog->settings->themes->get($GLOBALS['core']->blog->settings->system->theme.'_style');
+
+		if ($s === null) {
+			return;
+		}
+
+		$s = @unserialize($s);
+		if (!is_array($s)) {
+			return;
+		}
+
+		$ret = '';
+		$css = array();
+		$uri = array();
+		if (!isset($s['body_font']) || ($s['body_font'] == '')) {
+			// See if webfont defined for main font
+			if (isset($s['body_webfont_api']) && isset($s['body_webfont_family']) && isset($s['body_webfont_url'])) {
+				$uri[] = $s['body_webfont_url'];
+				switch ($s['body_webfont_api']) {
+					case 'js':
+						$ret .= sprintf('<script type="text/javascript" src="%s"></script>',$s['body_webfont_url'])."\n";
+						break;
+					case 'css':
+						$ret .= sprintf('<link type="text/css" href="%s" rel="stylesheet" />',$s['body_webfont_url'])."\n";
+						break;
+				}
+				# Main font
+				$selectors = 'body, .supranav li a span, #comments.me, a.comment-number';
+				self::prop($css,$selectors,'font-family',$s['body_webfont_family']);
+			}
+		}
+		if (!isset($s['alternate_font']) || ($s['alternate_font'] == '')) {
+			// See if webfont defined for secondary font
+			if (isset($s['alternate_webfont_api']) && isset($s['alternate_webfont_family']) && isset($s['alternate_webfont_url'])) {
+				if (!in_array($s['alternate_webfont_url'], $uri)) {
+					switch ($s['alternate_webfont_api']) {
+						case 'js':
+							$ret .= sprintf('<script type="text/javascript" src="%s"></script>',$s['alternate_webfont_url'])."\n";
+							break;
+						case 'css':
+							$ret .= sprintf('<link type="text/css" href="%s" rel="stylesheet" />',$s['alternate_webfont_url'])."\n";
+							break;
+					}
+				}
+				# Secondary font
+				$selectors = '#blogdesc, .supranav, #content-info, #subcategories, #comments-feed, #sidebar h2, #sidebar h3, #footer';
+				self::prop($css,$selectors,'font-family',$s['alternate_webfont_family']);
+			}
+		}
+		# Style directives
+		$res = '';
+		foreach ($css as $selector => $values) {
+			$res .= $selector." {\n";
+			foreach ($values as $k => $v) {
+				$res .= $k.':'.$v.";\n";
+			}
+			$res .= "}\n";
+		}
+		if ($res != '') {
+			$ret .= '<style type="text/css">'."\n".$res.'</style>'."\n";
+		}
+
+		return $ret;
 	}
 	
Index: themes/ductile/locales/fr/admin.po
===================================================================
--- themes/ductile/locales/fr/admin.po	(revision 810)
+++ themes/ductile/locales/fr/admin.po	(revision 1096)
@@ -65,4 +65,10 @@
 msgstr "Complet"
 
+msgid "Miscellaneous options"
+msgstr "Options diverses"
+
+msgid "Comment preview is not mandatory:"
+msgstr "Prévisualisation du commentaire optionnelle :"
+
 msgid "Presentation"
 msgstr "Présentation"
@@ -110,4 +116,28 @@
 msgstr "Secondaire :"
 
+msgid "none"
+msgstr "aucune"
+
+msgid "javascript (Adobe)"
+msgstr "javascript (Adobe)"
+
+msgid "stylesheet (Google)"
+msgstr "feuille de style (Google)"
+
+msgid "Set main font to default to use webfont below."
+msgstr "Mettre la police de caractère principale à Défaut pour utiliser la fonte web ci-dessous."
+
+msgid "Set secondary font to default to use webfont below."
+msgstr "Mettre la police de caractère secondaire à Défaut pour utiliser la fonte web ci-dessous."
+
+msgid "Webfont family:"
+msgstr "Famille"
+
+msgid "Webfont URL:"
+msgstr "URL de la fonte web"
+
+msgid "Webfont API:"
+msgstr "Type d'API"
+
 msgid "Ductile primary"
 msgstr "Principale de Ductile"
Index: themes/ductile/style.css
===================================================================
--- themes/ductile/style.css	(revision 1066)
+++ themes/ductile/style.css	(revision 1098)
@@ -621,11 +621,11 @@
 #sidebar li {
 	padding-left: 16px;
-	background: transparent url(img/squares.png) no-repeat -133px .5em;
+	background: transparent url(img/squares.png) no-repeat -133px 7px;
 	}
 #sidebar ul li ul li {
-	background: transparent url(img/squares.png) no-repeat -70px center;
+	background: transparent url(img/squares.png) no-repeat -70px -59px;
 	}
 #sidebar ul li ul li ul li {
-	background: transparent url(img/squares.png) no-repeat 0 center;
+	background: transparent url(img/squares.png) no-repeat 0 -126px;
 	}
 #sidebar .syndicate li {
Index: themes/ductile/tpl/_simple-entry.html
===================================================================
--- themes/ductile/tpl/_simple-entry.html	(revision 881)
+++ themes/ductile/tpl/_simple-entry.html	(revision 1096)
@@ -164,7 +164,7 @@
 			<p class="buttons">
 				<input type="submit" class="preview" name="preview" value="{{tpl:lang preview}}" />
-				<tpl:IfCommentPreview>
+				<tpl:IfPreviewIsNotMandatory>
 					<input type="submit" class="submit" value="{{tpl:lang send}}" />
-				</tpl:IfCommentPreview>
+				</tpl:IfPreviewIsNotMandatory>
 			</p>
 		</fieldset>
Index: unit-tests/dotclearTest.php
===================================================================
--- unit-tests/dotclearTest.php	(revision 1095)
+++ unit-tests/dotclearTest.php	(revision 1095)
@@ -0,0 +1,13 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Root path
+define('NAME_UNIT_TEST','unit-tests');
+define('ROOT_UNIT_TEST',substr(dirname(__FILE__),0,strpos(dirname(__FILE__),'/'.NAME_UNIT_TEST)));
+
+?>
Index: unit-tests/inc/libs/clearbricks/_commonTest.php
===================================================================
--- unit-tests/inc/libs/clearbricks/_commonTest.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/_commonTest.php	(revision 1095)
@@ -0,0 +1,31 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/_common.php');
+
+// Unit test class
+class clearbricksTest extends PHPUnit_Framework_TestCase
+{
+	public function setUp() {}
+	public function tearDown() {}
+   
+	public function test_clearbricks() {
+
+		$this->assertTrue(defined('CLEARBRICKS_VERSION'));
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/_mainTest.php
===================================================================
--- unit-tests/inc/libs/clearbricks/common/_mainTest.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/_mainTest.php	(revision 1095)
@@ -0,0 +1,38 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+//define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+//require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/_main.php');
+
+// Unit test class
+class commonTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+	public function setUp() {}
+	public function tearDown() {}
+
+	// Unit test methods
+	public function test_common() {
+		global $__autoload;
+		$a = array('crypt','dt','files','path','form','formSelectOption','html','http','text');
+
+		$this->assertNotEmpty($__autoload);
+		foreach ($a as $value) {
+			$this->assertTrue(isset($__autoload[$value]));
+		}
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/lib.cryp.Test.php
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.cryp.Test.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.cryp.Test.php	(revision 1095)
@@ -0,0 +1,36 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+//define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+//require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/lib.crypt.php');
+
+// Unit test class
+class cryptTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+	public function setUp() {}
+	public function tearDown() {}
+
+	// Unit test methods
+	public function test_hmac() {
+		$this->assertTrue(crypt::hmac('dotclear is da best','lorem impsum','md5') == 'c799ac384bfc6dbff9988764989010c0');
+	}
+
+	public function test_createPassword() {
+		$this->assertTrue(strlen(crypt::createPassword()) == 8);
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/lib.date.Test.php
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.date.Test.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.date.Test.php	(revision 1095)
@@ -0,0 +1,80 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+//define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+//require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/lib.date.php');
+
+// Unit test class
+class dtTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+	protected $d;
+
+	public function setUp() {
+		$this->d = new dt;
+		$this->d->setTZ('UTC');
+	}
+	public function tearDown() {}
+
+	// Unit test methods
+
+	public function test_str() {
+		$t = mktime(10, 42, 0, 8, 13, 2003);
+		$this->assertTrue($this->d->str('%A %e %B %Y %H:%M:%S',$t) == 'Wednesday 13 August 2003 10:42:00');
+	}
+
+	public function test_dt2str() {
+		$t = mktime(10, 42, 0, 8, 13, 2003);
+		$this->assertTrue($this->d->dt2str('%A %e %B %Y %H:%M:%S',date('D, d M Y H:i:s',$t)) == 'Wednesday 13 August 2003 10:42:00');
+	}
+
+	public function test_isoe8601() {
+		$t = mktime(10, 42, 0, 8, 13, 2003);
+		$this->assertTrue($this->d->iso8601($t) == '2003-08-13T10:42:00+00:00');
+	}
+
+	public function test_rfc822() {
+		$t = mktime(10, 42, 0, 8, 13, 2003);
+		$this->assertTrue($this->d->rfc822($t) == 'Wed, 13 Aug 2003 10:42:00 +0000');
+	}
+
+	public function test_setTZ() {
+		$this->d->setTZ('Europe/Paris');
+		$this->assertTrue($this->d->getTZ() == 'Europe/Paris');
+	}
+
+	public function test_getTZ() {
+		$this->assertTrue($this->d->getTZ() == 'UTC');
+	}
+
+	public function test_getTimeOffset() {
+		// Should cope with summer/winter time offset
+		$this->assertTrue($this->d->getTimeOffset('Europe/Paris') == 3600 || $this->d->getTimeOffset('Europe/Paris') == 7200);
+	}
+
+	public function test_toUTC() {
+		$this->assertTrue($this->d->toUTC(time()) == time());
+	}
+
+	public function test_addTimeZone() {
+		$this->assertTrue($this->d->addTimeZone('UTC') == time());
+	}
+
+	public function test_getZones() {
+		$this->assertNotEmpty($this->d->getZones(false,true));
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/lib.files.Test.php
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.files.Test.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.files.Test.php	(revision 1095)
@@ -0,0 +1,235 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+//define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+//require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/lib.files.php');
+
+// Unit test class
+class fileTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+
+	public function setUp() {}
+	public function tearDown() {}
+
+	// Unit test methods
+
+	public function test_scandir() {
+		// Test with an existing dir
+		$d = files::scandir(dirname(__FILE__));
+		$this->assertTrue(count($d) > 1);
+		// Test optional order
+		$this->assertTrue(files::scandir(dirname(__FILE__),true) == rsort($d));
+		// Test with an unexisting dir
+		try {
+			$this->assertCount(0,files::scandir(dirname(__FILE__).'-undefined'));
+		} catch (exception $e) {
+			$this->assertTrue($e->getMessage() == __('Unable to open directory.'));
+		}
+	}
+
+	public function test_getExtension() {
+		$this->assertTrue(files::getExtension(__FILE__) == 'php');
+	}
+
+	public function test_getMimeType() {
+		// Test with a known extension
+		$this->assertTrue(files::getMimeType(__FILE__.'.pdf') == 'application/pdf');
+		// Test with an unknown extension
+		$this->assertTrue(files::getMimeType(__FILE__) == 'text/plain');
+	}
+
+	public function test_mimeTypes() {
+		$this->assertNotEmpty($t=files::mimeTypes());
+		return $t;
+	}
+
+	/**
+	 * @depends test_mimeTypes
+	 */
+	public function test_registerMimeTypes(array $t) {
+		// Register an unknown mime type
+		files::registerMimeTypes(array('php' => 'text/plain'));
+		$this->assertTrue(count(files::mimeTypes()) == count($t)+1);
+		// Register an known mime type
+		files::registerMimeTypes(array('pdf' => 'application/pdf'));
+		$this->assertTrue(count(files::mimeTypes()) == count($t)+1);
+	}
+
+	public function test_isDeletable() {
+		// Test with a file
+		$this->assertTrue(files::isDeletable(__FILE__));
+		// Test with a full directory
+		$this->assertFalse(files::isDeletable(dirname(__FILE__)));
+		// Test with an unknown file
+		$this->assertNull(files::isDeletable(__FILE__.'-undefined'));
+	}
+
+	public function test_makeDir() {
+		// Try to create an empty simple dir
+		$d = dirname(__FILE__).'/simple-dir';
+		files::makeDir($d);
+		$this->assertTrue(is_dir($d));
+		// Try to create an empty deeper dir
+		$dd = $d.'/try/deeper';
+		try {
+			files::makeDir($dd);
+		} catch (exception $e) {
+			$this->assertTrue($e->getMessage() == __('Unable to create directory.'));
+		}
+		$this->assertFalse(is_dir($dd));
+		// Try to create an empty deeper dir, including creation of intermediary dir
+		files::makeDir($dd,true);
+		$this->assertTrue(is_dir($dd));
+		return $d;
+	}
+
+	/**
+	 * @depends test_makeDir
+	 */
+	public function test_deltree($d) {
+		// Delete an existing tree
+		$this->assertTrue(files::deltree($d));
+		$this->assertFalse(is_dir($d));
+		// Try to delete an unexisting tree
+		try {
+			files::deltree($d);
+		} catch (exception $e) {
+			return;
+		}
+		$this->fail(__('Expected exception not thrown.'));
+	}
+
+	public function test_touch() {
+		$f = tempnam(dirname(__FILE__),'dc_');
+		$t = time();
+		files::touch($f);
+		$this->assertTrue($t <= filemtime($f));
+		unlink($f);
+		// Try to touch an unexisting file
+		files::touch($f);
+		$this->assertFalse(file_exists($f));
+	}
+
+	public function test_inheritChmod() {
+		$f = tempnam(dirname(__FILE__),'dc_');
+		files::inheritChmod($f);
+		$this->assertTrue((fileperms($f) & 0x0FFF) == (fileperms(dirname($f)) & 0x0FFF));
+		unlink($f);
+	}
+
+	public function test_putContent() {
+		$f = tempnam(dirname(__FILE__),'dc_');
+		$f_content = 'Lorem ipsum';
+		files::putContent($f,$f_content);
+		$this->assertTrue(file_get_contents($f) == $f_content);
+		unlink($f);
+		// Try with a read-only file and unexisting file
+		$f = tempnam(dirname(__FILE__),'dc_');
+		chmod($f,0444);
+		try {
+			files::putContent($f,$f_content);
+			unlink($f);
+			files::putContent($f,$f_content);
+		} catch (exception $e) {
+			unlink($f);
+			return;
+		}
+		$this->fail(__('Expected exception not thrown.'));
+	}
+
+	public function test_size() {
+		$this->assertTrue(files::size(1024*42) == '42 KB');
+	}
+
+	public function test_str2bytes() {
+		$this->assertTrue(files::str2bytes('42K') == (1024*42));
+		$this->assertNotInternalType('integer',files::str2bytes('42X'));
+	}
+
+	public function test_uploadStatus() {
+		$f = array();
+		try {
+			files::uploadStatus($f);
+		} catch (exception $e) {
+			$this->assertTrue($e->getMessage() == __('Not an uploaded file.'));
+		}
+		$f['error'] = UPLOAD_ERR_OK;
+		$this->assertTrue(files::uploadStatus($f));
+		$f['error'] = 'another unknown status';
+		$this->assertTrue(files::uploadStatus($f));
+		try {
+			$f['error'] = UPLOAD_ERR_INI_SIZE;
+			files::uploadStatus($f);
+		} catch (exception $e) {
+			return;
+		}
+		$this->fail(__('Expected exception not thrown.'));
+	}
+
+	public function test_getDirList() {
+		$c = files::getDirList(dirname(__FILE__).'/../');
+		$this->assertNotEmpty($c);
+		$this->assertCount(2,$c);
+		$this->assertNotEmpty($c['dirs']);
+		$this->assertNotEmpty($c['files']);
+	}
+
+	public function test_tidyFileName() {
+		$this->assertTrue(files::tidyFileName('géù=fiel.txt') == 'geu_fiel.txt');
+	}
+}
+
+// Unit test class
+class pathTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+
+	public function setUp() {}
+	public function tearDown() {}
+
+	// Unit test methods
+
+	public function test_real() {
+		$p = dirname(__FILE__).'/../common/lib.files.Test.php';
+		$this->assertTrue(path::real($p) == __FILE__);
+		$p .= '-undefined';
+		$this->assertFalse(path::real($p));
+		// Non strict test
+		$this->assertTrue(path::real($p,false) == __FILE__.'-undefined');
+	}
+
+	public function test_clean() {
+		$p = dirname(__FILE__).'/..///common/lib.files.Test.php///';
+		$this->assertTrue(path::clean($p) == dirname(__FILE__).'/common/lib.files.Test.php');
+	}
+
+	public function test_info() {
+		$i = path::info(__FILE__);
+		$this->assertNotEmpty($i);
+		$this->assertCount(4,$i);
+		$this->assertTrue($i['dirname'] == dirname(__FILE__));
+		$this->assertTrue($i['basename'] == 'lib.files.Test.php');
+		$this->assertTrue($i['extension'] == 'php');
+		$this->assertTrue($i['base'] == 'lib.files.Test');
+	}
+
+	public function test_fullFromRoot() {
+		$this->assertTrue(path::fullFromRoot(NAME_UNIT_TEST,ROOT_UNIT_TEST) == ROOT_UNIT_TEST.'/'.NAME_UNIT_TEST);
+		$this->assertTrue(path::fullFromRoot('/'.NAME_UNIT_TEST,ROOT_UNIT_TEST) == '/'.NAME_UNIT_TEST);
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.1.xml
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.1.xml	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.1.xml	(revision 1095)
@@ -0,0 +1,4 @@
+<select name="ctrl-combo" id="ctrl-combo">
+	<option value="1">first label</option>
+	<option value="2">second label</option>
+</select>
Index: unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.2.xml
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.2.xml	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.form.Test.combo.2.xml	(revision 1095)
@@ -0,0 +1,11 @@
+<select name="ctrl-name" id="ctrl-id" class="ctrl-class" tabindex="13" disabled="disabled" aria-required="true">
+	<option value="1">first label</option>
+	<option value="2" selected="selected">second label</option>
+	<optgroup label="first group">
+		<option value="3">third label</option>
+	</optgroup>
+	<optgroup label="second group">
+		<option value="4">fourth label</option>
+		<option value="5">fifth label</option>
+	</optgroup>
+</select>
Index: unit-tests/inc/libs/clearbricks/common/lib.form.Test.php
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.form.Test.php	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.form.Test.php	(revision 1095)
@@ -0,0 +1,69 @@
+<?php
+# error reporting
+ini_set('display_errors',1);
+error_reporting(E_ALL|E_STRICT);
+
+# include PHPUnit
+require 'PHPUnit/Autoload.php';
+
+// Paths
+if (!defined('NAME_UNIT_TEST')) {
+	defined('NAME_UNIT_TEST','unit-tests');
+}
+$sync_test_path = preg_replace('#/'.NAME_UNIT_TEST.'#','',dirname(__FILE__));
+
+// File to test
+//define('CLEARBRICKS_PATH',ROOT_UNIT_TEST.'/inc/libs/clearbricks');
+//require_once(CLEARBRICKS_PATH.'/common/lib.date.php');
+require_once($sync_test_path.'/lib.form.php');
+
+// Unit test class
+class formTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+
+	public function setUp() {}
+	public function tearDown() {}
+
+	// Unit test methods
+
+	public function test_combo() {
+		$nid = 'ctrl-combo';
+		$data = array(
+			'first label' => 1,
+			'second label' => 2
+			);
+		$res = form::combo($nid,$data);
+		$this->assertXmlStringEqualsXmlFile(dirname(__FILE__).'/lib.form.Test.combo.1.xml',$res);
+		$nid = array('ctrl-name','ctrl-id');
+		$data = array(
+			'first label' => 1,
+			'second label' => 2,
+			'first group' => array('third label' => 3),
+			'second group' => array('fourth label' => 4,'fifth label' => 5));
+		$res = form::combo($nid,$data,2,'ctrl-class',13,true,'aria-required="true"');
+		$this->assertXmlStringEqualsXmlFile(dirname(__FILE__).'/lib.form.Test.combo.2.xml',$res);
+	}
+
+	public function test_radio() {
+		
+	}
+}
+
+// Unit test class
+class formSelectOptionTest extends PHPUnit_Framework_TestCase
+{
+	// Fixture
+	protected $c;
+
+	public function setUp() {
+		$this->c = new formSelectOption('option-label',13,'ctrl-class',' rel="author"');
+	}
+	public function tearDown() {}
+
+	// Unit test methods
+	public function test_render() {
+		$this->assertXmlStringEqualsXmlFile(dirname(__FILE__).'/lib.form.Test.render.1.xml',$this->c->render(true));
+	}
+}
+?>
Index: unit-tests/inc/libs/clearbricks/common/lib.form.Test.render.1.xml
===================================================================
--- unit-tests/inc/libs/clearbricks/common/lib.form.Test.render.1.xml	(revision 1095)
+++ unit-tests/inc/libs/clearbricks/common/lib.form.Test.render.1.xml	(revision 1095)
@@ -0,0 +1,1 @@
+<option value="13" rel="author" class="ctrl-class" selected="selected">option-label</option>
Index: unit-tests/readme.md
===================================================================
--- unit-tests/readme.md	(revision 1095)
+++ unit-tests/readme.md	(revision 1095)
@@ -0,0 +1,11 @@
+Dotclear and Clearbricks Unit tests
+===================================
+
+Running tests (from dotclear root directory):
+
+phpunit unit-tests
+
+
+References:
+
+* PHPUnit documentation: http://www.phpunit.de/manual/3.7/en/index.html
