Index: main/plugin-web/i18n/messages_en.xml
===================================================================
--- main/plugin-web/i18n/messages_en.xml	(revision 32549)
+++ main/plugin-web/i18n/messages_en.xml	(working copy)
@@ -1174,6 +1177,7 @@
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_TITLE">Inform online publication</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT">Select below the users and/or groups of the site that you want to inform the publication.&lt;br/&gt;</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_2">&lt;em&gt;The selected users will receive an email inviting them to visit the published page.&lt;/em&gt;</message>
+    <message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_3">You can add a message to the recipients here:</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_USERS">Users</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_GROUPS">Groups</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_BTN_OK">Send</message>
@@ -1181,6 +1185,8 @@
 	
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_SUBJECT">New publication on site {siteTitle}</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_BODY">{user},&#13;&#13;{sender} inform you that the page "{pageTitle}" has been published or updated on the site {siteTitle}.&#13;We invite you to read this new publication by following the link below: {pageUrl}.&#13;&#13;See you on the website {siteUrl}</message>
+
+    <message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_REFRESH_ERROR">An error occurred while refreshing the information on content's publication button.</message>
 	
     <message key="PLUGINS_WEB_CONTENT_CREATECONTENTMENU_ERROR_LANGUAGES">An error occurred: unable to collect the languages for this website</message>
     
Index: main/plugin-web/i18n/messages_fr.xml
===================================================================
--- main/plugin-web/i18n/messages_fr.xml	(revision 32549)
+++ main/plugin-web/i18n/messages_fr.xml	(working copy)
@@ -1175,6 +1178,7 @@
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_TITLE">Informer de la mise en ligne</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT">Sélectionnez ci-après les utilisateurs et/ou les groupes d'utilisateurs du site que vous souhaitez informer de la mise en ligne.&lt;br/&gt;</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_2">&lt;em&gt;Les personnes choisies recevront un email les invitant à visiter la page publiée.&lt;/em&gt;</message>
+	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_3">Vous pouvez ajouter un message à l'attention des destinataires :</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_USERS">Utilisateurs</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_GROUPS">Groupes</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_BTN_OK">Envoyer</message>
@@ -1183,6 +1187,8 @@
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_SUBJECT">Nouvelle publication sur le site {siteTitle}</message>
 	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_BODY">{user},&#13;&#13;{sender} vous informe que la page "{pageTitle}" a été publiée ou mise à jour sur le site {siteTitle}.&#13;Nous vous invitons à prendre connaissance de cette nouvelle publication en suivant le lien ci-après: {pageUrl}.&#13;&#13;A bientôt sur le site {siteUrl}.</message>
 	
+	<message key="PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_REFRESH_ERROR">Une erreur s'est produit durant le raffraîchissement du bouton d'information sur la mise en ligne.</message>
+	
 	<message key="PLUGINS_WEB_CONTENT_CREATECONTENTMENU_ERROR_LANGUAGES">Une erreur est survenue : impossible de récupérer la liste des langues disponibles pour ce site.</message>
     
 	<message key="PLUGINS_WEB_CONTENT_ADDCONTENTMENU_LABEL">Ajouter un contenu</message>
 	<!--  +
 		  | WIDGET
Index: main/plugin-web/plugin.xml
===================================================================
--- main/plugin-web/plugin.xml	(revision 32549)
+++ main/plugin-web/plugin.xml	(working copy)
@@ -3966,105 +3980,115 @@
                 </right>
             </extension>
             
-    		<!-- <extension id="org.ametys.web.page.notify"
+    		<extension id="org.ametys.web.page.Notify"
     				   point="org.ametys.core.ui.RibbonControlsManager"
     				   class="org.ametys.web.clientsideelement.LiveClientSideElement">
     				   
-				<action class="org.ametys.ribbon.control.button.LivePageButton">
-					<param name="action">org.ametys.web.page.NotifyPublicationAction</param>
+				<class name="Ametys.plugins.web.page.controller.LivePageController">
+					<action>Ametys.plugins.web.page.NotifyPublication.notify</action>
+					<selection-target-type>^page$</selection-target-type>
+					<selection-target-parameter>
+                        <name>isModifiable</name>
+                        <value>true</value>
+                    </selection-target-parameter>
 					
-					<param name="label" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_LABEL</param>
-					<param name="default-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_NOPAGE</param>
-					<param name="refresh-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_REFRESHING</param>
-					<param name="icon-small" file="true">img/page/notify_16.png</param>
-					<param name="icon-medium" file="true">img/page/notify_32.png</param>
-					<param name="icon-large" file="true">img/page/notify_48.png</param>
+					<label i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_LABEL</label>
+					<default-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_NOPAGE</default-description>
+					<refresh-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_REFRESHING</refresh-description>
+					<icon-small file="true">img/page/notify_16.png</icon-small>
+					<icon-medium file="true">img/page/notify_32.png</icon-medium>
+					<icon-large file="true">img/page/notify_48.png</icon-large>
 					
