Index: main/plugin-cms/i18n/messages_en.xml
===================================================================
--- main/plugin-cms/i18n/messages_en.xml	(revision 39874)
+++ main/plugin-cms/i18n/messages_en.xml	(working copy)
@@ -1025,6 +1025,9 @@
     
     <message key="WIDGET_COMBOBOX_ALL_OPTIONS">- All -</message>
     
+    <message key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE">This field is synchronized.&lt;br/&gt;Clik here to de-synchronized it</message>
+    <message key="WIDGET_SYNCHRONIZED_FIELD_BTN_SYNCHRONIZE">This field is not synchronized.&lt;br/&gt;Clik here to synchronized it</message>
+    
     <!-- +
          | FileUpload.js
          + -->
Index: main/plugin-cms/i18n/messages_fr.xml
===================================================================
--- main/plugin-cms/i18n/messages_fr.xml	(revision 39874)
+++ main/plugin-cms/i18n/messages_fr.xml	(working copy)
@@ -1025,7 +1025,10 @@
     <message key="WIDGET_SELECTTYPEDMETADATA_FILTER_LOAD_ERROR">Une erreur est survenue pendant le chargement des métadonnées</message>
     
     <message key="WIDGET_COMBOBOX_ALL_OPTIONS">- Tous -</message>
-     
+
+    <message key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE">Ce champ est synchronisé.&lt;br/&gt;Cliquez ici pour le désynchroniser</message>
+    <message key="WIDGET_SYNCHRONIZED_FIELD_BTN_SYNCHRONIZE">Ce champ est désynchronisé.&lt;br/&gt;Cliquez ici pour le synchroniser</message>
+
     <!-- +
          | FileUpload.js
          + -->
Index: main/plugin-cms/nodetypes/content_nodetypes.xml
===================================================================
--- main/plugin-cms/nodetypes/content_nodetypes.xml	(revision 39874)
+++ main/plugin-cms/nodetypes/content_nodetypes.xml	(working copy)
@@ -45,4 +45,9 @@
             </requiredPrimaryTypes>
         </childNodeDefinition>
     </nodeType>
+    <nodeType name="ametys:syncContent" isMixin="false" hasOrderableChildNodes="false" primaryItemName="">
+        <supertypes>
+            <supertype>ametys:defaultContent</supertype>
+        </supertypes>
+    </nodeType>
 </nodeTypes>
Index: main/plugin-cms/plugin.xml
===================================================================
--- main/plugin-cms/plugin.xml	(revision 39874)
+++ main/plugin-cms/plugin.xml	(working copy)
@@ -284,6 +284,14 @@
                 <nodetype>ametys:defaultContent</nodetype>
                 <nodetype-definition>nodetypes/content_nodetypes.xml</nodetype-definition>
             </extension>
+            
+            <extension point="org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint" 
+                           id="org.ametys.cms.repository.SynchronizedContentFactory"
+                           class="org.ametys.cms.repository.SynchronizableContentFactory">
+                    <scheme>syncContent</scheme>
+                    <nodetype>ametys:syncContent</nodetype> 
+                    <nodetype-definition>nodetypes/content_nodetypes.xml</nodetype-definition>
+                </extension>
         </extensions>
     </feature>
     
@@ -369,6 +377,9 @@
             <!-- Metadata manager -->
             <component role="org.ametys.cms.contenttype.MetadataManager"
                        class="org.ametys.cms.contenttype.MetadataManager"/>
+            
+            <component role="org.ametys.cms.contenttype.SynchronizedMetadataManager"
+                       class="org.ametys.cms.contenttype.SynchronizedMetadataManager"/>
         </components>
     </feature>
     
@@ -376,6 +387,9 @@
         <components>
             <component role="org.ametys.cms.lucene.MetadataIndexer"
                        class="org.ametys.cms.lucene.MetadataIndexer"/>
+                       
+            <component role="org.ametys.cms.lucene.SynchronizedMetadataIndexer"
+                       class="org.ametys.cms.lucene.SynchronizedMetadataIndexer"/>
         </components>
     </feature>
     
@@ -1146,6 +1160,23 @@
                     <file>js/org/ametys/cms/helper/ChooseLocation.i18n.js</file>
                     <file>js/org/ametys/cms/helper/ChooseLocation/LoadGoogleMaps.i18n.js</file>
                     <file>js/Ext/ux/GMapPanel3.i18n.js</file>
+                    
+                    <!-- synchronized -->
+                    <file>js/org/ametys/cms/widgets/sync/EmptyField.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SyncButtonField.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/MultipleHiddenField.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedWidgetHelper.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedTextField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedTextAreaField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedBooleanField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedComboField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedDateField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedDoubleField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedGeocodeWidget.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedLongField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedMultiselectField.i18n.js</file>
+                    <file>js/org/ametys/cms/widgets/sync/SynchronizedRichTextField.i18n.js</file>
+                    
 				</scripts>
 				<css>
 					<file>css/content/Widget.css</file>
@@ -4189,6 +4220,9 @@
         <components>
             <component role="org.ametys.cms.contenttype.DefaultContentIndexer"
                        class="org.ametys.cms.contenttype.DefaultContentIndexer"/>
+                       
+            <component role="org.ametys.cms.contenttype.SynchronizedContentIndexer"
+                       class="org.ametys.cms.contenttype.SynchronizedContentIndexer"/>
         </components>
     </feature>
     
Index: main/plugin-cms/resources/css/content/Widget.css
===================================================================
--- main/plugin-cms/resources/css/content/Widget.css	(revision 39874)
+++ main/plugin-cms/resources/css/content/Widget.css	(working copy)
@@ -279,4 +279,106 @@
 
 .ametys-metadata-filter-sort .x-grid3-row span.unused {
 	color: #6f6f6f;
+}
+
+/** Synchronized widget */
+.ametys-select div.x-form-synchro-wrap, 
+.ametys-select div.x-form-synchro-wrap2,
+.ametys-multi-select div.x-form-synchro-wrap, 
+.ametys-multi-select div.x-form-synchro-wrap2,
+.ametys-multi-select div.x-form-discipline-synchro-wrap2,
+.ametys-input div.x-form-synchro-wrap, 
+.ametys-input div.x-form-synchro-wrap2
+{
+    float: left;
+}
+
+.ametys-multi-select div.x-form-discipline-synchro-wrap2 div.x-form-field
+{
+    margin-bottom: 10px !important;
+}
+
+.ametys-input .remote-text-field {
+    margin-bottom: 8px !important;
+    margin-top: 5px !important;
+}
+
+.x-form-richtext-synchro2 {
+    width: 448px;
+}
+
+.x-form-richtext-synchro {
+    width: 490px;
+}
+
+.synchro-btn  {
+    border: 0 none !important;
+    background: none !important;
+    float: left;
+}
+
+.synchro-btn button {
+    height: 48px !important;
+    width: 34px !important;
+}
+
+.synchro-btn .x-btn-tl, 
+.synchro-btn .x-btn-tr, 
+.synchro-btn .x-btn-tc, 
+.synchro-btn .x-btn-ml, 
+.synchro-btn .x-btn-mr, 
+.synchro-btn .x-btn-mc, 
+.synchro-btn .x-btn-bl, 
+.synchro-btn .x-btn-br, 
+.synchro-btn .x-btn-bc {
+    background: 0;
+    background-image: none !important; 
+}
+
+.x-form-richtext-synchro2 .html-preview {
+    background-color: #F5F5F5  !important;
+    background-image: none !important;
+    border: 1px solid #C8CDD0 !important;
+    font-family: Verdana,Arial,sans-serif !important;
+    font-size: 0.9em;
+    padding: 4px 0 1px 6px !important;
+}
+
+.x-form-richtext-synchro2 .html-preview h1 {
+    margin: 6px 0 6px 0;
+    font-size: 1.4em;
+}
+
+.x-form-richtext-synchro2 .html-preview h2 {
+    margin: 6px 0 5px 0;
+    font-size: 1.2em;
+}
+
+.x-form-richtext-synchro2 .html-preview h3 {
+    margin: 6px 0 4px 0;
+    font-size: 1.1em;
+}
+
+.x-form-richtext-synchro2 .html-preview p {
+    margin: 3px 0 3px 0;
+}
+
+.x-form-richtext-synchro2 .html-preview strong {
+    font-weight: bold;
+}
+
+.x-form-richtext-synchro2 .html-preview em {
+    font-style: italic;
+}
+
+.x-form-richtext-synchro2 .hidden {
+    display: none;  
+}
+.x-form-richtext-synchro2 .html-preview ul,
+.x-form-richtext-synchro2 .html-preview ul li {
+    list-style: disc inside none !important;
+}
+
+.x-form-richtext-synchro2 span.mceEditor {
+    margin-bottom:5px;
 }
