在托管的Joomla站点上工作时,经常需要将公司应用程序与之集成。例如,将公司的ERP系统与基于Joomla的电子商务解决方案Virtuemart集成:每天需要在线发布数百种产品,并且这些产品的价格在一天之内会多次变动。此外,使用托管站点通常也意味着没有直接的数据库访问权限。
为了解决这个问题,创建了一个Joomla组件,它可以接收远程命令并将其发送到底层数据库,并支持事务处理。这个组件的架构相当简单:它发布了一个页面,用于处理传入的XML格式消息。它执行XML消息中包含的命令,并发送回响应消息。
消息交换不使用任何标准;既不是SOAP也不是其他RPC协议。创建了一个专有的基于HTTP的协议。
由于页面是公开的,如果没有安全检查,它可能会成为一个巨大的安全漏洞。因此,引入了一个基于密钥交换的简单安全机制。组件安装后,会生成一个唯一的安全令牌。这个安全令牌必须出现在请求参数中,才能被服务器接受。
Joomla组件实现了一个管理面板和一个站点部分。管理面板显示必须使用的安全密钥,而站点页面仅处理XML请求。不会解释如何创建Joomla组件,因为已经有太多关于这个话题的教程(可以从这里开始)。
有趣的点是使用安装后脚本生成唯一的安全密钥,以及处理XML请求的站点页面。
在Joomla安装包中,名为“component.xml”的XML文件包含所有安装指令。指令<scriptfile>script.php</scriptfile>指示安装过程查找一个名为component_nameInstallerScript的类,该类包含在安装包根文件夹中的script.php文件中。
这个类的一些明确定义的public方法在安装过程中被调用:function install($parent)在安装过程中调用。在这里,可以指定在这个阶段执行的附加步骤;function uninstall($parent)在卸载过程中调用;function update($parent)在更新过程中调用;function preflight($type, $parent)在安装和更新之前调用;function postflight($type, $parent)在安装和更新之后调用。
在postflight函数中使用,以写入包含安全令牌的XML文件:
function postflight($type, $parent) {
$string = '
站点部分由一个控制器管理,该控制器仅处理XML输出。请求必须指定URL参数“format=xml”。它注册了一个名为"cmdep.execcmd"的函数,用于处理请求并准备要发送回的XML响应。
这个函数的第一步是安全检查。如果令牌不匹配,它会回复错误。一旦验证了安全令牌,函数就读取请求的内容,并针对指定的每个命令执行所需的操作。
让看看消息结构:
<?xml version="1.0" encoding="utf-16"?>
<msg-req xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<commands>
<command type="sql" action="select" mandatory="false"></command>
<command type="sql" action="select" mandatory="false"></command>
</commands>
</msg-req>
如所见,msg-req包含一个commands集合,然后是一个或多个command。整个过程包含在一个事务中;每个command可以指定是否是事务的一部分。
一个command,现在只能是SQL语句,可以指定一些参数:"mandatory"参数指示command必须成功才能完成事务。如果设置为true并且命令失败,事务将回滚。"action"参数指示要执行的SQL语句类型。管理的类型有"INSERT"、"UPDATE"、"DELETE"、"SELECT"、"CALL"。参数值与命令的实际内容之间没有语义检查,这个参数只管理SQL命令的执行方式以及它将如何返回执行结果。因此,"INSERT"命令将返回最后插入的自动生成的ID,而"UPDATE"或"DELETE"命令返回受影响的行数。
"save"参数用于指定一个变量名,返回值将临时保存以供下一个命令使用。例如:
"TABLE1"有3个字段:"ID"是一个自动生成的数字字段,也是主键,后面跟着两个文本字段"FIELD1"和"FIELD2"。
"TABLE2"也有3个字段:一个自动生成的"ID","REF_ID"是"TABLE1"中"ID"字段的引用,以及类型为文本的"FIELD1"。
如果需要在这些表中插入相关记录,必须创建2个INSERT命令,强制(所以事务只有在两个命令都成功执行时才提交),第一个命令将返回值保存在名为"SAVED_ID"的变量中,第二个命令在下一个SQL语句中引用该变量,形式为"$[SAVED_ID]":
<command type="sql" action="insert" mandatory="true" save="ID">
<![CDATA[
INSERT INTO TABLE1 (FIELD1, FIELD2) VALUES ('VALUE1', 'VALUE2')
]]>
</command>
<command type="sql" action="insert" mandatory="true">
<![CDATA[
INSERT INTO TABLE2 (REF_ID, FIELD1) VALUES ($[SAVED_ID], 'VALUE2')
]]>
</command>
</code>
当第二个命令执行时,所有形式为$[...]的参数都会被替换为相应的值。因此,$[SAVED_ID]的值被替换为第一个语句中保存的值。
在PHP中,这个参数替换是通过使用关联数组实现的。返回值被保存到一个全局数组中,使用"saved"参数的值作为键:
$res_values = array();
$cmd_attr = $cmd->attributes();
$save = (string)$cmd_attr['save'];
if($save!=null) $res_values[$save]=$query_result[0];
然后,SQL语句中所有匹配$[...]的参数都会被替换,查询数组:
$tmp = (string)$cmd;
$ct = preg_match_all("/\$\[((?:\[\S*\]|[^\[])*)\]/", $tmp, $arr);
for($i=0; $i<$ct; $i++){
$tmp = str_replace($arr[0][$i], $res_values[$arr[1][$i]], $tmp);
}