Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all 525 articles
Browse latest View live

AEM CQ 56 - Dynamic chain select widget with combo boxes

$
0
0

Goal


Create a widget to chain combo boxes and populate combo box data dynamically. Here the lower level combo boxes data change based on values selected in the higher levels. Source code, Package Install and Demo video are available for download. Please leave a comment if you find bugs



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder (nt:folder) /apps/chainselect

2) Create folder (nt:folder) /apps/chainselect/install and deploy servlet GetDropDownData as OSGI component to return the data for combo boxes

package apps.mysample.chainselect;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@SlingServlet(
paths="/bin/mycomponents/chainselect/dropdowndata",
methods = "GET",
metatype = true,
label = "Dropdown Data Servlet"
)
public class GetDropDownData extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(GetDropDownData.class);

private static Map<String, String> LEVEL_1 = new HashMap<String, String>();
private static Map<String, Map<String, String>> LEVEL_2 = new HashMap<String, Map<String, String>>();
private static Map<String, Map<String, String>> LEVEL_3 = new HashMap<String, Map<String, String>>();

static{
fillStaticData();
}

private static void fillStaticData(){
LEVEL_1.put("ENTERTAINMENT", "Entertainment");
LEVEL_1.put("HEALTH", "Health");
LEVEL_1.put("PARTY", "Party");

Map<String, String> map = new LinkedHashMap<String, String>();

map.put("MOVIES", "Movies");
map.put("CELEB_NEWS", "Celebrity News");
map.put("TV", "TV");
map.put("MUSIC", "Music");
map.put("STYLE", "Style");

LEVEL_2.put("ENTERTAINMENT", map);

map = new LinkedHashMap<String, String>();

map.put("MENS_HEALTH", "Men's Health");
map.put("WOMENS_HEALTH", "Women's Health");
map.put("CHILD_HEALTH", "Children's Health");
map.put("ALT_MEDICINE", "Alternative Medicine");

LEVEL_2.put("HEALTH", map);

map = new LinkedHashMap<String, String>();

map.put("HOLLYWOOD", "Hollywood");
map.put("BOLLYWOOD", "Bollywood");

LEVEL_3.put("MOVIES", map);

map = new LinkedHashMap<String, String>();

map.put("MJ", "Michael Jackson");
map.put("RAHMAN", "A R Rahman");

LEVEL_3.put("MUSIC", map);
}

private void ouputInitData(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws ServletException{
Integer level = NumberUtils.createInteger(request.getParameter("level"));
String keyword = request.getParameter("keyword");

if(level == null){
level = 1;
}

try{
JSONWriter jw = new JSONWriter(response.getWriter());
Field field = null; Class clazz = this.getClass();
Map<String, String> map = null;

jw.object();

do{
try{
field = clazz.getDeclaredField("LEVEL_" + level);
}catch (NoSuchFieldException nfe){
break;
}

if(level == 1){
map = (Map<String, String>)field.get(null);
}else{
if(StringUtils.isEmpty(keyword)){
keyword = ((Map<String,Map<String, String>>)field.get(null)).keySet().iterator().next();
}

map = ((Map<String,Map<String, String>>)field.get(null)).get(keyword);
}

if(map == null){
break;
}

keyword = null;

jw.key(level.toString()).array();

for(Map.Entry<String, String> entry : map.entrySet()){
jw.array();
jw.value(entry.getKey()).value(entry.getValue());
jw.endArray();

if(StringUtils.isEmpty(keyword)){
keyword = entry.getKey();
}
}

jw.endArray();
level++;
}while(true);

jw.endObject();
}catch(Exception e){
LOG.error("Error getting dropdown data",e);
throw new ServletException(e);
}
}

private void ouputSavedText(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws ServletException{
try {
String[] lStrs = request.getParameter("levels").split(",");
String[] keywords = request.getParameter("keywords").split(",");
JSONWriter jw = new JSONWriter(response.getWriter());

Field field = null; Class clazz = this.getClass();
Map<String, String> map = null; Integer level = null;

jw.object();

for(int i = 0; i < lStrs.length; i++){
level = NumberUtils.createInteger(lStrs[i]);

try{
field = clazz.getDeclaredField("LEVEL_" + level);
}catch (NoSuchFieldException nfe){
continue;
}

if(level == 1){
map = (Map<String, String>)field.get(null);
}else{
map = ((Map<String,Map<String, String>>)field.get(null)).get(keywords[i - 1]);
}

if(map == null){
continue;
}

jw.key(level.toString()).array();

for(Map.Entry<String, String> entry : map.entrySet()){
jw.array();
jw.value(entry.getKey()).value(entry.getValue());
jw.endArray();
}

jw.endArray();
}

jw.endObject();
} catch (Exception e) {
LOG.error("Error getting dropdown data", e);
throw new ServletException(e);
}
}

@Override
protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");

String lStr = request.getParameter("levels");

if(StringUtils.isNotEmpty(lStr)){
ouputSavedText(request, response);
}else{
ouputInitData(request, response);
}
}
}

3) In the above servlet each level (LEVEL_1, LEVEL_2 etc) data structure has necessary data for combo box at that level. So when n-level combo boxes are created, necessary n-level data structures should also exist for serving them. A single ajax call returns the data for all combo boxes

4) Create clientlib (type cq:ClientLibraryFolder) /apps/chainselect/clientlib and set property categories with value cq.widgets

5) Create file (nt:file) /apps/chainselect/clientlib/js.txt, add

                     chainselect.js

6) Create file (nt:file) /apps/chainselect/clientlib/chainselect.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.ChainSelect = CQ.Ext.extend(CQ.Ext.Panel, {
panelValue: '',

constructor: function(config){
config = config || {};

if(!config.levels){
config.levels = "1";
}

config.levels = parseInt(config.levels, 10);

ExperienceAEM.ChainSelect.superclass.constructor.call(this, config);
},

getValue: function () {
var pData = {};

this.items.each(function(i){
if(!i.level || i.xtype !== "combo" || i.disabled){
return;
}

pData[i.level] = i.getValue();
});

return $.isEmptyObject(pData) ? "" : JSON.stringify(pData);
},

setValue: function (value) {
var pData = JSON.parse(value);
var levels = "", keywords = "", x = "", combo;

for(x in pData){
if(pData.hasOwnProperty(x)){
levels = levels + x + ",";
keywords = keywords + pData[x] + ",";
}
}

levels = levels.substring(0, levels.lastIndexOf(","));
keywords = keywords.substring(0, keywords.lastIndexOf(","));

var lData = this.getDropDownData({ levels : levels, keywords: keywords });

for(x in lData){
if(lData.hasOwnProperty(x)){
combo = this.findBy(function(comp){
return comp["level"] == x;
}, this);

combo[0].store.loadData(lData[x], false);
}
}

this.items.each(function(i){
if(!i.level || i.xtype !== "combo"){
return;
}

if(pData[i.level]){
i.setValue(pData[i.level]);
}else{
i.setDisabled(true);
}
});
},

validate: function(){
return true;
},

getName: function(){
return this.name;
},

getDropDownData: function(params){
if(!params){
params = { level : 1, keyword: "" }
}

var lData;

$.ajax({
url: '/bin/mycomponents/chainselect/dropdowndata',
dataType: "json",
type: 'GET',
async: false,
data: params,
success: function(data){
lData = data;
}
});

return lData;
},

initComponent: function () {
ExperienceAEM.ChainSelect.superclass.initComponent.call(this);

var lData = this.getDropDownData();

if(!lData){
CQ.Ext.Msg.alert("Error","Error getting levels data or no data available");
return;
}

for(var x = 1; x <= this.levels; x++){
this.add(new CQ.Ext.form.ComboBox({
store: new CQ.Ext.data.ArrayStore({
fields: ["id", "text"],
data: lData[x]
}),
mode: "local",
triggerAction: "all",
isFormField: false,
level: x,
fieldLabel: "Level " + x,
valueField: 'id',
displayField: 'text',
emptyText: 'Select level ' + x,
style: "margin-bottom:20px",
xtype: 'combo',
listeners:{
scope: this,
select: function(combo){
var keyword = combo.getValue();

var lowCombo = this.findBy(function(comp){
return comp["level"] == (combo.level + 1);
}, this);

if(!lowCombo || (lowCombo.length == 0)){
return;
}

lData = this.getDropDownData({ level : combo.level + 1, keyword: keyword });
var level = combo.level + 1;

do{
lowCombo = this.findBy(function(comp){
return comp["level"] == level;
}, this);

if(!lowCombo || (lowCombo.length == 0)){
break;
}

lowCombo = lowCombo[0];
lowCombo.clearValue();

if(lData[lowCombo.level]){
lowCombo.setDisabled(false);
lowCombo.store.loadData(lData[lowCombo.level], false);
}else{
lowCombo.setDisabled(true);
}

level = lowCombo.level + 1;
}while(true);
}
}
}));
}

this.panelValue = new CQ.Ext.form.Hidden({
name: this.name
});

this.add(this.panelValue);

var dialog = this.findParentByType('dialog');

dialog.on('beforesubmit', function(){
var value = this.getValue();

if(value){
this.panelValue.setValue(value);
}
},this);

this.panelValue.on('loadcontent', function(){
this.setValue(this.panelValue.getValue());
},this);
}
});

