CEP 扩展必须有签名才能运行,而所谓签名是验证扩展文件是否与签名时一致的手段,能保证你的扩展不被篡改和识别扩展作者。所以你会发现修改了别人的扩展插件后,扩展就无法运行了。
不过在开发者模式下, 宿主应用(如 PhotoShop)会无视签名,关于打开发者模式,在 「 1 」Hello World! 一文中介绍了。
签名
签名分为和自签名证书(self-signed certificates)或者商业签名证书(commercial certificates),
商业签名可以(也仅可以)在下列数字签名提供商中购买:
商业证书在使用 Adobe Extension Manager 安装时不会有如下警告:
不过在 CC 2015 之后 Adobe Extension Manager 已经被移除了(Adobe 现在想让用户都从它的 Adobe Add-Ons 市场上购买、下载插件)
不用付钱我们也可以使用自签名证书,自签名证书可以使用 ZXPSignCmd 创建。
ZXPSignCmd
ZXPSignCmd 是 Adobe 官方发布的签名与打包的命令行工具,有 Windows 和 OSX 2 个平台的版本。这里先介绍使用 ZXPSignCmd 创建证书和打包的方法,如果觉得命令行工具麻烦,可以使用我制作的 GUI 版本,后面会介绍。
创建证书
ZXPSignCmd -selfSignedCert <countryCode> <stateOrProvince> <organization> <commonName> <password> <outputPath.p12>
ZXPSignCmd -selfSignedCert <国家代码> <地区> <组织名> <证书所有者名称> <证书密码> <证书名.p12>
例子:
ZXPSignCmd -selfSignedCert CN Changsha nullice.com nullice 123456 我的证书.p12>
签名并打包
ZXPSignCmd -sign <inputDirectory> <outputZxp> <p12> <p12Password> -tsa <timestampURL>
ZXPSignCmd -sign <要打包的项目目录> <输出文件路径> <证书路径> <证书密码> -tsa <时间戳服务地址>
其中 -tsa <时间戳服务地址>
不需要可以省略。
ZXPSignCmd -sign "PS.fonTags\fonTags" "PS.fonTags\我的扩展.zxp" "我的证书.p12" "123456"
要注意的是这里输出文件路径如果已经存在了一个文件的话(比如曾经打包的),ZXPSignCmd 是不会自己覆盖它的,需要自己手动删除。
打包后输出的文件是 ZIP 格式的,可以用 ZIP 解压缩工具解压。
ZXP WinGUI
在 Windows 下除了直接使用 ZXPSignCmd ,还可以使用有图形界面的 ZXP WinGUI,注意这不是 Adobe 官方的,只是我自己制作的。是否使用请谨慎判断(开发用的工具能从官方渠道获取的就尽量用官方的,这不仅仅是为了自己的安全也是为了你开发软件的用户安全负责,CEP 的能调用的本地接口很多,如果被置入恶意代码的话很危险,出现像 XcodeGhost 一样的事件就不好了)。
ZXP WinGUI 实际只是直接调用 ZXPSignCmd ,不过除了图形界面以为还有这些方便使用的功能:
- 自动清除过期的生成文件(覆盖)
- 拖放文件夹输入项目目录
- 生成打包 ZXP 的批处理
其中生成打包 ZXP 的批处理,可以在填写配置后生成一个 .bat 批处理文件,以后执行这个批处理就可以打包了。
创建证书
签名并打包
另外如果你喜欢用 gulp 的话,可以看看这篇文章: Automate ZXP Packaging with Gulp.js
修改与汉化
打包后插件目录中文件就不可以修改或者删减了,否则都会使签名验证失败,无法载入。
这意味着你的扩展不能在插件目录中存储用户数据或者下载内容。
要存储这些扩展运行中产生的数据,请存储到类似 cs.getSystemPath(SystemPath.USER_DATA)
的系统目录中去,总之就是不要让你的扩展在扩展目录中存储数据,或者修改自己的文件。
对于汉化扩展,在修改之后扩展中的文件后,需要删除扩展目录中的 META-INF
文件夹,并重新签名。
安装扩展
过于有 Adobe Extension Manager 可以来安装和管理扩展,不过它已经不会在 CC 2015 以后的版本上了,Adobe 已经停止了对这个工具的支持(Announcement: Extension Manager End of Life Notification)
Adobe 想让人们都去它的 Adobe Add-Ons 市场下载扩展,不过实际上 Adobe Add-Ons 并不好用,尤其是国内网络环境下,它需要 Adobe Creative Cloud 客户端安装扩展不仅速度慢而且很容易失败。
所以目前的扩展主流是自己发布文件让
- 用户自己复制文件到扩展安装目录
- 用户执行 .JSX、.Bat 脚本,帮助用户一键复制文件到扩展安装目录
- 自己制作一个安装器软件
- Adobe Extension Manager 的开源替代品 :ZXPInstaller
其中 ZXPInstaller 是一个功能和 Adobe Extension Manager 差不多的软件,不过 40 多 MB 的体积真不想跟用户说下个几 MB 的扩展前先装个这家伙…
比较好的方法是使用一个类似下面这样的 JSX 脚本文件来安装扩展:
(function() {
var Installer, e;
$.extensionApp = {
COMPANY_NAME: 'nullice',
CONTACT_INFO: 'ui@nullice.com',
PRODUCT_NAME: 'fonTags',
PRODUCT_ID: 'fonTags',
PRODUCT_VERSION: '1.0',
MIN_VERSION: 1,
MAX_VERSION: 99,
RELATIVE_SRC_PATH: 'com.nullice.pschen.fonTags'
}
Installer = (function() {
Installer.prototype.logString = '';
Installer.prototype.photoshopVersions = {
10: "CS3",
11: "CS4",
12: "CS5",
13: "CS6",
14: "CC",
15: "CC 2014",
16: "CC 2015"
};
Installer.prototype.isWindows = function() {
return $.os.match(/windows/i);
};
Installer.prototype.isMac = function() {
return !this.isWindows();
};
function Installer(config) {
this.config = config;
this.configure();
this.preflight();
this.copyFiles();
this.teardown();
}
Installer.prototype.configure = function(config) {
var k, v, _ref;
if (this.config == null) {
throw Error("未定义配置");
}
_ref = this.config;
for (k in _ref) {
v = _ref[k];
this[k] = v;
}
this.CURRENT_PATH = File($.fileName).path;
this.LOG_FILE_POINTER = this.createNewLogFile();
this.CURRENT_PS_VERSION = parseInt(app.version.split('.')[0]);
this.CEP_FOLDER = 'CEP';
if (this.CURRENT_PS_VERSION === 14) {
this.CEP_FOLDER = 'CEPServiceManager4';
}
this.SYSTEM_PATH = "" + Folder.commonFiles + "/Adobe/" + this.CEP_FOLDER + "/extensions";
this.LOCAL_PATH = "" + Folder.userData + "/Adobe/" + this.CEP_FOLDER + "/extensions";
this.SYSTEM_POINTER = Folder("" + this.SYSTEM_PATH + "/" + this.PRODUCT_NAME);
this.LOCAL_POINTER = Folder("" + this.LOCAL_PATH + "/" + this.PRODUCT_NAME);
this.SRC_POINTER = Folder("" + this.CURRENT_PATH + "/" + this.RELATIVE_SRC_PATH);
return this.log("Product: " + this.PRODUCT_NAME + "\nVersion: " + this.PRODUCT_VERSION + "\nPhotoshop version: " + this.photoshopVersions[this.CURRENT_PS_VERSION] + "\nOperating system: " + $.os + "\nLocale: " + $.locale + "\nInstallation source: " + this.CURRENT_PATH);
};
Installer.prototype.preflight = function() {
if (this.CURRENT_PS_VERSION < this.MIN_VERSION) {
this.error("安装失败. " + this.PRODUCT_NAME + " 需要 " + this.photoshopVersions[this.MIN_VERSION] + " 或更新的版本. ");
}
if (this.CURRENT_PS_VERSION > this.MAX_VERSION) {
this.error("安装失败. " + this.PRODUCT_NAME + " 仅支持 " + this.photoshopVersions[this.MAX_VERSION] + "版本. ");
}
if (this.SYSTEM_POINTER.exists) {
this.rm(this.SYSTEM_POINTER);
}
if (this.LOCAL_POINTER.exists) {
return this.rm(this.LOCAL_POINTER);
}
};
Installer.prototype.teardown = function() {
alert("安装完成\n\n请重启应用程序以使用 " + this.PRODUCT_NAME + ".");
return this.log("安装完成");
};
Installer.prototype.rm = function(obj) {
var file, path, _i, _len, _ref;
if (obj instanceof File || obj.getFiles().length === 0) {
path = obj.fsName;
if (obj.remove()) {
return this.log("rm " + path);
}
this.error("失败: rm " + path + " (" + obj.error + ")");
}
_ref = obj.getFiles().reverse();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
file = _ref[_i];
arguments.callee.call(this, file);
}
arguments.callee.call(this, obj);
return true;
};
Installer.prototype.cp = function(src, dest) {
var file, isFile, newDest, path, _i, _len, _ref;
if (src instanceof File) {
if (src.copy(dest)) {
return this.log("cp " + src.fsName + " -> " + dest.fsName);
}
this.error("错误: cp " + src.fsName + " -> " + dest.fsName + " (" + src.error + ")");
}
if (!dest.create()) {
this.error("创建失败: " + dest.fsName);
}
_ref = src.getFiles();
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
file = _ref[_i];
isFile = file instanceof File;
path = "" + (encodeURI(dest.fsName)) + "/" + file.name;
newDest = isFile ? File(path) : Folder(path);
arguments.callee.apply(this, [file, newDest]);
}
return true;
};
Installer.prototype.error = function(msg) {
this.log(msg);
throw Error(msg);
};
Installer.prototype.copyFiles = function() {
return this.cp(this.SRC_POINTER, this.LOCAL_POINTER);
};
Installer.prototype.log = function(msg) {
var file;
file = this.LOG_FILE_POINTER;
if (!file.open('e')) {
throw Error("无法打开日志文件");
}
file.seek(0, 2);
if (!file.writeln(msg)) {
throw Error("无法创建日志文件");
}
return true;
};
Installer.prototype.createNewLogFile = function() {
var file;
file = new File("" + this.CURRENT_PATH + "/" + this.PRODUCT_NAME + ".log");
if (!file.open('w')) {
throw Error("无法创建日志文件");
}
if (this.isMac()) {
file.lineFeed = 'unix';
}
file.encoding = "UTF8";
return file;
};
return Installer;
})();
try {
new Installer($.extensionApp);
} catch (_error) {
e = _error;
alert("安装失败:\n" + e + "\n\n可以查看脚本所在文件夹中的安装日志.");
}
}).call(this);