-					<param name="multiselection-disabled-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_TOOMANYPAGE</param>
+					<selection-description-multiselectionforbidden i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_TOOMANYPAGE</selection-description-multiselectionforbidden>
 					
-					<param name="invalid-page-start-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_START</param>
-					<param name="invalid-page-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_DESCRIPTION</param>
-					<param name="invalid-page-end-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_END</param>
+					<invalid-page-start-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_START</invalid-page-start-description>
+					<invalid-page-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_DESCRIPTION</invalid-page-description>
+					<invalid-page-end-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_PAGE_END</invalid-page-end-description>
 					
-					<param name="invalid-hierarchy-start-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_START</param>
-					<param name="invalid-hierarchy-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_DESCRIPTION</param>
-					<param name="invalid-hierarchy-end-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_END</param>
+					<invalid-hierarchy-start-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_START</invalid-hierarchy-start-description>
+					<invalid-hierarchy-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_DESCRIPTION</invalid-hierarchy-description>
+					<invalid-hierarchy-end-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_INVALID_HIERARCHY_END</invalid-hierarchy-end-description>
 					
-					<param name="allright-start-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_START</param>
-					<param name="allright-end-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_END</param>
-					<param name="allright-page-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_PAGE</param>
+					<allright-start-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_START</allright-start-description>
+					<allright-end-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_END</allright-end-description>
+					<allright-page-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_PAGE</allright-page-description>
 					
-					<param name="error-description" i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_ERROR</param>
-				</action>    
+					<error-description i18n="true">PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DESCRIPTION_ERROR</error-description>
+				</class>    
 				<scripts>
-					<file>js/org/ametys/ribbon/control/button/LivePageButton.js</file>
-					<file>js/org/ametys/web/page/PreviewLive.js</file>
-					<file>js/org/ametys/web/page/NotifyPublicationBox.js</file>
+					<file>js/Ametys/plugins/web/page/controller/LivePageController.js</file>
+					<file>js/Ametys/plugins/web/page/NotifyPublication.js</file>
 				</scripts>
 				<right>Web_Rights_Page_NotifyOnlinePub</right>
-    		</extension> -->
+    		</extension>
     	</extensions>
     </feature>
-    
+
     <feature name="scheduled-publication">
     	<extensions>
-    		<!-- <extension id="org.ametys.web.publication.schedule"
+    		<extension id="org.ametys.web.publication.schedule"
 					   point="org.ametys.core.ui.RibbonControlsManager" 
 					   class="org.ametys.web.clientsideelement.ScheduledPageClientSideElement">
-				<action class="org.ametys.ribbon.control.button.ScheduleActionButton">
-					<param name="action">org.ametys.web.page.SchedulePublication</param>
+				<class name="Ametys.plugins.web.page.controller.ScheduledPublicationController">
+					<action>Ametys.plugins.web.page.SchedulePublication.schedulePublication</action>
+					<selection-target-type>^page$</selection-target-type>
+                    <selection-target-parameter>
+                        <name>isModifiable</name>
+                        <value>true</value>
+                    </selection-target-parameter>
 					
-					<param name="label" i18n="true">PAGE_SCHEDULE_PUBLICATION_LABEL</param>
-					<param name="default-description" i18n="true">PAGE_SCHEDULE_PUBLICATION_DESCRIPTION</param>
-					<param name="refresh-description" i18n="true">PAGE_SCHEDULE_PUBLICATION_REFRESHING</param>
-					<param name="multiselection-disabled-description" i18n="true">PAGE_SCHEDULE_PUBLICATION_TOOMANYPAGE</param>
-					<param name="no-modifiable-description" i18n="true">PLUGINS_WEB_UITOOL_PAGE_NOMODIFIABLE_DESC</param>
-					<param name="error-description" i18n="true">PAGE_SCHEDULE_PUBLICATION_ERROR</param>
-					<param name="norights-description" i18n="true">PAGE_SCHEDULE_PUBLICATION_NORIGHTS</param>
+					<toggle-enabled>true</toggle-enabled>
 					
-					<param name="icon-small" file="true">img/publication/publication_16.png</param>
-					<param name="icon-medium" file="true">img/publication/publication_32.png</param>
-					<param name="icon-large" file="true">img/publication/publication_48.png</param>
+					<label i18n="true">PAGE_SCHEDULE_PUBLICATION_LABEL</label>
+					<default-description i18n="true">PAGE_SCHEDULE_PUBLICATION_DESCRIPTION</default-description>
+					<refresh-description i18n="true">PAGE_SCHEDULE_PUBLICATION_REFRESHING</refresh-description>
+					<multiselection-disabled-description i18n="true">PAGE_SCHEDULE_PUBLICATION_TOOMANYPAGE</multiselection-disabled-description>
+					<nomodifiable-page-description i18n="true">PLUGINS_WEB_UITOOL_PAGE_NOMODIFIABLE_DESC</nomodifiable-page-description>
+					<error-description i18n="true">PAGE_SCHEDULE_PUBLICATION_ERROR</error-description >
+					<noright-page-description i18n="true">PAGE_SCHEDULE_PUBLICATION_NORIGHTS</noright-page-description>
 					
