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)
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>
This comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
DeleteThis comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteAwesome! good info on this post.
ReplyDeleteThanks