Ubuntu 22.04 安装 WiKiss 0.3 指南(PHP 8.1 兼容修复实录)

在 Ubuntu 22.04 + Apache + PHP 8.1 上从 0 到 1 安装 WiKiss 0.3:依赖与权限初始化、虚拟主机、_config.php 核心配置,以及 7 类 9 处 PHP 8.1 兼容最小修复(fread TypeError、get_magic_quotes_gpc、/e 修饰符、create_function 等)全部实测实录与一键补丁脚本。

WiKissWikiPHP自托管知识库2026/10/30

Ubuntu 22.04 安装 WiKiss 0.3 指南(PHP 8.1 兼容修复实录)

WiKiss 是 TigerWiki 的直接后继者,一个单文件 index.php 驱动的极简 PHP Wiki:无数据库、页面即 pages/ 目录下的纯文本 .txt 文件、历史版本落在 historique/ 目录。官方最新稳定版 0.3 发布于 2007 年(发布包 wikiss-0.3.tgz),代码远早于 PHP 7/8 时代——本文在 Ubuntu 22.04 + Apache 2.4 + PHP 8.1 环境从 0 到 1 装通它,并给出全部 7 类 9 处最小兼容修复(每一处都有实测报错与修复后验证),全程实测可复现。

如果你还在选型,建议先读 《PmWiki 介绍与选型指南》 与 《Otter Wiki 介绍与选型指南》 对比同族 Wiki 引擎:PmWiki 同为 PHP 文件型引擎但活跃维护,Otter Wiki 则是 Python/Flask + git 的现代方案。装完后的进阶路线:日常页面操作与口令鉴权见《WiKiss 0.3 核心使用与鉴权配置详解》,页面语法与常用操作见《WiKiss 0.3 页面语法与常用操作详解》,全部配置项口径见《WiKiss 0.3 配置参数详解》。

一、环境与版本口径

项目取值
操作系统Ubuntu 22.04(Jammy)
Web 服务器Apache 2.4(apache2 2.4.52 系)
PHP8.1.2(php8.1 + libapache2-mod-php8.1 + php8.1-mbstring)
WiKiss0.3(官方发布包 0.3.tgz,约 25 KB)
部署目录/var/www/wikiss
数据目录pages/(页面)、historique/(历史备份)

WiKiss 0.3 对 PHP 的硬性要求极低:一个 PHP 运行环境加 mbstring 扩展即可。index.php 启动时会检查 extension_loaded('mbstring'),缺了 mbstring 虽不至于白屏,但中文/多字节内容的编码检测分支会失效,必须安装。

二、安装依赖

sudo apt update
sudo apt install -y apache2 libapache2-mod-php php8.1-mbstring
php -v          # 确认 PHP 8.1.x
php -m | grep -i mbstring   # 确认 mbstring 模块在列

三、下载与解压

官方下载地址(WiKiss 项目托管在 tuxfamily.org):

cd /tmp
wget http://download.tuxfamily.org/wikiss/0.x/0.3.tgz
tar xzf 0.3.tgz
sudo mv wikiss /var/www/wikiss
ls /var/www/wikiss
# COPYING  _config.php  historique  index.php  pages  plugins  style.css  template.html  tigerwiki2wikiss.php

目录职责一句话说明(后续运维都围绕它们展开):

  • index.php:唯一的入口与全部业务逻辑(路由、渲染、鉴权、保存、搜索)
  • pages/:所有 Wiki 页面,每页一个 标题.txt 纯文本文件
  • historique/:编辑历史,historique/<页面名>/<时间戳>.bak
  • plugins/:插件目录,wkp_*.php 自动加载(自带 wkp_ListPlugins、wkp_Rss、wkp_Tables)
  • _config.php:站点配置(口令、标题、语言等)
  • template.html + style.css:站点外观模板

若 download.tuxfamily.org 直连超时,走代理下载:https_proxy=http://192.168.1.62:7897 wget http://download.tuxfamily.org/wikiss/0.x/0.3.tgz(地址按自身网络替换)。文件校验基准:0.3.tgz 约 25,159 字节,解压后顶层文件如上所列。

四、权限初始化

WiKiss 通过 Web 进程写文件,pages/、historique/、plugins/ 三个目录及目录内文件都要归 Apache 运行用户(Ubuntu 上是 www-data)可写:

sudo chown -R www-data:www-data /var/www/wikiss
sudo chmod -R u+rwX /var/www/wikiss
ls -la /var/www/wikiss/pages/    # 三个自带页面:Accueil.txt / Aide.txt / AideTables.txt

权限不对的典型症状:编辑保存时页面"看起来保存了"但刷新后内容丢失,Apache error.log 出现 Could not write page! 或 fopen(): Failed to open stream: Permission denied。历史备份目录 historique/ 同理,权限缺失时保存会静默丢备份。

五、Apache 虚拟主机

最小可用配置(/etc/apache2/sites-available/wikiss.conf):

<VirtualHost *:80>
    ServerName wiki.example.com
    DocumentRoot /var/www/wikiss
    <Directory /var/www/wikiss>
        Options -Indexes
        AllowOverride None
        Require all granted
    </Directory>
    ErrorLog ${APACHE_LOG_DIR}/wikiss-error.log
    CustomLog ${APACHE_LOG_DIR}/wikiss-access.log combined
</VirtualHost>
sudo a2ensite wikiss
sudo a2dissite 000-default   # 可选:关掉默认站
sudo apachectl configtest && sudo systemctl reload apache2

WiKiss 的 URL 形态是 ./?page=页面名 查询串路由,不需要 mod_rewrite。Options -Indexes 建议保留,避免 pages/ 等目录被列目录。

六、_config.php 基础配置

复制默认配置即可生效(包内 _config.php 本身就是带默认值的可运行配置):

sudo cp /var/www/wikiss/_config.php /var/www/wikiss/_config.php.orig
sudoedit /var/www/wikiss/_config.php

实测关心的核心项(0.3 默认值 → 建议值):

变量默认值说明与建议
$PASSWORD""(空=匿名可编辑)设强口令,如 Q1w2E3r4T!;设置后编辑/保存需口令,浏览仍匿名
$WIKI_TITLEWiKiss : le Wiki Kiss站点标题,浏览器标签与 RSS 标题都取它
$START_PAGEAccueil首页页面名,中文站可改 首页
$LANGfr界面语言,仅支持内置几种语言包,无中文包
$PAGES_DIRpages/页面目录,一般不动
$BACKUP_DIRhistorique/历史备份目录,设空字符串则关闭备份(不建议)
$TIME_FORMAT%Y/%m/%d %R页面时间戳格式
$LOCAL_HOUR0时区偏移小时数,中国填 8

$PASSWORD 的鉴权机制:保存时 POST sc 字段等于口令即通过,通过后种一年期 Cookie AutorisationWiKiss = md5($PASSWORD)。所以口令强度就是全站编辑权的安全边界——0.3 没有用户体系,所有编辑者共享同一个口令(详见本专栏《WiKiss 0.3 核心使用与鉴权配置详解》)。

七、PHP 8.1 兼容修复(7 类 9 处,全部实测)

原生 WiKiss 0.3 在 PHP 8.1 上"能打开首页,但一用就崩"。以下 9 处修复覆盖浏览、搜索、保存、渲染全链路,每一处都给出触发场景与实测报错。修复原则是最小侵入:改签名、加守卫,不重写逻辑。

修复 1:移除 mbstring.func_overload 设置(index.php:86)

// 原:ini_set("mbstring.func_overload", MB_OVERLOAD_STRING);
// 改:直接注释——PHP 8.0 起该特性连同 MB_OVERLOAD_STRING 常量一起被移除
// ini_set("mbstring.func_overload",MB_OVERLOAD_STRING); // PHP8: constant removed

不注释的报错:PHP Fatal error: Uncaught Error: Undefined constant "MB_OVERLOAD_STRING"。这是首页能打开的前提修复——该行位于初始化路径,常量不存在直接致命。

修复 2:^字符 转义的 preg_replace /e(index.php:384)

// 原:$CONTENT = preg_replace("/\^(.)/Umsie", "'&#'.ord('$1').';'", $CONTENT);
// 改:
$CONTENT = preg_replace_callback("/\^(.)/Ums", function($m){ return '&#'.ord($m[1]).';'; }, $CONTENT);

/e 修饰符(把替换串当 PHP 代码执行)在 PHP 7 就已废弃、PHP 8 不再支持,preg_replace 遇到它返回 NULL 并告警 The /e modifier is no longer supported。此函数负责渲染 ^é 之类的 HTML 实体转义。

修复 3:裸常量数组键 $_GET[query](index.php:508)

