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
ZXPSignCmd

创建证书

1
2
ZXPSignCmd -selfSignedCert <countryCode> <stateOrProvince> <organization> <commonName> <password> <outputPath.p12>
ZXPSignCmd -selfSignedCert <国家代码> <地区> <组织名> <证书所有者名称> <证书密码> <证书名.p12>

例子:

1
2
ZXPSignCmd -selfSignedCert CN Changsha nullice.com nullice 123456 我的证书.p12>
`

签名并打包

1
2
ZXPSignCmd -sign <inputDirectory> <outputZxp> <p12> <p12Password> -tsa <timestampURL>
ZXPSignCmd -sign <要打包的项目目录> <输出文件路径> <证书路径> <证书密码> -tsa <时间戳服务地址>

其中 -tsa <时间戳服务地址> 不需要可以省略。

1
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

Extension Manager 已死
Extension Manager 已死

Adobe 想让人们都去它的 Adobe Add-Ons 市场下载扩展,不过实际上 Adobe Add-Ons 并不好用,尤其是国内网络环境下,它需要 Adobe Creative Cloud 客户端安装扩展不仅速度慢而且很容易失败。

!()[http://cdn-styletin-old.nullice.com/img_20160317_71097-102d20954665383b.png]

所以目前的扩展主流是自己发布文件让

  • 用户自己复制文件到扩展安装目录
  • 用户执行 .JSX.Bat 脚本,帮助用户一键复制文件到扩展安装目录
  • 自己制作一个安装器软件
  • Adobe Extension Manager 的开源替代品 : ZXPInstaller

其中 ZXPInstaller 是一个功能和 Adobe Extension Manager 差不多的软件,不过 40 MB 的体积真不想跟用户说下个几 MB 的扩展前先装个这家伙…

比较好的方法是使用一个类似下面这样的 JSX 脚本文件来安装扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
(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);