2.1 Discuz! X5.0:新增插件接口 第1部分
本篇涵盖:新增系统函数 ~ 分类信息类型
新增系统函数
DISCUZ_PLUGIN()
X5.0 中新增了 DISCUZ_PLUGIN() 函数,代替类似“/source/plugin/xxx”目录的写法,例如:
require_once DISCUZ_PLUGIN('xxx').'/index.php';
提示:如果您的插件要同时兼容 Discuz!ᵂ 您必须使用此函数,但是,涉及前端调用的路径写法是 URI 的一部分,而非后台目录文件调用,无需修改。
DISCUZ_TEMPLATE()
X5.0 中新增了 DISCUZ_TEMPLATE() 函数,代替类似“/template/xxx”目录的写法,例如:
$f = DISCUZ_TEMPLATE('xxx').'/data.htm';
提示:如果您的插件要同时兼容 Discuz!ᵂ 您必须使用此函数,但是,涉及前端调用的路径写法是 URI 的一部分,而非后台目录文件调用,无需修改。
新增系统常量
DISCUZ_DATA
X5.0 中新增了 DISCUZ_DATA 常量,代替类似“/data”目录的写法,例如:
$lock = DISCUZ_DATA.'cache/data.php';
提示:如果您的插件要同时兼容 Discuz!ᵂ 您必须使用此函数。由于 Discuz!ᵂ 中所有永久性文件都会存入对象存储,因此 DISCUZ_DATA 目录禁止写入永久文件,否则审核员有权将应用打回。
DISCUZ_ROOT_STATIC
对于 Discuz!ᵂ,由于 /data 目录禁止写入,您可以把永久文件写入到此目录中,它有以下特性:
- 此目录与 DISCUZ_ROOT 意义相同,但只有存放的静态文件(md、txt、js、css、json、xml、图片等)可对外展示;
- 如保存的文件和实际产品文件及路径相同,不会产生覆盖效果,也不会对外展示;
- 此目录占用的空间将计算到 Discuz!ᵂ 用户的平台存储中;
提示:此常量只针对 Discuz!ᵂ 有实际作用,X5.0 开源版此常量与 DISCUZ_ROOT 相等。
多语言
Discuz!ᵂ 原生支持多语言内核,您只需自定义多语言 key 并设置此 key 对应的语言包文件路径即可
设置自定义语言
i18n('set', 'mylang', DISCUZ_ROOT.'./i18n/en');
设置多语言 mylang 的语言包文件路径为 DISCUZ_ROOT.'/i18n/en' 目录,支持插件路径
读取设置的自定义语言列表
i18n('get');
把设置的语言 key 赋值给 $_G['cookie']['i18n'] 即可实时切换语言
其他第三方插件若要兼容 i18n 对应的语言 key,参考此范例 https://gitee.com/Discuz/DiscuzXPluginSample/tree/master/sample/i18n/mylang
语言包文件
X5.0 的插件文件中可以直接写语言包文件,而不必写在 XML 中,之前写在 XML 中的语言包在更新缓存后会自动写入到语言包文件中,语言包文件位于为 /source/plugin/myplugin/i18n/SC_UTF8/lang_plugin.php
$scriptlang['myplugin'] = [
'nice' => '不错!',
];
$templatelang['myplugin'] = [
'nice' => '不错!',
];
$systemlang['myplugin'] = [
'nice' => '不错!',
];
$installlang['myplugin'] = [
'name' => '插件名称',
]
XML 中的 name、license、description、intro、copyright、version、var.title、var.description、var.extra、modules.menu 可以写进 $installlang 中
同时,语言包也兼容 i18n,可以针对相应的 i18n 语言定制额外语种的语言包,兼容 i18n 的语言包文件位为 /source/plugin/myplugin/i18n/en/lang_plugin.php,我们统一约定用2个字母的缩写代表各个语言,例如 en、de、fr 等等
JS 语言包
JS 语言包为 X5.0 新增的语言包类型,文件名为 lang_js.php,默认语言包位于 /source/plugin/myplugin/i18n/SC_UTF8/ 目录下,语言包内容参考以下格式:
$lang = [
'jslang' => 'JS 语言包',
];
所有调用了 common.js 的页面中,在您的 JS 文件中通过 $L('jslang') 方式调用 JS 语言包:
function sample_notice() {
alert($L('jslang'))
}
自定义后台菜单
这是 Discuz!ᵂ 中的新增内容,插件无需添加 menu 扩展,即可实现菜单的添加与删除
您所添加的菜单请务必在卸载插件后清理完毕
准备工作
如果在非安装、卸载脚本中使用,最好是判断下方法是否存在 示例:
if(!function_exists('set_admin_menu')){
include_once libfile('function/plugin');
}
添加菜单
完整示例:
if(!function_exists('set_admin_menu')){
include_once libfile('function/plugin');
}
$menus = array(
array('侧边菜单1','plugins?id=1'),
array('侧边菜单2','plugins?id=2'),
);
set_admin_menu('顶部菜单名称', $menus);
删除菜单
完整示例:
if(!function_exists('remove_admin_menu')){
include_once libfile('function/plugin');
}
remove_admin_menu('顶部菜单名称');//单独删除
remove_admin_menu(array('顶部菜单名称1','顶部菜单名称2'));//批量删除
自定义新平台
这是 Discuz!ᵂ 中的新增内容,开发者可直接添加一个平台管理中心,在这里,您可以完全设计整个后台的菜单以及界面。同时在“站长”->“多平台管理” 站长还能自行调整。
XML 格式规范
这是平台配置 XML 文件的模板:
<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
<name><![CDATA[新平台]]></name>
<title><![CDATA[新平台]]></title>
<framecss><![CDATA[xxx.css]]></framecss>
<pagecss><![CDATA[ccc.css]]></pagecss>
<logo><![CDATA[<a class="logo"><img src="static/image/admincp/logo.svg"></a>]]></logo>
<navbar><![CDATA[<form></form>]]></navbar>
<defaultId><![CDATA[默认首页的menuId]]></defaultId>
<menu>
<menuId>主菜单1</menuId>
<sub>
<subId>action_operation_do1</subId>
<title>子菜单1</title>
</sub>
<sub>
<subId>action_operation_do2</subId>
<title>子菜单2</title>
</sub>
</menu>
<menu>
<menuId>主菜单2</menuId>
<sub>
<subId>plugin_id:pmod1</subId>
<title>子菜单3</title>
</sub>
<sub>
<subId>plugin_id:pmod2</subId>
<title>子菜单4</title>
</sub>
</menu>
<userdef><![CDATA[1]]></userdef>
</root>
- name:平台名称,在多平台列表中展示的名称;
- title:平台网页标题,显示在浏览器标题栏中的名称;
- location: 跳转 URL,如果只想在多平台菜单中添加一个跳转链接,而不使用 X5 自带的后台时可使用此参数
- logo:左上角 LOGO 区域 HTML;
- navbar:右侧搜索区域 HTML;
- framecss:框架页 CSS,可直接写 CSS 代码,也可以写文件 URL (.css 结尾);
- pagecss:内容页 CSS,规则同 framecss;
- menu:主菜单节点,多个主菜单按照顺序书写;
- menuId:菜单 ID、外显名称,可写语言包名或中文;
- sub 子菜单节点;
- subId:子菜单 ID,格式“action_operation_do”,action、operation、do 为相应页面 GET 参数拼接的内容,opeartion、do 可省略;如果是跳转到插件页面,格式为 “plugin_id:pmod”,id 为插件唯一标识符 ID,pmod 为后台模块的参数,pmod 可省略;
- title:外显名称,可写语言包名或中文;
- type:1=区域开始,2=区域结束;
- showMethod:显示此菜单项的条件,外调方法,系统值不建议修改。插件用 id::method 方式调用具体方法,系统会调用 plugin/id/ 中 class platform_id 的静态方法 method();
- listMethod:菜单显示内容由具体方法返回,格式同 showMethod;
- subPerms:当前菜单项关联的其他子菜单 ID,多个用逗号“,”分割, ID 格式见 subId;
添加平台
$xml = '<?xml version="1.0" encoding="ISO-8859-1"?>
...
';
menu::platform_add('test', $xml); // test 为平台标识,$xml 为配置数据
删除平台
menu::platform_del('test');// test 为平台标识
platform.class.php 详解
开发者需书写一个 platform.class.php 脚本
plugin/myplugin/platform.class.php 完整示例:
class platform_myplugin {
// myplugin:showMethodSample
public static function showMethodSample() {
return true;
}
// myplugin:listMethodSample
public static function listMethodSample() {
return array(
array('menu_members_edit', 'members_search', 0, '', '', array('members_clean', 'members_repeat')),
array(cplang('nav_home'), '', 1, 'homestatus'),
array('menu_maint_doing', 'doing', 0, 'doingstatus'),
array('menu_maint_share', 'share', 0, 'sharestatus'),
array(cplang('nav_home'), '', 2, 'homestatus'),
);
}
}
showMethodSample 为 showMethod 的范例,返回 true/false 即可,true 时菜单项显示 listMethodSample 为 listMethod 的范例,返回的内容会替换掉相应位置中的数据。其中返回的数组与 XML 中 sub 下节点的对应关系为:
0:title 1:subId 2:type 3:showMethod 4:listMethod 5:subPerms
第三方登录
由于 X5.0 增加了全新的账号管理系统,通过此全新的账号平台,开发者可很方便的开发出自己的登录系统
第三方登录类固定的文件为 /acccount.class.php,例如插件目录为 sample,那么文件为 /source/plugin/sample/account.class.php
class account_sample extends account_base {
// 不自动生成头像
public bool $interface_noAutoAvatar = true;
// 不支持绑定
public bool $interface_noBind = false;
public function __construct() {
$this->conf = parent::getConfig('plugin_sample');
}
public static function name() {
return '测试登录';
}
public static function icon() {
return '<svg t="1715821283732" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28056" width="20" height="20"><path d="M840.7 626.2c-33.3 33.3-144 23.5-144 23.5s-9.7-110.7 23.5-144c33.3-33.3 77.3-0.5 77.3 43.2 44.3 0 76.5 44 43.2 77.3zM611.2 560.7c-29.7 0-53.9-24.3-53.9-53.9V245.3c0-29.7 24.3-53.9 53.9-53.9 29.7 0 53.9 24.3 53.9 53.9v261.5c0 29.7-24.3 53.9-53.9 53.9zM226.3 614.7c-29.7 0-53.9-24.3-53.9-53.9V338.5c0-29.7 24.3-53.9 53.9-53.9 29.7 0 53.9 24.3 53.9 53.9v222.2c0.1 29.7-24.2 54-53.9 54zM482.9 551.5c-29.7 0-53.9-24.3-53.9-53.9V121.2c0-29.7 24.3-53.9 53.9-53.9 29.7 0 53.9 24.3 53.9 53.9v376.3c0 29.7-24.2 54-53.9 54zM354.6 571.5c-29.7 0-53.9-24.3-53.9-53.9v-337c0-29.7 24.3-53.9 53.9-53.9 29.7 0 53.9 24.3 53.9 53.9v337c0.1 29.7-24.2 53.9-53.9 53.9zM445.7 960c-67.4 0-130.8-26.2-178.4-73.9-47.7-47.7-73.9-111-73.9-178.4 0-18.2 14.8-33 33-33s33 14.8 33 33c0 102.8 83.6 186.4 186.4 186.4 102.8 0 186.4-83.6 186.4-186.4 0-18.2 14.8-33 33-33s33 14.8 33 33c0 67.4-26.2 130.8-73.9 178.4S513.1 960 445.7 960z" fill="#E95431" p-id="28057"></path></svg>';
}
// 通知发送
public function notificationAdd($touid, $note, $notestring) {
}
// 用于自动登录,判断是否在环境中
public function inEnv() {
return isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'xxx');
}
// 登录(绑定)跳转
public function login($referer = '', $op = 0) {
if(empty($_POST)) {
echo
<<<EOF
<form method="post" action="$_SERVER[PHP_SELF]?method=plugin_sample">
User:<input name="user"><br />
Pass:<input name="pass" type="password"><br />
<input type="submit">
</form>
EOF;
} else {
$data = array(
'test1' => array('pass' => '1', 'uid' => 'sampleUid1'),
'test2' => array('pass' => '1', 'uid' => 'sampleUid2'),
);
if(!isset($data[$_POST['user']])) {
echo 'User not found';
exit;
}
if($data[$_POST['user']]['pass'] != $_POST['pass']) {
echo 'Password error';
exit;
}
$callback = $this->conf['callbackUrl'].'&uid='.$data[$_POST['user']]['uid'];
echo '<a href="'.$callback.'">callback</a>';
}
exit;
}
// 登录回调,/api/account/callback.php?id=sample 会调用此方法
public function getLoginUser() {
global $_G;
$account = new account();
$param = array(
'type' => 'plugin_sample',
'atype' => parent::getAccountType('sample'),
'account' => $_GET['uid'],
'bindname' => $_GET['uid'],
);
if(!$account->checkUser($param)) {
if($_G['uid']) {
$account->userBind($_G['uid'], $param);
} else {
$param += array(
'username' => $_GET['uid'],
'password' => '',
);
$msg = $account->userRegister($param);
if($msg) {
if($msg == 'profile_username_duplicate' || $msg == 'profile_email_duplicate') {
dheader('Location: '.$_G['siteurl'].'?index=member&mod=register');
}
showmessage($msg);
}
}
} else {
$account->userLogin();
}
dheader('Location: '.$_G['siteurl']);
}
// 管理后台
public function admincp() {
global $_G;
$conf = $_G['setting']['account_plugin_confs']['sample'];
if(!submitcheck('submit')) {
showformheader('account&method=plugin_sample');
...
showformfooter();
} else {
...
cpmsg('setting_update_succeed', 'action=account&method=plugin_sample', 'succeed');
}
}
}
第三方登录插件需要在插件安装的时候通过以下方法注册:
account_base::registerAccount('sample');
卸载的时候取消注册:
account_base::unregisterAccount('sample');
第三方登录最完整的参考范例可直接阅读 X5.0 内置的 QQ 登录和微信登录(位于 /source/class/account 目录下),类中方法及用法是完全一致的
DIY 模块
X5.0 允许通过插件拓展 DIY 的模块,您需要把 DIY 模块文件写到 /source/plugin/myplugin/block 目录下即可,目录中文件的内容同系统 /source/class/block。
子文件
由于 X5.0 用子文件 /source/child 代替了原先的 /source/include 目录,因此您可以通过注册接管的方式完全代替任何一个系统子文件的原始调用,您需要把子文件写到 /source/plugin/myplugin/child 目录下。
<?php
//childfile:forum/viewthread/postarr
if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}
...
文件中添加 //childfile:...... 格式的注释,注册需要接管子文件。您可以在自己接管的子文件中,完全代替系统原有的逻辑,也可以继续调用系统原有逻辑,并在其之前、之后调用自己的逻辑。
forum/viewthread/postarr 表示接管 /source/app/forum/child/viewthread/postarr.php 文件。
global/core/ip 表示接管 /source/child/core/ip.php 文件。
友情提示:由于子文件接管的特殊性,同一个子文件有且只有一个插件接管。admin/* 禁止接管。
秘籍提示:更多全局 childfile 请搜索 'global/core',都可以用插件接管哦!
后台二次校验脚本
后台二次校验脚本为一个特殊的 childfile,插件可通过接管 global/adminvalidate/[name] 实现后台的二次校验,加强后台的安全
在 /source/plugin/myplugin/child/ 目录下添加一个脚本
<?php
//childfile:global/adminvalidate/xxxx
//二次登录逻辑
详细范例见 sample 插件。
后台二次校验模式开启方法:
删除 “admin.php” 文件,然后在 config_global.php 中设置
$_config['admincp']['validate']['method'] = 'sample';
通过 https://website/?app=admin 访问后台管理中心
针对登录还有配套的退出
$_config['admincp']['logout']['method'] = 'sample';
对应下面的 childfile
//childfile:global/adminlogout/xxxx
日志系统
X5.0 对系统的日志进行了全面的优化和开放,您可以把插件的日志接入到此处,站长可以在“操作日志”处统一查看。
添加日志的方法通过以下系统函数
logger('myplugin:text', $_G['member'], $_G['member']['uid'], array('p' => $text, 't' => $time));
myplugin:text 中,myplugin 为插件ID,text 为自定义日志类型。后续参数可酌情添加。
日志名称的语言包为
$scriptlang['sample']['log_text'] = 'text日志';
日志后台文件写到 /source/plugin/myplugin/log 目录下,文件名固定为 log_日志类型.php,例如上例中的 log_text.php
<?php
if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
exit('Access Denied');
}
showtableheader('', 'fixpadding');
showtablerow('class="header"', array('class="td23"', 'class="td24"', 'class="td23"', 'class="td23"', 'class="td23"'), array(
cplang('time'),
'text',
cplang('logs_device'),
));
foreach($logs as $k => $logrow) {
$data = json_decode($logrow['data'], true);
$device = json_decode($logrow['device'], true);
showtablerow('', array('class="smallefont"', 'class="smallefont"', 'class="bold"', 'class="smallefont"', 'class="smallefont"'), array(
dgmdate($logrow['dateline']),
is_array($data['p']) ? print_r($data['p'], 1) : $data['p'],
$_G['group']['allowviewip'] ? 'ClientIP: '.$device['client_ip'].'&nbsp;&nbsp;<a href="javascript:;" onclick="togglelog('.$logrow['id'].')">'.cplang('more').'</a>' : '-',
));
echo showdevice($logrow['id'], $device, 6);
}
管理中心小组件
X5.0 的管理中心首页可通过小组件的方式进行拓展,要增加小组件您需要将脚本写到 /source/plugin/myplugin/admin/admin_widget.php 文件中,文件结构为:
<?php
namespace myplugin;
if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
exit('Access Denied');
}
class admin_widget {
public static function widget_a() {
}
public static function widget_b_left() {
}
...
}
admin_widget 类中可以写多个 “widget_” 开头的小组件方法,默认小组件位于右侧,如果想写左侧的小组件,请让方法名以 “_left” 结尾。
设置项组件
X5.0 中可以定义自己的设置项,要增加设置项组件您需要将脚本写到 /source/plugin/myplugin/admin/component 目录中,一个文件一个组件,每个文件都以 component_ 开头命名,文件结构为:
component_a.php
<?php
namespace myplugin\admin;
if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
exit('Access Denied');
}
class component_a {
var $name = '组件名称';
// 编辑时显示的内容
function show(&$var, &$extra) {
...
}
// 数据提交入口后进行打包
function serialize($params, &$value) {
...
}
// 数据重新编辑前进行解包
function unserialize($params, &$value) {
...
}
}
定义后,插件代码中可用下面的函数进行调用:
showcomponent('测试1', 'abc1', 'yellow', 'myplugin:component_a');
表单提交后可通过下面的函数对组件的数据进行序列化封装,通常系统会自动调用
serializecomponent();
插件变量的优化
-
X5.0 中,上面提到的设置项组件可直接用于插件变量、模板变量中。
-
“显示顺序”设置为负数的变量将隐藏,隐藏的变量用户无法设置,可用于应用内部使用。
-
配置在“版块”、“用户组”面板且变量名为“fields_”开头的变量,将会把变量存储在 X5.0 新增的 fields(JSON类型) 字段中。
自定义多媒体代码解析脚本
X5.0 中可以自定义 [media] 标签中视频解析的脚步,要增加新的解析脚本您需要将脚本写到 /source/plugin/myplugin/media 目录中,一个文件一个脚本,每个文件都以 media_ 开头命名,文件结构为:
media_video.php
<?php
namespace myplugin;
class media_video {
public static $version = '1.0';
public static $name = 'name';
public static $checkurl = ['video.com/'];
public static function parse($url, $width, $height) {
if(preg_match('/^https?:\/\/video.com\/(\d+)/i', $url, $matches)) {
$iframe = 'https://www.video.com/iframe/'.$matches[1];
$flv = $imgurl = '';
}
return [$flv, $iframe, $url, $imgurl];
}
}
分类信息类型
X5.0 中可以定义自己的分类信息类型,要增加新的类型您需要将脚本写到 /source/plugin/myplugin/threadtype 目录中,一个文件一个类型,每个文件都以 threadtype_ 开头命名,文件结构为:
threadtype_a.php
<?php
namespace myplugin;
if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
exit('Access Denied');
}
class threadtype_a {
var $name = '类型名称';
// 编辑时显示的内容
function show($option, $params) {
...
}
// 结果输出时显示的内容
function view($viewtype, $option, $params, $value) {
...
}
// 数据提交入口后进行打包
function serialize($params, &$value) {
...
}
// 数据重新编辑前进行解包
function unserialize($params, &$value) {
...
}
}
整理自 Discuz! 官方开放平台技术文档(open.dismall.com),由 WorkBuddy 整理发布。