-					<param name="on-icon-small" file="true">img/publication/publication_on_16.png</param>
-					<param name="on-icon-medium" file="true">img/publication/publication_on_32.png</param>
-					<param name="on-icon-large" file="true">img/publication/publication_on_48.png</param>
+					<icon-small file="true">img/publication/publication_16.png</icon-small>
+					<icon-medium file="true">img/publication/publication_32.png</icon-medium>
+					<icon-large file="true">img/publication/publication_48.png</icon-large>
 					
-					<param name="forthcoming-icon-small" file="true">img/publication/publication_fc_16.png</param>
-					<param name="forthcoming-icon-medium" file="true">img/publication/publication_fc_32.png</param>
-					<param name="forthcoming-icon-large" file="true">img/publication/publication_fc_48.png</param>
+					<on-icon-small file="true">img/publication/publication_on_16.png</on-icon-small>
+					<on-icon-medium file="true">img/publication/publication_on_32.png</on-icon-medium>
+					<on-icon-large file="true">img/publication/publication_on_48.png</on-icon-large>
 					
-					<param name="outofdate-icon-small" file="true">img/publication/publication_ood_16.png</param>
-					<param name="outofdate-icon-medium" file="true">img/publication/publication_ood_32.png</param>
-					<param name="outofdate-icon-large" file="true">img/publication/publication_ood_48.png</param>
+					<forthcoming-icon-small file="true">img/publication/publication_fc_16.png</forthcoming-icon-small>
+					<forthcoming-icon-medium file="true">img/publication/publication_fc_32.png</forthcoming-icon-medium>
+					<forthcoming-icon-large file="true">img/publication/publication_fc_48.png</forthcoming-icon-large>
 					
-					<param name="scheduled-page-valid-start-description" i18n="true">PAGE_SCHEDULED_DESCRIPTION_START</param>
-					<param name="scheduled-page-valid-description" i18n="true">PAGE_SCHEDULED_DESCRIPTION</param>
-					<param name="scheduled-page-valid-end-description" i18n="true">PAGE_SCHEDULED_DESCRIPTION_END</param>
+					<outofdate-icon-small file="true">img/publication/publication_ood_16.png</outofdate-icon-small>
+					<outofdate-icon-medium file="true">img/publication/publication_ood_32.png</outofdate-icon-medium>
+					<outofdate-icon-large file="true">img/publication/publication_ood_48.png</outofdate-icon-large>
 					
-					<param name="scheduled-page-outofdate-start-description" i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION_START</param>
-					<param name="scheduled-page-outofdate-description" i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION</param>
-					<param name="scheduled-page-outofdate-end-description" i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION_END</param>
+					<scheduled-page-valid-start-description i18n="true">PAGE_SCHEDULED_DESCRIPTION_START</scheduled-page-valid-start-description>
+					<scheduled-page-valid-description i18n="true">PAGE_SCHEDULED_DESCRIPTION</scheduled-page-valid-description>
+					<scheduled-page-valid-end-description i18n="true">PAGE_SCHEDULED_DESCRIPTION_END</scheduled-page-valid-end-description>
 					
-					<param name="scheduled-page-forthcoming-start-description" i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION_START</param>
-					<param name="scheduled-page-forthcoming-description" i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION</param>
-					<param name="scheduled-page-forthcoming-end-description" i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION_END</param>
+					<scheduled-page-outofdate-start-description i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION_START</scheduled-page-outofdate-start-description>
+					<scheduled-page-outofdate-description i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION</scheduled-page-outofdate-description>
+					<scheduled-page-outofdate-end-description i18n="true">PAGE_SCHEDULED_OUTOFDATE_DESCRIPTION_END</scheduled-page-outofdate-end-description>
 					
-					<param name="scheduled-page-startdate-start-description" i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION_START</param>
-					<param name="scheduled-page-startdate-description" i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION</param>
-					<param name="scheduled-page-startdate-end-description" i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION_END</param>
+					<scheduled-page-forthcoming-start-description i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION_START</scheduled-page-forthcoming-start-description>
+					<scheduled-page-forthcoming-description i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION</scheduled-page-forthcoming-description>
+					<scheduled-page-forthcoming-end-description i18n="true">PAGE_SCHEDULED_FORTHCOMING_DESCRIPTION_END</scheduled-page-forthcoming-end-description>
 					
