2.2 通过 plugin.php 暴露页面

不用在后台配模块,文件一放就能用 plugin.php 打开自己的页面。

先说结论:不注册也能访问

2.1 讲了要在后台配模块才能挂到导航上。但有一条捷径经常被忽略:只要 .inc.php 文件存在于 source/plugin/ 下,plugin.php 就能直接把它跑起来,不需要后台做任何配置。

plugin.php 是插件入口脚本,id 参数定位要执行的文件,规则只有两条:

URL 写法 实际执行的文件
plugin.php?id=xxx:yyy source/plugin/xxx/yyy.inc.php
plugin.php?id=xxx 两者同名时的简写

冒号左边是插件标识符(插件目录名),右边是模块名(.inc.php 的文件名,不含后缀)。两者完全相同时可以全省——plugin.php?id=hello 与 plugin.php?id=hello:hello 等价。

这条规则的价值在于解耦:功能代码和菜单配置彻底分开。交付插件时代码里写死 plugin.php?id=你的插件:你的模块,站长想挂在哪儿就挂在哪儿,代码一行都不用动。

建一个 hello 插件

动手做一遍。新建目录 source/plugin/hello/,在里面建 hello.inc.php:

<?php
// source/plugin/hello/hello.inc.php
defined('IN_DISCUZ') or exit('Access Denied');

// 1. 取参数,没传就用默认值
$name = isset($_GET['name']) ? trim(daddslashes($_GET['name'])) : 'world';

// 2. 校验:空值直接踢回去
if ($name === '') {
    $name = 'world';
}

// 3. 组装模板变量
$tpltitle = 'Hello';

// 4. 渲染模板,参数一就是模板名
include template('hello');

四步的顺序固定:取参数 → 校验 → 赋值 → 渲染,中间任何一步都不能省,尤其第二步。

模板文件放在 source/plugin/hello/template/hello.htm,和脚本同名:

&lt;!-- source/plugin/hello/template/hello.htm --&gt;
{template common/header}
&lt;p&gt;Hello, {$name}!&lt;/p&gt;
{template common/footer}

模板里直接写 {$name} 就行——template() 会把当前作用域的变量交给模板编译,不需要声明数组。

建完刷新,浏览器里直接敲 plugin.php?id=hello 就能看到页面。带参数就是 plugin.php?id=hello&name=Discuz,页面上显示 Hello, Discuz!。

三种参数来源要分清

Discuz! 里能拿到用户输入的地方不止一个,混用容易出安全漏洞:

来源 写法 适合
URL 查询串 $_GET['xxx'] 分页、筛选、简单标识
表单提交 $_POST['xxx'] 写操作、批量提交
系统统一变量 $_G['xxx'] 全站配置,不要用 $_GET 覆盖
关键区别在 $_G。核心初始化的 class_core.php 会把全局配置、登录状态、当前版块信息统统塞进 $_G,这是代码里唯一该信任的来源。$_GET 是用户可以随便伪造的,拿它做权限判断、拼 SQL、读文件路径之前都得先转义。

Discuz! 提供了 daddslashes() 处理入库前的转义,dhtmlspecialchars() 处理输出前的转义。顺序不能颠倒:入库前 daddslashes(),输出前 dhtmlspecialchars()。

模板怎么被找到的

include template('hello') 里的 'hello' 不带路径、不带后缀,Discuz! 会自己去插件的 template/ 子目录找同名文件,再按需去当前模板套系里找覆盖版,找不到就退回默认模板。

// 这两种写法都指向 source/plugin/hello/template/hello.htm
include template('hello');
include template('source/plugin/hello/template/hello.htm');

除非要手动指定绝对路径,否则用第一种。模板编译产物缓存在 data/cache/,改完模板记得更新缓存,否则看到的是旧版本。想让页面带上统一的页头页脚,模板里加 {template common/header} 和 {template common/footer},这两行是运行时引入。

权限与校验必须写在代码里

用 plugin.php 直达时最容易忽略这条:能通过 URL 访问到你的文件,等于任何人都能访问到。后台的使用等级设置在这里连菜单过滤都不生效,因为它压根没进后台配置。一个管理页面的正确开头:

<?php
defined('IN_DISCUZ') or exit('Access Denied');

// $_G['uid'] 非 0 即已登录
if (!$_G['uid']) {
    showmessage('请先登录', 'member.php?mod=logging&action=login');
}

// $_G['adminid'] 1~3 为管理身份,0 是普通用户
if (!$_G['adminid']) {
    showmessage('抱歉,您没有权限使用此功能');
}

// 参数白名单校验,不要直接拿请求参数当表名或文件名
$action = isset($_GET['action']) ? $_GET['action'] : '';
if (!in_array($action, array('list', 'detail'), true)) {
    showmessage('参数错误');
}

$_G['adminid'] 是最省事的判断依据:0 普通用户、1 论坛管理员、2 超级版主、3 论坛版主。更细的权限(版主能不能管某个版)得查用户组权限。

showmessage() 是 Discuz! 的标准提示函数,第一个参数是提示文字,第二个是跳转地址。所有异常出口都应该走 showmessage(),别用 echo 拼 HTML 输出,那会跳过头部模板,页面上会出现没有导航的裸页面。

X3.4 起的变化:plugin.php 也能挂钩子

早期版本里,plugin.php 只是个转发器,不参与钩子机制。X3.4 起 plugin.php 可以作为 CURSCRIPT 参与钩子调用,意思是你的插件页面里也能用 {hook/xxx} 插模块。类名是把扩展名去掉写成 plugin,放在 source/plugin/hello/hello.class.php 里:

// plugin 这个后缀指的是 CURSCRIPT = plugin.php
class plugin_hello_plugin extends plugin_hello {
    function plugin() {}                 // 本页模块执行前
    function plugin_output($value) {}    // 模块执行完、模板输出前
    function plugin_message($value) {}   // showmessage() 执行时
}

完整的方法语义见 2.3。

常见错误速查

现象 原因 处理
404 标识符或模块名拼错 核对目录名与文件名
页面空白且无报错 模板不存在或缓存没更新 核对 template/ 下同名文件,更新缓存
页面出现裸 PHP 源码 模板没走 template() 一律用 include template()
普通用户能看到管理内容 没做权限校验 开头加 $_G['adminid'] 判断
提交后出现单引号报错 入库前没转义 用 daddslashes()
输出时出现 &lt;script&gt; 输出前没转义 用 dhtmlspecialchars()

小结

plugin.php?id=标识:模块 定位 source/plugin/标识/模块.inc.php,标识与模块同名时可简写。这个入口不依赖后台配置,是插件页面最省事的落地方式。

四步流程记牢:取参数、校验、赋值、渲染。权限判断永远写在代码里,不指望后台等级。2.3 讲另一条路:不新增页面,直接插进系统现有的页面里。