CQ.Ext.reg("chainselect", ExperienceAEM.ChainSelect);


7) The component structure in CRXDE Lite



8) Here is a sample dialog xml with chainselect widget configured ( with 3 level combos )

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
helpPath="en/cq/current/wcm/default_components.html#Text"
title="Text"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Widget"
anchor="100%"
title="Text"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<chainselect
jcr:primaryType="cq:Widget"
border="false"
layout="form"
levels="3"
name="./chainselect"
padding="10px"
xtype="chainselect"/>
</items>
</tab1>
</items>
</jcr:root>



AEM CQ 56 - Programmatically open a component dialog

$
0
0

Goal


Open a component dialog programmatically. To open a component dialog you generally do a double click, here a custom button click also opens the same dialog. Check the demo

Solution


1) In your jsp or where ever you want to open component dialog add the below simple js function. Here i added it in a text component jsp, and the button opens it's component dialog

<%@include file="/libs/foundation/global.jsp"%>

<cq:text property="text"/>

<input type=button onClick="openComponentPropertiesDialog('<%= currentNode.getPath() %>')" value="Open Component Dialog"/>

<script>
function openComponentPropertiesDialog(path){
var editRollOver = CQ.utils.WCM.getEditables()[path];
CQ.wcm.EditBase.showDialog(editRollOver, CQ.wcm.EditBase.EDIT);
}
</script>





AEM CQ 56 - Disable SideKick Activate, Deactivate for Non-Administrators

$
0
0

Goal


Disable the Sidekick Activate and Deactivate buttons if logged-in user is not in group administrators. Check the demo and Source code( not package install )



Solution


1) Code a servlet apps.experienceaem.sidekick.GetUserGroups to return the logged-in user groups. Here is the source

package apps.experienceaem.sidekick;

import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Iterator;

@SlingServlet(
paths = "/bin/experienceaem/getgroups",
methods = "GET",
metatype = false,
label = "Get user groups"
)
public class GetUserGroups extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(GetUserGroups.class);

@Override
protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");

JSONWriter jw = new JSONWriter(response.getWriter());

try {
ResourceResolver resolver = request.getResourceResolver();
User user = resolver.adaptTo(User.class);

Iterator<Group> groups = user.memberOf();
jw.object();
jw.key(user.getID()).array();

while(groups.hasNext()){
jw.value(groups.next().getID());
}

jw.endArray();
jw.endObject();
} catch (Exception e) {
log.error("Error getting groups", e);
throw new ServletException(e);
}
}
}

2) Login to CRXDE Lite, create folder (nt:folder) /apps/skdisable

3) Create clientlib (type cq:ClientLibraryFolder/apps/skdisable/clientlib and set a property categories of String type to cq.widgets

4) Create file ( type nt:file ) /apps/skdisable/clientlib/js.txt, add the following

                         disable.js

5) Create file ( type nt:file ) /apps/skdisable/clientlib/disable.js, add the following code

(function(){
if( ( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)){
var SK_INTERVAL = setInterval(function(){
var sk = CQ.WCM.getSidekick();

if(sk && sk.panels){
clearInterval(SK_INTERVAL);

$.ajax({
url: '/bin/experienceaem/getgroups',
dataType: "json",
type: 'GET',
async: false,
success: function(data){
data = data[CQ.User.getCurrentUser().getUserID()];

if(data.indexOf("administrators") !== -1){
return;
}
var pagePanel = sk.panels["PAGE"];

var buttons = pagePanel.findBy(function(comp){
return comp["name"] == "PUBLISH" || comp["name"] == "DEACTIVATE";
}, pagePanel);

CQ.Ext.each(buttons, function(button){
button.setDisabled(true);
});
}
});
}
}, 250);
}
})();

AEM CQ 56 - PathField Widget with Search

$
0
0

Goal


Extend PathFieldwidget to support select by search in browse dialog. Check the demo, Package install download ( contains a simple component with pathfieldwithsearch widget configured)



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/pathfieldsearch

2) Create node /apps/pathfieldsearch/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/pathfieldsearch/clientlib/js.txt and add

                       pathfieldwithsearch.js

4) Create file (nt:file) /apps/pathfieldsearch/clientlib/pathfieldwithsearch.js and add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.PathFieldWithSearch = CQ.Ext.extend(CQ.form.PathField, {
// the tree creation was copied from /libs/cq/ui/widgets/source/widgets/BrowseDialog.js
getTreePanel: function(){
var treeRootConfig = CQ.Util.applyDefaults(this.treeRoot, {
name: "content",
text: CQ.I18n.getMessage("Site"),
draggable: false,
singleClickExpand: true,
expanded:true
});

var treeLoaderConfig = CQ.Util.applyDefaults(this.treeLoader, {
dataUrl: CQ.HTTP.externalize("/bin/tree/ext.json"),
requestMethod:"GET",
baseParams: {
predicate: "hierarchy",
_charset_: "utf-8"
},
baseAttrs: {
"singleClickExpand":true
},
listeners: {
beforeload: function(loader, node){
this.baseParams.path = node.getPath();
}
}
});

return this.browseDialog.treePanel = new CQ.Ext.tree.TreePanel({
region:"west",
lines: CQ.themes.BrowseDialog.TREE_LINES,
bodyBorder: CQ.themes.BrowseDialog.TREE_BORDER,
bodyStyle: CQ.themes.BrowseDialog.TREE_STYLE,
height: "100%",
width: 250,
autoScroll: true,
containerScroll: true,
root: new CQ.Ext.tree.AsyncTreeNode(treeRootConfig),
loader: new CQ.Ext.tree.TreeLoader(treeLoaderConfig),
defaults: {
"draggable": false
}
});
},

getSearchPanel: function(){
var reader = new CQ.Ext.data.JsonReader({
id: "path",
root: "hits",
fields: [ "path", "title" ]
});

//querybuilder servlet returns the search results
var searchStore = new CQ.Ext.data.Store({
proxy: new CQ.Ext.data.HttpProxy({
url: "/bin/querybuilder.json"
}),
baseParams: {
"p.limit": "100",
"p.offset": "0",
"type": "cq:Page"
},
reader: reader,
autoLoad: false
});

var searchTemplate = new CQ.Ext.XTemplate(
'<tpl for=".">',
'<div class="search-result">{title} - {path}</div>',
'</tpl>'
);

this.browseDialog.searchResultsView = new CQ.Ext.DataView({
id: "pathfield-browsedialog-searchpanel",
store: searchStore,
tpl: searchTemplate,
itemSelector: "div.search-result",
selectedClass: "search-result-selected",
singleSelect: true,
style: { margin: "8px 0 0 0" }
});

this.browseDialog.searchField = new CQ.Ext.form.TextField({
width: "220",
hideLabel: true,
enableKeyEvents: true,
listeners:{
keypress: function(t,e){
//initiate search on enter
if (e.getKey() == e.ENTER) {
searchStore.reload( { params: { fulltext: t.getValue() } } );
}
}
}
});

var button = {
xtype: "button",
text: "Search",
width: 60,
tooltip: 'Search',
style: { margin: "0 0 0 10px" },
handler: (function () {
searchStore.reload( { params: { fulltext: this.browseDialog.searchField.getValue() } } );
}).createDelegate(this)
};

return new CQ.Ext.Panel({
region: "center",
border: false,
layout: "form",
autoScroll:true,
items: [ {
xtype: 'panel', layout: 'hbox', border: false,
items: [this.browseDialog.searchField, button]
}, this.browseDialog.searchResultsView ]
});
},

initComponent : function(){
ExperienceAEM.PathFieldWithSearch.superclass.initComponent.call(this);

this.on("dialogopen", function(){
var bd = this.browseDialog;

if(bd.searchField == null){
//remove the existing tree and add a new tree panel; couldn't successfully move the existing tree
//to new panel with search results, so remove and add new
bd.remove(this.browseDialog.treePanel, true);

var items = new CQ.Ext.Panel({
border:false,
layout: "border",
defaults: {
bodyStyle: 'padding:15px'
},
items: [ this.getTreePanel(), this.getSearchPanel()]
});

bd.setWidth(600);
bd.add(items);

bd.loadAndShowPath(this.getValue());
}else{
bd.searchField.setValue("");
bd.searchResultsView.getStore().removeAll();
}
});

this.on('dialogselect', function(){
var searchView = this.browseDialog.searchResultsView;

if(searchView && searchView.getSelectedRecords().length > 0){
this.setValue(searchView.getSelectedRecords()[0].get("path"));
}
});
}
});
CQ.Ext.reg("pathfieldwithsearch", ExperienceAEM.PathFieldWithSearch);

5) Create file (nt:file) /apps/pathfieldsearch/clientlib/css.txt and add

                       pathfieldwithsearch.css

6)  Create file (nt:file) /apps/pathfieldsearch/clientlib/pathfieldwithsearch.css and add the following code