-					<param name="scheduled-page-enddate-start-description" i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION_START</param>
-					<param name="scheduled-page-enddate-description" i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION</param>
-					<param name="scheduled-page-enddate-end-description" i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION_END</param>
-				</action>
+					<scheduled-page-startdate-start-description i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION_START</scheduled-page-startdate-start-description>
+					<scheduled-page-startdate-description i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION</scheduled-page-startdate-description>
+					<scheduled-page-startdate-end-description i18n="true">PAGE_SCHEDULED_STARTDATE_DESCRIPTION_END</scheduled-page-startdate-end-description>
+					
+					<scheduled-page-enddate-start-description i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION_START</scheduled-page-enddate-start-description>
+					<scheduled-page-enddate-description i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION</scheduled-page-enddate-description>
+					<scheduled-page-enddate-end-description i18n="true">PAGE_SCHEDULED_ENDDATE_DESCRIPTION_END</scheduled-page-enddate-end-description>
+				</class>
 				<scripts>
-					<file>js/org/ametys/ribbon/control/button/ScheduleActionButton.js</file>
-					<file>js/org/ametys/web/page/SchedulePublication.js</file>
+					<file>js/Ametys/plugins/web/page/controller/ScheduledPublicationController.js</file>
+					<file>js/Ametys/plugins/web/page/SchedulePublication.js</file>
 				</scripts>
 				<right>Web_Rights_Page_Schedule</right>
-			</extension> -->
-    	
+			</extension>
     	</extensions>
     </feature>
 		
@@ -9857,4 +9881,45 @@
             </extension>
         </extensions>
     </feature>
+    <feature name="widgets.others.fogroup">
+        <extensions>
+            <extension id="edition.fogroup"
+                point="org.ametys.core.ui.widgets.WidgetsManager"
+                class="org.ametys.core.ui.widgets.StaticClientSideWidget">
+                <class name="Ametys.web.form.widget.FOGroup">
+                    <ftypes>string</ftypes>
+                    <supports-enumerated>false</supports-enumerated>
+                    <supports-non-enumerated>true</supports-non-enumerated>
+                    <supports-multiple>true</supports-multiple>
+                    <supports-non-multiple>true</supports-non-multiple>
+                </class>
+                <scripts>
+                    <file plugin="core-ui">js/Ametys/form/widget/AbstractQueryableComboBox.js</file>
+                    <file plugin="core-ui">js/Ametys/form/widget/Group.js</file>
+                    <file>js/Ametys/web/form/widget/FOGroup.js</file>
+                </scripts>                
+            </extension>
+        </extensions>
+    </feature>    
+    <feature name="widgets.others.fouser">
+        <extensions>
+            <extension id="edition.fouser"
+                point="org.ametys.core.ui.widgets.WidgetsManager"
+                class="org.ametys.core.ui.widgets.StaticClientSideWidget">
+                <class name="Ametys.web.form.widget.FOUser">
+                    <ftypes>user</ftypes>
+                    <supports-enumerated>false</supports-enumerated>
+                    <supports-non-enumerated>true</supports-non-enumerated>
+                    <supports-multiple>true</supports-multiple>
+                    <supports-non-multiple>true</supports-non-multiple>
+                </class>
+                <scripts>
+                    <file plugin="core-ui">js/Ametys/form/widget/AbstractQueryableComboBox.js</file>
+                    <file plugin="core-ui">js/Ametys/form/widget/User.js</file>
+                    <file>js/Ametys/web/form/widget/FOUser.js</file>
+                </scripts>                
+            </extension>
+        </extensions>
+    </feature>
+    
 </plugin>
