Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A wiki for phpBB with version history, side-by-side compare, and a moderation qu
- Every edit is saved as a new version; compare any two versions side by side.
- Edits can wait for approval: moderators get a **Pending articles** list with Approve and Disapprove. Approving a version older than the live one is blocked, so a stale draft can't overwrite newer content.
- Moderators can take an article offline or delete it, and articles can be made sticky or set to redirect.
- A **Recent changes** page at `/wiki/recent-changes` lists every version saved to the wiki that is or was live, newest first, with RSS and Atom feeds at `/wiki/recent-changes.rss` and `/wiki/recent-changes.atom` (the latest 50). The ACP setting **Recent changes feed** under **Extensions > Wiki > Settings** offers RSS, Atom or both, and the header advertises the offered feeds to feed readers. Versions waiting for approval never appear.
- A contributions page for every member, at `/wiki/contributions/<username>`, lists every version they saved across all articles, newest first. Author names on articles and in the version history link to it. Versions waiting for approval, and those of switched-off articles, show only to moderators and to the author.
- Files can be uploaded to an article by members who hold the **Can upload files to articles** permission. They are checked against the board's allowed file types and size limit, stored outside public view under `store/phpbbmodders_wiki/`, and read only through the wiki, which checks that the reader may see the article. In the text, `[[File:Diagram.png]]` shows an uploaded image and `[[File:notes.zip|the notes]]` links to any other file. The member who uploaded a file, or a moderator who may delete versions, can remove it, and deleting an article deletes its files.
- Notifications when an edit is waiting for approval.
Expand Down
15 changes: 15 additions & 0 deletions acp/main_module.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public function main($id, $mode)

$forum_id = $request->variable('wiki_discussion_forum_id', 0);
$icon = trim($request->variable('wiki_icon', ''));
$feed_format = $request->variable('wiki_feed_format', '');

if (!\phpbbmodders\wiki\wiki\feed::is_valid_setting($feed_format))
{
trigger_error($language->lang('WIKI_FEED_FORMAT_INVALID') . adm_back_link($this->u_action), E_USER_WARNING);
}

if (!\phpbbmodders\wiki\wiki\icon::is_valid($icon))
{
Expand All @@ -78,11 +84,20 @@ public function main($id, $mode)

$config->set('wiki_discussion_forum_id', $forum_id);
$config->set('wiki_icon', $icon);
$config->set('wiki_feed_format', $feed_format);

trigger_error($language->lang('CONFIG_UPDATED') . adm_back_link($this->u_action));
}

$feed_format = isset($config['wiki_feed_format']) ? $config['wiki_feed_format'] : \phpbbmodders\wiki\wiki\feed::BOTH;
$feed_options = '';
foreach (array(\phpbbmodders\wiki\wiki\feed::BOTH => 'WIKI_FEED_FORMAT_BOTH', \phpbbmodders\wiki\wiki\feed::RSS => 'WIKI_FEED_FORMAT_RSS', \phpbbmodders\wiki\wiki\feed::ATOM => 'WIKI_FEED_FORMAT_ATOM') as $value => $label)
{
$feed_options .= '<option value="' . $value . '"' . ($feed_format === $value ? ' selected="selected"' : '') . '>' . $language->lang($label) . '</option>';
}