#pathfield-browsedialog-searchpanel .search-result {
padding: 3px 3px 3px 0;
}

#pathfield-browsedialog-searchpanel .search-result:hover {
background-color: gainsboro;
cursor: pointer;
}

#pathfield-browsedialog-searchpanel .search-result-selected {
background-color: gainsboro;
}


AEM CQ 56 - Sample EditConfig Listener for Drag & Drop

$
0
0

Goal


Simple cq:editConfig listener fired when an image is drag and dropped from content finder onto the component. Check the demo

Solution


1) Set a listener on cq:editConfig, fired on ready event



2) Add the following code in listener

function () {
if (!this.subDropTargets || ( this.subDropTargets.length === 0 )) {
return;
}

var dt = this.subDropTargets[0];

dt.notifyEnter = function (dragSource, e, data) {
CQ.wcm.EditRollover.DropTarget.prototype.notifyEnter.call(this, dragSource, e, data);

setTimeout(function () {
alert("Image Dropped - " + data.records[0].id)
}, 500);
}
}

AEM CQ 56 - HTML5SmartImage Save Existing Image Maps

$
0
0

Goal


In CQ 56, working with Html 5 Smart Image you must have noticed that image maps added on an existing image are cleared if a new image is uploaded or drag dropped into the working area. If you have too many maps and do not want the existing maps to be cleared on new image drop, following solution may help. Check the demo video

Solution


One way to preserve map values is to handle the image changes on server side by coding a JCR observation listener or sling post processor or servlet filter, but if you'd like to handle it on the client side and don't mind adding a new xtype (unless it's really necessary and you cant figure out an other way, never change the existing xtype behavior, always extend and register a new xtype). Here we extend the xtype html5smartimage, add necessary save maps functionality and register it as savemapshtml5smartimage

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/saveimagemaps

2) Create node /apps/saveimagemaps/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/saveimagemaps/clientlib/js.txt and add

                       saveimagemaps.js

4) Create file (nt:file) /apps/saveimagemaps/clientlib/saveimagemaps.js and add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.Html5SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
initComponent: function () {
ExperienceAEM.Html5SmartImage.superclass.initComponent.call(this);
var mapTool = null;

CQ.Ext.each(this.imageToolDefs, function(tool){
if(tool.toolId == "smartimageMap"){
mapTool = tool;
}
});

var mapValue = null;

this.on("loadimage", function(){
if(mapTool.initialValue){
mapValue = mapTool.initialValue;
}else if(!mapTool.initialValue && mapValue){
mapTool.initialValue = mapValue;
}
});
}
});

CQ.Ext.reg("savemapshtml5smartimage", ExperienceAEM.Html5SmartImage);

5) Use the xtype savemapshtml5smartimage in your image component



AEM CQ 56 - Slide Show Component

$
0
0

Goal


Download package install for creating a simple slide show component. Check Demo

This component uses multi-image widget discussed here

The multi-image widget script was enhanced to support additional textfields, textareas etc. In the slide show created we add a textfield for title and textarea for description overlayed on image displayed

Add images using drag and drop from content finder, as the upload functionality in image widget has a bug yet to be fixed

Thank you David Gonzalez, Justin Edelson for ideas in developing this show...

Dialog


Here is the dialog in CRX



Dialog on page




Slideshow





AEM CQ 56 - Sort Tree Nodes of Browse Dialog in pathfield

$
0
0

Goal


Sort browse dialog tree nodes of pathfield (CQ.form.PathField) on dialog open. Package install (contains a sample component using sortable pathfield)


Browse dialog of Product






Browse dialog tree nodes in ascending order






Browse dialog tree nodes in descending order






Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/sort-pathfield-tree

2) Create node /apps/sort-pathfield-tree/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/sort-pathfield-tree/clientlib/js.txt and add

                       sort-tree-nodes.js

4) Create file (nt:file) /apps/sort-pathfield-tree/clientlib/sort-tree-nodes.js and add the following code

CQ.Ext.ns("ExperienceAEM");

//asc=true for ascending order and asc=false for descending order
ExperienceAEM.sortTags = function(pathfield, asc){
pathfield.browseDialog.treePanel.on('load', function(node){
node.childNodes.sort(function(a,b){
a = a["text"].toLowerCase();
b = b["text"].toLowerCase();
return asc ? ( a > b ? 1 : (a < b ? -1 : 0) ) : ( a > b ? -1 : (a < b ? 1 : 0) ) ;
});
})
};

5) Here is a sample component dialog using sort on pathfield

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Dialog"
helpPath="en/cq/current/wcm/default_components.html#Text"
title="Text"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Widget"
anchor="100%"
title="Text"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<path
jcr:primaryType="cq:Widget"
fieldLabel="Select path"
name="./path"
xtype="pathfield">
<listeners
jcr:primaryType="nt:unstructured"
dialogopen="function(){ExperienceAEM.sortTags(this, false);} "/>
</path>
</items>
</tab1>
</items>
</jcr:root>







AEM CQ 56 - Add new Switcher (Top Menu) Item

$
0
0

Goal


Add a link in welcome screen (http://localhost:4502/libs/cq/core/content/welcome.html) and new switcher (top menu) item in Classic UI console (/siteadmin, /damadmin etc.)

In the following steps we are going to add a sample CRXDE switcher item for navigating to CRXDE (http://localhost:4502/crx/de/index.jsp)


Welcome screen






Classic UI






Solution


1) To overlay, create folder /apps/wcm of type nt:folder and /apps/wcm/core of type nt:folder

2) Create folder /apps/wcm/core/content of type sling:Folder

3) Create node /apps/wcm/core/content/crxde of type cq:Widget and following properties (another way is to copy /libs/wcm/core/content/siteadmin to /apps/wcm/core/content, rename accordingly, use vlt to sync node to your IDE and update the xml with necessary properties )

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:mixinTypes="[cq:Console]"
jcr:primaryType="cq:Widget"
sling:redirect="{Boolean}false"
sling:resourceType="/apps/switcher-item-crxde/sample-crxde"
sling:vanityOrder="{Long}143"
sling:vanityPath="/sample-crxde"
consoleDescription="Show CRXDE"
consoleTitle="CRXDE"
iconClass="crxde"
jsLibs="[cq.wcm.admin]"
title="CRXDE"/>


4) The property sling:resourceType above is basically the action associated with that switcher item; so on click, user is taken to resource /apps/switcher-item-crxde/sample-crxde. Lets create the necessary resource component

5) Create folder /apps/switcher-item-crxde of type nt:folder 

6) For adding necessary icon css ( iconClass property in step 3 ) create clientlib /apps/switcher-item-crxde/clientlib of type cq:ClientLibraryFolder with categories of type String[] and values cq.shared cq.widgets




7) Create file (nt:file) /apps/switcher-item-crxde/clientlib/css.txt, add
               
                                               ui.css

8) Create file (nt:file) /apps/switcher-item-crxde/clientlib/ui.css, add

#cq-switcher .cq-switcher-crxde {
background: url(/crx/de/icons/16x16/unstructured.png) no-repeat;
}

#cq-switcher .cq-switcher-crxde-inactive {
background: url(/crx/de/icons/16x16/unstructured.png) no-repeat;
}

#apps .crxde {
background: url(/crx/de/icons/16x16/unstructured.png) no-repeat scroll 15px 20px transparent;
}


9) Create node /apps/switcher-item-crxde/sample-crxde of type sling:Folder 

10) Create file (nt:file) /apps/switcher-item-crxde/sample-crxde/sample-crxde.jsp, add the following code for redirecting user to CRXDE

<script>
window.location.href = "/crx/de";
</script>




AEM CQ 6 - Add new left rail nav item

$
0
0

Goal


Add a new left rail nav item in Touch UI console. To reach user admin a AEM user has to navigate through sub navs Tools -> Operations -> Security -> Users, here we provide a link on left rail main nav

As of this writing AEM 6 is still in beta, so this extension may or may not work in release version





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/add-left-rail-item-users

2) Create node /apps/add-left-rail-item-users/clientlib of type cq:ClientLibraryFolder and add a String property categories with value granite.ui.foundation.admin

3) Create file (nt:file) /apps/add-left-rail-item-users/clientlib/js.txt and add

                       add-nav-item-user.js

4) Create file (nt:file) /apps/add-left-rail-item-users/clientlib/add-nav-item-user.js and add the following code

(function($) {
var INTERVAL = setInterval(function(){
var pRoot = $("nav[data-coral-columnview-id=root]");

if(pRoot && pRoot.length > 0){
clearInterval(INTERVAL);
pRoot.first().append("<a class='coral-ColumnView-item' href='/libs/granite/security/content/useradmin.html'>Users</a>")
}
});
})(Granite.$);

AEM CQ 561 - Using Manuscripts

$
0
0

Goal


Add a Manuscript on Geometrixx page. Manuscripts are simple text files particularly useful for a business user who likes to create content pieces or articles in a simple text editor with no unsightly text-formatting controls like the ones you see in a WYSIWYG editor. Drag and drop features make it easy for presentation editors to add them across pages. A manuscripts Article Component example is also available on Geometrixx Media site /content/geometrixx-media/en/entertainment/summer-blockbuster-hits-and-misses.html. Check demo

