本帖最后由 ustc_zzzz 于 2018-9-8 08:00 编辑

在插件中同时使用 PlaceholderAPI 和 JavaScript

这篇文章的第一个主角,PlaceholderAPI,算是插件开发平台非常常用的插件了,它可以针对特定玩家,将文字的一部分映射到不同的字符串上。至于第二个主角,JavaScript,也是插件开发者特别喜欢使用的脚本语言,因为它易于上手,几乎不需要插件使用者多少学习成本。如果说具体实现的话,Java 8 也内置了名为 nashorn 的 JavaScript 引擎,大大方便了开发者调用执行。

很多插件喜欢把 PlaceholderAPI 和 JavaScript 结合。JavaScript 本身在 Java 平台支持预编译,也就是将 JavaScript 代码解析编译后再执行,虽然会略微拖慢加载插件的速度,但能提升 JavaScript 代码,尤其是反复执行的 JavaScript 代码的执行效率,因此也值得去做。不过本人发现很少有插件同时使用 PlaceholderAPI 和预编译 JavaScript,因此本人在这里把自己同时使用这两者的一点点经验分享一下。这里本人再针对常见的一些实现方式说一说可能出现的小问题。

本文不针对任何插件平台,因此接下来出现的 Java 代码使用以下约定:

  • papi("xxxxxx", player)
    代表针对“player”返回“%xxxxxx%”对应的值。
  • escape(string)
    代表将“string”转义,通常的操作包括“'”替换成“\'”,“\”替换成“\\”等。
  • pattern
    代表匹配 PlaceholderAPI 的正则表达式,比如说可以是“Pattern.compile("[%]([^ %]+)[%]")”。
  • engine
    代表 JavaScript 引擎,nashorn 可通过“new ScriptEngineManager(null).getEngineByName("nashorn")”得到。

第一步:直接替换

一个最常见的做法是直接替换,比如这样:

  1. StringBuffer sb = new StringBuffer();
  2. Matcher m = pattern.matcher("%player_name% === 'Notch'");

  3. while (m.find()) m.appendReplacement(sb, "'" + papi(m.group(1), player) + "'");
  4. Object result = engine.eval(sb.toString());
复制代码

这种做法就是直接替换了,在这里的话,也就是把“%player_name%”直接替换成玩家名字,比如说“Dinnerbone”,然后整个表达式变成了“'Dinnerbone' === 'Notch'”,直接执行就可以比较了。有的插件替换时甚至不会加上两边的引号,由配置文件的编辑者自己写成诸如“'%player_name%' === 'Notch'”的形式。

但这显然有一个问题:不安全。比如说,如果我把我的名字设置成:“xxxx', player.setOp(true), 'xxxx”,那最后表达式就变成了“'xxxx', player.setOp(true), 'xxxx' === 'Notch'”。这段表达式可以正常执行,但是它执行的时候,就把目标玩家设置成 OP 了。

这种攻击有一个成形的术语,被称作“代码注入”(Code Injection)。当然了,玩家名字不会出现单引号以及括号等奇怪的东西,但是世界上有成百上千种 PlaceholderAPI 呢,开发者有足够的勇气,保证所有的 PlaceholderAPI 都不会被玩家恶意利用吗?

第二步:转义

这种问题的解决方案自然是转义(Escaping):

  1. StringBuffer sb = new StringBuffer();
  2. Matcher m = pattern.matcher("%player_name% === 'Notch'");

  3. while (m.find()) m.appendReplacement(sb, "'" + escape(papi(m.group(1), player)) + "'");
  4. Object result = engine.eval(sb.toString());
复制代码

现在的话,如果我把我的名字设置成了:“xxxx', player.setOp(true), 'xxxx”,那么在处理后,表达式将会是:“'xxxx\', player.setOp(true), \'xxxx' === 'Notch'”。由于中间的两个引号进行了转义,因此即使是这种名字,我也不需要担心玩家会恶意利用某个 PlaceholderAPI 执行我不想执行的代码了。

我们现在回到预编译 JavaScript 的问题。我们注意到,如果我们想要执行这段代码,我们需要将代码用 PlaceholderAPI 处理一遍,而由于玩家不同,因此如果采用这样的写法,我们根本无法预编译,再在使用的时候执行以提高效率。

第三步:暴露接口

办法自然是有的。我们完全可以把根据 PlaceholderAPI 替换的接口抽象出来,然后执行的时候提供不同的实现就可以了:

  1. StringBuffer sb = new StringBuffer();
  2. SimpleScriptContext context = new SimpleScriptContext();
  3. Matcher m = pattern.matcher("%player_name% === 'Notch'");

  4. while (m.find()) m.appendReplacement(sb, "papi('" + escape(m.group(1)) + "')");
  5. CompiledScript compiled = engine.compile(sb.toString());

  6. // end of compilation
  7. // --------------------------------
  8. // beginning of execution

  9. Function<String, Object> papiFunction = string -> papi(string, player);
  10. context.setAttribute("papi", papiFunction, ScriptContext.ENGINE_SCOPE);
  11. Object result = compiled.eval(context);
复制代码

我们在这里,把“%player_name% === 'Notch'”转换成了“papi('player_name') === 'Notch'”,将替换的接口抽象到了一个名为“papi”的方法。接着我们初始化了一个“SimpleScriptContext”,并在执行代码前动态设置它对应的“papi”的值是某个 Lambda 表达式。藉由这种方式,我们得以把预编译 JavaScript 的过程放在加载插件的时候完成,而对于执行代码来说,我们只需要事先设置一下某个全局量的值,而无需每次解析 JavaScript 代码。

总结

这里大概整理了一下本人同时使用 PlaceholderAPI 和预编译 JavaScript 的一点经验,因此文章也没指望写太长,也算不上是一篇教程。希望这篇文章能够对类似需求的人有一定帮助。

转载请联系本人。

Markdown 备份

整个主题帖使用 Markdown 编写,并使用相关工具转换为 BBCode。该部分内容为备份,和主题正文无关。