$template->assign_vars(array(
'S_WIKI_FEED_OPTIONS' => $feed_options,
'WIKI_ICON' => isset($config['wiki_icon']) ? $config['wiki_icon'] : \phpbbmodders\wiki\wiki\icon::DEFAULT_ICON,
'U_ACTION' => $this->u_action,
'S_WIKI_FORUM_OPTIONS' => '<option value="0"' . (empty($config['wiki_discussion_forum_id']) ? ' selected="selected"' : '') . '>' . $language->lang('WIKI_DISCUSSION_FORUM_NONE') . '</option>'
Expand Down
4 changes: 4 additions & 0 deletions adm/style/acp_wiki_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ <h1>{L_ACP_WIKI_SETTINGS}</h1>
<dt><label for="wiki_icon">{L_WIKI_ICON}{L_COLON}</label><br><span>{L_WIKI_ICON_EXPLAIN}</span></dt>
<dd><input id="wiki_icon" type="text" name="wiki_icon" size="30" maxlength="50" value="{WIKI_ICON}"></dd>
</dl>
<dl>
<dt><label for="wiki_feed_format">{L_WIKI_FEED_FORMAT}{L_COLON}</label><br><span>{L_WIKI_FEED_FORMAT_EXPLAIN}</span></dt>
<dd><select id="wiki_feed_format" name="wiki_feed_format">{S_WIKI_FEED_OPTIONS}</select></dd>
</dl>
<dl>
<dt><label for="wiki_discussion_forum_id">{L_WIKI_DISCUSSION_FORUM}{L_COLON}</label><br><span>{L_WIKI_DISCUSSION_FORUM_EXPLAIN}</span></dt>
<dd><select id="wiki_discussion_forum_id" name="wiki_discussion_forum_id">{S_WIKI_FORUM_OPTIONS}</select></dd>
Expand Down
10 changes: 10 additions & 0 deletions config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ phpbbmodders_wiki_index:
path: /wiki/
defaults: { _controller: phpbbmodders.wiki.overview:base }

phpbbmodders_wiki_recent_changes_feed:
path: /wiki/recent-changes.{_format}
defaults: { _controller: phpbbmodders.wiki.recent_changes:feed }
requirements:
_format: rss|atom

phpbbmodders_wiki_recent_changes:
path: /wiki/recent-changes
defaults: { _controller: phpbbmodders.wiki.recent_changes:page }

phpbbmodders_wiki_contributions:
path: /wiki/contributions/{username}
defaults: { _controller: phpbbmodders.wiki.contributions:view }
Expand Down
15 changes: 15 additions & 0 deletions config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ services:
- '%core.php_ext%'
- '%tables.wiki.article%'

phpbbmodders.wiki.recent_changes:
class: phpbbmodders\wiki\controller\recent_changes
arguments:
- '@auth'
- '@config'
- '@dbal.conn'
- '@controller.helper'
- '@pagination'
- '@request'
- '@template'
- '@user'
- '%tables.wiki.article%'
- '%core.root_path%'
- '%core.php_ext%'

phpbbmodders.wiki.contributions:
class: phpbbmodders\wiki\controller\contributions
arguments:
Expand Down
277 changes: 277 additions & 0 deletions controller/recent_changes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
<?php
/**
*
* Wiki extension for the phpBB Forum Software package
*
* @copyright (c) 2026, phpBB Modders, https://www.phpbbmodders.com/
* @license GNU General Public License, version 2 (GPL-2.0)
*
*/
namespace phpbbmodders\wiki\controller;

use phpbbmodders\wiki\wiki\feed;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
* Recent changes: every version saved to the wiki that is or was live, newest
* first, as a page and as an RSS or Atom feed.
*
* Versions waiting for approval, and versions of articles with no live version,
* never appear: the page and the feed are public, and those are not.
*/
class recent_changes
{
/** @var \phpbb\auth\auth */
protected $auth;

/** @var \phpbb\config\config */
protected $config;

/** @var \phpbb\db\driver\driver_interface */
protected $db;

/** @var \phpbb\controller\helper */
protected $helper;

/** @var \phpbb\pagination */
protected $pagination;

/** @var \phpbb\request\request */
protected $request;

/** @var \phpbb\template\template */
protected $template;

/** @var \phpbb\user */
protected $user;

/** @var string */
protected $article_table;

/** @var string */
protected $phpbb_root_path;

/** @var string */
protected $php_ext;

/**
* @param \phpbb\auth\auth $auth
* @param \phpbb\config\config $config
* @param \phpbb\db\driver\driver_interface $db
* @param \phpbb\controller\helper $helper
* @param \phpbb\pagination $pagination
* @param \phpbb\request\request $request
* @param \phpbb\template\template $template
* @param \phpbb\user $user
* @param string $article_table
* @param string $phpbb_root_path
* @param string $php_ext
*/
public function __construct(\phpbb\auth\auth $auth, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\controller\helper $helper, \phpbb\pagination $pagination, \phpbb\request\request $request, \phpbb\template\template $template, \phpbb\user $user, $article_table, $phpbb_root_path, $php_ext)
{
$this->auth = $auth;
$this->config = $config;
$this->db = $db;
$this->helper = $helper;
$this->pagination = $pagination;
$this->request = $request;
$this->template = $template;
$this->user = $user;
$this->article_table = $article_table;
$this->phpbb_root_path = $phpbb_root_path;
$this->php_ext = $php_ext;
}

/**
* The feed formats the ACP offers.
*
* @return string The feed class's RSS, ATOM or BOTH
*/
protected function setting()
{
return isset($this->config['wiki_feed_format']) ? $this->config['wiki_feed_format'] : feed::BOTH;
}

/**
* SQL condition for the versions anyone may see: the live one, and the ones a
* newer live version replaced.
*
* @return string
*/
protected function visible_where()
{
return '(a.article_approved = 1 OR EXISTS (
SELECT 1 FROM ' . $this->article_table . ' b
WHERE b.article_url = a.article_url
AND b.article_approved = 1
AND b.article_last_edit > a.article_last_edit))';
}

/**
* Versions, newest first.
*
* @param int $limit
* @param int $start
* @return array Rows with the editor's name
*/
protected function get_changes($limit, $start = 0)
{
$sql = 'SELECT a.article_id, a.article_title, a.article_url, a.article_last_edit, a.article_approved, a.article_edit_reason,
u.user_id, u.username, u.user_colour
FROM ' . $this->article_table . ' a
LEFT JOIN ' . USERS_TABLE . ' u ON (u.user_id = a.article_user_id)
WHERE ' . $this->visible_where() . '
ORDER BY a.article_last_edit DESC, a.article_id DESC';
$result = $this->db->sql_query_limit($sql, $limit, $start);
$rows = $this->db->sql_fetchrowset($result);
$this->db->sql_freeresult($result);

return $rows;
}

/**
* The recent changes page.
*
* @return Response
*/
public function page()
{
$this->user->add_lang_ext('phpbbmodders/wiki', 'common');

if (!$this->auth->acl_get('u_wiki_view'))
{
trigger_error('NOT_AUTHORISED');
}

include_once($this->phpbb_root_path . 'includes/functions_content.' . $this->php_ext);

$sql = 'SELECT COUNT(a.article_id) AS total
FROM ' . $this->article_table . ' a
WHERE ' . $this->visible_where();
$result = $this->db->sql_query($sql);
$total = (int) $this->db->sql_fetchfield('total');
$this->db->sql_freeresult($result);

$per_page = max(1, (int) $this->config['topics_per_page']);
$start = $this->pagination->validate_start($this->request->variable('start', 0), $per_page, $total);

$rows = $this->get_changes($per_page, $start);
$live = $this->get_live_times(array_unique(array_column($rows, 'article_url')));

foreach ($rows as $row)
{
$status = contributions::status($row['article_approved'], $row['article_last_edit'], isset($live[$row['article_url']]) ? $live[$row['article_url']] : 0);

$this->template->assign_block_vars('changes', array(
'ARTICLE_TITLE' => $row['article_title'],
'EDIT_REASON' => $row['article_edit_reason'],
'EDIT_TIME' => $this->user->format_date($row['article_last_edit']),
'USER' => \phpbbmodders\wiki\wiki\author::link($this->helper, $row['user_id'], $row['username'], $row['user_colour']),
'STATUS' => $this->user->lang('CONTRIBUTION_' . strtoupper($status)),
'U_VERSION' => $this->helper->route('phpbbmodders_wiki_article', array('id' => $row['article_id'])),
'U_ARTICLE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $row['article_url'])),
'S_LIVE' => $status === contributions::STATUS_LIVE,
));
}

$this->template->assign_block_vars('navlinks', array(
'FORUM_NAME' => $this->user->lang('WIKI'),
'U_VIEW_FORUM' => $this->helper->route('phpbbmodders_wiki_index', array()),
));

$this->pagination->generate_template_pagination($this->helper->route('phpbbmodders_wiki_recent_changes', array()), 'pagination', 'start', $total, $per_page, $start);

$this->template->assign_vars(array(
'TOTAL_ITEMS' => $this->user->lang('TOTAL_ITEMS', $total),
));

return $this->helper->render('recent_changes.html', $this->user->lang('WIKI_RECENT_CHANGES'));
}

/**
* The feed, when its format is offered.
*
* @param string $_format feed::RSS or feed::ATOM
* @return Response
*/
public function feed($_format)
{
$this->user->add_lang_ext('phpbbmodders/wiki', 'common');

// A feed reader is a guest, so the guest's right to read the wiki decides
if (!$this->auth->acl_get('u_wiki_view'))
{
return new Response($this->user->lang('NOT_AUTHORISED'), 403, array('Content-Type' => 'text/plain; charset=UTF-8'));
}

if (!feed::offers($this->setting(), $_format))
{
return new Response($this->user->lang('PAGE_NOT_FOUND'), 404, array('Content-Type' => 'text/plain; charset=UTF-8'));
}

$rows = $this->get_changes(feed::LIMIT);
$entries = array();
foreach ($rows as $row)
{
$link = $this->helper->route('phpbbmodders_wiki_article', array('id' => $row['article_id']), false, false, UrlGeneratorInterface::ABSOLUTE_URL);
$summary = $this->user->lang('WIKI_FEED_SAVED_BY', $row['username'] === null ? '' : $row['username']);
if ($row['article_edit_reason'] !== '')
{
$summary .= ' ' . $this->user->lang('WIKI_FEED_REASON', $row['article_edit_reason']);
}

$entries[] = array(
'id' => $link,
'title' => $row['article_title'],
'link' => $link,
'summary' => $summary,
'author' => $row['username'] === null ? '' : $row['username'],
'time' => (int) $row['article_last_edit'],
);
}

$meta = array(
'title' => $this->user->lang('WIKI_FEED_TITLE', $this->config['sitename']),
'description' => $this->user->lang('WIKI_RECENT_CHANGES_EXPLAIN'),
'link' => $this->helper->route('phpbbmodders_wiki_recent_changes', array(), false, false, UrlGeneratorInterface::ABSOLUTE_URL),
'self' => $this->helper->route('phpbbmodders_wiki_recent_changes_feed', array('_format' => $_format), false, false, UrlGeneratorInterface::ABSOLUTE_URL),
'updated' => $rows ? (int) $rows[0]['article_last_edit'] : time(),
);

return new Response(feed::build($_format, $meta, $entries), 200, array(
'Content-Type' => ($_format === feed::ATOM ? 'application/atom+xml' : 'application/rss+xml') . '; charset=UTF-8',
'Cache-Control' => 'public, max-age=300',
));
}

/**
* When the live version of each article was saved.
*
* @param string[] $urls Article URL names
* @return array URL name => timestamp; articles with no live version are left out
*/
protected function get_live_times(array $urls)
{
if (!$urls)
{
return array();
}

$sql = 'SELECT article_url, MAX(article_last_edit) AS live_last_edit
FROM ' . $this->article_table . '
WHERE article_approved = 1
AND ' . $this->db->sql_in_set('article_url', $urls) . '
GROUP BY article_url';
$result = $this->db->sql_query($sql);

$times = array();
while ($row = $this->db->sql_fetchrow($result))
{
$times[$row['article_url']] = (int) $row['live_last_edit'];
}
$this->db->sql_freeresult($result);

return $times;
}
}
9 changes: 0 additions & 9 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,6 @@ wiki content to visually match the rest of the forum, others will want it
to look like an actual wiki. Scope depends heavily on the table of contents
item above landing first, since a real wiki layout wants one.