Solution


1) Access manuscripts admin page http://localhost:4502/manuscriptsadmin

2) Select a folder and click New in right panel

3) Enter title Quick Brown Fox, click Create




4) Double click on the created manuscript to open copy editor /quick-brown-fox.copyeditor.html

6) Copy or enter content. Here a single # before Header text marks it as html H1. click Save on top right in editor to save content




7)  Open a page in design mode and add Article component of Geometrixx Media in allowed components of par



8) Drag and drop Article component onto the par from sidekick




9) Drag and drop the Quick Brown Fox manuscript from content finder onto the added Article component




10) The same manuscript can be reused on any number of pages and can be modified using the copy editor



AEM CQ 561 - Working with Launches

$
0
0

Goal


Create a future release of page. Launches feature is useful when a business user likes to add or modify content released (promoted) to publish in a future date. Any content added in current source page is optionally, automatically synced, so that launch page always has latest content.

Creating a Launch


1) Access Geometrixx English page http://localhost:4502/cf#/content/geometrixx/en.html and make a copy of it ( Sidekick -> Page tab -> Copy Page). We'll work on the copy




2) Goto Websites console http://localhost:4502/siteadmin, select the page and click New Launch from grid toolbar




3) Enter name for launch ( visible in Sidekick for identifying this launch ). The Launch date entered below is for informational purpose and content is not automatically promoted when date is reached



4)  You can now access the created launch from control center http://localhost:4502/libs/launches/content/admin.html, but a better way is to reach from source page context; Access the page http://localhost:4502/cf#/content/geometrixx/en1.html -> Sidekick -> Versioning tab -> Launches -> (Select launch) Future English Page and click Switch to open launch page http://localhost:4502/cf#/content/launches/future_english_page/content/geometrixx/en1.html



5) Let try the auto content sync; Modify source page http://localhost:4502/cf#/content/geometrixx/en1.html and content updates automatically propagate to launch page. Click the Launches Switch in sidekick to see if launch page is modified




6) While you are on launch page http://localhost:4502/cf#/content/launches/future_english_page/content/geometrixx/en1.html modify the page.




7) As content is still not promoted, launch page content is different form source page http://localhost:4502/cf#/content/geometrixx/en1.html

8) Click on launch page http://localhost:4502/cf#/content/launches/future_english_page/content/geometrixx/en1.html -> Sidekick -> Page tab -> Promote Launch




9) Check source page http://localhost:4502/cf#/content/geometrixx/en1.html, and you can see the content promoted for activation




10) So launches are a convenient way to leisurely work on future versions of page without creating an entirely new page

AEM CQ 561 - Working with Scaffolding

$
0
0

Goal


Generate pages with similar structure and varying content quickly. Scaffolding provides form like interface with fields to generate pages, with content entered into the scaffold. Check demo

Creating a Scaffold Page


1) Let's work on creating pages with a par, some static content in header, footer with text and image components in middle section. So the steps involved are

              a) Create a page from siteadmin or copy existing page
              b) Drag and drop necessary components onto the page par ie. text and image components
              c) Enter content for text by opening component dialog; drag image from content finder

The resulting page is like one below



2) If too many pages in your site have similar structure but only the text and image change, its generally useful to create Scaffolding to generate such pages. With Scaffolding we get rid of two steps - Create/Copy page by selecting templates from page dialog, Drag and drop components from sidekick; Depending on the number of components in page, page structure and number of look-alike pages to be generated scaffolding can save a lot of time

3) To create a scaffold, access Tools console http://localhost:4502/miscadmin and select Scaffolding in left tree; In the right pane select New, a dialog with single template Scaffolding Template appears. Enter title My Text Image. Click Create



4) Double click on My Text Image to open the scaffolding editor http://localhost:4502/cf#/etc/scaffolding/my-text-image.html. Click Sidekick -> Page tab -> Page properties and select the following

                       Target Template: The template used for page creation. In step 1 to create the page manually, we used a simple Basic Template.As we are generating pages of Basic Template select it
                       Target Path: The path where generated pages are stored



Here is the code for basic template page component jsp




5) After entering the details in page properties, a basic scaffold is created with Title to enter page title (stored as jcr:title) and Tags.




6) Every scaffolding has Dialog Editor to work on its form fields like Title above. Click on the Sidekick design mode (L shape at the bottom of sidekick) to access page having link for Dialog Editor. Click on the link to open Dialog Editor




   Dialog editor url http://localhost:4502/etc/scaffolding/my-text-image/_jcr_content/dialog.html



    Dialog in CRXDE Lite



7) Using the dialog editor we can add new properties for existing fields, but to add new fields a developer has to visit CRXDE

If a page is manually created, the following structure appears in CRX /content




With a Scaffold page we need to make sure the properties bordered above are created in CRX. So we add the necessary fields in Scaffold dialog. Access the created scaffold page in CRXDE http://localhost:4502/crx/de/index.jsp#/etc/scaffolding/my-text-image and add widget for Text field in the dialog - /etc/scaffolding/my-text-image/jcr:content/dialog/items/tab1/items/text ( you can copy an existing  richtext widget node available eg.  /etc/scaffolding/geometrixx/news/jcr:content/dialog/items/tab1/items/text)




The name of field is set to ./jcr:content/par/text/text. So any value entered for this field is stored in path /jcr:content/par/text/text relative to the newly created page

Similarly we need hidden fields for storing the value of sling:resourceType - foundation/components/text and textIsRich - true; so we create necessary hidden widgets



8) Similarly we need html5smartimage widget and hidden field for storing image specific values. As you can see the image specific values are stored under ./jcr:content/par/image relative to new page



The hidden field for storing Image Component resourceType




9) We have the scaffolding form now ready for creating pages http://localhost:4502/cf#/etc/scaffolding/my-text-image.html. Enter Title, Text, drag an Image from content finder, click Create and your page will be created (eg. http://localhost:4502/content/my-scaffolding-pages/ocean.html )



10) Here is the scaffolding page dialog xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Dialog"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Panel"
title="Properties">
<items jcr:primaryType="cq:WidgetCollection">
<title
jcr:primaryType="cq:Widget"
fieldLabel="Title"
name="./jcr:content/jcr:title"/>
<text
jcr:primaryType="cq:Widget"
fieldLabel="Text"
name="./jcr:content/par/text/text"
xtype="richtext">
<rtePlugins jcr:primaryType="nt:unstructured">
<table
jcr:primaryType="nt:unstructured"
features="*"/>
<format
jcr:primaryType="nt:unstructured"
features="*"/>
<lists
jcr:primaryType="nt:unstructured"
features="*"/>
<justify
jcr:primaryType="nt:unstructured"
features="*"/>
<edit
jcr:primaryType="nt:unstructured"
features="[paste-wordhtml]"/>
<findreplace
jcr:primaryType="nt:unstructured"
features="*"/>
<paraformat
jcr:primaryType="nt:unstructured"
features="*"/>
<subsuperscript
jcr:primaryType="nt:unstructured"
features="*"/>
<misctools
jcr:primaryType="nt:unstructured"
features="*"/>
<links
jcr:primaryType="nt:unstructured"
features="*"/>
<spellcheck
jcr:primaryType="nt:unstructured"
features="*"
invalidStyle="background-color: #ffdddd;"/>
<undo
jcr:primaryType="nt:unstructured"
features="*"/>
<image
jcr:primaryType="nt:unstructured"
features="*"/>
</rtePlugins>
</text>
<textResourceType
jcr:primaryType="cq:Widget"
ignoreData="{Boolean}true"
name="./jcr:content/par/text/sling:resourceType"
value="foundation/components/text"
xtype="hidden"/>
<richFlag
jcr:primaryType="cq:Widget"
ignoreData="{Boolean}true"
name="./jcr:content/par/text/textIsRich"
value="true"
xtype="hidden"/>
<image
jcr:primaryType="cq:Widget"
border="true"
cropParameter="./jcr:content/par/image/imageCrop"
ddGroups="[media]"
fieldLabel="Image"
fileNameParameter="./jcr:content/par/image/fileName"
fileReferenceParameter="./jcr:content/par/image/fileReference"
height="400"
hideLabel="false"
mapParameter="./jcr:content/par/image/imageMap"
name="./jcr:content/par/image/file"
requestSuffix="/jcr:content/par/image.img.png"
rotateParameter="./jcr:content/par/image/imageRotate"
sizeLimit="100"
xtype="html5smartimage"/>
<imageResourceType
jcr:primaryType="cq:Widget"
ignoreData="{Boolean}true"
name="./jcr:content/par/image/sling:resourceType"
value="foundation/components/image"
xtype="hidden"/>
</items>
</tab1>
<tab4
jcr:primaryType="cq:Panel"
title="Tags / Keywords">
<items jcr:primaryType="cq:WidgetCollection">
<tags
jcr:primaryType="cq:Widget"
name="./jcr:content/cq:tags"
xtype="tags"/>
</items>
</tab4>
</items>
</jcr:root>