// 原:htmlentities($_GET[query],ENT_COMPAT,"UTF-8")
// 改:htmlentities($_GET['query'],ENT_COMPAT,"UTF-8")

PHP 7 对未加引号的键名会先按常量查找、找不到降级为字符串并告警;PHP 8 直接抛 Uncaught Error: Undefined constant "query"。此处在搜索表单渲染路径上,搜索功能必崩。

修复 4:表格插件外层的 /e(plugins/wkp_Tables.php:62-64)

// 原:
$CONTENT = preg_replace("/((^ *\|[^\n]*\| *$\n)+)/me",
   '"<table class=\"wikitable\">".stripslashes($this->make_table("$1"))."</table>\n"', $CONTENT);
// 改:
$CONTENT = preg_replace_callback("/((^ *\|[^\n]*\| *$\n)+)/m",
   function($m){ return "<table class=\"wikitable\">".stripslashes($this->make_table($m[1]))."</table>\n"; }, $CONTENT);

wkp_Tables 插件负责 | 单元格 | 单元格 | 表格语法的外层包裹。不修的后果:渲染含表格的页面时 PHP 告警、表格区域直接消失。

修复 5:空文件句柄的 fread/fclose 致命错误(index.php:209、214)

// 原:$CONTENT = "\n" . @fread($file, @filesize($PAGES_DIR . $PAGE_TITLE . ".txt")) . "\n";
// 攮:
$CONTENT = "\n" . (($file && (@filesize($PAGES_DIR . $PAGE_TITLE . ".txt") > 0))
   ? @fread($file, @filesize($PAGES_DIR . $PAGE_TITLE . ".txt")) : "") . "\n";
...
// 原:@fclose($file);
// 攮:if ($file) @fclose($file);

触发场景(实测两连崩):搜索请求 /?action=search&query=Aide 会把 $PAGE_TITLE 置为"搜索结果"这个不存在的页面名,fopen 返回 false,但分支因 $action <> "" 仍然进入,于是 fread(false) 抛 Uncaught TypeError: fread(): Argument #1 ($stream) must be of type resource, bool given——搜索功能 500。同理新建页面/空页面路径还会触发 fclose(false) 的 TypeError 和空文件 fread($file, 0) 的 ValueError: Argument #2 ($length) must be greater than 0。注意 @ 错误抑制符压不住 TypeError/ValueError,这类异常在 PHP 8 是不能静默的。上面的守卫一次性覆盖三个变体(修复 5 与修复 7 同段代码)。

修复 6:get_magic_quotes_gpc() 已被整体移除(index.php:165、181)

// 原:if (get_magic_quotes_gpc()) fputs($file, trim(stripslashes($safe_content)));
// 攮:if (false) fputs($file, trim(stripslashes($safe_content)));

该函数在 PHP 5.4 废弃、PHP 8.0 直接删除。保存路径实测报错:Uncaught Error: Call to undefined function get_magic_quotes_gpc() in /var/www/wikiss/index.php:165。更隐蔽的是危害顺序:fopen(..., "w") 已把文件清空,随后才在此行崩溃——结果是旧内容已丢、新内容没写入,页面上只剩一个 0 字节文件。magic quotes 在 PHP 8 永远是关的,两处(正文 165、备份 181)都替换为 false 即可(或直接删掉条件分支保留 else 侧代码)。

修复 7:空文件 fread 长度守卫

见修复 5 的代码块(同一段),@filesize(...) 为 0 时 fread($file, 0) 在 PHP 8 抛 ValueError。实测触发链:修复 6 未打时保存崩溃留下 0 字节文件,随后浏览该页即崩——两个问题会连环出现。

修复 8:表格插件内层的第二个 /e(plugins/wkp_Tables.php make_table)

// 原:
$s = preg_replace('/\|(([hlrtb]* ){0,1})\s*(\d*)\s*,{0,1}(\d*)\s*(.*?)\|/e',
   '"<td".("$3"?" colspan=\"$3\"":" ").("$4"?" rowspan=\"$4\"":" ").$this->table_style("$1").">$5</td>"', $s);
// 攮:
$s = preg_replace_callback('/\|(([hlrtb]* ){0,1})\s*(\d*)\s*,{0,1}(\d*)\s*(.*?)\|/',
   function($m){ return "<td".($m[3]?" colspan=\"{$m[3]}\"":" ").($m[4]?" rowspan=\"{$m[4]}\"":" ").$this->table_style($m[1]).">{$m[5]}</td>"; }, $s);