Index: main/plugin-web/resources/js/Ametys/plugins/web/page/NotifyPublication.js
===================================================================
--- main/plugin-web/resources/js/Ametys/plugins/web/page/NotifyPublication.js	(revision 0)
+++ main/plugin-web/resources/js/Ametys/plugins/web/page/NotifyPublication.js	(revision 0)
@@ -0,0 +1,192 @@
+/*
+ *  Copyright 2015 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.define(
+	'Ametys.plugins.web.page.NotifyPublication', {
+
+		singleton:true,
+		
+		/**
+		 * @private
+		 * @property {Ametys.window.DialogBox} _box The dialog box
+		 */
+		
+		/**
+		 * @private
+		 * @property {Ext.form.Panel} _form the form panel for users and groups 
+		 */
+		
+		/**
+		 * Notify the publication of a page
+		 * @param {Ametys.ribbon.element.ui.ButtonController} controller the controller calling this function
+		 */
+		notify: function(controller)
+		{
+			var targets = controller.getMatchingTargets();
+			if (targets.length > 0)
+			{
+				this._delayedInitialize(controller);
+				
+				this._box.show();
+				
+				this._form.getForm().findField("users").setValue('');
+				this._form.getForm().findField("groups").setValue('');
+				
+				this._form.getForm().findField("comment").setValue('');
+			}
+		},
+		
+		/**
+		 * Initialize the notification dialog box
+		 * @param {Ametys.ribbon.element.ui.ButtonController} controller the button's controller
+		 */
+		_delayedInitialize: function(controller)
+		{
+			if (!this._box)
+			{
+				this._form = Ext.create('Ext.form.Panel', {
+					
+					bodyStyle: 'padding:5px',
+					border: false,
+					labelWidth: 120,
+					autoScroll: true,
+					
+					defaults: {
+						cls: 'ametys',
+						labelWidth: 80,
+						labelSeparator: '',
+						msgTarget: 'side',
+						multiple: true
+					},
+					
+					items: [{
+								xtype: 'component',
+								cls: 'hint',
+								html: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT'/>",
+								style: {
+									 marginBottom: '10px',
+								}
+							},
+					        {
+								xtype: 'edition.fouser',
+								name: 'users',
+								fieldLabel: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_USERS'/>"
+							},
+							{
+								xtype: 'edition.fogroup',
+								name: 'groups',
+								fieldLabel: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_GROUPS'/>"
+							},
+							{
+								xtype: 'component',
+								cls: 'hint',
+								html: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_2'/>",
+								style: {
+									 marginTop: '10px',
+								}
+							},
+							{
+								xtype: 'component',
+								cls: 'hint',
+								html: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_HINT_3'/>",
+								style: {
+									 marginTop: '10px',
+									 marginBottom: '10px'
+								}
+							},
+							{
+								 xtype : 'edition.textarea',
+								 name: 'comment',
+								 hideLabel: true,
+								 maxLength: 200,
+								 width: 325,
+								 height: 120,
+								 listeners: {
+									 'afterrender': function() {
+										 this.el.dom.style.marginLeft = "7px"
+									 }
+								 }
+							}
+					]
+				});
+		
+				this._box = new Ametys.window.DialogBox ({
+					title: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_TITLE'/>",
+					icon: Ametys.getPluginResourcesPrefix("web") + "/img/page/notify_16.png",
+			
+					layout: 'fit',
+					width: 400,
+					height: 340,
+					
+					items: [this._form],
+						
+					closeAction: 'hide',
+					buttons : [ 
+					    {
+							text : "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_BTN_OK'/>",
+							handler: Ext.bind(this._sendNotification, this, [controller], false)
+					    }, 
+					    {
+							text : "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_DIALOG_BTN_CANCEL'/>",
+							handler: Ext.bind(this._hideBox, this) 
+					    }
+					]
+				});
+			}
+		},
+		
+		/**
+		 * Actual notification sending process through a server call
+		 * @param {Ametys.ribbon.element.ui.ButtonController} controller the button's controller
+		 */
+		_sendNotification: function (controller)
+		{
+			var targets = controller.getMatchingTargets();
+			if (targets.length > 0)
+			{
+				var target = targets[0];
+				
+				var form = this._form.getForm();
+				if (form.isValid())
+				{
+					var users = form.findField('users').getValue();
+					var groups = form.findField('groups').getValue();
+					var comment = form.findField('comment').getValue();
+	
+					controller.serverCall('sendOnlineNotification',
+							[target.getParameters().id, !Ext.isEmpty(users) ? users : [], !Ext.isEmpty(groups) ? groups : [], comment], 
+							Ext.bind(this._hideBox, this),
+							{ 
+								errorMessage: { 
+									msg: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_ERROR'/>", 
+									category: this.self.getName() 
+								}
+							},
+							true
+					);
+				}
+			}
+		},
+
+		/**
+		 * @private
+		 * Handler invoked when the box's 'Cancel' button is clicked. Hides the box.
+		 */
+		_hideBox: function ()
+		{
+			this._box.hide();
+		}
+	}
+);
Index: main/plugin-web/resources/js/Ametys/plugins/web/page/controller/LivePageController.js
===================================================================
--- main/plugin-web/resources/js/Ametys/plugins/web/page/controller/LivePageController.js	(revision 0)
+++ main/plugin-web/resources/js/Ametys/plugins/web/page/controller/LivePageController.js	(revision 0)
@@ -0,0 +1,126 @@
+/*
+ *  Copyright 2015 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.
+ */
+
+/**
+ * This controller handles the notification when a page is published, the live preview and the preview
+ */
+Ext.define(
+	'Ametys.plugins.web.page.controller.LivePageController', {
+		extend: 'Ametys.ribbon.element.ui.ButtonController',
+		
+		constructor: function(config)
+		{
+			this.callParent(arguments);
+			
+			// FIXME en v3 on faisait aussi un refresh sur les messages : "zoningChanged" et "typeChanged" 
+			// Quel est leur équivalent v4 ?
+			Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onModified, this); 
+			Ametys.message.MessageBus.on(Ametys.message.Message.WORKFLOW_CHANGED, this._onModified, this);
+		},
+
+		/**
+		 * @private
+		 * Listener for modified messages
+		 * Will update the lock state of the buttons effectively upon the current selection.
+		 * @param {Ametys.message.Message} message modified message.
+		 */
+		_onModified: function (message)
+		{
+			this.refresh();
+		},
+		
+		
+		updateState: function()
+		{
+			this._getStatus(this.getMatchingTargets());
+		},
+
+		/**
+		 * @private
+		 * Get the status
+		 * @param targets The page targets
+		 */
+		_getStatus: function(targets)
+		{
+			if (targets.length > 0)
+			{
+				var pageIds = [];
+				Ext.Array.each(targets, function(target) {
+					pageIds.push(target.getParameters().id);
+				});
+
+				this.serverCall('getStatus',
+						[pageIds], 
+						Ext.bind(this._getStatusCb, this),
+						{ 
+							errorMessage: { 
+								msg: "<i18n:text i18n:key='PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_REFRESH_ERROR'/>",
+								category: this.self.getName() 
+							}
+						},
+						true
+				);
+			}
+		},
+		
+		/**
+		 * Callback for the button reloading process
+		 * @param {Object} response the server's response
+		 * @param {Object} response['allright-pages'] the pages with no problem
+		 * @param {Object} response['invalid-hierarchy'] the pages having an invalid hierarchy
+		 * @param {Object} response['invalid-pages'] the invalid pages
+		 */
+		_getStatusCb: function(response)
+		{
+			var description = this._handlingMultiple(this.getInitialConfig('default-description'), "allright", response['allright-pages']);
+			description = this._handlingMultiple(description, "invalid-page", response['invalid-hierarchy']);
+			description = this._handlingMultiple(description, "invalid-hierarchy", response['invalid-pages']);
+			
+			this.setDescription(description);
+			this.setDisabled(response["allright-pages"].length == 0);
+		},
+		
+		/**
+		 * Generates the tooltip to set 
+		 * @param {String} description the description where to add text in html
+		 * @param {String} paramPrefix the parameter prefix
+		 * @param {Object} pages the pages 
+		 */
+		_handlingMultiple: function(description, paramPrefix, pages)
+		{
+			if (pages.length > 0)
+			{
+				if (description != "")
+				{
+					description += "<br/><br/>";
+				}
+				
+				description += this.getInitialConfig(paramPrefix + "-start-description");
+				for (var i=0; i < pages.length; i++)
+				{
+					if (i != 0) 
+					{
+						description += ", ";
+					}
+					description += pages[i].description;
+				}
+				description += this.getInitialConfig()[paramPrefix + "-end-description"];
+			}
+			
+			return description;
+		}
+	}
+);
Index: main/plugin-web/resources/js/Ametys/web/form/widget/FOGroup.js
===================================================================
--- main/plugin-web/resources/js/Ametys/web/form/widget/FOGroup.js	(revision 0)
+++ main/plugin-web/resources/js/Ametys/web/form/widget/FOGroup.js	(revision 0)
@@ -0,0 +1,27 @@
+/*
+ *  Copyright 2015 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.
+ */
+
+/**
+ * This class provides a widget to select groups from the FO.
+ */
+Ext.define(
+		'Ametys.web.form.widget.FOGroup', {
+			extend: 'Ametys.form.widget.Group',
+			
+			xtype: 'edition.fogroup',
+			groupsManagerRole: 'org.ametys.core.group.GroupsManager.FO'
+	}
+);
\ No newline at end of file
Index: main/plugin-web/resources/js/Ametys/web/form/widget/FOUser.js
===================================================================
--- main/plugin-web/resources/js/Ametys/web/form/widget/FOUser.js	(revision 0)
+++ main/plugin-web/resources/js/Ametys/web/form/widget/FOUser.js	(revision 0)
@@ -0,0 +1,27 @@
+/*
+ *  Copyright 2015 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.
+ */
+
+/**
+ * This class provides a widget to select users from the FO.
+ */
+Ext.define(
+		'Ametys.web.form.widget.FOUser', {
+			extend: 'Ametys.form.widget.User',
+			
+			xtype: 'edition.fouser',
+			usersManagerRole: 'org.ametys.core.user.UsersManager.FO'
+	}
+);
Index: main/plugin-web/sitemap-back.xmap
===================================================================
--- main/plugin-web/sitemap-back.xmap	(revision 32549)
+++ main/plugin-web/sitemap-back.xmap	(working copy)
 		  	@@ -127,7 +123,6 @@
 		  	<map:action name="service-configure" src="org.ametys.web.repository.page.actions.ConfigureServiceAction"/>
 		  	<map:action name="add-shared-content" src="org.ametys.web.repository.page.actions.AddSharedContentAction"/>
 		  	<map:action name="set-content-view" src="org.ametys.web.repository.page.actions.SetContentViewAction"/>