AEM CQ 56 - Disable Cancel Inheritance in Live Copies

$
0
0

Goal


Multi Site Manager is a fantastic concept, very useful in creating multinational sites. You create a source site, create live copies of it for each region and maintain the relationship between source site and live copies, so that any updates on source can be rolled out to live copies

An admin of live copy can break the inheritance relationship with source, by clicking on a component's Cancel Inheritance Lock Icon



In this post we'll work on logic to provide an option to live copy creator, to disable cancel inheritance in live copies. So the live copy author cannot edit components rolled out from source; he can always add new components, but cannot alter existing ones.Check demo and Download package install



when a live copy creator checks the Disable Cancel Inheritance option in Create Size Wizard, a property cq:isDisableCancelInheritance is set to true on the live copy root jcr:content node
     


Page dialogs, components read the property cq:isDisableCancelInheritance and act accordinly



Solution


With few JS lines of code we can disable the cancel inheritance options

1) Login to CRXDE Lite, create folder (nt:folder) /apps/msm-disable-cancel-inheritance

3) Create clientlib (type cq:ClientLibraryFolder/apps/msm-disable-cancel-inheritance/clientlib and set a property categories of String type to cq.widgets

4) Create file ( type nt:file ) /apps/msm-disable-cancel-inheritance/clientlib/js.txt, add the following

                         disable-cancel-inheritance.js

5) Create file ( type nt:file ) /apps/msm-disable-cancel-inheritance/clientlib/disable-cancel-inheritance.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.CreateSite = {
disableCancelInheritanceFlag: false,

//add the disable cancel inheritance option to create site wizard
addDisableCancelInheritance: function(grid){
var toolBar = grid.getTopToolbar();

var newMenu = toolBar.findBy(function(comp){
return comp["iconCls"] == "cq-siteadmin-create-page-icon";
}, toolBar)[0];

var newSite = newMenu.menu.findBy(function(comp){
return comp["iconCls"] == "cq-siteadmin-create-site-icon";
}, newMenu)[0];

newSite.on('click', function(){
var dlg = CQ.WCM.getDialog("", "cq-siteadmin-csw", true);

dlg.navHandler = function(d) {
CQ.wcm.CreateSiteWizard.prototype.navHandler.call(this, d);
var idx = this.activePage + d;

//we are at the live copy page in wizard
if(idx == 4){
var liveCopyPanel = this.wizPanel.layout.activeItem;
liveCopyPanel.add(new CQ.Ext.form.Checkbox({
fieldDescription: "Live copy owners will not be able to cancel component inheritance",
fieldLabel: 'Disable Cancel Inheritance',
name: "./cq:isDisableCancelInheritance",
inputValue: true,
checked: false
}));
liveCopyPanel.doLayout();
}
};
})
},

disableCancelInheritance: function(){
var sk = CQ.WCM.getSidekick();
var pathTokens = sk.getPath().split("/");
var siteSourcePath = "/" + pathTokens[1] + "/" + pathTokens[2] + "/jcr:content.json";

$.ajax({ url: siteSourcePath, async: false, dataType: "json",
success: function(data){
this.disableCancelInheritanceFlag = eval(data["cq:isDisableCancelInheritance"]);
}.createDelegate(this)
});

if(!this.disableCancelInheritanceFlag){
return;
}

var editables = CQ.utils.WCM.getEditables();

CQ.Ext.iterate(editables, function(path, editable) {
editable.on(CQ.wcm.EditBase.EVENT_BEFORE_EDIT, function(){
var INTERVAL = setInterval(function(){
var dialog = editable.dialogs[CQ.wcm.EditBase.EDIT];

if(dialog){
clearInterval(INTERVAL);
dialog.editLockButton.setDisabled(true);
dialog.editLockButton.setTooltip("Creator of this livecopy has disabled cancel inheritance");
}
}, 200);
});

//disable some inheritance specific options in context menu
editable.addElementEventListener(editable.element.dom, "contextmenu" , function(){
var msm = this["msm:liveRelationship"];

if(!msm || !msm["msm:status"] || !msm["msm:status"]["msm:isSourceExisting"]){
return;
}

var component = this.element.linkedEditComponent;

if (!component || !component.menuComponent) {
return;
}

var menu = component.menuComponent;
var opts = [ menu.find('text', "Delete"), menu.find('text', "Cut") ];

CQ.Ext.each(opts, function(opt){
if(opt && opt.length > 0){
opt[0].setDisabled(true);
}
});
}, true, editable);
});
},

//register this function as listener for "loadcontent" event on dialog
disableSkCancelInheritance: function(dialog){
if(!this.disableCancelInheritanceFlag){
return;
}

var fields = CQ.Util.findFormFields(dialog.formPanel);

CQ.Ext.iterate(fields, function(name, f){
CQ.Ext.each(f, function(field){
if(field.lockPanel){
field.lockPanel.setDisabled(true);
}else if(field.fieldEditLockBtn){
field.fieldEditLockBtn.setDisabled(true);
field.fieldEditLockBtn.setTooltip("Creator of this livecopy has disabled cancel inheritance");
}
})
});
}
};

(function(){
var E = ExperienceAEM.CreateSite;

if(window.location.pathname == "/siteadmin"){
var INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp("cq-siteadmin-grid");

if(grid){
clearInterval(INTERVAL);
E.addDisableCancelInheritance(grid);
}
}, 250);
}else if( ( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)){
CQ.WCM.on("editablesready", E.disableCancelInheritance, E);
}
})();

4) In the above code addDisableCancelInheritance function adds a checkbox option Disable Cancel Inheritance to create size wizard; when checked the live copy user cannot disable cancel inheritance, when unchecked the live copies will have cancel inheritance lock enabled

5) The following logic in disableCancelInheritance function, iterates through available editables on page, and disables the lock icon in edit dialogs

 editable.on(CQ.wcm.EditBase.EVENT_BEFORE_EDIT, function(){
var INTERVAL = setInterval(function(){
var dialog = editable.dialogs[CQ.wcm.EditBase.EDIT];

if(dialog){
clearInterval(INTERVAL);
dialog.editLockButton.setDisabled(true);
dialog.editLockButton.setTooltip("Creator of this livecopy has disabled cancel inheritance");
}
}, 200);
});

6) We should also disable some context menu options that allow the user to cancel inheritance like deleting a component rolled out from source. Below logic in function disableCancelInheritance does the part of disabling such context menu options

editable.addElementEventListener(editable.element.dom, "contextmenu" , function(){
var msm = this["msm:liveRelationship"];
....
....
....
}, true, editable);

7) The Page properties dialog of sidekick also allows user to cancel inheritance. The function disableSkCancelInheritance takes care of cancel inheritance lock icons in page properties. For this logic to execute we register a loadcontent event listener on page component dialog

                   function(d){ ExperienceAEM.CreateSite.disableSkCancelInheritance(d); }




This is a manual task and user has to add listener on dialogs of "disable cancel inheritance required page components". I couldn't find a way to automate it, which would have been ideal

8) This extension was not extensively tested with all available integrations like Sitecatalyst, Target. There is scope for some options that come with integrations allowing user to cancel inheritance, so there is always room for improvement

Leave a comment if you find bugs...

AEM CQ 561 - Add a button to Sidekick Bottom Toolbar

$
0
0

Goal


Add a button for viewing the page in wcmmode disabled. Here, on clicking the button added to sidekick bottom toolbar (beside Edit button), allows the user to view page in wcm disabled mode. View Demo




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/sidekick-button-wcmmode-disabled

2) Create node /apps/sidekick-button-wcmmode-disabled/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) We need a button icon, so create file (nt:file) /apps/sidekick-button-wcmmode-disabled/clientlib/css.txt and add

                       wcmmode-disabled.css

4) Add the following code in /apps/sidekick-button-wcmmode-disabled/clientlib/wcmmode-disabled.css


#CQ .cq-sidekick .x-window-bbar .cq-sidekick-wcmmode-disabled {
background-image:url(wcmmode-disabled.png);
}


5) Download and check-in the image wcmmode-disabled.png to /apps/sidekick-button-wcmmode-disabled/clientlib

6) Create file (nt:file) /apps/sidekick-button-wcmmode-disabled/clientlib/js.txt and add

                       add-wcmmode-disabled-button.js

7) Create file (nt:file) /apps/sidekick-button-wcmmode-disabled/clientlib/add-wcmmode-disabled-button.js and add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.Sidekick = {
WCMMODE_DISABLED_BUTTON_ID: "experience-aem-sk-button-wcmmode-disabled",

//add the button to sidekick bottom bar
addWCMModeDisabled: function(sk){
var bbar = sk.getBottomToolbar();
var dButton = bbar.getComponent(0);

//if the sidekick is reloaded, remove existing and add a fresh one
if(dButton.getId() == this.WCMMODE_DISABLED_BUTTON_ID){
bbar.remove(dButton, true);
}

dButton = new CQ.Ext.Button({
id: this.WCMMODE_DISABLED_BUTTON_ID,
iconCls: "cq-sidekick-wcmmode-disabled",
tooltip: {
title: "Disabled",
text: "Switch to wcmmode=disabled"
},
handler: function() {
var win = CQ.WCM.isContentWindow(window) ? window.parent : window;
win.location.href = sk.getPath() + ".html?wcmmode=disabled";
},
scope: sk
});

//add the button as first component in bottom toolbar
bbar.insert(0, dButton );
}
};