## Recent Changes feed

A chronological list of *every* edit/approval across all articles, not
just the newest one per article. "Latest Articles" on the overview page
only shows the most recent version per distinct article — it can't answer
"what changed on the wiki in the last day," which is the primary
navigation view on most real wikis. Worth pairing with an RSS/Atom feed
the same way the board already has `feed.php` for forum activity.

## Per-article edit protection

Not built yet. Permissions are board-wide today: `u_wiki_edit` lets a user
Expand Down
4 changes: 4 additions & 0 deletions event/listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,12 @@ public function page_header($event)
if ($this->auth->acl_get('u_wiki_view'))
{
$this->user->add_lang_ext('phpbbmodders/wiki', 'common');
$feed_setting = isset($this->config['wiki_feed_format']) ? $this->config['wiki_feed_format'] : \phpbbmodders\wiki\wiki\feed::BOTH;
$this->template->assign_vars(array(
'U_WIKI' => $this->helper->route('phpbbmodders_wiki_index', array()),
'U_WIKI_RECENT_CHANGES' => $this->helper->route('phpbbmodders_wiki_recent_changes', array()),
'U_WIKI_FEED_RSS' => \phpbbmodders\wiki\wiki\feed::offers($feed_setting, \phpbbmodders\wiki\wiki\feed::RSS) ? $this->helper->route('phpbbmodders_wiki_recent_changes_feed', array('_format' => 'rss')) : '',
'U_WIKI_FEED_ATOM' => \phpbbmodders\wiki\wiki\feed::offers($feed_setting, \phpbbmodders\wiki\wiki\feed::ATOM) ? $this->helper->route('phpbbmodders_wiki_recent_changes_feed', array('_format' => 'atom')) : '',
'WIKI_ICON_CLASSES' => \phpbbmodders\wiki\wiki\icon::classes(isset($this->config['wiki_icon']) ? $this->config['wiki_icon'] : \phpbbmodders\wiki\wiki\icon::DEFAULT_ICON),
));
}
Expand Down
Loading
Loading