-		  	<map:action name="send-notification" src="org.ametys.web.repository.page.actions.SendOnlineNotificationAction"/>
 		  	
 		  	<map:action name="add-tag" src="org.ametys.web.tags.actions.AddTagAction"/>
 		  	<map:action name="update-tag" src="org.ametys.web.tags.actions.UpdateTagAction"/>

@@ -803,13 +771,6 @@
 				<map:serialize type="xml"/>
 			</map:match>
 			
-			<map:match pattern="page/send-notification">
-				<map:act type="send-notification"> 
-					<map:generate type="action-result"/>
-					<map:serialize type="xml"/>
-				</map:act>
-			</map:match>
-		
     		<!-- + 
     		     | ZONES / SERVICES
     		     + -->
Index: main/plugin-web/src/org/ametys/web/clientsideelement/LiveClientSideElement.java
===================================================================
--- main/plugin-web/src/org/ametys/web/clientsideelement/LiveClientSideElement.java	(revision 32542)
+++ main/plugin-web/src/org/ametys/web/clientsideelement/LiveClientSideElement.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2010 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -17,43 +17,73 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
+import javax.mail.MessagingException;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.commons.lang.StringUtils;
 
+import org.ametys.core.group.Group;
+import org.ametys.core.group.GroupsManager;
 import org.ametys.core.ui.Callable;