这是 make_table() 里把单元格文本转 <td> 的内层正则。它不致命但很迷惑:preg_replace 遇 /e 返回 NULL,$s 变 NULL,最终 stripslashes($s) 返回空串——页面渲染出 <table class="wikitable"></table> 空表格且无任何报错行号可循。实测打完此补丁,三行两列表格完整渲染为 <tr><td>...</td></tr> 结构。

修复 9:create_function() 已被移除(plugins/wkp_Tables.php make_table)

// 原:
$s = preg_replace_callback(array_fill(0,$nblinks,"/\[LINK\]/"),
   create_function('$m','global $matches_links;static $idxlink=0;return "[".$matches_links[1][$idxlink++]."]";'), $s);
// 攮:
$s = preg_replace_callback(array_fill(0,$nblinks,"/\[LINK\]/"),
   function($m) use ($matches_links){ static $idxlink=0; return "[".$matches_links[1][$idxlink++]."]"; }, $s);

create_function() 本质是 eval 包装,PHP 7.2 废弃、PHP 8.0 移除。触发场景:表格单元格内含 [文字|链接] 形式的链接(make_table 会先把链接暂存为 [LINK] 占位再回填)。实测修复后 | home | [Accueil|http://example.com] | 单元格内链接正常渲染为 <a href="http://example.com">Accueil</a>。

一键补丁脚本

把下面内容存为 /tmp/patch-php8.php,在站点根目录执行 php /tmp/patch-php8.php:

<?php
// WiKiss 0.3 PHP8.1 兼容补丁(7 类 9 处)
$f = '/var/www/wikiss/index.php';
$c = file_get_contents($f);
$pairs = [
  // FIX1 mbstring.func_overload
  ['ini_set("mbstring.func_overload",MB_OVERLOAD_STRING);',
   '// ini_set("mbstring.func_overload",MB_OVERLOAD_STRING); // PHP8: constant removed'],
  // FIX2 ^char escape /e -> callback
  ['$CONTENT = preg_replace("/\\^(.)/Umsie", "\'&#\'.ord(\'$1\').\';\'", $CONTENT);',
   '$CONTENT = preg_replace_callback("/\\^(.)/Ums", function($m){ return \'&#\'.ord($m[1]).\';\'; }, $CONTENT);'],
  // FIX3 bare constant key
  ['$_GET[query]', '$_GET[\'query\']'],
  // FIX5+7 fread false/zero-length guard
  ['$CONTENT = "\n" . @fread($file, @filesize($PAGES_DIR . $PAGE_TITLE . ".txt")) . "\n";',
   '$CONTENT = "\n" . (($file && (@filesize($PAGES_DIR . $PAGE_TITLE . ".txt") > 0)) ? @fread($file, @filesize($PAGES_DIR . $PAGE_TITLE . ".txt")) : "") . "\n";'],
  // FIX5b fclose guard
  ['      @fclose($file);', '      if ($file) @fclose($file);'],
  // FIX6 magic quotes removed
  ['get_magic_quotes_gpc()', 'false'],
];
foreach ($pairs as $p) { $c = str_replace($p[0], $p[1], $c); }
file_put_contents($f, $c);

$t = '/var/www/wikiss/plugins/wkp_Tables.php';
$c = file_get_contents($t);
// FIX4 outer table /e -> callback
$o = '$CONTENT = preg_replace(
      "/((^ *\\|[^\\n]*\\| *$\\n)+)/me",
      \'"<table class=\\"wikitable\\">".stripslashes($this->make_table("$1"))."</table>\\n"\',
      $CONTENT);';
$n = '$CONTENT = preg_replace_callback(
      "/((^ *\\|[^\\n]*\\| *$\\n)+)/m",
      function($m){ return "<table class=\\"wikitable\\">".stripslashes($this->make_table($m[1]))."</table>\\n"; },
      $CONTENT);';
$c = str_replace($o, $n, $c);
// FIX8 inner td /e -> callback
$o2 = '$s = preg_replace(\'/\\|(([hlrtb]* ){0,1})\\s*(\\d*)\\s*,{0,1}(\\d*)\\s*(.*?)\\|/e\',
         \'"<td".("$3"?" colspan=\\"$3\\"":" ").("$4"?" rowspan=\\"$4\\"":" ").$this->table_style("$1").">$5</td>"\',$s);';