(function(){
var E = ExperienceAEM.Sidekick;

if( ( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)){
//when the sidekick is ready CQ fires sidekickready event
CQ.WCM.on("sidekickready", function(sk){
//after the sidekick content is loaded, add wcmmode disabled button
sk.on("loadcontent", function(){
E.addWCMModeDisabled(sk);
});
});
}
})();

8) Here is the extension structure in CRX







AEM CQ 56 - Disable HTML5SmartImage Map Tools

$
0
0

Goal


Disable some of the not required html5smartimage (CQ.html5.form.SmartImage) Image Map tools. Image map tools allow an author to create hotspots on image as shapes (Rectangle, Circle etc. ) by specifying the mapParameter in image configuration. Here we disable shapes other than Rectangle forcing the author to use only rectangles while creating hotspots





Solution


We need a clientlib to add necessary js logic for disabling the tools

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/disable-imagemap-tools

2) Create node /apps/disable-imagemap-tools/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets. Here if you cannot use otb category cq.widgets, add a custom category (my.custom.clientlib) and make sure you conditionally include the clientlib in page component jsp (<cq:includeClientLib categories="my.custom.clientlib" />)

3) Create file (nt:file) /apps/disable-imagemap-tools/clientlib/js.txt and add

                       disable-tools.js

4) Create file (nt:file) /apps/disable-imagemap-tools/clientlib/disable-tools.js and add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.Html5SmartImage = {
mapToolRectangleOnly: function(image){
var mapTool = null;

CQ.Ext.each(image.imageToolDefs, function(tool){
if(tool.toolId == "smartimageMap"){
mapTool = tool;
}
});

var toolBar = mapTool.userInterface.getTopToolbar();

var tools = toolBar.findBy(function(comp){
return comp["toggleGroup"] == "mapperTools";
}, toolBar);

CQ.Ext.each(tools, function(tool){
if( (tool.text != "Rectangle") && (tool.text != "Edit") ){
tool.setDisabled(true);
}
});
}
};

5) Add a listener on component's html5smartimage widget. Please note that it is not advisable modifying foundation components (say /libs/foundation/components/image). This solution is for custom components. So i have an image component /apps/disable-imagemap-tools/image and adding a listener on the custom image component's node /apps/disable-imagemap-tools/image/dialog/items/image




Here we are registering a loadcontent event listener with function created in the clientlib above

                       ExperienceAEM.Html5SmartImage.mapToolRectangleOnly(f)

Image instance is passed to the js function; logic executes on image load and disables shapes other than Rectangle

AEM CQ 561 - Add a new Image Metadata Field in Touch UI

$
0
0

Goal


Add a new metadata field for assets in Touch UI (Coral UI). Here we add a field for author to enter overlay text, that can be displayed on image while rendering. View demo





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touch-ui-add-dam-metadata-field

2) Create node /apps/touch-ui-add-dam-metadata-field/clientlib of type cq:ClientLibraryFolder and add a String property categories with value granite.ui.foundation

3) Create file (nt:file) /apps/touch-ui-add-dam-metadata-field/clientlib/js.txt and add

                       add-overlay-text.js

4) Create file (nt:file) /apps/touch-ui-add-dam-metadata-field/clientlib/add-overlay-text.js and add the following code

(function(document, Granite, $) {
"use strict";

var ns = ".foundation-form";

$(document).on("foundation-mode-change" + ns, function(e, mode, group) {
if(group !== "cq-damadmin-admin-bulkproperties"){
return;
}

if (mode !== "edit"&& mode !== "default"){
return;
}

//the id is defined here /libs/dam/gui/content/assets/metadataeditor/items/content
var form = $("#assetpropertiesform");

var overlayTextField = $(form).find("[name='./jcr:content/metadata/overlayText']");

//field already added
if(overlayTextField && overlayTextField.length > 0){
return;
}

var assetPath = $(form).attr("action");
assetPath = assetPath.substring(0, assetPath.lastIndexOf(".html"));

$.ajax({
url: assetPath + "/jcr:content/metadata.json",
dataType: "json",
success: function(data){
var value = data["overlayText"];

if(!value){
value = "";
}

overlayTextField = "<div class='grid-1'>" +
"<label>" +
"<span>Overlay Text</span>" +
"<span class='foundation-field-editable'>" +
"<span class='foundation-field-readonly'>" + value + "</span>" +
"<input type='text' size=54 name='./jcr:content/metadata/overlayText' value='" + value + "' class='foundation-field-edit' />" +
"</span>" +
"</label>" +
"</div>";

var asset = $(form).find(".assets-metadata-view");
asset.append(overlayTextField);
}
});
});
})(document, Granite, Granite.$);

In the above code, logic listens to granite framework foundation-mode-change event, checks the dam editor mode for edit and adds overlayText field

5) The component structure in CRXDE



AEM CQ 561 - Hide Sidekick

$
0
0

Goal


Hide the Sidekick in page authoring, just in case you need to :)

Solution


In you page component jsp add the following script

<script type="text/javascript">
(function(){
CQ.WCM.on("editablesready", function(){
CQ.WCM.getSidekick().hide();
});
})();
</script>

An other way is to add the js logic in a client lib

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/hide-sidekick

2) Create node /apps/hide-sidekick/clientlib of type cq:ClientLibraryFolder and add a String property categories with value experience-aem.sidekick

3) Create file (nt:file) /apps/hide-sidekick/clientlib/js.txt and add

                       hide-sidekick.js

4) Create file (nt:file) /apps/hide-sidekick/clientlib/hide-sidekick.js and add the following code

(function(){
if( ( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)){
CQ.WCM.on("editablesready", function(){
CQ.WCM.getSidekick().hide();
});
}
})();

5) Include the clientlib in your page component jsp

                     <cq:includeClientLib categories="experience-aem.sidekick"/>

AEM CQ 6 - MSM, Setting JCR Permissions on Live Copy Components

$
0
0

Goal


This post is for MSM(Multi Site Manager) administrators who need more control on live copy components. A live copy author can click Cancel Inheritance on page components to break the inheritance relation of component with blue print site.

Using jcr:write privileges on the component nodes at JCR level, we can restrict live copy authors from breaking the relation and modifying content. Check Demo, Download Source Code and Package Install

The package was tested and works on 561; For a different approach (does not provide detail component level permissions) on dealing with the same problem check this post

Before we move to the solution part, here is how it works

1) Create a live copy Geo One (/content/geometrixx-outdoors1) for site Geometrixx Outdoors Site (/content/geometrixx-outdoors)

2) Blueprint site admin open's the page (eg. http://localhost:4502/cf#/content/geometrixx-outdoors/en/toolbar/about-us.html) in author environment for providing permissions on live copy components. Click the component menu option Set Cancel Inheritance. This option is added by the extension



3) Set Cancel Inheritance menu option opens the following dialog. Here you select live copy, user/group on which the privilege jcr:write needs to be set



   Click ok and a notification appears on the top right of screen



4) So for live copy page /content/geometrixx-outdoors1/en/toolbar/about-us, component  /jcr:content/par/text_8d9e user author is denied the jcr:write permission



5) Login to CRXDE Lite http://localhost:4502/crx/de and browse to node of live copy Geo One node /content/geometrixx-outdoors1/en/toolbar/about-us/jcr:content/par/text_8d9e. The jcr:write permission is denied to author



6) Login as author and try to click Cancel Inheritance of the component, on live copy Geo One page http://localhost:4502/cf#/content/geometrixx-outdoors1/en/toolbar/about-us.html



   Error is shown on top of screen



7) When user author clicks on the Cancel Inheritance of dialog, dialog becomes editable but any content added is not saved and UI shows error

Dialog becomes editable on clicking cancel inheritance lock icon


Click ok and error pops



8) So using the extension a blueprint admin can control which users/groups can modify the components of a live copy page.

Even without the extension, a blueprint site admin can set the permissions on page component nodes by visiting CRXDE, but setting privileges in the context of page is more intuitive which is what this extension provides

Solution


We need a servlet to handle the get/set of privileges and clientlib for the UI parts. The package install above is created using the multimodule-content-package-archetype, for detailed notes on how to create a package using multimodule-content-package-archetype in Intellij check this post

Servlet


The doPost method in below servlet creates jcr:write privilege on the live copy component node and doGet returns the privilege on live copy node for a principal, as json