+import org.ametys.core.user.User;
+import org.ametys.core.user.UsersManager;
+import org.ametys.core.util.I18nUtils;
 import org.ametys.core.util.I18nizableText;
-import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.core.util.mail.SendMailHelper;
+import org.ametys.runtime.config.Config;
 import org.ametys.web.WebConstants;
 import org.ametys.web.repository.page.Page;
+import org.ametys.web.repository.site.Site;
+import org.ametys.web.site.SiteConfigurationExtensionPoint;
 import org.ametys.web.skin.Skin;
 import org.ametys.web.skin.SkinsManager;
-import org.ametys.web.synchronization.SynchronizeComponent;
 
 /**
  * Page live UI item
  */
 public class LiveClientSideElement extends AbstractPageClientSideElement
 {
+    /** The {@link UsersManager} */
+    private UsersManager _userManager;
+    
+    /** The {@link UsersManager} for front-end users */
+    private UsersManager _foUsersManager;
+    
+    /** The {@link GroupsManager} for front-end groups */
+    private GroupsManager _foGroupsManager;
+    
+    /** The {@link SiteConfigurationExtensionPoint} */
+    private SiteConfigurationExtensionPoint _siteConfiguration;
+    
+    /** The {@link I18nUtils} */
+    private I18nUtils _i18nUtils;
+
     /** The {@link SkinsManager}*/
-    protected SkinsManager _skinManager;
+    private SkinsManager _skinManager;
+    
     /** The repository */
-    protected Repository _repository;
+    private Repository _repository;
     
     @Override
     public void service(ServiceManager smanager) throws ServiceException
     {
         super.service(smanager);
-        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
+        _userManager = (UsersManager) smanager.lookup(UsersManager.ROLE);
+        _foUsersManager = (UsersManager) smanager.lookup(UsersManager.ROLE + ".FO");
+        _foGroupsManager = (GroupsManager) smanager.lookup(GroupsManager.ROLE + ".FO");
+        _siteConfiguration = (SiteConfigurationExtensionPoint) smanager.lookup(SiteConfigurationExtensionPoint.ROLE);
+        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
         _skinManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
         _repository = (Repository) smanager.lookup(Repository.class.getName());
-        _synchronizeComponent = (SynchronizeComponent) smanager.lookup(SynchronizeComponent.ROLE);
     }
     
     /**
@@ -61,6 +91,7 @@
      * @param pageIds The page id
      * @return the result
      */
+    @SuppressWarnings("unchecked")
     @Callable
     public Map<String, Object> getStatus (List<String> pageIds)
     {
@@ -136,11 +167,50 @@
     }
     
     /**
+     * Send a notification on a content
+     * @param pageId the id of the page
+     * @param logins the logins of the users that will be notified
+     * @param groupIds the ids of the groups that will be notified
+     * @param comment the comment of the notifier 
+     * @return an empty map
+     */
+    @Callable
+    public Map sendOnlineNotification(String pageId, List<String> logins, List<String> groupIds, String comment)
+    { 
+        Page page = _resolver.resolveById(pageId);
+        
+        String login = _currentUserProvider.getUser();
+        User notifier = _userManager.getUser(login);
+        
+        String from = _getSender(notifier, page);
+        String subject = _getSubject (notifier, page);
+
+        Set<User> users = _getDistinctUsers (logins, groupIds);
+        for (User user : users)
+        {
+            if (user.getEmail() != null)
+            {
+                try
+                {
+                    String body = _getBody(notifier, user, page, comment);
+                    SendMailHelper.sendMail(subject, null, body, user.getEmail(), from);
+                }
+                catch (MessagingException e) 
+                {
+                    getLogger().error("Unable to send mail to user " + user.getEmail(), e);
+                }
+            }
+        }
+        
+        return java.util.Collections.EMPTY_MAP;
+    }
+    
+    /**
      * Get i18n description for all right page
      * @param page The page
      * @return The {@link I18nizableText} description
      */
-    protected I18nizableText _getAllRightPageDescription (Page page)
+    private I18nizableText _getAllRightPageDescription (Page page)
     {
         List<String> i18nParameters = new ArrayList<>();
         i18nParameters.add(page.getTitle());
@@ -154,7 +224,7 @@
      * @param page The page
      * @return The {@link I18nizableText} description
      */
-    protected I18nizableText _getInvalidPageDescription (Page page)
+    private I18nizableText _getInvalidPageDescription (Page page)
     {
         List<String> i18nParameters = new ArrayList<>();
         i18nParameters.add(page.getTitle());
@@ -168,7 +238,7 @@
      * @param page The page
      * @return The {@link I18nizableText} description
      */
-    protected I18nizableText _getInvalidHierarchyDescription (Page page)
+    private I18nizableText _getInvalidHierarchyDescription (Page page)
     {
         List<String> i18nParameters = new ArrayList<>();
         i18nParameters.add(page.getTitle());
@@ -177,4 +247,117 @@
         return  new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters);
     }
     
+    /**
+     * Get the email sender
+     * @param sender The user responsible for the action
+     * @param page The published page
+     * @return The sender
+     */
+    private String _getSender (User sender, Page page)
+    {
+        if (sender != null && sender.getEmail() != null)
+        {
+            return sender.getFullName() + " <" + sender.getEmail() + ">";
+        }
+        
+        String from = _siteConfiguration.getValueAsString(page.getSiteName(), "site-mail-from");
+        if (StringUtils.isBlank(from))
+        {
+            from = Config.getInstance().getValueAsString("smtp.mail.from");
+        }
+        return from;
+    }
+    
+    /**
+     * Get the email subject
+     * @param sender The user responsible for the notification
+     * @param page The published page
+     * @return The email subject
+     */
+    private String _getSubject (User sender, Page page)
+    {
+        Site site = page.getSite();
+        
+        Map<String, I18nizableText> subjectI18nParameters = new HashMap<String, I18nizableText>();
+        subjectI18nParameters.put("user", new I18nizableText(sender.getFullName()));
+        subjectI18nParameters.put("siteTitle", new I18nizableText(site.getTitle()));
+        subjectI18nParameters.put("pageTitle", new I18nizableText(page.getTitle()));
+        
+        I18nizableText msg = new I18nizableText("plugin.web", "PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_SUBJECT", subjectI18nParameters);
+        
+        return _i18nUtils.translate(msg, page.getSitemapName());
+    }
+    
+    /**
+     * Get the email body
+     * @param sender The user responsible for the notification
+     * @param user The recipient of the notification
+     * @param page The published page
+     * @param comment 
+     * @return The email body
+     */
+    private String _getBody (User sender, User user, Page page, String comment)
+    {
+        Site site = page.getSite();
+        
+        Map<String, I18nizableText> bodyI18nParameters = new HashMap<String, I18nizableText>();
+        bodyI18nParameters.put("sender", new I18nizableText(sender.getFullName()));
+        bodyI18nParameters.put("user", new I18nizableText(user.getFullName()));
+        bodyI18nParameters.put("siteTitle", new I18nizableText(site.getTitle()));
+        bodyI18nParameters.put("siteUrl", new I18nizableText(site.getUrl()));
+        bodyI18nParameters.put("pageTitle", new I18nizableText(page.getTitle()));
+        bodyI18nParameters.put("pageUrl", new I18nizableText(site.getUrl() + "/" + page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html"));
+
+        I18nizableText msg = null;
+        if (StringUtils.isNotEmpty(comment))
+        {
+            bodyI18nParameters.put("comment", new I18nizableText(comment));
+            msg = new I18nizableText("plugin.web", "PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_BODY_WITH_COMMENT", bodyI18nParameters);
+        }
+        else
+        {
+            msg = new I18nizableText("plugin.web", "PLUGINS_WEB_PAGE_NOTIFY_ONLINE_PUBLICATION_MAIL_BODY", bodyI18nParameters);
+        }
+        
+        return _i18nUtils.translate(msg, page.getSitemapName());
+    }
+    
+    private Set<User> _getDistinctUsers (List<String> logins, List<String> groupIds)
+    {
+        Set<User> users = new HashSet<User>();
+        
+        for (String login : logins)
+        {
+            if (StringUtils.isNotEmpty(login))
+            {
+                User user = _foUsersManager.getUser(login);
+                if (user != null)
+                {
+                    users.add(user);
+                }
+            }
+        }
+        
+        for (String groupId : groupIds)
+        {
+            if (StringUtils.isNotEmpty(groupId))
+            {
+                Group group = _foGroupsManager.getGroup(groupId);
+                if (group != null)
+                {
+                    Set<String> userLogins = group.getUsers();
+                    for (String userLogin : userLogins)
+                    {
+                        User user = _foUsersManager.getUser(userLogin);
+                        if (user != null)
+                        {
+                            users.add(user);
+                        }
+                    }
+                }
+            }
+        }
+        
+        return users;
+    }
 }