$n2 = '$s = preg_replace_callback(\'/\\|(([hlrtb]* ){0,1})\\s*(\\d*)\\s*,{0,1}(\\d*)\\s*(.*?)\\|\',
         function($m){ return "<td".($m[3]?" colspan=\\"{$m[3]}\\"":" ").($m[4]?" rowspan=\\"{$m[4]}\\"":" ").$this->table_style($m[1]).">{$m[5]}</td>"; },$s);';
$c = str_replace($o2, $n2, $c);
// FIX9 create_function -> closure
$o3 = 'create_function(\'$m\',
            \'global $matches_links;static $idxlink=0;return "[".$matches_links[1][$idxlink++]."]";\') ,$s);';
$n3 = 'function($m) use ($matches_links){ static $idxlink=0; return "[".$matches_links[1][$idxlink++]."]"; } ,$s);';
$c = str_replace($o3, $n3, $c);
file_put_contents($t, $c);
echo "patched\n";

补丁后先做语法校验再重载:

php -l /var/www/wikiss/index.php
php -l /var/www/wikiss/plugins/wkp_Tables.php
sudo systemctl reload apache2

八、安装验证

按顺序打勾(全部为实测通过的判定条件):

# 1. 首页 HTTP 200 且包含默认欢迎内容
curl -s -o /dev/null -w '%{http_code}\n' http://wiki.example.com/          # 期待 200

# 2. 搜索不 500(修复 5/7 的试金石)
curl -s -o /dev/null -w '%{http_code}\n' 'http://wiki.example.com/?action=search&query=Aide'   # 期待 200

# 3. 带口令保存页面成功(302 跳转到页面视图;PASSWORD 为空则 sc 留空)
curl -s -X POST 'http://wiki.example.com/' \
  --data-urlencode 'page=TestS9' \
  --data-urlencode 'content=第一页| A | B |
| 1 | 2 |' \
  --data-urlencode 'sc=Q1w2E3r4T!' \
  -o /dev/null -w '%{http_code}\n'    # 期待 302
ls /var/www/wikiss/pages/              # 出现 TestS9.txt

# 4. 页面视图 200 且表格完整渲染
curl -s 'http://wiki.example.com/?page=TestS9' | grep -o 'wikitable'

# 5. 历史备份落盘
ls /var/www/wikiss/historique/TestS9/  # 出现 <日期>-<时分>.bak

# 6. RSS 输出正常
curl -s -o /dev/null -w '%{http_code}\n' http://wiki.example.com/rss.xml   # 期待 200

页面创建、编辑、删除与口令机制的完整行为,继续读本专栏《WiKiss 0.3 核心使用与鉴权配置详解》;Wiki 标记语法(标题/粗斜体/列表/表格对齐合并)见本专栏《WiKiss 0.3 语法与表格插件详解》。

九、FAQ

Q:首页能开,搜索/保存就 500? 三类根因都在本文修复 3/5/6/7:裸常量键名、fread 空句柄、get_magic_quotes_gpc。tail /var/log/apache2/wikiss-error.log 按报错行号对号入座。

Q:表格渲染成空的 <table></table>? 修复 8(make_table 内层 /e)。这是唯一"静默失败"的补丁点——没有致命报错,只有空输出。

Q:保存后页面变 0 字节? 修复 6。fopen("w") 清空文件后才崩在 get_magic_quotes_gpc(),先修它再谈数据恢复(从 historique/<页面名>/ 的 .bak 找回,方法见本专栏《WiKiss 0.3 常用 CLI 命令与运维实操》)。

Q:^é 渲染成乱码 é? WiKiss 0.3 的 ^字符 转义按单字节处理(正则无 u 修饰符),UTF-8 多字节字符会被拆成 &#195; + 半个字节。这不是 bug 修复能低成本解决的——中文站直接写原生 UTF-8 字符即可,^X 语法仅对 ASCII 字符有意义(如 ^< 显示字面 <)。

Q:口令 test 太弱可以吗? 实测环境用 test 只为演示;生产务必换强口令(如 Q1w2E3r4T!)。口令即编辑权,无用户体系、无失败锁定,泄露即全站可写。

Q:需要 MySQL 吗? 不需要。页面、历史、配置全是文件,整个 Wiki 的备份就是备一个目录(运维细节见 CLI 篇)。