Goal
This post is on developing a
Adobe CC HTML Extension to login to
AEM 6, search and download Adobe CC assets (eg. Adobe Illustrator 18.1 files) uploaded to
AEM DAM from CC products like Illustrator, InDesign, Photoshop...
CC HTML Extensions (
short guide and
samples ) run on
Common Extensibility Platform (CEP) of Adobe Illustrator 18.1, Adobe InDesign 10.1 etc. (the best part is, if you are not/little interacting with the host in extensions, without any changes they can run on various Adobe CC products). For example (though i've not tried) there are very good changes the sample extension developed in this post will run with minor changes (like adding an entry in CSXS/manifest.xml) in PhotoShop...
For downloading AEM assets (images, content fragments) and place them on InDesign pages
check this post
Demo |
AI 18.1 IDSN 10.1 CC HTML Extension |
Extension Source |
AEM 6 Package Install
I just put together the pieces, thanks to my fantastic adobe colleagues and internet for quick code snippets
Login panel in Illustrator ( click Window -> Extensions -> Search AEM )
AEM Asset Search in Illustrator
Login Panel in InDesign
Asset Search in InDesign
Solution
AEM CRX Updates
1) Login to
CRXDE Lite (http://localhost:4502/crx/de), create folder
config.author and node
org.apache.sling.security.impl.ReferrerFilter of type
sling:OsgiConfig and set the
allow.empty property to
true (
Package Install). This setting allows the CEP panel (explained in next steps) to post credentials successfully to AEM. It's an observation that
Chromium Embedded of CEP doesn't send
referer header with requests (a bug in CEP?). To circumvent the problem we set
allow.empty to
true (it may be a security issue in publish environments; this CEP extension connects to and downloads assets from author instance, so set the configuration in
config.author folder)
CC HTML Extension
1) Create the following folder structure (
Download source). This folder structure is installed as extension. The
lib folder (containing helper files for creating the extension) is excluded from final executable created
search-aem
css
style.css
CSXS
manifest.xml
html
search-aem.html
js
CSInterface-5.2.js
login-search.js
.debug
build.xml
2) Use ant for assembling the extension for dev and distribution purposes. Ant targets to copy files to extensions folder and create installer.
Develop & Test
a. Create
CEP/extensions folder in user's home if it doesn't exist. On my windows it's C:\Users\nalabotu\
AppData\Roaming\Adobe\CEP\extensions and MAC
/Library/Application Support/Adobe/CEP/extensions
b. Set
PlayerDebugMode to
1 ( for debugging extensions the flag PlayerDebugMode needs to be set to 1)
On Windows, Open registry (Run ->
regedit), HKEY_CURRENT_USER\Software\Adobe\CSXS.5
On local MAC (/Users/nalabotu/Library/Preferences/
com.adobe.CSXS.5.plist)
c. Create a
.debug file in extension root. Add following xml; extension debugger will be available on
http://localhost:8098/ when the
extension panel is open in
Illustrator 18.1. Extension
id should be the same id added in
CSXS/manifest.xml (next steps)
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
<Extension Id="com.experience.aem.cep.search">
<HostList>
<Host Name="ILST" Port="8098"/>
</HostList>
</Extension>
</ExtensionList>
d. For development purposes we can use the following
ant target (in build.xml) to copy files over to user's extensions folder eg. C:\Users\nalabotu\AppData\Roaming\Adobe\CEP\extensions
<target name="copy">
<delete dir="${live.extensions.dir}" />
<mkdir dir="${live.extensions.dir}"/>
<copy todir="${live.extensions.dir}" failonerror="false">
<fileset dir="${basedir}">
<exclude name="*.xml"/>
<exclude name="*.iml"/>
<exclude name="lib/**"/>
<exclude name="temp/**"/>
<exclude name="docs/**"/>
</fileset>
</copy>
</target>
e. Executing
ant target
copy, copies the extension files to extensions folder (eg. C:\Users\nalabotu\AppData\Roaming\Adobe\CEP\extensions)
f. To debug the extension, open CC product (here Adobe Illustrator 18.1), make sure extension panel (
Search AEM is open), open
chrome browser and access
http://localhost:8098/. Check log files, for example on my local C:\Users\nalabotu\AppData\Local\Temp\
csxs5-IDSN.log, C:\Users\nalabotu\AppData\Local\Temp\
csxs5-ILST.log, C:\Users\nalabotu\AppData\Local\Temp\
cef_debug.log; For MAC the log files are in /Users/<user>/Library/Logs/CSXS
Distribute & Install
a. To distribute the extension for installation on user's CC products, create a
ZXP.
b.
ZXP should be
CA (Certificate authority ) signed or self signed.
c. To create a
self signed key and certificate in pkcs12 format, use the following
openssl commands (A sample certificate
experience-aem-cep.p12 is available in
extension source,
lib folder,
password is
experience-aem)
openssl req -x509 -days 365 -newkey rsa:2048 -keyout experience-aem-cep-key.pem -out experience-aem-cep-cert.pem
openssl pkcs12 -export -in experience-aem-cep-cert.pem -inkey experience-aem-cep-key.pem -out experience-aem-cep.p12
d. A signing toolkit
ZXPSignCmd (available in extension source or can be downloaded at
adobe labs ). It's different on MAC (the one available in source is Windows executable)
e. Use following
ant target
zxp (in build.xml) to create the extension installer
search-aem.zxp in extension source/temp folder
<!--
Use this target for creating zxp, installed using Adobe Extension Manager
The ZXPSignCmd library is different for MAC, the following targets use windows library
-->
<target name="zxp" if="isWindows">
<delete dir="${basedir}/temp" />
<mkdir dir="${basedir}/temp" />
<copy todir="${basedir}/temp" failonerror="false">
<fileset dir="${basedir}">
<exclude name="*.xml"/>
<exclude name="*.iml"/>
<exclude name="lib/**"/>
<exclude name="temp/**"/>
<exclude name="docs/**"/>
<exclude name=".debug"/>
</fileset>
</copy>
<exec executable="${basedir}/lib/ZXPSignCmd">
<arg line="-sign ${basedir}/temp" />
<arg value="${basedir}/temp/search-aem.zxp" />
<arg value="${basedir}/lib/experience-aem-cep.p12" />
<arg value="experience-aem" /> <!-- cert password -->
</exec>
</target>
f. To install the extension, use
Adobe Extension Manager CC. Click
Install on top right, browse to zxp folder and select
search-aem.zxp. If the certificate used is not
CA signed (ie. a self signed) Extension Manager alerts with warning...
g. The installed extension. Open Adobe Illustrator 18.1 (
Window -> Extensions) or InDesign 10.1 (
Window -> Extensions) and you should see the
Search AEM menu item
3) The most important part of the extension is
CSXS/manifest.xml. The following xml fragment of manifest says this extension runs on
Illustrator 18.1 and InDesign 10.1
<HostList>
<Host Name="ILST" Version="18.1"/>
<Host Name="IDSN" Version="10.1"/>
<!-- Add other CC products here -->
</HostList>
4) The extension declaration with
id com.experience.aem.cep.search (provided in
.debug file for debugging in browser)
<MainPath>./html/search-aem.html</MainPath> instructs Illustrator to load the html file
html/search-aem.html when extension is opened by clicking
Window -> Extensions -> Search AEM
<Extension Id="com.experience.aem.cep.search">
<DispatchInfo>
<Resources>
<MainPath>./html/search-aem.html</MainPath>
</Resources>
<Lifecycle>
<AutoVisible>true</AutoVisible>
</Lifecycle>
<UI>
<Type>Panel</Type>
<Menu>Search AEM</Menu>
<Geometry>
<Size>
<Height>400</Height>
<Width>350</Width>
</Size>
</Geometry>
</UI>
</DispatchInfo>
</Extension>
4) For AEM developers, developing the actual extension should be pretty easy, its standard html, css, javascript
5) The following code in
search-aem.html provides extension view. It loads
angular,
underscore from cdns.
CSInterface-5.2.js is a helper CEP file to interact with host (Illustrator or InDesign) to run native code, for example in later steps we code a simple script to open downloaded file )
<!DOCTYPE html>
<html lang="en" ng-app="SearchAEM">
<head>
<meta charset="utf-8">
<title>Login Search AEM</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div ng-controller="pc">
<div class="sign-in" ng-show="showLogin">
<div>
<h3>DIGITAL ASSET MANAGEMENT</h3>
</div>
<div>
<label>User Name</label>
<input type="text" ng-model="j_username" ng-enter="login()"/>
</div>
<div>
<label>Password</label>
<input type="password" ng-model="j_password" ng-enter="login()"/>
</div>
<div>
<label>DAM Host</label>
<input type="text" value="{{damHost}}" ng-model="damHost" ng-enter="login()"/>
</div>
<button type="button" ng-click="login()">Sign In</button>
</div>
<div ng-show="!showLogin">
<div class="top-left">
<input type="text" placeholder="Enter search text" ng-model="term" ng-enter="search()"/>
</div>
<div class="top-right">
<button type="button" ng-click="search()">Search</button>
</div>
<div class="results">
<div class="result-block" ng-repeat="result in results" ng-click="download(result)">
<div>
<img ng-src="{{result.imgPath}}"/>
</div>
<div>
{{result.name}}
</div>
</div>
</div>
</div>
</div>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script>
<script src="../js/CSInterface-5.2.js"></script>
<script src="../js/login-search.js"></script>
</body>
</html>
6) The
js/login-search.js provides necessary
login/search/download from AEM functionality. Discussing Angular or Underscore logic is beyond the scope of this post; JS frameworks are not mandatory for developing CC HTML extensions, if you know HTML, CSS, Javascript that's good enough
'use strict';
(function () {
var underscore = angular.module('underscore', []);
underscore.factory('_', function () {
return window._;
});
var cep = angular.module('cep', []);
cep.factory('cep', ['$window', function ($window) {
return $window.cep;
}]);
cep.service('csi', CSInterface);
var aem = angular.module('aem', ['underscore', 'cep']);
aem.service('login', [ '$http' , '_', function ($http, _) {
return {
login: function (username, password, damHost) {
var jSecurityCheck = damHost + "/libs/granite/core/content/login.html/j_security_check";
var data = {
j_username: username,
j_password: password,
j_validate: true
};
return $http.post(jSecurityCheck, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
transformRequest: function(obj) {
var params = [];
angular.forEach(obj, function(value, key){
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
});
return params.join("&");
}
});
}
}
}]);
aem.factory('search', [ '_', '$http', function (_, $http) {
return function (defaults) {
this.aem = "http://localhost:4502";
this.params = _.extend({}, defaults);
this.numPredicates = 0;
this.host = function(aem){
this.aem = aem;
return this;
};
this.fullText = function (value) {
if (!value) {
return this;
}
this.params[this.numPredicates + '_fulltext'] = value;
this.numPredicates++;
return this;
};
this.http = function(){
var builder = this;
return $http({
method: 'GET',
url: builder.aem + "/bin/querybuilder.json",
params: builder.params
});
}
}
}]);
var app = angular.module('SearchAEM', ['aem']);
app.directive('ngEnter', function () {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
});
app.controller('pc', [ '$scope', 'login', 'search', '$http', 'csi', 'cep',
function ($scope, login, search, $http, csi, cep) {
$scope.damHost = "localhost:4502";
$scope.showLogin = true;
$scope.login = function () {
if (!$scope.j_username || !$scope.j_password || !$scope.damHost) {
alert("Enter credentials");
return;
}
$scope.damHost = $scope.damHost.trim();
if ($scope.damHost.indexOf("http://") == -1) {
$scope.damHost = "http://" + $scope.damHost;
}
login.login($scope.j_username, $scope.j_password, $scope.damHost)
.success(function (data) {
$scope.showLogin = false;
})
.error(function () {
alert("Trouble logging-in, Invalid Credentials?")
})
};
var searchDefaults = {
'path': "/content/dam",
'type': 'dam:Asset',
'orderby': '@jcr:content/jcr:lastModified',
'orderby.sort': 'desc',
'p.hits': 'full',
'p.nodedepth': 2,
'p.limit': 25,
'p.offset': 0
};
$scope.search = function () {
if (!$scope.term) {
alert("Enter search term");
return;
}
$scope.results = [];
var mapHit = function(hit) {
var result;
result = {};
result.name = hit["jcr:path"].substring(hit["jcr:path"].lastIndexOf("/") + 1);
result.url = $scope.damHost + hit["jcr:path"];
result.imgPath = $scope.damHost + hit["jcr:path"] + "/jcr:content/renditions/cq5dam.thumbnail.140.100.png";
return result;
};
new search(searchDefaults).host($scope.damHost)
.fullText($scope.term)
.http()
.then(function(resp) {
$scope.results = _.compact(_.map(resp.data.hits, mapHit));
});
};
$scope.download = function(result){
$http.get(result.url, {
responseType: "blob"
}).success(function(data) {
var reader = new FileReader();
reader.onload = function() {
var filePath = csi.getSystemPath(SystemPath.MY_DOCUMENTS)
+ "/" + result.name;
cep.fs.writeFile(filePath, reader.result.split(',')[1], cep.encoding.Base64);
csi.evalScript("(function(){app.open(new File('" + filePath + "'));})();", function(){
alert("File " + result.name + " downloaded as " + filePath)
});
};
reader.readAsDataURL(data);
}).error(function() {
alert("Error downloading file");
});
};
}]);
}());
7) The following script at #183, executes
app.open call to open the downloaded file in Illustrator or InDesign. It's a simple script, provided inline to the
evalScript() function; for developing sleek panels with complex extend script logic (Extendscript is similar to javascript, used to code CC extensions) you can use
<ScriptPath></ScriptPath> in
CSXS/manifest.xml (not explained in this post).
app is a global variable available in host (Illustrator or InDesign)
csi.evalScript("(function(){app.open(new File('" + filePath + "'));})();", function(){
alert("File " + result.name + " downloaded as " + filePath)
});
8) A note on using
JQuery in CC HTML Extensions. If you get error
$ is undefined when executing jquery selectors, add
window.module=undefined in html. I am not aware of the details behind
NodeJS and JQuery ($) not able to co-exist, but having
window.module=undefined makes NodeJS unavailable in extension panel and a developer can code using JQuery
<script type="text/javascript">
window.module = undefined;
</script>