\ No newline at end of file
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/EmptyField.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/EmptyField.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/EmptyField.js	(revision 0)
@@ -0,0 +1,62 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('string', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('boolean', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('date', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('long', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('double', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('rich_text', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+org.ametys.utils.Widgets.registerWidget ('geocode', 'empty', 'org.ametys.cms.widgets.sync.EmptyField');
+
+org.ametys.cms.widgets.sync.EmptyField = function(config) 
+{
+	config.name = config.name + '_empty';
+	
+	org.ametys.cms.widgets.sync.EmptyField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.EmptyField, Ext.form.Hidden, {});
+
+org.ametys.cms.widgets.sync.EmptyField.prototype.onRender = function (ct, position)
+{
+	// Unregister field from html areas if exists
+	var toolPanel = this.findParentBy (function (ct, cmp) { return ct.tool != null});
+	if (toolPanel != null)
+	{
+		var htmlAreas = toolPanel.tool._htmlAreas;
+		if (htmlAreas != null && htmlAreas.indexOf(this.id) != -1)
+		{
+			htmlAreas.remove(this.id);
+		}
+	}
+	
+	org.ametys.cms.widgets.sync.EmptyField.superclass.onRender.call(this, ct, position);
+}
+
+org.ametys.cms.widgets.sync.EmptyField.prototype.setValue = function(values) 
+{
+	org.ametys.cms.widgets.sync.EmptyField.superclass.setValue.call(this, values);
+	
+	if (this.copyValueTo)
+    {
+	    var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm(this);
+	    form.el.dom.elements[this.copyValueTo].value = values;
+    }
+};
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/MultipleHiddenField.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/MultipleHiddenField.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/MultipleHiddenField.js	(revision 0)
@@ -0,0 +1,35 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+org.ametys.cms.widgets.sync.MultipleHiddenField = function(config) 
+{
+	org.ametys.cms.widgets.sync.MultipleHiddenField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.MultipleHiddenField, Ext.form.Hidden, {});
+
+org.ametys.cms.widgets.sync.MultipleHiddenField.prototype.getValue = function ()
+{
+	var values = org.ametys.cms.widgets.sync.MultipleHiddenField.superclass.getValue.call(this);
+	if (!Ext.isArray(values))
+	{
+		values = values.split(",");
+	}
+	return values;
+};
+
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SyncButtonField.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SyncButtonField.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SyncButtonField.js	(revision 0)
@@ -0,0 +1,69 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('boolean', 'synchro-button', 'org.ametys.cms.widgets.sync.SyncButtonField');
+
+org.ametys.cms.widgets.sync.SyncButtonField = function(config) 
+{
+	config.name = config.name + '_empty';
+	
+	org.ametys.cms.widgets.sync.SyncButtonField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SyncButtonField, Ext.form.Hidden, {});
+
+org.ametys.cms.widgets.sync.SyncButtonField.prototype.onRender = function (ct, position)
+{
+	org.ametys.cms.widgets.sync.SyncButtonField.superclass.onRender.call(this, ct, position);
+	
+	// Init to false
+	this.setSyncValue('false');
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+	
+};
+
+/**
+ * Override
+ */
+org.ametys.cms.widgets.sync.SyncButtonField.prototype.initValue = function ()
+{
+	// Do nothing !
+}
+
+org.ametys.cms.widgets.sync.SyncButtonField.prototype.setValue = function(values) 
+{
+	org.ametys.cms.widgets.sync.SyncButtonField.superclass.setValue.call(this, values);
+	
+	var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm (this);
+	if (form != null)
+	{
+		var widget = form.findField(this.name.substring(0, this.name.indexOf('_sync_empty')));
+		if (widget != null && !widget._isInitialized)
+		{
+			widget.setWidgetValue(widget.getValue()); // init the synchro widget
+			widget.clearInvalid();
+		}
+	}
+};
+
+org.ametys.cms.widgets.sync.SyncButtonField.prototype.setSyncValue = function (value)
+{
+	org.ametys.cms.widgets.sync.SyncButtonField.superclass.setValue.call(this, value);
+}
+
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedBooleanField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedBooleanField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedBooleanField.i18n.js	(revision 0)
@@ -0,0 +1,112 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('boolean', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedBooleanField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedBooleanField = function(config) 
+{
+	if (!config.width)
+		//config.width = 100; // default width
+	
+	org.ametys.cms.widgets.sync.SynchronizedBooleanField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedBooleanField, org.ametys.form.BooleanField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+
+org.ametys.cms.widgets.sync.SynchronizedBooleanField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedBooleanField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element .x-form-field').wrap({cls: 'x-form-field-wrap2 x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap2,
+		html: '&lt;br/&gt;&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteField = new org.ametys.form.BooleanField ({
+		renderTo: this.wrap2,
+		cls: 'remote-text-field',
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote",
+		width: this.width
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+    
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(50);
+    }
+	
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedBooleanField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedBooleanField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedBooleanField.prototype.setWidgetValue = function (values)
+{
+	this.setValue(values);
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedBooleanField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedComboField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedComboField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedComboField.i18n.js	(revision 0)
@@ -0,0 +1,150 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerEnumeratedWidget ('string', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedComboField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedComboField = function(config, enumeratedValues) 
+{
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	
+	config.valueField = 'value';
+	config.displayField = 'displayText';
+	config.mode = 'local';
+	
+	// The store
+	config.store = new Ext.data.ArrayStore ({
+		id: 0,
+		fields: ['value', {name: 'displayText', sortType: Utils.sortAsNonAccentedUCString}],
+		data: enumeratedValues,
+		sortInfo: {field: 'displayText'} // order
+	})
+	
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	org.ametys.cms.widgets.sync.SynchronizedComboField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedComboField, org.ametys.form.ComboField, {
+	forceSelection : true,
+	triggerAction: 'all',
+	/**
+	 * @cfg {String} synchroButtonText The button text to display on the action button.
+	 *      Note that if you supply a value for {@link #buttonCfg}, the
+	 *      buttonCfg.text value will be used instead if available.
+	 */
+	synchroButtonText: "",
+	/**
+	 * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between
+	 *      the button and the text field (defaults to 3). Note that this only
+	 *      applies if {@link #buttonOnly} = false.
+	 */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedComboField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+
+org.ametys.cms.widgets.sync.SynchronizedComboField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedComboField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element .x-form-field-wrap').wrap({cls: 'x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap2,
+		html: '&lt;br/&gt;&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteDisplayField = new org.ametys.form.TextField ({
+		renderTo: this.wrap2,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote_display",
+		width: this.width
+    });
+	
+	this.remoteField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_remote"
+    });
+	
+	// Sync field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+    
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(this.width);
+    }
+    
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedComboField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedComboField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedComboField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+	
+	var rawValue = this.remoteField.getValue();
+	var index = this.store.findExact("value", rawValue);
+	if (index != -1)
+	{
+		var text = this.store.getAt(index).data.displayText;
+		this.remoteDisplayField.setValue(text);
+	}
+	else
+	{
+		this.remoteDisplayField.setValue(rawValue);
+	}
+}
+
+org.ametys.cms.widgets.sync.SynchronizedComboField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDateField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDateField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDateField.i18n.js	(revision 0)
@@ -0,0 +1,160 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('date', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedDateField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedDateField = function(config) 
+{
+	if (!config.width)
+		config.width = 240 - 40; // default width
+	
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	config.altFormats = 'c';
+	
+	org.ametys.cms.widgets.sync.SynchronizedDateField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedDateField, Ext.form.DateField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Boolean} buttonOnly True to display the field as a button with no visible
+     * text field (defaults to true).  If true, all inherited TextField members will still be available.
+     */
+	buttonOnly: false,
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png',
+	/**
+	 * @cfg {Number} width The text field width in pixel(defaults to 470)
+	 */
+    xtype: 'synchrotextfield'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedDateField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+
+org.ametys.cms.widgets.sync.SynchronizedDateField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedDateField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element div.x-form-field-wrap').wrap({cls: 'x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap2,
+		html: '&lt;br/&gt;&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteDisplayField = new org.ametys.form.TextField ({
+		renderTo: this.wrap2,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote_display",
+		width: this.width
+    });
+	
+	this.remoteField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_remote"
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+	
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(this.width);
+    }
+    
+    if (this.desc)
+	{
+    	this.itemCt.child('div.x-form-element div.x-form-synchro-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 7px;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		var tooltip = new Ext.ToolTip({
+	        target: this.id + '-img',
+	        html: this.desc
+	    });
+	}
+    
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+    
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDateField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedDateField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDateField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+	
+	var rawValue = this.remoteField.getValue();
+	if (rawValue &amp;&amp; rawValue != '')
+	{
+		var d = Date.parseDate(rawValue, Date.patterns.ISO8601Long)
+		this.remoteDisplayField.setValue(this.formatDate(d));
+	}
+	else
+	{
+		this.remoteDisplayField.setValue(rawValue);
+	}
+	
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDateField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDoubleField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDoubleField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedDoubleField.i18n.js	(revision 0)
@@ -0,0 +1,137 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('double', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedDoubleField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedDoubleField = function(config) 
+{
+	if (!config.width)
+		config.width = 200; // default width
+	
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	config.allowDecimals = true;
+	
+	org.ametys.cms.widgets.sync.SynchronizedDoubleField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedDoubleField, Ext.form.NumberField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Boolean} buttonOnly True to display the field as a button with no visible
+     * text field (defaults to true).  If true, all inherited TextField members will still be available.
+     */
+	buttonOnly: false,
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png',
+	/**
+	 * @cfg {Number} width The text field width in pixel(defaults to 470)
+	 */
+    xtype: 'synchrotextfield'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedDoubleField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDoubleField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedDoubleField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap = this.el.wrap({cls:'x-form-field-wrap2 x-form-synchro-wrap2'});
+	this.wrap2 = this.wrap.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap,
+		html: '&lt;br/&gt;&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteField = new org.ametys.form.TextField ({
+		renderTo: this.wrap,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote",
+		width: this.width
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap2,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+	
+    if (this.desc)
+	{
+		this.itemCt.child('div.x-form-element div.x-form-field-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 7px;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		var tooltip = new Ext.ToolTip({
+	        target: this.id + '-img',
+	        html: this.desc
+	    });
+	}
+    
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDoubleField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedDoubleField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDoubleField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedDoubleField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
\ No newline at end of file
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedGeocodeWidget.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedGeocodeWidget.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedGeocodeWidget.i18n.js	(revision 0)
@@ -0,0 +1,128 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('geocode', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget');
+
+/**
+ * @class This class provides a widget to select a map location
+ * @extends org.ametys.cms.widgets.GeocodeWidget
+ */
+org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget = function(config) 
+{
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	
+	org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget, org.ametys.cms.widgets.GeocodeWidget, {
+	/**
+	 * @cfg {String} synchroButtonText The button text to display on the action button.
+	 *      Note that if you supply a value for {@link #buttonCfg}, the
+	 *      buttonCfg.text value will be used instead if available.
+	 */
+	synchroButtonText: "",
+	/**
+	 * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between
+	 *      the button and the text field (defaults to 3). Note that this only
+	 *      applies if {@link #buttonOnly} = false.
+	 */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element div.x-form-field-wrap').wrap({cls: 'x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	// Remote field
+	this.remoteDisplayField = new org.ametys.form.TextField ({
+		renderTo: this.wrap2,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote_display",
+		width: this.width,
+		cls: 'remote-text-field'
+    });
+	
+	this.remoteField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_remote"
+    });
+	
+	// Sync field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+    
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(this.width);
+    }
+}
+
+org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.prototype.setValue = function (value, dom, prefix, contentId) 
+{
+	org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.superclass.setValue.call(this, value, dom, prefix, contentId);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.prototype.setWidgetValue = function (value)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, value);
+	
+	var remoteValue = this.remoteField.getValue();
+	this.remoteDisplayField.setValue(this._getReadableRemoteValue(remoteValue));
+}
+
+org.ametys.cms.widgets.GeocodeWidget.prototype._getReadableRemoteValue = function(value)
+{
+	if (value &amp;&amp; value != '')
+	{
+		var part = value.split(',');
+		var latitude = part[0];
+		var longitude = part[1];
+		
+		return parseFloat(latitude).toFixed(4) + "°" + "<i18n:text i18n:key='PLUGINS_CMS_GEOCODE_DIRECTIONS_NORTH' i18n:catalogue='plugin.cms'/>" + ", "
+		+ parseFloat(longitude).toFixed(4) + "°" + "<i18n:text i18n:key='PLUGINS_CMS_GEOCODE_DIRECTIONS_EST' i18n:catalogue='plugin.cms'/>";
+	}
+	else
+	{
+		return '';
+	}
+};
+
+org.ametys.cms.widgets.sync.SynchronizedGeocodeWidget.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
\ No newline at end of file
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedLongField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedLongField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedLongField.i18n.js	(revision 0)
@@ -0,0 +1,138 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('long', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedLongField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedLongField = function(config) 
+{
+	if (!config.width)
+		config.width = 200; // default width
+	
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	config.allowDecimals = false;
+	
+	org.ametys.cms.widgets.sync.SynchronizedLongField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedLongField, Ext.form.NumberField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Boolean} buttonOnly True to display the field as a button with no visible
+     * text field (defaults to true).  If true, all inherited TextField members will still be available.
+     */
+	buttonOnly: false,
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png',
+	/**
+	 * @cfg {Number} width The text field width in pixel(defaults to 470)
+	 */
+    xtype: 'synchrotextfield'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedLongField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+
+org.ametys.cms.widgets.sync.SynchronizedLongField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedLongField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap = this.el.wrap({cls:'x-form-field-wrap2 x-form-synchro-wrap2'});
+	this.wrap2 = this.wrap.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap,
+		html: '&lt;br/&gt;&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteField = new org.ametys.form.TextField ({
+		renderTo: this.wrap,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote",
+		width: this.width
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap2,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>"
+    }));
+	
+    if (this.desc)
+	{
+		this.itemCt.child('div.x-form-element div.x-form-field-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 7px;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		var tooltip = new Ext.ToolTip({
+	        target: this.id + '-img',
+	        html: this.desc
+	    });
+	}
+    
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedLongField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedLongField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedLongField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedLongField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedMultiselectField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedMultiselectField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedMultiselectField.i18n.js	(revision 0)
@@ -0,0 +1,182 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerEnumeratedWidget ('string', 'multiselect-synchro', 'org.ametys.cms.widgets.sync.SynchronizedMultiselectField');
+org.ametys.utils.Widgets.registerEnumeratedWidget ('string', 'sorted-multiselect-synchro', 'org.ametys.cms.widgets.sync.SynchronizedMultiselectField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField = function(config, enumeratedValues) 
+{
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	
+	config.valueField = 'value';
+	config.displayField = 'displayText';
+	config.mode = 'local';
+	
+	// The store
+	config.store = new Ext.data.ArrayStore ({
+		id: 0,
+		fields: ['value', 'displayText'],
+		data: enumeratedValues,
+		sortInfo: {field: 'displayText'} // order
+	})
+	
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	org.ametys.cms.widgets.sync.SynchronizedMultiselectField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedMultiselectField, Ext.ux.form.MultiSelect, {
+	forceSelection : true,
+	triggerAction: 'all',
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Boolean} buttonOnly True to display the field as a button with no visible
+     * text field (defaults to true).  If true, all inherited TextField members will still be available.
+     */
+	buttonOnly: false,
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png',
+	/**
+	 * @cfg {Number} width The text field width in pixel(defaults to 470)
+	 */
+    xtype: 'synchrotextfield'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedMultiselectField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap = this.itemCt.child('div.x-form-element div.x-form-field').wrap({cls: 'x-form-synchro-wrap2'});
+	this.wrap2 = this.wrap.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	this.br = new org.ametys.HtmlContainer({
+		renderTo: this.wrap,
+		html: '&lt;br/&gt;'
+    });
+	
+	// Remote field
+	this.remoteDisplayField = new org.ametys.form.TextField ({
+		renderTo: this.wrap,
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote_display",
+		width: this.width
+    });
+	
+	this.remoteField = new org.ametys.cms.widgets.sync.MultipleHiddenField ({
+		renderTo: this.wrap,
+		name: this.name + "_remote"
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap2,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>",
+        style: {
+        	marginTop: '80px'
+    	}
+    }));
+	
+    if (this.desc)
+	{
+    	this.itemCt.child('div.x-form-element div.x-form-synchro-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 7px;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		var tooltip = new Ext.ToolTip({
+	        target: this.id + '-img',
+	        html: this.desc
+	    });
+	}
+    
+//    org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedMultiselectField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+	
+	var remoteValues = this.remoteField.getValue();
+	if (!Ext.isArray (remoteValues))
+	{
+		remoteValues = remoteValues.split(",");
+	}
+	
+	var displayRemoteValues = [];
+	for (var i=0; i &lt; remoteValues.length; i++)
+	{
+		var index = this.store.findExact('value', remoteValues[i]);
+		if (index != -1)
+		{
+			var text = this.store.getAt(index).data.displayText;
+			displayRemoteValues.push(text);
+		}
+		else
+		{
+			displayRemoteValues.push(remoteValues[i]);
+		}
+	}
+	
+	this.remoteDisplayField.setValue(displayRemoteValues.join(","));
+}
+
+org.ametys.cms.widgets.sync.SynchronizedMultiselectField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedRichTextField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedRichTextField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedRichTextField.i18n.js	(revision 0)
@@ -0,0 +1,143 @@
+/*
+ *  Copyright 2010 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('rich_text', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedRichTextField');
+org.ametys.utils.Widgets.registerWidget ('rich_text', 'small-richtext-synchro', 'org.ametys.cms.widgets.sync.SynchronizedRichTextField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedRichTextField = function(config) 
+{
+	config.itemCls = "ametys-input";
+	config.labelSeparator = '';
+	
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	if (!config.height)
+		config.height = 200; // default height
+	
+	org.ametys.cms.widgets.sync.SynchronizedRichTextField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedRichTextField, Ext.form.TextArea, {
+	/**
+     * @cfg {String} buttonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	buttonText: "",
+	/**
+     * @cfg {Boolean} buttonOnly True to display the field as a button with no visible
+     * text field (defaults to true).  If true, all inherited TextField members will still be available.
+     */
+	buttonOnly: false,
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+org.ametys.cms.widgets.sync.SynchronizedRichTextField.prototype.getBtnConfig = function ()
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+}
+
+
+org.ametys.cms.widgets.sync.SynchronizedRichTextField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedRichTextField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap = this.el.wrap({cls:'x-form-field-wrap2 x-form-synchro-wrap2 x-form-richtext-synchro2', width: 448});
+	this.wrap2 = this.wrap.wrap({cls:'x-form-field-wrap x-form-synchro-wrap x-form-richtext-synchro', width: 490});
+	
+	this.remoteHtmlPreview = new org.ametys.HtmlContainer ({
+		renderTo: this.wrap,
+		width: this.width - 8,
+		height: 120,
+		autoScroll: true,
+		style: {
+            marginTop: '10px'
+        },
+        cls: 'html-preview',
+		html: ''
+	});
+	
+	// Remote field
+	this.remoteField = new Ext.form.TextArea ({
+		renderTo: this.wrap,
+		name: this.name + "_remote",
+		cls: 'hidden'
+    });
+	this.remoteField.addListener ('focus', this._updateHtml, this);
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap,
+		name: this.name + "_sync"
+    });
+	
+	// Button to synchronized field
+	var btnCfg = this.getBtnConfig();
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap2,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>",
+        style: {
+        	marginTop: '166px'
+    	}
+    }));
+	
+    if (this.desc)
+	{
+		this.itemCt.child('div.x-form-element div.x-form-field-wrap').insertSibling({
+			id: this.id + '-img',
+			tag:'img',
+			style: 'padding-left: 20px; padding-top : 7px;',
+			src: getPluginResourcesUrl('core') + '/img/administrator/config/help.gif'}, 'after');
+	
+		var tooltip = new Ext.ToolTip({
+	        target: this.id + '-img',
+	        html: this.desc
+	    });
+	}
+}
+
+org.ametys.cms.widgets.sync.SynchronizedRichTextField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedRichTextField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedRichTextField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+	this.remoteHtmlPreview.update(this.remoteField.getValue());
+}
+
+org.ametys.cms.widgets.sync.SynchronizedRichTextField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextAreaField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextAreaField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextAreaField.i18n.js	(revision 0)
@@ -0,0 +1,122 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('string', 'textarea-synchro', 'org.ametys.cms.widgets.sync.SynchronizedTextAreaField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedTextAreaField = function(config) 
+{
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	
+	org.ametys.cms.widgets.sync.SynchronizedTextAreaField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedTextAreaField, org.ametys.form.TextAreaField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+
+org.ametys.cms.widgets.sync.SynchronizedTextAreaField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedTextAreaField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element .x-form-field').wrap({cls: 'x-form-field-wrap2 x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	if (this.multiple)
+	{
+		this.multipleHint = new org.ametys.HtmlContainer ({
+			renderTo: this.wrap2,
+			cls: 'multiple-field-hint',
+			html: '<i18n:text i18n:key="WORKSPACE_CMS_TOOLS_WIDGET_SEPARATED_BY_COMA" i18n:catalogue="workspace.cms"/>',
+			style: {
+	        	clear: 'left'
+	    	}
+		})
+	}
+	
+	// Remote field
+	this.remoteField = new org.ametys.form.TextAreaField ({
+		renderTo: this.wrap2,
+		cls: 'remote-text-field',
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote",
+		width: this.width
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync",
+		value: 'false'
+    });
+	
+	// Button to synchronized field
+	var btnCfg = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>",
+        style: {
+        	marginTop: this.multiple ? '5px' : '0px'
+    	}
+    }));
+    
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(this.width);
+    }
+	
+    // org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextAreaField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedTextAreaField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextAreaField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextAreaField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextField.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextField.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedTextField.i18n.js	(revision 0)
@@ -0,0 +1,122 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+// Register widget
+org.ametys.utils.Widgets.registerWidget ('string', 'synchro', 'org.ametys.cms.widgets.sync.SynchronizedTextField');
+
+/**
+ * @class This class provides a widget for synchronized text field
+ * @extends Ext.form.TextField
+ * @xtype synchrotextfield
+ */
+org.ametys.cms.widgets.sync.SynchronizedTextField = function(config) 
+{
+	if (!config.width)
+		config.width = 490 - 40; // default width
+	
+	org.ametys.cms.widgets.sync.SynchronizedTextField.superclass.constructor.call(this, config);
+}; 
+
+Ext.extend(org.ametys.cms.widgets.sync.SynchronizedTextField, org.ametys.form.TextField, {
+	/**
+     * @cfg {String} synchroButtonText The button text to display on the action button.
+     * Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
+     * value will be used instead if available.
+     */
+	synchroButtonText: "",
+	/**
+     * @cfg {Number} synchroButtonOffset The number of pixels of space reserved between the button and the text field
+     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
+     */
+	synchroButtonOffset: 5,
+	/**
+	 * @cfg {String} synchroButtonIcon The button icon path for the action button.
+	 */
+	synchroButtonIcon: getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png'
+});
+
+
+org.ametys.cms.widgets.sync.SynchronizedTextField.prototype.onRender = function(ct, position)
+{
+	org.ametys.cms.widgets.sync.SynchronizedTextField.superclass.onRender.call(this, ct, position);
+	
+	this.wrap2 = this.itemCt.child('div.x-form-element .x-form-field').wrap({cls: 'x-form-field-wrap2 x-form-synchro-wrap2'});
+	this.wrap3 = this.wrap2.wrap({cls:'x-form-field-wrap x-form-synchro-wrap'});
+	
+	if (this.multiple)
+	{
+		this.multipleHint = new org.ametys.HtmlContainer ({
+			renderTo: this.wrap2,
+			cls: 'multiple-field-hint',
+			html: '<i18n:text i18n:key="WORKSPACE_CMS_TOOLS_WIDGET_SEPARATED_BY_COMA" i18n:catalogue="workspace.cms"/>',
+			style: {
+	        	clear: 'left'
+	    	}
+		})
+	}
+	
+	// Remote field
+	this.remoteField = new org.ametys.form.TextField ({
+		renderTo: this.wrap2,
+		cls: 'remote-text-field',
+		readOnly: true,
+		hideLabel:true,
+		name: this.name + "_remote",
+		width: this.width
+    });
+	
+	// Remote field
+	this.syncField = new Ext.form.Hidden ({
+		renderTo: this.wrap2,
+		name: this.name + "_sync",
+		value: 'false'
+    });
+	
+	// Button to synchronized field
+	var btnCfg = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig (this);
+    this.syncBtn = new Ext.Button(Ext.apply(btnCfg, {
+    	renderTo: this.wrap3,
+        cls: 'synchro-btn',
+        tooltip: "<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>",
+        style: {
+        	marginTop: this.multiple ? '5px' : '0px'
+    	}
+    }));
+    
+    if (this.width != null &amp;&amp; this.width &gt; 0)
+    {
+    	this.wrap2.setWidth(this.width);
+    }
+	
+    // org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init (this);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextField.prototype.setValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedTextField.superclass.setValue.call(this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextField.prototype.setWidgetValue = function (values)
+{
+	org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue (this, values);
+}
+
+org.ametys.cms.widgets.sync.SynchronizedTextField.prototype.validateValue = function (value)
+{
+	return org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue (this, value);
+}
Index: main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedWidgetHelper.i18n.js
===================================================================
--- main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedWidgetHelper.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/org/ametys/cms/widgets/sync/SynchronizedWidgetHelper.i18n.js	(revision 0)
@@ -0,0 +1,320 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.cms.widgets.sync');
+
+/**
+ * Helper for synchronized widgets
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper = function()
+{
+}
+
+/**
+ * Initialize the widget value. Call this function at the end of the widget 'onRender' function
+ * @param widget the widget
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.init = function (btn)
+{
+	var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm (btn);
+	
+	if (form != null)
+	{
+		var widget = form.findField(btn.name.substring(0, btn.name.indexOf('_sync_empty')));
+		
+		widget.syncBtn.setIcon(getPluginResourcesUrl('cms') + '/img/widgets/sync/no_synchro_btn.png');
+		widget.syncBtn.setTooltip("<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_SYNCHRONIZE" i18n:catalogue="plugin.cms"/>");
+		
+		widget.syncField.setValue('false');
+		
+		widget.getEl().removeClass('x-item-disabled');
+		widget.setReadOnly(false);
+		
+		widget.remoteField.getEl().addClass('x-item-disabled');
+		
+		if (widget.remoteDisplayField)
+		{
+			widget.remoteDisplayField.getEl().addClass('x-item-disabled');
+		}
+		if (widget.button)
+		{
+			widget.button.enable();
+		}
+	}
+}
+
+/**
+ * Get the synchronize button configuration. Call this function on the widget 'getBtnConfig' function
+ * @param widget the widget
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getBtnConfig = function (widget)
+{
+	var btnCfg = Ext.applyIf(widget.buttonCfg || {}, {
+		id: widget.name + '-btn',
+        text: widget.synchroButtonText, 
+        icon: widget.synchroButtonIcon,
+        
+        handler: org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.synchronize,
+    	scope: widget,
+    	
+    	scale: 'large'
+    });
+	
+	return btnCfg;
+}
+
+/**
+ * Synchronize or desynchronize widget
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.synchronize = function ()
+{
+	if (this.syncField.getValue() == 'true')
+	{
+		org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useLocal(this);
+	}
+	else
+	{
+		org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useRemote(this);
+	}
+	
+	this.validateValue (this, this.getValue());
+	
+	this.fireEvent('synchronize-change', this, this.syncField.getValue());
+}
+
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.getValue = function (widget)
+{
+	if (widget.syncField.getValue() == 'false')
+	{
+		return widget.getValue();
+	}
+	else
+	{
+		return widget.remoteField.getValue();
+	}
+}
+
+/**
+ * Set the widget value. Call this function on the widget 'setValue' function
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.setValue = function (widget, values)
+{
+	var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm (widget);
+	
+	if (form != null)
+	{
+		widget._isInitialized = true;
+		
+		// Remote value
+		var remoteValue = form.findField(widget.name + '_remote_empty').getValue ();
+		widget.remoteField.setValue(remoteValue != null ? remoteValue : '');
+		
+		// Synchronized value
+		var syncValue = form.findField(widget.name + '_sync_empty').getValue ();
+		widget.syncField.setValue(syncValue != null ? syncValue : 'false');
+		
+		if (syncValue == 'true')
+		{
+			org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useRemote (widget);
+		}
+		else
+		{
+			org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useLocal (widget);
+		}
+	}
+}
+
+/**
+ * Get the form container for the widget
+ * @param widget the widget
+ * @private
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm = function (widget)
+{
+	var ownerCt = widget.ownerCt;
+	while (ownerCt != null)
+	{
+		if (typeof (ownerCt.getForm) == 'function')
+		{
+			return ownerCt.getForm ();
+		}
+		ownerCt = ownerCt.ownerCt;
+	}
+	return null;
+}
+
+/**
+ * Validate the value.  Call this function on the widget 'validateValue' function
+ * @param widget the widget
+ * @param value the value to validate
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper.validateValue = function (widget, value)
+{
+	if (widget.syncField.getValue() == 'false')
+	{
+		// clear invalid on remote field
+		widget.remoteField.clearInvalid();
+		if (widget.remoteDisplayField)
+    	{
+    		widget.remoteDisplayField.clearInvalid();
+    	}
+		
+		if (Ext.isFunction(widget.validator))
+		{
+			var msg = widget.validator(value);
+	        if(msg !== true)
+	        {
+	        	widget.markInvalid(msg);
+	            return false;
+	        }
+		}
+		
+		// check mandatory
+		if (value.length &lt; 1 || value === widget.emptyText)
+		{
+			if (widget.allowBlank)
+	        {
+				widget.clearInvalid();
+	            return true;
+	        }
+	        else 
+	        {
+	        	widget.markInvalid(widget.blankText);
+	            return false;
+	        }
+		}
+		
+		// check min length
+		if (value.length &lt; widget.minLength)
+	    {
+	    	widget.markInvalid(String.format(widget.minLengthText, widget.minLength));
+	        return false;
+	    }
+		
+		// check max length
+	    if (value.length &gt; widget.maxLength)
+	    {
+	    	widget.markInvalid(String.format(widget.maxLengthText, widget.maxLength));
+	        return false;
+	    }	
+	    
+	    // check regex
+	    if (widget.syncField.getValue() == 'false' &amp;&amp; widget.regex &amp;&amp; !widget.regex.test(value))
+	    {
+	    	widget.markInvalid(widget.regexText);
+	        return false;
+	    }
+	}
+	else
+	{
+		// clear invalid on local field
+		widget.clearInvalid();
+		
+		var remoteValue = widget.remoteField.getValue();
+		
+		if (Ext.isFunction(widget.validator))
+		{
+			var msg = widget.validator(remoteValue);
+	        if(msg !== true)
+	        {
+	        	widget.remoteField.markInvalid(msg);
+	        	if (widget.remoteDisplayField)
+	        	{
+	        		widget.remoteDisplayField.markInvalid(msg);
+	        	}
+	            return false;
+	        }
+		}
+		
+		// check mandatory
+		if (remoteValue.length &lt; 1 || remoteValue === widget.emptyText)
+		{
+			if (widget.allowBlank)
+	        {
+	    		widget.remoteField.clearInvalid();
+	    		if (widget.remoteDisplayField)
+	        	{
+	        		widget.remoteDisplayField.clearInvalid();
+	        	}
+	            return true;
+	        }
+	        else 
+	        {
+	        	widget.remoteField.markInvalid(widget.blankText);
+	        	if (widget.remoteDisplayField)
+	        	{
+	        		widget.remoteDisplayField.markInvalid(widget.blankText);
+	        	}
+	            return false;
+	        }
+		}
+	}
+    
+    return true;
+}
+
+/**
+ * Use the local value
+ * @param widget the widget
+ * @private
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useRemote = function (widget)
+{
+	widget.syncBtn.setIcon(getPluginResourcesUrl('cms') + '/img/widgets/sync/synchro_btn.png');
+	widget.syncField.setValue('true');
+	
+	var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm (widget);
+	if (form != null)
+		form.findField(widget.name + '_sync_empty').setSyncValue('true');
+	widget.syncBtn.setTooltip("<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_DESYNCHRONIZE" i18n:catalogue="plugin.cms"/>");
+	widget.getEl().addClass('x-item-disabled');
+	widget.setReadOnly(true);
+	widget.remoteField.getEl().removeClass('x-item-disabled');
+	if (widget.remoteDisplayField)
+	{
+		widget.remoteDisplayField.getEl().removeClass('x-item-disabled');
+	}
+	if (widget.button)
+	{
+		widget.button.disable();
+	}
+}
+
+/**
+ * Use the remote value
+ * @param widget the widget
+ * @private
+ */
+org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._useLocal = function (widget)
+{
+	widget.syncBtn.setIcon(getPluginResourcesUrl('cms') + '/img/widgets/sync/no_synchro_btn.png');
+	widget.syncField.setValue('false');
+	var form = org.ametys.cms.widgets.sync.SynchronizedWidgetHelper._getForm (widget);
+	if (form != null)
+		form.findField(widget.name + '_sync_empty').setSyncValue('false');
+	widget.syncBtn.setTooltip("<i18n:text i18n:key="WIDGET_SYNCHRONIZED_FIELD_BTN_SYNCHRONIZE" i18n:catalogue="plugin.cms"/>");
+	widget.getEl().removeClass('x-item-disabled');
+	widget.setReadOnly(false);
+	widget.remoteField.getEl().addClass('x-item-disabled');
+	if (widget.remoteDisplayField)
+	{
+		widget.remoteDisplayField.getEl().addClass('x-item-disabled');
+	}
+	if (widget.button)
+	{
+		widget.button.enable();
+	}
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/AbstractSynchronizedContentType.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/AbstractSynchronizedContentType.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/AbstractSynchronizedContentType.java	(revision 0)
@@ -0,0 +1,193 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.contenttype;
+
+import java.util.Collections;
+
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+
+import org.ametys.runtime.util.I18nizableText;
+
+/**
+ * Type of content to be used for Synchronizable contents
+ */
+public abstract class AbstractSynchronizedContentType extends DefaultContentType
+{
+    /** Remote suffix */
+    public static final String REMOTE_SUFFIX = "_remote";
+    /** Synchro suffix */
+    public static final String SYNC_SUFFIX = "_sync";
+    /** Synchro widget */
+    public static final String SYNCHRO_WIDGET = "synchro";
+    /** Suffix for synchronized widget */
+    public static final String SYNCHRO_WIDGET_SUFFIX = "-synchro";
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        super.service(manager);
+        _contentIndexer = (SynchronizedContentIndexer) manager.lookup(SynchronizedContentIndexer.ROLE);
+    }
+    
+    /**
+     * Determines if a metadata is synchronized
+     * @param metadataName The metadata name
+     * @return true if the metadata is synchronized
+     */
+    public abstract boolean isSynchronized(String metadataName);
+    
+    @Override
+    protected MetadataDefinition _parseMetadata(Configuration metadataConfiguration, MetadataAndRepeaterDefinitionParser defParser) throws ConfigurationException
+    {
+        MetadataDefinition metadataDefinition = super._parseMetadata(metadataConfiguration, defParser);
+        
+        String metadataName = metadataDefinition.getName();
+        
+        if (isSynchronized(metadataName))
+        {
+            _metadata.put(metadataName + REMOTE_SUFFIX, _getRemoteMetadataDefinition (metadataName, metadataDefinition));
+            _metadata.put(metadataName + SYNC_SUFFIX, _getSyncMetadataDefinition (metadataName, metadataDefinition));
+        }
+        
+        return metadataDefinition;
+    }
+    
+    @Override
+    protected void _parseMetadataSets(Configuration config, boolean allowSameName) throws ConfigurationException
+    {
+        for (String metadataName : _metadata.keySet())
+        {
+            if (isSynchronized(metadataName) && !metadataName.endsWith(REMOTE_SUFFIX) && !metadataName.endsWith(SYNC_SUFFIX))
+            {
+                _metadata.get(metadataName + REMOTE_SUFFIX).setEnumerator(_metadata.get(metadataName).getEnumerator());
+            }
+        }
+        super._parseMetadataSets(config, allowSameName);
+    }
+    
+    @Override
+    protected void _fillMetadataSetElement(Configuration metadataSetConfig, MetadataSetElement metadataSetElement, boolean isEdition) throws ConfigurationException
+    {
+        for (Configuration elementConfig : metadataSetConfig.getChildren())
+        {
+            String elementConfigName = elementConfig.getName();
+            
+            if (elementConfigName.equals("metadata-ref"))
+            {
+                String metadataName = elementConfig.getAttribute("name");
+                MetadataDefinitionReference metadataDefRef = new MetadataDefinitionReference(metadataName);
+                
+                if (!metadataSetElement.addElement(metadataDefRef))
+                {
+                    throw new ConfigurationException("Metadata with name " + metadataName + " is already referenced", elementConfig);
+                }
+                
+                if (isEdition && isSynchronized(metadataName))
+                {
+                    MetadataDefinitionReference remoteDefRef = new MetadataDefinitionReference(metadataName + REMOTE_SUFFIX);
+                    metadataSetElement.addElement(remoteDefRef);
+                    
+                    MetadataDefinitionReference syncDefRef = new MetadataDefinitionReference(metadataName + SYNC_SUFFIX);
+                    metadataSetElement.addElement(syncDefRef);
+                }
+
+                _fillMetadataSetElement(elementConfig, metadataDefRef, isEdition);
+            }
+            else if (elementConfigName.equals("fieldset"))
+            {
+                Fieldset fieldset = new Fieldset();
+                I18nizableText label = _parseI18nizableText(elementConfig, "label");
+                fieldset.setLabel(label);
+                _fillMetadataSetElement(elementConfig, fieldset, isEdition);
+                metadataSetElement.addElement(fieldset);
+            }
+            else if (elementConfigName.equals("dublin-core"))
+            {
+                _fillMetadataSetDublinCore(metadataSetElement);
+            }
+        }
+    }
+
+    
+    private MetadataDefinition _getRemoteMetadataDefinition (String metadataName, MetadataDefinition metadataDefinition)
+    {
+        MetadataDefinition remoteMetaDef = new MetadataDefinition();
+        remoteMetaDef.setName(metadataName + REMOTE_SUFFIX);
+        remoteMetaDef.setId(metadataDefinition.getId() + REMOTE_SUFFIX);
+        MetadataType type = metadataDefinition.getType();
+        remoteMetaDef.setType(type);
+        if (type == MetadataType.RICH_TEXT)
+        {
+            remoteMetaDef.setRichTextTransformer(metadataDefinition.getRichTextTransformer());
+        }
+        remoteMetaDef.setLabel(metadataDefinition.getLabel());
+        remoteMetaDef.setDescription(metadataDefinition.getDescription());
+        remoteMetaDef.setMultiple(metadataDefinition.isMultiple());
+        remoteMetaDef.setWidget("empty");
+        String remoteFieldName = "content.input." + metadataName + REMOTE_SUFFIX;
+        remoteMetaDef.setWidgetParameters(Collections.singletonMap("copyValueTo", new I18nizableText(remoteFieldName)));
+        
+        return remoteMetaDef;
+    }
+    
+    private MetadataDefinition _getSyncMetadataDefinition (String metadataName, MetadataDefinition metadataDefinition)
+    {
+        MetadataDefinition syncMetaDef = new MetadataDefinition();
+        syncMetaDef.setId(metadataDefinition.getId() + SYNC_SUFFIX);
+        syncMetaDef.setName(metadataName + SYNC_SUFFIX);
+        syncMetaDef.setType(MetadataType.BOOLEAN);
+        syncMetaDef.setLabel(metadataDefinition.getLabel());
+        syncMetaDef.setDescription(metadataDefinition.getDescription());
+        syncMetaDef.setWidget("synchro-button");
+        
+        return syncMetaDef;
+    }
+    
+    @Override
+    public MetadataDefinition getMetadataDefinition(String metadataName)
+    {
+        MetadataDefinition metaDef = super.getMetadataDefinition(metadataName);
+        if (metaDef == null)
+        {
+            return null;
+        }
+        
+        String widget = metaDef.getWidget();
+        if (isSynchronized(metadataName))
+        {   
+            if (widget != null && !widget.endsWith(SYNCHRO_WIDGET))
+            {
+                metaDef.setWidget(widget + SYNCHRO_WIDGET_SUFFIX);
+            }
+            else if (widget == null)
+            {
+                metaDef.setWidget(SYNCHRO_WIDGET);
+            }
+        }
+        else if (widget != null && widget.endsWith(SYNCHRO_WIDGET))
+        {
+            // the metadata is no more synchronized, use the default widget
+            widget = widget.substring(0, widget.indexOf(SYNCHRO_WIDGET));
+            metaDef.setWidget(widget.length() > 0 ? widget : null);
+        }
+        
+        return metaDef;
+    }
+   
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedContentIndexer.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedContentIndexer.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedContentIndexer.java	(revision 0)
@@ -0,0 +1,38 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.contenttype;
+
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+
+import org.ametys.cms.lucene.SynchronizedMetadataIndexer;
+
+/**
+ * Synchronized content specific indexer.
+ */
+public class SynchronizedContentIndexer extends DefaultContentIndexer
+{
+    /** The avalon role. */
+    @SuppressWarnings("hiding")
+    public static final String ROLE = SynchronizedContentIndexer.class.getName();
+
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        super.service(manager);
+        _metadataIndexer = (SynchronizedMetadataIndexer) manager.lookup(SynchronizedMetadataIndexer.ROLE);
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedMetadataManager.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedMetadataManager.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/SynchronizedMetadataManager.java	(revision 0)
@@ -0,0 +1,230 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.contenttype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+
+import org.apache.avalon.framework.context.Context;
+import org.apache.avalon.framework.context.ContextException;
+import org.apache.avalon.framework.context.Contextualizable;
+import org.apache.cocoon.components.ContextHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.io.IOUtils;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.transformation.RichTextTransformer;
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.metadata.CompositeMetadata;
+import org.ametys.plugins.repository.metadata.ModifiableRichText;
+import org.ametys.plugins.repository.metadata.RichText;
+import org.ametys.runtime.user.User;
+import org.ametys.runtime.util.IgnoreRootHandler;
+import org.ametys.runtime.util.parameter.Enumerator;
+import org.ametys.runtime.util.parameter.ParameterHelper;
+
+
+/**
+ * This manager handle synchronized metadata for synchronized contents
+ */
+public class SynchronizedMetadataManager extends MetadataManager implements Contextualizable
+{
+    /** Avalon Role. */
+    @SuppressWarnings("hiding")
+    public static final String ROLE = SynchronizedMetadataManager.class.getName();
+    
+    /** Request attribute for edition mode */
+    public static final String EDITION_MODE_REQUEST_ATTR = SynchronizedMetadataManager.class.getName() + "$isEditionMode";
+
+    private Context _context;
+    
+    @Override
+    public void contextualize(Context context) throws ContextException
+    {
+        _context = context;
+    }
+    
+    @Override
+    protected boolean hasMetadata(CompositeMetadata metadata, String metadataName)
+    {
+        if (!_isEditionMode() && metadata.getBoolean(metadataName + AbstractSynchronizedContentType.SYNC_SUFFIX, false))
+        {
+            return metadata.hasMetadata(metadataName + AbstractSynchronizedContentType.REMOTE_SUFFIX);
+        }
+        return super.hasMetadata(metadata, metadataName);
+    }
+    
+    @Override
+    protected void _saxSingleStringMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadatas, String metadataName, String prefix, boolean checkRead, Enumerator enumerator) throws AmetysRepositoryException, SAXException
+    {
+        String metadataForValues = _getMetadataName(metadatas, metadataName);
+        
+        if (!checkRead || _canRead(content, prefix + metadataName))
+        {
+            String[] values = metadatas.getStringArray(metadataForValues);
+            
+            if (values != null)
+            {
+                if (enumerator != null)
+                {
+                    _saxEnumeratedStringMetadata (contentHandler, content, metadataName, enumerator, values);
+                }
+                else
+                {
+                    for (String value : values)
+                    {
+                        XMLUtils.createElement(contentHandler, metadataName, value);
+                    }
+                }
+            }      
+        }
+    }
+    
+    @Override
+    protected void _saxRichTextMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadata, MetadataDefinition metadataDef, String metadataName, String prefix, boolean checkRead, boolean editionRendering) throws AmetysRepositoryException, SAXException, IOException
+    {
+        if (!checkRead || _canRead(content, prefix + metadataName))
+        {
+            String metadataNameForValue = _getMetadataName(metadata, metadataName);
+            
+            RichText richText = metadata.getRichText(metadataNameForValue);
+            AttributesImpl attrs = new AttributesImpl();
+            String mimeType = richText.getMimeType();
+            
+            attrs.addCDATAAttribute("mime-type", richText.getMimeType());
+            attrs.addCDATAAttribute("lastModified", ParameterHelper.valueToString(richText.getLastModified()));
+            
+            XMLUtils.startElement(contentHandler, metadataName, attrs);
+            
+            if (editionRendering)
+            {
+                if (!(richText instanceof ModifiableRichText))
+                {
+                    throw new SAXException("");
+                }   
+                
+                RichTextTransformer richTextTransformer = metadataDef.getRichTextTransformer();
+                StringBuilder result = new StringBuilder(2048);
+                
+                try
+                {
+                    richTextTransformer.transformForEditing((ModifiableRichText) richText, result);
+                }
+                catch (IOException e)
+                {
+                    throw new AmetysRepositoryException("Unable to transform a rich text into a string", e);
+                }
+                
+                XMLUtils.data(contentHandler, result.toString());
+            }
+            else
+            {
+                if (mimeType.equals("text/xml"))
+                {
+                    RichTextTransformer richTextTransformer = metadataDef.getRichTextTransformer();
+                    richTextTransformer.transformForRendering(richText, new IgnoreRootHandler(contentHandler));
+                }
+                else if (mimeType.equals("text/plain"))
+                {
+                    String encoding = richText.getEncoding();
+                    InputStream is = richText.getInputStream();
+                    
+                    try
+                    {
+                        XMLUtils.data(contentHandler, IOUtils.toString(is, encoding));
+                    }
+                    finally
+                    {
+                        IOUtils.closeQuietly(is);
+                    }
+                }
+                else
+                {
+                    throw new AmetysRepositoryException("Mime-type " + mimeType + " is not supported for rich text rendering");
+                }
+            }
+            
+            XMLUtils.endElement(contentHandler, metadataName);
+        }
+    }
+    
+    @Override
+    protected void _saxSingleDateMetadata(ContentHandler contentHandler, Content content, CompositeMetadata metadatas, String metadataName, String prefix, boolean checkRead) throws AmetysRepositoryException, SAXException
+    {
+        if (!checkRead || _canRead(content, prefix + metadataName))
+        {
+            String metadataNameForValues = _getMetadataName(metadatas, metadataName);
+            Date[] values = metadatas.getDateArray(metadataNameForValues);
+            
+            if (values != null)
+            {
+                for (Date value : values)
+                {
+                    XMLUtils.createElement(contentHandler, metadataName, ParameterHelper.valueToString(value));
+                }
+            }
+        }
+    }
+    
+    @Override
+    protected void _saxUserMetadata (ContentHandler contentHandler, Content content, CompositeMetadata metadata, String metadataName, String prefix, boolean checkRead) throws AmetysRepositoryException, SAXException
+    {
+        if (!checkRead || _canRead(content, prefix + metadataName))
+        {
+            String metadataNameForValues = _getMetadataName(metadata, metadataName);
+            String[] logins = metadata.getStringArray(metadataNameForValues);
+            if (logins != null)
+            {
+                for (String login : logins)
+                {
+                    User user = _usersManager.getUser(login);
+                    if (user != null)
+                    {
+                        AttributesImpl attrs = new AttributesImpl();
+                        attrs.addAttribute("", "login", "login", "CDATA", login);
+                        attrs.addAttribute("", "email", "email", "CDATA", user.getEmail());
+                        XMLUtils.createElement(contentHandler, metadataName, attrs, user.getFullName());
+                    }
+                }
+            }
+        }
+    }
+    
+    private boolean _isEditionMode ()
+    {
+        Request request = ContextHelper.getRequest(_context);
+        
+        if (request.getAttribute(EDITION_MODE_REQUEST_ATTR) != null)
+        {
+            return (Boolean) request.getAttribute(EDITION_MODE_REQUEST_ATTR);
+        }
+        return false;
+    }
+    
+    private String _getMetadataName (CompositeMetadata metadataHolder, String metadataName)
+    {
+        if (!_isEditionMode() && metadataHolder.getBoolean(metadataName + AbstractSynchronizedContentType.SYNC_SUFFIX, false))
+        {
+            return metadataName + AbstractSynchronizedContentType.REMOTE_SUFFIX;
+        }
+        return metadataName;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/lucene/SynchronizedMetadataIndexer.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/lucene/SynchronizedMetadataIndexer.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/lucene/SynchronizedMetadataIndexer.java	(revision 0)
@@ -0,0 +1,75 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.lucene;
+
+import java.io.IOException;
+
+import org.apache.lucene.document.Document;
+import org.xml.sax.SAXException;
+
+import org.ametys.cms.contenttype.AbstractSynchronizedContentType;
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.MetadataDefinition;
+import org.ametys.cms.contenttype.MetadataSetElement;
+import org.ametys.cms.repository.Content;
+import org.ametys.plugins.repository.metadata.CompositeMetadata;
+
+/**
+ * Metadata indexer specific to synchronized contents, which handles synchronized metadata values.
+ */
+public class SynchronizedMetadataIndexer extends MetadataIndexer
+{
+    /** The avalon role. */
+    @SuppressWarnings("hiding")
+    public static final String ROLE = SynchronizedMetadataIndexer.class.getName();
+    
+    @Override
+    public void indexMetadata(MetadataSetElement metadataSet, CompositeMetadata metadata, String prefix, Content content, ContentType cType, Document document, MetadataSetElement element, String name, String fieldName, MetadataDefinition definition) throws SAXException, IOException, Exception
+    {
+        if (cType instanceof AbstractSynchronizedContentType)
+        {
+            String metadataNameToIndex = _getMetadataNameToIndex ((AbstractSynchronizedContentType) cType, metadata, name);
+            super.indexMetadata(metadataSet, metadata, prefix, content, cType, document, element, metadataNameToIndex, name, definition);
+        }
+    }
+    
+    @Override
+    protected boolean hasMetadata(CompositeMetadata metadata, String metadataName)
+    {
+        if (metadata.getBoolean(metadataName + AbstractSynchronizedContentType.SYNC_SUFFIX, false))
+        {
+            return metadata.hasMetadata(metadataName + AbstractSynchronizedContentType.REMOTE_SUFFIX);
+        }
+        return super.hasMetadata(metadata, metadataName);
+    }
+    
+    /**
+     * Get the name of metadata to index
+     * @param cType the synchronized content type
+     * @param metadataHolder The metadata holder
+     * @param metadataName The metadata name
+     * @return The true metadata name to index
+     */
+    protected String _getMetadataNameToIndex (AbstractSynchronizedContentType cType, CompositeMetadata metadataHolder, String metadataName)
+    {
+        if (cType.isSynchronized(metadataName) && metadataHolder.getBoolean(metadataName + AbstractSynchronizedContentType.SYNC_SUFFIX, false))
+        {
+            return metadataName + AbstractSynchronizedContentType.REMOTE_SUFFIX;
+        }
+        
+        return metadataName;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContent.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContent.java	(revision 0)
@@ -0,0 +1,1001 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.repository;
+
+import java.util.Date;
+
+import javax.jcr.Node;
+
+import org.ametys.cms.contenttype.MetadataType;
+import org.ametys.cms.repository.DefaultContent;
+import org.ametys.cms.repository.ModifiableContentFactory;
+import org.ametys.cms.repository.ModifiableDefaultContent;
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.metadata.BinaryMetadata;
+import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
+import org.ametys.plugins.repository.metadata.RichText;
+import org.ametys.plugins.repository.metadata.UnknownMetadataException;
+
+/**
+ * Synchronizable Content
+ * 
+ * @param <F> the actual type of factory.
+ */
+public class SynchronizableContent<F extends ModifiableContentFactory> extends ModifiableDefaultContent<F>
+{
+    /** sync suffix */
+    public static final String SYNC_SUFFIX = "_sync";
+
+    /** remote suffix */
+    public static final String REMOTE_SUFFIX = "_remote";
+
+    /**
+     * Constructor
+     * 
+     * @param node
+     * @param parentPath
+     * @param factory
+     */
+    public SynchronizableContent(Node node, String parentPath, F factory)
+    {
+        super(node, parentPath, factory);
+    }
+    
+    @Override
+    public String getTitle() throws UnknownMetadataException, AmetysRepositoryException
+    {
+        return getString(DefaultContent.METADATA_TITLE);
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public String getString (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return "";
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getString(metadataName + REMOTE_SUFFIX, "");
+        }
+        else
+        {
+            return metadataHolder.getString(metadataName, "");
+        }
+    }
+    
+    /**
+     * Get the remote metadata value
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public String getRemoteString (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return "";
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getString(metadataName + REMOTE_SUFFIX, "");
+    }
+    
+    /**
+     * Get a metadata value by its path : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public String[] getStringArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new String[] {};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getStringArray(metadataName + REMOTE_SUFFIX, new String[] {});
+        }
+        else
+        {
+            return metadataHolder.getStringArray(metadataName, new String[] {});
+        }
+    }
+    
+    /**
+     * Get the remote metadata value
+     * @param metadataPath the path of the metadata 
+     * @return the metadata value
+     */
+    public String[] getRemoteStringArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new String[] {};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getStringArray(metadataName + REMOTE_SUFFIX, new String[] {});
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value or -1 if not set
+     */
+    public double getDouble (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return -1;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getDouble(metadataName + REMOTE_SUFFIX, -1);
+        }
+        else
+        {
+            return metadataHolder.getDouble(metadataName, -1);
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public double getRemoteDouble (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return -1;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getDouble(metadataName + REMOTE_SUFFIX, -1);
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value or -1 if not set
+     */
+    public double[] getDoubleArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new double[]{};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getDoubleArray(metadataName + REMOTE_SUFFIX, new double[]{});
+        }
+        else
+        {
+            return metadataHolder.getDoubleArray(metadataName, new double[]{});
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata name
+     * @return the metadata value
+     */
+    public double[] getRemoteDoubleArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new double[]{};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getDoubleArray(metadataName + REMOTE_SUFFIX, new double[]{});
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata name
+     * @return the metadata value or -1 if not set
+     */
+    public long getLong (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return -1;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getLong(metadataName + REMOTE_SUFFIX, -1);
+        }
+        else
+        {
+            return metadataHolder.getLong(metadataName, -1);
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public long getRemoteLong (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return -1;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getLong(metadataName + REMOTE_SUFFIX, -1);
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value or -1 if not set
+     */
+    public long[] getLongArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new long[]{};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getLongArray(metadataName + REMOTE_SUFFIX, new long[]{});
+        }
+        else
+        {
+            return metadataHolder.getLongArray(metadataName, new long[]{});
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public long[] getRemoteLongArray (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return new long[]{};
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getLongArray(metadataName + REMOTE_SUFFIX, new long[]{});
+    }
+    
+    /**
+     * Get the metadata binary value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata binary value or null if not set
+     */
+    public BinaryMetadata getBinary (String metadataPath)
+    {
+        try
+        {
+            String[] pathSegments = metadataPath.split("/");
+            
+            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+            
+            for (int i = 0;  i < pathSegments.length - 1; i++)
+            {
+                metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+            }
+            
+            String metadataName = pathSegments[pathSegments.length - 1];
+            if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+            {
+                return metadataHolder.getBinaryMetadata(metadataName + REMOTE_SUFFIX, false);
+            }
+            else
+            {
+                return metadataHolder.getBinaryMetadata(metadataName, false);
+            }
+        }
+        catch (UnknownMetadataException e)
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @param createNew <code>true</code> to create automatically the {@link BinaryMetadata} when missing.
+     * @return the metadata value or <code>null</code> if not set
+     */
+    public BinaryMetadata getRemoteBinary (String metadataPath, boolean createNew)
+    {
+        try
+        {
+            String[] pathSegments = metadataPath.split("/");
+            
+            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+            
+            for (int i = 0;  i < pathSegments.length - 1; i++)
+            {
+                metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], createNew);
+            }
+            
+            String metadataName = pathSegments[pathSegments.length - 1];
+            
+            return metadataHolder.getBinaryMetadata(metadataName + REMOTE_SUFFIX, createNew);
+        }
+        catch (UnknownMetadataException e)
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @return the metadata value or null if not set
+     */
+    public boolean getBoolean (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return false;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getBoolean(metadataName + REMOTE_SUFFIX, false);
+        }
+        else
+        {
+            return metadataHolder.getBoolean(metadataName, false);
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public boolean getRemoteBoolean (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return false;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getBoolean(metadataName + REMOTE_SUFFIX, false);
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata name
+     * @return the metadata value or null if not set
+     */
+    public Date getDate (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return null;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        
+        if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+        {
+            return metadataHolder.getDate(metadataName + REMOTE_SUFFIX, null);
+        }
+        else
+        {
+            return metadataHolder.getDate(metadataName, null);
+        }
+    }
+    
+    /**
+     * Get the remote metadata value 
+     * @param metadataPath the metadata path
+     * @return the metadata value
+     */
+    public Date getRemoteDate (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            if (!metadataHolder.hasMetadata(pathSegments[i]))
+            {
+                return null;
+            }
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.getDate(metadataName + REMOTE_SUFFIX, null);
+    }
+    
+    /**
+     * Get the rich text value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataName the metadata name
+     * @return the metadata value or null if not set
+     */
+    public RichText getRichText (String metadataName)
+    {
+        return getRichText(metadataName, false);
+    }
+    
+    /**
+     * Get the rich text value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata name
+     * @param createNew <code>true</code> to create automatically the {@link BinaryMetadata} when missing.
+     * @return the metadata value or null if not set
+     */
+    public RichText getRichText (String metadataPath, boolean createNew)
+    {
+        try
+        {
+            String[] pathSegments = metadataPath.split("/");
+            
+            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+            
+            for (int i = 0;  i < pathSegments.length - 1; i++)
+            {
+                metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], createNew);
+            }
+            
+            String metadataName = pathSegments[pathSegments.length - 1];
+            if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+            {
+                return metadataHolder.getRichText(metadataName + REMOTE_SUFFIX, createNew);
+            }
+            else
+            {
+                return metadataHolder.getRichText(metadataName,  createNew);
+            }
+        }
+        catch (UnknownMetadataException e)
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Get the rich text value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata name
+     * @param createNew <code>true</code> to create automatically the {@link BinaryMetadata} when missing.
+     * @return the metadata value or null if not set
+     */
+    public RichText getRemoteRichText (String metadataPath, boolean createNew)
+    {
+        try
+        {
+            String[] pathSegments = metadataPath.split("/");
+            
+            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+            
+            for (int i = 0;  i < pathSegments.length - 1; i++)
+            {
+                metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], createNew);
+            }
+            
+            String metadataName = pathSegments[pathSegments.length - 1];
+            return metadataHolder.getRichText(metadataName + REMOTE_SUFFIX, createNew);
+        }
+        catch (UnknownMetadataException e)
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Get the rich text value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata name
+     * @return the metadata value or null if not set
+     */
+    public BinaryMetadata getBinaryMetadata (String metadataPath)
+    {
+        try
+        {
+            String[] pathSegments = metadataPath.split("/");
+            
+            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+            
+            for (int i = 0;  i < pathSegments.length - 1; i++)
+            {
+                metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+            }
+            
+            String metadataName = pathSegments[pathSegments.length - 1];
+            
+            if (metadataHolder.getBoolean(metadataName + SYNC_SUFFIX, false))
+            {
+                return metadataHolder.getBinaryMetadata(metadataName + REMOTE_SUFFIX);
+            }
+            else
+            {
+                return metadataHolder.getBinaryMetadata(metadataName);
+            }
+        }
+        catch (UnknownMetadataException e)
+        {
+            return null;
+        }
+    }
+    
+    /**
+     * Get the metadata value : if synchronized, get the remote value, otherwise the local one.
+     * @param metadataPath the metadata path
+     * @param type the metadata type
+     * @return Object or null if not found
+     */
+    public Object getMetadata(String metadataPath, MetadataType type)
+    {
+        boolean multiple = false;
+        try
+        {
+            multiple = _isMultiple(metadataPath);
+        }
+        catch (UnknownMetadataException e)
+        {
+            // The metadata does not exist
+            return null;
+        }
+        
+        switch (type)
+        {
+            case STRING:
+                if (multiple)
+                {
+                    return getStringArray(metadataPath);
+                }
+                else
+                {
+                    return getString(metadataPath);
+                }
+
+            case LONG:
+                if (multiple)
+                {
+                    return getLongArray(metadataPath);
+                }
+                else
+                {
+                    return getLong(metadataPath);
+                }
+
+            case DOUBLE:
+                if (multiple)
+                {
+                    return getDoubleArray(metadataPath);
+                }
+                else
+                {
+                    return getDouble(metadataPath);
+                }
+
+            case BOOLEAN:
+                return getBoolean(metadataPath);
+
+            case DATE:
+                return getDate(metadataPath);
+
+            case RICH_TEXT:
+                return getRichText(metadataPath);
+
+            case FILE:
+            case BINARY:
+                return getBinaryMetadata(metadataPath);
+
+            default:
+                throw new IllegalStateException("This type of metadata " + metadataPath + " is not supported.");
+        }
+    }
+    
+    /**
+     * Test if the given metadata is multiple.
+     * @param metadataPath the metadata path.
+     * @return <code>true</code> if the given metadata is multiple,
+     *         <code>false</code> otherwise.
+     * @throws UnknownMetadataException if the named metadata does not exist.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    protected boolean _isMultiple (String metadataPath)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i]);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        return metadataHolder.isMultiple(metadataName);
+    }
+
+    /**
+     * Set the synchronization status of a metadata
+     * @param metadataPath the path of the metadata
+     * @param sync true to set as synchronized
+     */
+    public void synchronizeMetadata(String metadataPath, boolean sync)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + SYNC_SUFFIX, sync);
+    }
+    
+    /**
+     * Set a boolean remote metadata.
+     * @param metadataPath the metadata path.
+     * @param value the boolean value of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, boolean value)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, value);
+    }
+
+    /**
+     * Set a multi-valued boolean remote metadata.
+     * @param metadataPath the metadata path.
+     * @param values the boolean values of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, boolean[] values)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, values);
+    }
+
+    /**
+     * Set a double remote metadata.
+     * @param metadataPath the metadata path.
+     * @param value the double value of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, double value)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, value);
+    }
+
+    /**
+     * Set a multi-valued double remote metadata.
+     * @param metadataPath the metadata path.
+     * @param values the double values of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, double[] values)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, values);
+    }
+
+    /**
+     * Set a long remote metadata.
+     * @param metadataPath the metadata path.
+     * @param value the long value of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, long value)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, value);
+    }
+
+    /**
+     * Set a multi-valued long remote metadata.
+     * @param metadataPath the metadata path.
+     * @param values the long values of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, long[] values)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, values);
+    }
+
+    /**
+     * Set a String remote metadata.
+     * @param metadataPath the metadata path.
+     * @param value the String value of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, String value)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, value);
+    }
+
+    /**
+     * Set a multi-valued String remote metadata.
+     * @param metadataPath the metadata path.
+     * @param values the String values of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, String[] values)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, values);
+    }
+
+    /**
+     * Set a date remote metadata.
+     * @param metadataPath the metadata path.
+     * @param value the date value of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, Date value)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, value);
+    }
+
+    /**
+     * Set a multi-valued data remote metadata.
+     * @param metadataPath the metadata path.
+     * @param values the date values of this metadata.
+     * @throws AmetysRepositoryException if the metadata cannot be set.
+     */
+    public void setRemoteMetadata(String metadataPath, Date[] values)
+    {
+        String[] pathSegments = metadataPath.split("/");
+        
+        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
+        
+        for (int i = 0;  i < pathSegments.length - 1; i++)
+        {
+            metadataHolder = metadataHolder.getCompositeMetadata(pathSegments[i], true);
+        }
+        
+        String metadataName = pathSegments[pathSegments.length - 1];
+        metadataHolder.setMetadata(metadataName + REMOTE_SUFFIX, values);
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContentFactory.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContentFactory.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/SynchronizableContentFactory.java	(revision 0)
@@ -0,0 +1,38 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.repository;
+
+import javax.jcr.Node;
+
+import org.ametys.cms.repository.SynchronizableContent;
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.RepositoryConstants;
+
+/**
+ * Synchronizable content Factory
+ * 
+ */
+public class SynchronizableContentFactory extends SynchronizableTraversableContentFactory
+{
+    /** nodetype for synchronizable content */
+    public static final String SYNCHRONIZABLE_CONTENT_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":syncContent";
+
+    @Override
+    public SynchronizableContent getAmetysObject(Node node, String parentPath) throws AmetysRepositoryException
+    {
+        return new SynchronizableContent(node, parentPath, this);
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/SynchronizableTraversableContentFactory.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/SynchronizableTraversableContentFactory.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/SynchronizableTraversableContentFactory.java	(revision 0)
@@ -0,0 +1,92 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.repository;
+
+import org.ametys.cms.repository.ModifiableContentFactory;
+import org.ametys.plugins.repository.AmetysObject;
+import org.ametys.plugins.repository.AmetysObjectFactory;
+import org.ametys.plugins.repository.AmetysObjectIterable;
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.UnknownAmetysObjectException;
+import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
+import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
+import org.ametys.plugins.repository.jcr.TraversableAmetysObjectHelper;
+
+/**
+ * {@link AmetysObjectFactory} handling {@link SynchronizableContent}.
+ */
+public class SynchronizableTraversableContentFactory extends ModifiableContentFactory
+{
+    /**
+     * Returns the {@link AmetysObject} at the given subPath, relative to the
+     * given {@link DefaultTraversableAmetysObject}.
+     * 
+     * @param <A> the actual type of {@link AmetysObject}.
+     * @param object the context {@link DefaultTraversableAmetysObject}.
+     * @param path the sub path. Cannot be <code>null</code>, empty or absolute.
+     * @return the {@link AmetysObject} at the given subPath, relative to the
+     *         given {@link DefaultTraversableAmetysObject}.
+     * @throws AmetysRepositoryException if an error occurs.
+     * @throws UnknownAmetysObjectException if no such object exists.
+     */
+    public <A extends AmetysObject> A getChild(JCRTraversableAmetysObject object, String path) throws AmetysRepositoryException, UnknownAmetysObjectException
+    {
+        return TraversableAmetysObjectHelper.<A> getChild(object, this, path, _resolver, getLogger());
+    }
+
+    /**
+     * Returns all children of the given {@link DefaultTraversableAmetysObject}.
+     * 
+     * @param <A> the actual type of {@link AmetysObject}s
+     * @param object a {@link DefaultTraversableAmetysObject}.
+     * @return a List containing all children object in the Ametys hierarchy.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public <A extends AmetysObject> AmetysObjectIterable<A> getChildren(JCRTraversableAmetysObject object) throws AmetysRepositoryException
+    {
+        return TraversableAmetysObjectHelper.getChildren(object, this, _resolver, getLogger());
+    }
+
+    /**
+     * Tests if a given object has a child with a given name.
+     * 
+     * @param object the context object.
+     * @param name the name to test.
+     * @return <code>true</code> is the given object has a child with the given
+     *         name, <code>false</code> otherwise.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public boolean hasChild(JCRTraversableAmetysObject object, String name) throws AmetysRepositoryException
+    {
+        return TraversableAmetysObjectHelper.hasChild(object, name, _ametysFactoryExtensionPoint, getLogger());
+    }
+
+    /**
+     * Creates a child to the given object.
+     * 
+     * @param <A> the actual type of {@link AmetysObject}.
+     * @param object the parent {@link AmetysObject}.
+     * @param name the new object's name.
+     * @param type the new object's type.
+     * @return the newly created {@link AmetysObject}.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public <A extends AmetysObject> A createChild(JCRTraversableAmetysObject object, String name, String type) throws AmetysRepositoryException
+    {
+        return TraversableAmetysObjectHelper.<A> createChild(object, this, name, type, _ametysFactoryExtensionPoint, _resolver, getLogger());
+    }
+
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/SynchronizeExpression.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/SynchronizeExpression.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/SynchronizeExpression.java	(revision 0)
@@ -0,0 +1,51 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.cms.repository;
+
+import org.ametys.plugins.repository.query.expression.AndExpression;
+import org.ametys.plugins.repository.query.expression.BooleanExpression;
+import org.ametys.plugins.repository.query.expression.Expression;
+import org.ametys.plugins.repository.query.expression.MetadataExpression;
+import org.ametys.plugins.repository.query.expression.NotExpression;
+import org.ametys.plugins.repository.query.expression.OrExpression;
+import org.ametys.plugins.repository.query.expression.StringExpression;
+
+/**
+ * Constructs an {@link Expression} for synchronized fields
+ *
+ */
+public class SynchronizeExpression extends OrExpression
+{
+    private Expression _expr;
+
+    /**
+     * Creates an {@link Expression} for a synchronized metadata
+     * @param metadata the metadata
+     * @param value the value to test
+     */
+    public SynchronizeExpression(String metadata, String value)
+    {
+        Expression exprSync = new AndExpression(new BooleanExpression(metadata + SynchronizableContent.SYNC_SUFFIX, true), new StringExpression(metadata + SynchronizableContent.REMOTE_SUFFIX, Operator.EQ, value));
+        Expression exprNotSync = new AndExpression(new OrExpression(new NotExpression(new MetadataExpression(metadata + SynchronizableContent.SYNC_SUFFIX)), new BooleanExpression(metadata + SynchronizableContent.SYNC_SUFFIX, Operator.NE, true)), new StringExpression(metadata, Operator.EQ, value));
+        _expr = new OrExpression(exprSync, exprNotSync);
+    }
+    
+    @Override
+    public String build()
+    {
+        return _expr.build();
+    }
+}