package apps.experienceaem.msm;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Properties;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.jcr.security.*;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(metatype = false)
@Service
@Properties({
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/msm/acl"),
@Property(name = "sling.servlet.methods", value = { "GET", "POST" } )
})
public class AccessControl extends SlingAllMethodsServlet {

private final static Logger log = LoggerFactory.getLogger(AccessControl.class);

/**
* Returns the ACL of path
*
* @param session
* @param path
* @return
* @throws Exception
*/
private JackrabbitAccessControlList getACL(Session session, String path) throws Exception{
AccessControlManager acMgr = session.getAccessControlManager();

JackrabbitAccessControlList acl = null;
AccessControlPolicyIterator app = acMgr.getApplicablePolicies(path);

while (app.hasNext()) {
AccessControlPolicy pol = app.nextAccessControlPolicy();

if (pol instanceof JackrabbitAccessControlPolicy) {
acl = (JackrabbitAccessControlList) pol;
break;
}
}

if(acl == null){
for (AccessControlPolicy pol : acMgr.getPolicies(path)) {
if (pol instanceof JackrabbitAccessControlPolicy) {
acl = (JackrabbitAccessControlList) pol;
break;
}
}
}

return acl;
}

@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String liveCopies = request.getParameter("liveCopies");
String path = request.getParameter("path");
String principal = request.getParameter("principal");
String type = request.getParameter("type");

if(StringUtils.isEmpty(liveCopies) || StringUtils.isEmpty(path) || StringUtils.isEmpty(principal)){
throw new RuntimeException("Required parameters missing");
}

if(StringUtils.isEmpty(type)){
type = "ALLOW";
}

try{
Session session = request.getResourceResolver().adaptTo(Session.class);
AccessControlManager acMgr = session.getAccessControlManager();

for(String copy: liveCopies.split(",")){
String compPath = copy + path;
JackrabbitAccessControlList acl = getACL(session, compPath);

if(acl == null){
throw new RuntimeException("ACL not found for path: " + compPath);
}

UserManager uMgr = ((JackrabbitSession) session).getUserManager();
Authorizable authorizable = uMgr.getAuthorizable(principal);

Privilege[] p = new Privilege[]{ acMgr.privilegeFromName(Privilege.JCR_WRITE) };
acl.addEntry(authorizable.getPrincipal(), p, type.equalsIgnoreCase("ALLOW"));

acMgr.setPolicy(compPath, acl);
}

session.save();

JSONWriter jw = new JSONWriter(response.getWriter());
jw.object().key("success").value("success").endObject();
}catch(Exception e){
log.error("Error adding acl in path - " + path + ", for - " + liveCopies, e);
throw new ServletException(e);
}
}

@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");

String path = request.getParameter("path");
String privilege = request.getParameter("privilege");

try{
Session session = request.getResourceResolver().adaptTo(Session.class);

Privilege privileges[] = null; String pName = null;
JSONObject map = null, allow = new JSONObject(), deny = new JSONObject();
JSONArray arr = null;

JackrabbitAccessControlList acl = getACL(session, path);
AccessControlEntry entries[] = acl.getAccessControlEntries();

for(AccessControlEntry entry : entries){
privileges = entry.getPrivileges();

map = ((JackrabbitAccessControlEntry)entry).isAllow() ? allow : deny;

for(Privilege p : privileges){
pName = p.getName();

if(StringUtils.isNotEmpty(privilege) && !privilege.equals(pName)){
continue;
}

try{
arr = (JSONArray)map.get(pName);
}catch(JSONException je){
arr = new JSONArray();
map.put(pName, arr);
}

arr.put(entry.getPrincipal().getName());
}
}

JSONWriter jw = new JSONWriter(response.getWriter());
jw.object().key("allow").value(allow).key("deny").value(deny).endObject();
}catch(Exception e){
log.error("Error getting privileges for path - " + path, e);
throw new ServletException(e);
}
}
}


UI Client lib


We need a clientlib with necessary JS code for adding Set Cancel Inheritance menu option and subsequent dialog

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/msm-disable-cancel-inheritance-users

2) Create node /apps/msm-disable-cancel-inheritance-users/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/msm-disable-cancel-inheritance-users/clientlib/js.txt and add

                       disable-cancel-inheritance-for-users.js

4) Create file (nt:file) /apps/msm-disable-cancel-inheritance-users/clientlib/disable-cancel-inheritance-for-users.js and add the following code

CQ.Ext.ns("ExperienceAEM.MSM");

ExperienceAEM.MSM.Blueprint = {
liveCopies: [],

//the dialog for user to choose the live copy, user/group
getCancelInheritanceDialog: function(path){
path = path.substring(path.indexOf("/jcr:content"));

var allow = new CQ.Ext.form.Label( { text: "Allow : Select a live copy" });
var deny = new CQ.Ext.form.Label( { text: "Deny : Select a live copy" });

var getLiveCopies = function(lBox){
var lCopies = lBox.getValue();

if(lCopies == "ALL"){
var items = lBox.getStore().data.items;
lCopies = [];

CQ.Ext.each(items, function(item){
if(item.id == "ALL"){
return;
}

lCopies.push(item.id);
});

lCopies = lCopies.join(",");
}

return lCopies;
};

//updates the labels with users/groups allow/deny the jcr:write permission
var showPrivileges = function(lBox){
if(lBox.getValue() == "ALL"){
allow.setText("Allow : Select a live copy");
deny.setText("Deny : Select a live copy");
return;
}

//get the allow/deny permissions as json
$.ajax({ url: "/bin/experience-aem/msm/acl", dataType: "json",
data: { path : lBox.getValue() + path, privilege: "jcr:write" },
success: function(data){
var privs = data["allow"];

if(privs && !CQ.Ext.isEmpty(privs["jcr:write"])){
allow.setText("Allow : " + privs["jcr:write"].join(""));
}else{
allow.setText("Allow : None set");
}

privs = data["deny"];

if(privs && privs["jcr:write"]){
deny.setText("Deny : " + privs["jcr:write"].join(""));
}else{
deny.setText("Deny : None set");
}
},
type: "GET"
});
};

var dialogConfig = {
"jcr:primaryType": "cq:Dialog",
title: "Set Inheritance Options - " + path,
modal: true,
width: 600,
height: 300,
items: [{
xtype: "panel",
layout: "form",
bodyStyle :"padding: 20px",
items: [{
xtype: "panel",
border: false,
bodyStyle :"margin-bottom: 10px",
items: allow
},{
xtype: "panel",
border: false,
bodyStyle :"margin-bottom: 25px",
items: deny
},{
anchor: "95%",
xtype: "combo",
style: "margin-bottom: 20px",
mode: 'local',
fieldLabel: "Select Live Copy",
store: new CQ.Ext.data.ArrayStore({
id: 0,
fields: [ 'id', 'text' ],
data: this.liveCopies
}),
valueField: 'id',
displayField: 'text',
triggerAction: "all",
listeners:{
scope: this,
'select': function(combo){
//when a livecopy is selected, make an ajax call to get the jcr:write permission
showPrivileges(combo);
}
}
},{
valueField: "id",
displayField: "name",
fieldLabel: "Select User/Group",
style: "margin-bottom: 20px",
autoSelect: true,
xtype: "authselection"
},{
xtype: 'radiogroup',
columns: 6,
fieldLabel: "Cancel Inheritance ",
items: [{
boxLabel: ' Allow',
name: 'type',
value: 'ALLOW',
checked: true
},{
name: 'type',
boxLabel: ' Deny',
value: 'DENY',
checked: false
}]
}]
}],
ok: function () {
var lBox = this.findByType("combo")[0];
var uBox = this.findByType("authselection")[0];
var tBox = this.findByType("radiogroup")[0];

var options = {
path: path,
liveCopies: getLiveCopies(lBox),
principal: uBox.getValue(),
type: tBox.getValue().value
};

this.close();

//save the user/group allow/deny privileges on the live copy component
$.ajax({
url: "/bin/experience-aem/msm/acl",
dataType: "json",
data: options,
success: function(){
CQ.Notification.notify("Cancel Inheritance","Access controls set for " + options.principal);
},
error: function(){
CQ.Notification.notify("Cancel Inheritance","Error setting access controls for " + options.principal);
},
type: 'POST'
});
}
};

return CQ.WCM.getDialog(dialogConfig);
},

//get the livecopies for a blueprint. If the site has not live copies "Set Cancel Inheritance" menu option is not shown
readLiveCopies: function(){
var sk = CQ.WCM.getSidekick();

$.ajax({ url: sk.getPath() + "/jcr:content.blueprint.json", async: false, dataType: "json",
success: function(data){
if(!data){
return;
}

var liveCopies = data["msm:targets"];

//return if there are no live copies
if(CQ.Ext.isEmpty(liveCopies)){
return;
}

this.liveCopies.push( [ "ALL", "All" ] );

CQ.Ext.each(liveCopies, function(lCopy){
this.liveCopies.push([ lCopy, lCopy ])
}, this);
}.createDelegate(this)
});
},

//browse editables and add the Set Cancel Inheritance menu option
addSetCancelInheritance: function () {
this.readLiveCopies();

if(CQ.Ext.isEmpty(this.liveCopies)){
return;
}

var editables = CQ.utils.WCM.getEditables();

CQ.Ext.iterate(editables, function (path, editable) {
if(!editable.addElementEventListener){
return;
}

editable.addElementEventListener(editable.element.dom, "contextmenu", function () {
var component = this.element.linkedEditComponent;

if (!component || !component.menuComponent) {
return;
}

var menu = component.menuComponent;

if (menu.cancelInheritanceSet) {
return;
}

menu.addSeparator();

menu.add({
text: "Set Cancel Inheritance",
handler: function () {
var dialog = ExperienceAEM.MSM.Blueprint.getCancelInheritanceDialog(path);
dialog.show();
}
});

menu.cancelInheritanceSet = true;
}, true, editable);
});
}
};

(function() {
var E = ExperienceAEM.MSM.Blueprint;

if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) {
if (CQ.WCM.isEditMode()) {
CQ.WCM.on("editablesready", E.addSetCancelInheritance, E);
}
}
})();




AEM CQ 561 - Adding Password Expiry Feature to Classic UI

$
0
0

Goal


For users existing in CQ, this extension helps providing a Password Expiry like feature in Classic UI. Check Demo, Source Code and Package Install

Assuming, only the administrators have access to /useradmin console and user nodes

Package logic adds a new textfield widget passwordExpiryInDays to the form in User Admin console (http://localhost:4502/useradmin)



Stored in CRX (http://localhost:4502/crx/de) as passwordExpiryInDays



When a value exists for the property passwordExpiryInDays on any user profile node (eg. /home/users/geometrixx/author/profile), logic checks if the value of  passwordLastChanged date (this is set when user changes the password using Password Expired dialog, explained in next section) on user node (eg. /home/users/geometrixx/author ) + passwordExpiryInDays on user profile node (eg. /home/users/geometrixx/author/profile) is LESS than today's date on server; if it is, Password Expired dialog starts annoying the user on Classic UI admin and authoring. if passwordLastChanged doesn't exist, logic works with user's jcr:created date


On Admin UI




On Authoring UI




When user enters new password, the servlet /bin/experience-aem/pm/expiry sets property passwordLastChanged on user node

Please note, this works on Classic UI only and since most of the authoring activity happens on Classic UI in 561, the touch UI was not handled for password expiry. More enhancements are yet to be added like Password Strength, Forgot Password, Password Expiry Reminders etc.


Solution


We need a servlet GetPasswordOptions with doGet method returning json data for user password expired and doPost setting passwordLastChanged on user node. The package is created using multimodule-content-package-archetype, for detailed notes on how to create a package using multimodule-content-package-archetype in Intellij check this post

package apps.experienceaem.pm;

import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;

@Component(metatype = false)
@Service
@Properties({
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/pm/expiry"),
@Property(name = "sling.servlet.methods", value = { "GET", "POST" } )
})
public class GetPasswordOptions extends SlingAllMethodsServlet {

private final static Logger log = LoggerFactory.getLogger(GetPasswordOptions.class);

@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");

try{
ResourceResolver resolver = request.getResourceResolver();
JackrabbitSession session = (JackrabbitSession)resolver.adaptTo(Session.class);
Authorizable user = session.getUserManager().getAuthorizable(session.getUserID());

ValueMap profileMap = resolver.getResource(user.getPath() + "/profile").adaptTo(ValueMap.class);

JSONWriter jw = new JSONWriter(response.getWriter());
jw.object();

if(!profileMap.containsKey("passwordExpiryInDays")){
jw.key("expired").value(false);
}else{
int passwordExpiryInDays = profileMap.get("passwordExpiryInDays", Integer.class);

ValueMap resMap = resolver.getResource(user.getPath()).adaptTo(ValueMap.class);
Date lastChangedDate = resMap.containsKey("passwordLastChanged") ? resMap.get("passwordLastChanged", Date.class)
: resMap.get("jcr:created", Date.class);

jw.key("passwordLastChanged").value(lastChangedDate);

//calculate the expiry date based on server time
Calendar expiryDate = Calendar.getInstance();
expiryDate.setTime(lastChangedDate);
expiryDate.add(Calendar.DAY_OF_YEAR, passwordExpiryInDays);

Calendar today = Calendar.getInstance();
jw.key("expired").value(expiryDate.getTimeInMillis() < today.getTimeInMillis());
jw.key("userPath").value(user.getPath());
jw.key("passwordExpiryInDays").value(passwordExpiryInDays);
}

jw.endObject();
}catch(Exception e){
log.error("Error", e);
throw new ServletException(e);
}
}

@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");

try{
ResourceResolver resolver = request.getResourceResolver();
JackrabbitSession session = (JackrabbitSession)resolver.adaptTo(Session.class);

Authorizable user = session.getUserManager().getAuthorizable(session.getUserID());
Node node = resolver.getResource(user.getPath()).adaptTo(Node.class);

//set the last changed date to time on server
node.setProperty("passwordLastChanged", Calendar.getInstance());
session.save();

JSONWriter jw = new JSONWriter(response.getWriter());
jw.object().key("success").value("success").endObject();
}catch(Exception e){
log.error("Error", e);
throw new ServletException(e);
}
}
}


On Classic UI, we need to add the Password Expiry (days) widget to user form and necessary logic to mask the UI when user password has expired, so create a clientlib

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/password-expiration

2) Create node /apps/password-expiration/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.security

3) Create file (nt:file) /apps/password-expiration/clientlib/js.txt and add

                       password-expiration.js

4) Create file (nt:file) /apps/password-expiration/clientlib/password-expiration.js and add the following code

CQ.Ext.ns("ExperienceAEM.PasswordMgmt");

ExperienceAEM.PasswordMgmt = {
getChangePasswordDialog: function(userPath, passwordExpiryInDays){
var dialogCfg = CQ.Util.copyObject(CQ.UserInfo.PASSWORD_DIALOG_CFG);

dialogCfg.title = "Password Expired";
dialogCfg.buttons = CQ.Dialog.OK;
dialogCfg.closable = false;

dialogCfg.ok = function(){
var dialog = this;

var find = function(panel, name){
return panel.findBy(function(comp){
return comp["name"] == name;
}, panel);
};

var currentPassword = find(dialog, ":currentPassword");
var passwords = find(dialog, "rep:password");

if(currentPassword[0].getValue() && passwords[0].getValue() && passwords[1].getValue()){
if(passwords[0].getValue() == passwords[1].getValue()){
var options = {
_charset_: "utf-8",
":status": "browser",
":currentPassword": currentPassword[0].getValue(),
"rep:password": passwords[0].getValue()
};

$.ajax({
url: userPath + ".rw.html",
dataType: "html",
data: options,
success: function(){
$.ajax({
url: "/bin/experience-aem/pm/expiry",
dataType: "json",
success: function(){
CQ.Notification.notify("Password changed","New password expires in " + passwordExpiryInDays + " days");

dialog.close();
CQ.Ext.getBody().unmask();

if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) {
location.reload();
}
},
error: function(j, t, e){
alert("Error changing password, couldn't set last changed date");
},
type: 'POST'
});
},
error: function(j, t, e){
alert("Either the old password is incorrect or there is some error");
},
type: 'POST'
});
}
}
};

return CQ.WCM.getDialog(dialogCfg);
},

showChangePasswordDialog: function(){
$.ajax({ url: "/bin/experience-aem/pm/expiry", dataType: "json", async: false,
success: function(data){
var expired = data["expired"];

if(expired){
var dialog = this.getChangePasswordDialog(data["userPath"], data["passwordExpiryInDays"]);
CQ.Ext.getBody().mask();

if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) {
var cf = CQ.WCM.getContentFinder();

if(cf){
cf.getEl().mask();
}

if (CQ.WCM.isEditMode() || CQ.WCM.isDesignMode()) {
CQ.WCM.on("sidekickready", function(sk){
sk.getEl().mask()
});
}
}

dialog.show();
}
}.createDelegate(this),
type: "GET"
});
},

addExpiryOptions: function(propPanel){
var userForm = propPanel.userForm;
var emailComp = userForm.find('name', 'email')[0];

var passwordExpiryInDays = {
"xtype":"textfield",
"fieldLabel": "Expire password (days)",
"anchor":"15%",
"name":"passwordExpiryInDays"
};

userForm.insert(userForm.items.indexOf(emailComp) + 1, passwordExpiryInDays);

userForm.setAutoScroll(true);
userForm.doLayout();
}
};

(function() {
var PM = ExperienceAEM.PasswordMgmt;

CQ.Ext.onReady(function(){
PM.showChangePasswordDialog();
});

if(window.location.pathname == "/useradmin"){
var fields = CQ.security.data.AuthRecord.FIELDS;
fields.push({"name": "passwordExpiryInDays"});
fields.push({"name": "passwordExpiryReminderInDays"});

var UA_INTERVAL = setInterval(function(){
var userAdmin = CQ.Ext.getCmp("cq-useradmin");

if(userAdmin && userAdmin.userProperties){
clearInterval(UA_INTERVAL);
PM.addExpiryOptions(userAdmin.userProperties);
}
}, 250);
}
})();

5) The extension structure in CRXDE




Viewing all 525 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>