class/GwikiPage.php
<?php
namespace XoopsModules\Gwiki;
use const _MI_GWIKI_WIKI404;
use const ENT_HTML5;
/**
* gwikiPage.php - class to access wiki page data
*
* This file is part of gwiki - geekwright wiki
*/
\define('_WIKI_CAMELCASE_REGEX', '(([A-Z]{1,}[a-z\x80-\xff0-9\:]+){2,}\d*)');
\define('_WIKI_KEYWORD_REGEX', '([A-Za-z\x80-\xff0-9.\:-]{1,})');
/**
* gwikiPage.php - class to access wiki page data
*
*
* @category Class
* @package Gwiki
* @author Richard Griffith <richard@geekwright.com>
* @copyright 2013-2015 geekwright, LLC. All rights reserved.
* @license gwiki/docs/license.txt GNU General Public License (GPL)
*/
class GwikiPage
{
//------------------------------------------------------------
// Properties - public, protected, and private
//------------------------------------------------------------
protected $currentid;
protected $currentkeyword;
public $gwiki_id;
public $keyword;
public $display_keyword;
public $title;
public $body;
public $parent_page;
public $page_set_home;
public $page_set_order;
public $meta_description;
public $meta_keywords;
public $lastmodified;
public $uid;
public $admin_lock;
public $active;
public $search_body;
public $toc_cache;
public $show_in_index;
public $gwiki_version;
public $page_id; // an integer id for the keyword
public $wikiHomePage; // the home page
public $currentprefix; // Prefix of current keyword, if any
public $currentprefixid; // id of current Prefix
public $currenttemplateid; // template for current Prefix (0=use default)
public $attachments;
public $renderedPage;
private $numberOfRecentItems = 10;
// $wikiLinkURL is a sprintf format string, with keyword as only arg. Better link establised in __construct()
private $wikiLinkURL = 'index.php?page=%s';
public $dateFormat;
public $defaultThumbSize;
private $tocIdPrefix = 'toc';
private $tocAnchorFmt = '#%s';
private $imageLib = [];
private $useCamelCase;
private $autoNameFormat;
private $module_id;
private $wikiDir; // dirname of the gwiki module
private $gwikiVersion = 1; // wiki syntax version for future backward compatibility
private $highlightArg;
private $noWikiQueue = []; // hold no wiki content during rendering
private $noWikiIndex = 0;
private $tocQueue = []; // track headers for toc
private $tocIndex = 0;
private $refQueue = []; // track reference
private $refIndex = 0;
private $refShown = false;
private $wikiPageLinks = []; // link in current page
// Out Of Bounds data - not cleared with resetPage
private $pageIndexPrefix = ''; // current prefix for the pageindex
//------------------------------------------------------------
// Methods
//------------------------------------------------------------
/**
* class constructor
*/
public function __construct()
{
$this->resetPage();
$dir = \basename(\dirname(__DIR__));
$this->wikiDir = $dir;
// $helper = Xmf\Module\Helper::getHelper($dir);
$helper = \XoopsModules\Gwiki\Helper::getInstance();
$this->wikiLinkURL = $helper->getConfig('wikilink_template');
$this->wikiHomePage = $helper->getConfig('wiki_home_page');
$this->dateFormat = $helper->getConfig('date_format');
$this->imageLib = \explode(',', $helper->getConfig('imagelib_pages'));
$this->useCamelCase = $helper->getConfig('allow_camelcase');
$this->defaultThumbSize = $helper->getConfig('default_thumb_size');
$this->autoNameFormat = $helper->getConfig('auto_name_format');
$this->module_id = $helper->getModule()->getVar('mid');
if (!\defined('_MI_GWIKI_WIKIHOME')) {
$this->loadLanguage('modinfo', $dir);
}
if (!\defined('_MD_GWIKI_PAGE_PERM_EDIT_ANY_NUM')) {
$this->loadLanguage('main', $dir);
}
}
/**
* load language resources
*
* @param string $name language file name (main, modinfo, etc.)
* @param string $domain domain/module
* @param null $language language
*/
private function loadLanguage($name, $domain = '', $language = null)
{
// global $xoopsConfig;
// if (!@require_once XOOPS_ROOT_PATH . "/modules/{$domain}/language/" . $xoopsConfig['language'] . "/{$name}.php") {
// require_once XOOPS_ROOT_PATH . "/modules/{$domain}/language/english/{$name}.php";
// }
\xoops_loadLanguage($name, $domain, $language);
}
/**
* Reset all page properties
*/
protected function resetPage()
{
$this->gwiki_id = null;
$this->keyword = '';
$this->display_keyword = '';
$this->title = '';
$this->body = '';
$this->parent_page = '';
$this->page_set_home = '';
$this->page_set_order = '';
$this->meta_description = '';
$this->meta_keywords = '';
$this->lastmodified = 0;
$this->uid = 0;
$this->admin_lock = 0;
$this->active = 0;
$this->search_body = '';
$this->toc_cache = '';
$this->show_in_index = 1;
$this->gwiki_version = $this->gwikiVersion;
$this->page_id = 0;
$this->created = 0;
$this->renderedPage = '';
$this->currentprefix = '';
$this->currentprefixid = '';
$this->currenttemplateid = 0;
$this->attachments = [];
$this->tocQueue = [];
$this->tocIndex = 0;
$this->refQueue = [];
$this->refIndex = 0;
$this->refShown = false;
$this->wikiPageLinks = [];
}
/**
* escape a string to be "safe" for use in database
*
* @param string $value string to be escaped
*
* @return string
*/
public function escapeForDB($value)
{
global $xoopsDB;
return $value = $xoopsDB->escape($value);
}
/**
* set the count for recent item list
*
* @param int $count item count
*/
public function setRecentCount($count)
{
$count = (int)$count;
if ($count > 1 && $count < 1000) {
$this->numberOfRecentItems = $count;
}
}
/**
* Set the URL pattern for wiki links
*
* @param string $url sprintf pattern for URL. Will get page name as parameter.
*/
public function setWikiLinkURL($url)
{
$this->wikiLinkURL = $url;
}
/**
* Get the URL pattern for wiki links
*
* @return string
*/
public function getWikiLinkURL()
{
return $this->wikiLinkURL;
}
/**
* git the wiki directory (dirname)
*
* @return string
*/
public function getWikiDir()
{
return $this->wikiDir;
}
/**
* get max upload size from ini
*
* @return int|string
*/
public function getMaxUploadSize()
{
$maxSizeIni = \trim(\ini_get('upload_max_filesize'));
$last = mb_strtolower($maxSizeIni[mb_strlen($maxSizeIni) - 1]);
$val = mb_substr($maxSizeIni, 0, -1);
switch ($last) {
// The 'G' modifier is available since PHP 5.1.0
case 'g':
$val *= 1024;
// no break
case 'm':
$val *= 1024;
// no break
case 'k':
$val *= 1024;
}
return $val;
}
/**
* set format for TOC links
*
* @param string $prefix prefix
* @param string $linkformat anchor
*/
public function setTocFormat($prefix, $linkformat)
{
$this->tocIdPrefix = $prefix;
$this->tocAnchorFmt = $linkformat;
}
/**
* Make sure that keyword obeys formatting rules or switch to illegal name
*
* @param mixed $keyword - wiki page name
*
* @return string
*/
public function makeKeyword($keyword)
{
if (!\preg_match('#^' . \_WIKI_KEYWORD_REGEX . '$#', $keyword)) {
$keyword = _MI_GWIKI_WIKI404;
} else { // check for undefined prefix
$prefix = $this->getPrefix($keyword);
if ($prefix && !$prefix['defined']) {
$keyword = _MI_GWIKI_WIKI404;
}
}
return $keyword;
}
/**
* add namespace prefix to a wiki word
*
* @param int $nsid namespace (prefix) id
* @param string $page keyword
*
* @return bool|string
*/
public function makeKeywordFromPrefix($nsid, $page)
{
if ($nsid >= 0) {
$pfx = $this->getPrefixFromId($nsid);
if (empty($page)) {
if ($pfx['prefix_auto_name']) {
$page = \date($this->autoNameFormat);
} else {
$page = $pfx['prefix_home'];
}
}
$page = $pfx['prefix'] . ':' . $page;
}
return $page;
}
/**
* Capture out of bounds data traveling with keyword. Such data is sent
* in keyword(oob) construct. This function processes any oob data and
* returns a clean keyword.
* oob data is used this way to pass page specific data in any url
*
* Only call this if you will NOT be calling normalizeKeyword or the
* OOB data will be lost.
*
* @param mixed $keyword - wiki page name possibily containing OOB data
*
* @return string - keyword with no OOB data
*/
public function getOOBFromKeyword($keyword)
{
$oob = null;
if (')' === mb_substr($keyword, -1)) {
$lparen = mb_strpos($keyword, '(');
if (false !== $lparen) {
$inparen = mb_substr($keyword, $lparen);
$inparen = mb_substr($inparen, 1, -2);
$keyword = mb_substr($keyword, 0, $lparen);
$oob = $inparen;
}
}
// currently this is the only use
$this->pageIndexPrefix = mb_strtolower($oob??'');
return $keyword;
}
/**
* If page exists, fix case of page name to that specified in database
*
* @param string $keyword - wiki page name
*
* @return string normalized keyword
*/
public function normalizeKeyword($keyword)
{
global $xoopsDB;
$keyword = $this->getOOBFromKeyword($keyword);
$keyword = $this->escapeForDB($keyword);
$sql = 'SELECT keyword FROM ' . $xoopsDB->prefix('gwiki_pages') . " WHERE keyword='{$keyword}' AND active=1 ";
$result = $xoopsDB->query($sql);
$content = $xoopsDB->fetchArray($result);
if ($content) {
$keyword = $content['keyword'];
} else {
$keyword = $this->makeKeyword($keyword);
}
return $keyword;
}
/**
* Get the gwiki_id of the active page for the keyword
*
* @param mixed $keyword - wiki page name
*
* @return int - id of page
*/
public function getCurrentId($keyword)
{
global $xoopsDB;
$sql = 'SELECT gwiki_id FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= " WHERE keyword='{$keyword}' AND active = 1 ORDER BY gwiki_id DESC LIMIT 1";
$result = $xoopsDB->query($sql);
[$id] = $xoopsDB->fetchRow($result);
return (int)$id;
}
/**
* Add current page as a new revision
*
* @param bool $leave_inactive true to save page as inactive
*
* @return mixed
*/
public function addRevision($leave_inactive = false)
{
global $xoopsDB;
$page = $this->escapeForDB($this->keyword);
if (empty($this->display_keyword)) {
$this->display_keyword = $page;
}
$this->tocQueue = [];
$this->tocIndex = 0;
$this->refQueue = [];
$this->refIndex = 0;
$this->wikiPageLinks = [];
// eliminate things we don't want in search page because they
// are misleading and/or change outside of the page itself
$search[] = "#{(PageIndex|RecentChanges)([^\"<\n]+?)?}#si";
$replace[] = '';
$search[] = '#\{toc\}#i';
$replace[] = '';
$search[] = '#\{pagesettoc\}#i';
$replace[] = '';
$tempbody = \preg_replace($search, $replace, $this->body) . "\n\n";
$this->search_body = \strip_tags($this->renderPage($tempbody));
$this->toc_cache = \serialize($this->tocQueue);
$this->gwiki_version = $this->gwikiVersion; // new revisions always for current engine
// if we are adding to a page set, auto increment the order if none specified
if (!empty($this->page_set_home) && '' === $this->page_set_order) {
$this->page_set_order = $this->getNextPageSetOrder($this->page_set_home);
}
// this will usually fail (duplicate)
$sql = 'INSERT INTO ' . $xoopsDB->prefix('gwiki_pageids') . " (keyword, created) VALUES('{$page}', UNIX_TIMESTAMP())";
$result = $xoopsDB->query($sql);
if ($result) {
$page_id = $xoopsDB->getInsertId();
$this->page_id = $page_id;
}
if ($leave_inactive) {
// allow a save that is not activated (for conflict management, and maybe more)
$this->active = 0;
$sql = 'INSERT INTO ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' (keyword, display_keyword, title, body, parent_page, page_set_home, page_set_order';
$sql .= ', meta_description, meta_keywords';
$sql .= ', lastmodified, uid, admin_lock, active, search_body, toc_cache, show_in_index, gwiki_version)';
$sql .= ' VALUES (';
$sql .= '\'' . $page . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->display_keyword) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->title) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->body) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->parent_page) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->page_set_home) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->page_set_order) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->meta_description) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->meta_keywords) . '\' ,';
$sql .= 'UNIX_TIMESTAMP() ,';
$sql .= '\'' . $this->escapeForDB($this->uid) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->admin_lock) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->active) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->search_body) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->toc_cache) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->show_in_index) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->gwiki_version) . '\' )';
$result = $xoopsDB->query($sql);
if ($result) {
$result = $xoopsDB->getInsertId();
$this->gwiki_id = $result;
}
} else {
$sql = 'UPDATE ' . $xoopsDB->prefix('gwiki_pages') . " SET active = 0 WHERE keyword='{$page}' and active = 1 ";
$result = $xoopsDB->query($sql);
if ($result) {
$previous_rows = $xoopsDB->getAffectedRows();
$this->active = 1;
$sql = 'INSERT INTO ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' (keyword, display_keyword, title, body, parent_page, page_set_home, page_set_order';
$sql .= ', meta_description, meta_keywords, lastmodified';
$sql .= ', uid, admin_lock, active, search_body, toc_cache, show_in_index, gwiki_version)';
$sql .= ' VALUES (';
$sql .= '\'' . $page . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->display_keyword) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->title) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->body) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->parent_page) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->page_set_home) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->page_set_order) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->meta_description) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->meta_keywords) . '\' ,';
$sql .= 'UNIX_TIMESTAMP() ,';
$sql .= '\'' . $this->escapeForDB($this->uid) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->admin_lock) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->active) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->search_body) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->toc_cache) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->show_in_index) . '\' ,';
$sql .= '\'' . $this->escapeForDB($this->gwiki_version) . '\' )';
$result = $xoopsDB->query($sql);
if ($result) {
$result = $xoopsDB->getInsertId();
$this->gwiki_id = $result;
$this->updatePageLinks();
/** @var \XoopsNotificationHandler $notificationHandler */
$notificationHandler = \xoops_getHandler('notification');
$tags['PAGE_NAME'] = $page;
$tags['PAGE_TITLE'] = $this->title;
if (empty($tags['PAGE_TITLE'])) {
$tags['PAGE_TITLE'] = $this->display_keyword;
}
if (empty($tags['PAGE_TITLE'])) {
$tags['PAGE_TITLE'] = $page;
}
$tags['PAGE_LINK'] = \sprintf($this->wikiLinkURL, $page);
$tags['NAMESPACE'] = $this->currentprefix;
if ($previous_rows < 1) {
// only for new
$notificationHandler->triggerEvent('global', 0, 'new_page', $tags, [], $this->module_id);
if ($this->currentprefixid) { // have namespace
$notificationHandler->triggerEvent('namespace', $this->currentprefixid, 'new_ns_page', $tags, [], $this->module_id);
}
}
// for all cases (new is also an update)
$notificationHandler->triggerEvent('page', $this->page_id, 'page_watch', $tags, [], $this->module_id);
$notificationHandler->triggerEvent('global', 0, 'upd_page', $tags, [], $this->module_id);
if ($this->currentprefixid) { // have namespace
$notificationHandler->triggerEvent('namespace', $this->currentprefixid, 'upd_ns_page', $tags, [], $this->module_id);
}
}
}
}
return $result;
}
/**
* update gwiki_pagelinks table - expects $page to be current
*
* @param bool $render do a fresh page render before updating
*/
private function updatePageLinks($render = false)
{
global $xoopsDB;
if ($render) {
// eliminate things we don't want in search page because they
// are misleading and/or change outside of the page itself
$search[] = "#{(PageIndex|RecentChanges)([^\"<\n]+?)?}#si";
$replace[] = '';
$search[] = '#\{toc\}#i';
$replace[] = '';
$search[] = '#\{pagesettoc\}#i';
$replace[] = '';
$tempbody = \preg_replace($search, $replace, $this->body) . "\n\n";
$this->renderPage($tempbody);
}
$page = $this->escapeForDB($this->keyword);
$sql = 'DELETE FROM ' . $xoopsDB->prefix('gwiki_pagelinks');
$sql .= ' WHERE from_keyword = \'' . $page . '\'';
$result = $xoopsDB->query($sql);
if (!empty($this->wikiPageLinks)) {
$sql = 'INSERT INTO ' . $xoopsDB->prefix('gwiki_pagelinks') . ' (from_keyword, to_keyword) VALUES ';
$values = '';
foreach ($this->wikiPageLinks as $i => $v) {
if (!empty($values)) {
$values .= ', ';
}
$values .= '(\'' . $page . '\', \'' . $this->escapeForDB($i) . '\')';
}
$sql .= $values;
$result = $xoopsDB->query($sql);
}
}
/**
* get the next higher unused page_set_order for a given page_set_home
*
* @param string $page_set_home keyword of page set home
*
* @return int
*/
private function getNextPageSetOrder($page_set_home)
{
global $xoopsDB;
$page_set_order = 1;
$keyword = $this->escapeForDB($page_set_home);
$sql = 'SELECT MAX(page_set_order) FROM ' . $xoopsDB->prefix('gwiki_pages') . " WHERE active = 1 and page_set_home = '{$keyword}' ";
$result = $xoopsDB->query($sql);
if ($result) {
$myrow = $xoopsDB->fetchRow($result);
$page_set_order = $myrow[0] + 1;
}
return $page_set_order;
}
/**
* Check if the current user may edit the current page
* Since the class can be used outside the module where permissions are assigned, we have to work at this a bit
*
* @return bool mayEdit
*/
public function checkEdit()
{
global $xoopsUser, $xoopsDB;
$mayEdit = false;
$keyword = $this->keyword;
$module_id = $this->module_id;
$dir = $this->wikiDir;
$helper = \XoopsModules\Gwiki\Helper::getInstance();
$groups = XOOPS_GROUP_ANONYMOUS;
if (\is_object($xoopsUser)) {
$groups = $xoopsUser->getGroups();
}
$grouppermHandler = \xoops_getHandler('groupperm');
$edit_any = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_EDIT_ANY_NUM, $groups, $module_id);
$edit_pfx = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_EDIT_PFX_NUM, $groups, $module_id);
$create_any = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_CREATE_ANY_NUM, $groups, $module_id);
$create_pfx = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_CREATE_PFX_NUM, $groups, $module_id);
// check for namespace prefix
$prefix = $this->getPrefix($keyword);
if ($prefix) {
if ($prefix['defined']) {
if (\is_array($groups)) {
$groupwhere = ' IN (' . \implode(', ', $groups) . ') ';
} else {
$groupwhere = " = '" . $groups . "'";
}
$sql = 'SELECT group_prefix_id FROM ' . $xoopsDB->prefix('gwiki_group_prefix') . ' WHERE prefix_id = \'' . $prefix['prefix_id'] . '\' AND group_id ' . $groupwhere;
$result = $xoopsDB->query($sql);
$rows = $xoopsDB->getRowsNum($result);
$xoopsDB->freeRecordSet($result);
if ($rows) { // prefix is assigned to one or more of user's groups
if (($edit_pfx || $create_pfx) && $this->gwiki_id) {
$mayEdit = true;
}
if ($create_pfx && !$this->gwiki_id) {
$mayEdit = true;
}
}
if (($edit_any || $create_any) && $this->gwiki_id) {
$mayEdit = true;
}
if ($create_any && !$this->gwiki_id) {
$mayEdit = true;
}
} else { // allow edit, but no create if prefix is undefined
if ($edit_any && $this->gwiki_id) {
$mayEdit = true;
}
}
} else {
if (($edit_any || $create_any) && $this->gwiki_id) {
$mayEdit = true;
}
if ($create_any && !$this->gwiki_id) {
$mayEdit = true;
}
}
return $mayEdit;
}
/**
* get user name based on id
*
* @param int $uid user id
*
* @return string
*/
public function getUserName($uid)
{
global $xoopsConfig;
$uid = (int)$uid;
if ($uid > 0) {
$memberHandler = \xoops_getHandler('member');
$user = $memberHandler->getUser($uid);
if (\is_object($user)) {
return '<a href="' . XOOPS_URL . "/userinfo.php?uid=$uid\">" . \htmlspecialchars($user->getVar('uname'), \ENT_QUOTES) . '</a>';
}
}
return $xoopsConfig['anonymous'];
}
/**
* get array of prefixes user can edit
*
* @param bool $createonly true to show only prefixes with create permission
*
* @return string[]|false
*/
public function getUserNamespaces($createonly = false)
{
global $xoopsUser, $xoopsDB;
$dir = $this->wikiDir;
/** @var \XoopsModuleHandler $moduleHandler */
$moduleHandler = \xoops_getHandler('module');
$module = $moduleHandler->getByDirname($dir);
$module_id = $module->getVar('mid');
$groups = XOOPS_GROUP_ANONYMOUS;
if (\is_object($xoopsUser)) {
$groups = $xoopsUser->getGroups();
}
$grouppermHandler = \xoops_getHandler('groupperm');
$edit_any = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_EDIT_ANY_NUM, $groups, $module_id);
$edit_pfx = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_EDIT_PFX_NUM, $groups, $module_id);
$create_any = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_CREATE_ANY_NUM, $groups, $module_id);
$create_pfx = $grouppermHandler->checkRight('wiki_authority', \_MD_GWIKI_PAGE_PERM_CREATE_PFX_NUM, $groups, $module_id);
if (\is_array($groups)) {
$groupwhere = ' IN (' . \implode(', ', $groups) . ') ';
} else {
$groupwhere = " = '" . $groups . "'";
}
$sql = 'SELECT DISTINCT p.prefix_id, prefix FROM ';
$sql .= $xoopsDB->prefix('gwiki_prefix') . ' p, ';
$sql .= $xoopsDB->prefix('gwiki_group_prefix') . ' g ';
$sql .= ' WHERE group_id ' . $groupwhere;
$sql .= ' AND p.prefix_id = g.prefix_id';
$sql .= ' ORDER BY prefix ';
$prefixes = [];
$result = $xoopsDB->query($sql);
if ($create_any) {
$prefixes[] = ['prefix_id' => -1, 'prefix' => ' '];
}
while (false !== ($myrow = $xoopsDB->fetchArray($result))) {
$prefixes[] = $myrow;
}
// make sure we have some edit/create permission. We need full keyword to be certain, so let edit sort it out.
$mayEdit = ($edit_any || $create_any || $edit_pfx || $create_pfx);
if ($createonly) {
$mayEdit = ($create_any || $create_pfx);
}
if ($mayEdit) {
return $prefixes;
}
return false;
}
/**
* Get keyword for an id
*
* @param int $page_id id
*
* @return string|null wiki keyword (page)
*/
public function getKeywordById($page_id)
{
global $xoopsDB;
$keyword = null;
$sql = 'SELECT keyword FROM ' . $xoopsDB->prefix('gwiki_pageids') . " WHERE page_id = '{$page_id}' ";
$result = $xoopsDB->query($sql);
if ($result) {
$myrow = $xoopsDB->fetchRow($result);
$keyword = $myrow[0];
}
return $keyword;
}
/**
* lookup id for a keyword
*
* @param string $keyword keyword
*
* @return int
*/
public function getPageId($keyword)
{
global $xoopsDB;
$page_id = 0;
$keyword = $this->escapeForDB($keyword);
$sql = 'SELECT page_id FROM ' . $xoopsDB->prefix('gwiki_pageids') . " WHERE keyword = '{$keyword}' ";
$result = $xoopsDB->query($sql);
if ($result) {
$myrow = $xoopsDB->fetchRow($result);
$page_id = $myrow[0] ?? 0;
}
return $page_id;
}
/**
* record a page hit for a keyword
*
* @param string $keyword keyword
*
* @return int count of rows updated
*/
public function registerHit($keyword)
{
global $xoopsDB;
$keyword = $this->escapeForDB($keyword);
$sql = 'UPDATE ' . $xoopsDB->prefix('gwiki_pageids') . " SET hit_count = hit_count + 1 WHERE keyword = '{$keyword}' ";
$result = $xoopsDB->queryF($sql);
// nothing to do if it fails
return $xoopsDB->getAffectedRows();
}
/**
* set a specific revison as active for a keyword
*
* @param string $keyword wiki keyword
* @param int $id id of revision to activate
*
* @return mixed
*/
public function setRevision($keyword, $id)
{
global $xoopsDB;
$keyword = $this->escapeForDB($keyword);
$id = (int)$id;
$page = $this->getPage($keyword, $id);
if (!$page) {
return false;
}
$sql = 'UPDATE ' . $xoopsDB->prefix('gwiki_pages') . " SET active = 0 WHERE keyword='{$keyword}' and active = 1 ";
$result = $xoopsDB->query($sql);
$sql = 'UPDATE ' . $xoopsDB->prefix('gwiki_pages') . " SET active = 1 WHERE keyword='{$keyword}' AND gwiki_id='{$id}'";
$result = $xoopsDB->query($sql);
$this->updatePageLinks(true);
return $result;
}
/**
* load a page with optional revision id
*
* @param string $keyword keyword
* @param int|null $id optional page id
*
* @return bool
*/
public function getPage($keyword, $id = null)
{
global $xoopsDB;
$this->resetPage();
$this->keyword = $keyword;
$prefix = $this->getPrefix($keyword);
if ($prefix && $prefix['defined']) {
$this->currentprefix = $prefix['prefix'];
$this->currentprefixid = $prefix['prefix_id'];
$this->currenttemplateid = $prefix['prefix_template_id'];
}
$keyword = $this->escapeForDB($keyword);
$this->page_id = $this->getPageId($keyword);
if (empty($id)) {
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_pages') . ' natural left join ' . $xoopsDB->prefix('gwiki_pageids') . " WHERE keyword='{$keyword}' and active = 1 ";
} else {
$id = (int)$id;
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_pages') . ' natural left join ' . $xoopsDB->prefix('gwiki_pageids') . " WHERE keyword='{$keyword}' and gwiki_id = {$id} ";
}
$result = $xoopsDB->query($sql);
$page = false;
$rows = $xoopsDB->getRowsNum($result);
if ($rows > 0) {
$page = $xoopsDB->fetchArray($result);
$this->gwiki_id = $page['gwiki_id'];
$this->keyword = $page['keyword'];
$this->display_keyword = $page['display_keyword'];
$this->title = $page['title'];
$this->body = $page['body'];
$this->parent_page = $page['parent_page'];
$this->page_set_home = $page['page_set_home'];
$this->page_set_order = $page['page_set_order'];
$this->meta_description = $page['meta_description'];
$this->meta_keywords = $page['meta_keywords'];
$this->lastmodified = $page['lastmodified'];
$this->uid = $page['uid'];
$this->admin_lock = $page['admin_lock'];
$this->active = $page['active'];
$this->search_body = $page['search_body'];
$this->toc_cache = $page['toc_cache'];
$this->show_in_index = $page['show_in_index'];
$this->gwiki_version = $page['gwiki_version'];
$this->page_id = $page['page_id'];
$this->created = $page['created'];
$page['author'] = $this->getUserName($page['uid']);
$page['revisiontime'] = \date($this->dateFormat, $page['lastmodified']);
$page['createdtime'] = \date($this->dateFormat, $page['created']);
$page['createdmonth'] = \date('M', $page['created']);
$page['createdday'] = \date('d', $page['created']);
$page['createdyear'] = \date('Y', $page['created']);
$temp = $this->renderPageSetNav($keyword);
if ($temp) {
$page['pageset'] = $temp;
}
}
return $page;
}
/**
* Check for a prefix (namespace)
*
* @param mixed $keyword - wiki page name
*
* @return bool
*/
public function getPrefix($keyword)
{
/*
gwiki_prefix columns
prefix_id
prefix
prefix_home
prefix_template_id
prefix_is_external
prefix_external_url < sprintf template for page in external namespace
*/ global $xoopsDB;
$prefix = false;
$keyword = $this->escapeForDB($keyword);
$pos = mb_strpos($keyword, ':');
// split namespace and page reference on first colon
if (false !== $pos && $pos > 0) {
$pre = mb_substr($keyword, 0, $pos);
$page = mb_substr($keyword, $pos + 1);
$q_pre = $this->escapeForDB($pre);
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_prefix') . " WHERE prefix='{$q_pre}' ";
$result = $xoopsDB->query($sql);
$rows = $xoopsDB->getRowsNum($result);
if ($rows > 0) {
$prefix = $xoopsDB->fetchArray($result);
if ('' === $page) {
$page = $prefix['prefix_home'];
} // supply home page if empty
$prefix['defined'] = true;
// external namespace
if ($prefix['prefix_is_external']) {
$prefix['actual_page'] = \sprintf($prefix['prefix_external_url'], $page);
} else { // local namespace
$prefix['actual_page'] = $prefix['prefix'] . ':' . $page;
}
} else { // we have an undefined prefix
$prefix['defined'] = false;
}
}
return $prefix;
}
/**
* get prefix string for an id
*
* @param int $pid prefix id
*
* @return string namespace prefix, or empty string
*/
public function getPrefixFromId($pid)
{
global $xoopsDB;
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_prefix') . ' WHERE prefix_id =' . $pid;
$result = $xoopsDB->query($sql);
while (false !== ($myrow = $xoopsDB->fetchArray($result))) {
return $myrow;
}
return '';
}
/**
* get template for the current page
*
* @return string template name
*/
public function getTemplateName()
{
$template = 'gwiki_view.tpl';
if ($this->currenttemplateid) {
$template = $this->wikiDir . '_prefix_' . $this->currentprefixid . '.tpl';
}
return $template;
}
/**
* get attachment info associated with a page
*
* @param string $page keyword
*
* @return array
*/
public function getAttachments($page)
{
global $xoopsDB;
$this->attachments = [];
$q_keyword = $this->escapeForDB($page);
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_page_files') . " WHERE keyword='{$q_keyword}' ";
$result = $xoopsDB->query($sql);
$rows = $xoopsDB->getRowsNum($result);
while (false !== ($row = $xoopsDB->fetchArray($result))) {
$row['iconlink'] = XOOPS_URL . '/modules/' . $this->wikiDir . '/assets/icons/48px/' . $row['file_icon'] . '.png';
$row['userlink'] = $this->getUserName($row['file_uid']);
$row['size'] = \number_format($row['file_size']);
$row['date'] = \date($this->dateFormat, $row['file_upload_date']);
$this->attachments[] = $row;
}
return $this->attachments;
}
/**
* Make a link from a wiki keyword
*
* @param mixed $keyword - wiki page name
* @param mixed $altkey - alternate text for link. If empty, display_keyword will be used.
*
* @return string
*/
public function wikiLink($keyword, $altkey = null)
{
global $xoopsDB;
// HACK - get rid of spaces in page
// WikiCreole site is filled with page references such as [[Creole 1.0 Poll]] which resolve as
// hrefs like http://wikicreole.org/wiki/Creole1.0Poll
//
// will assume this is considered normal wikiish behavior, and try to emulate.
// Also seems to capitalize each portion, ie 'Ab and Cd' yields 'AbAndCd' - emulate this, too.
$org_keyword = $keyword;
if (mb_strpos(\trim($keyword), ' ')) {
$keys = \explode(' ', $keyword);
foreach ($keys as $i => $k) {
$keys[$i] = \ucfirst($k);
}
$keyword = \implode('', $keys);
}
// $keyword=str_replace (' ', '', $keyword);
// check for namespace prefix
$prefix = $this->getPrefix($keyword);
if ($prefix && $prefix['defined']) {
$link = $prefix['actual_page'];
// external namespace
if ($prefix['prefix_is_external']) {
$linktext = $org_keyword;
if ($altkey) {
$linktext = $altkey;
}
$linktext = \stripslashes($linktext);
$ret = '<a href="' . $link . '" target="_blank" title="' . \_MD_GWIKI_PAGE_EXT_LINK_TT . '">' . $linktext . '<span class="wikiextlink"> </span></a>';
return $ret;
} // interal namespace
$keyword = $link; // we may have modified the keyword
}
$sql = 'SELECT keyword, display_keyword, title FROM ' . $xoopsDB->prefix('gwiki_pages') . " WHERE keyword='{$keyword}' and active = 1 ";
$result = $xoopsDB->query($sql);
$rows = $xoopsDB->getRowsNum($result);
if ($rows) { // existing page
[$keyword, $display_keyword, $title] = $xoopsDB->fetchRow($result);
$display_keyword = \htmlentities($display_keyword, \ENT_QUOTES);
if (empty($display_keyword)) {
$display_keyword = $org_keyword;
}
$keyword = mb_strtolower($keyword);
$newpage = '';
} else { // new page link
$display_keyword = $org_keyword;
$newpage = '<span class="wikinewpage"> </span>';
$title = \sprintf(\_MD_GWIKI_PAGE_CREATE_TT, $keyword);
}
if (!empty($altkey)) {
$display_keyword = $altkey;
}
$title = \htmlspecialchars($title, \ENT_QUOTES | ENT_HTML5);
$display_keyword = \stripslashes($display_keyword);
// track where this page links
if (isset($this->wikiPageLinks[$keyword])) {
++$this->wikiPageLinks[$keyword];
} else {
$this->wikiPageLinks[$keyword] = 1;
}
$url = \sprintf($this->wikiLinkURL, $keyword);
return \sprintf('<a href="%s" title="%s">%s%s</a>', $url, $title, $display_keyword, $newpage);
}
/**
* callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function wikiCCLink($matches)
{
return $this->wikiLink($matches[1]);
}
/**
* get tabbed page index
*
* @return array
*/
private function getIndexTabs()
{
global $xoopsDB;
$tabs = [];
$sql = 'SELECT SUBSTRING(display_keyword,1,1) as letter, count(*) as count ';
$sql .= ' FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' WHERE active=1 AND show_in_index=1 ';
$sql .= ' GROUP BY letter ';
$result = $xoopsDB->query($sql);
$currentset = false;
$rows = $xoopsDB->getRowsNum($result);
if ($rows) {
while (false !== ($row = $xoopsDB->fetchArray($result))) {
$row['letter'] = mb_strtolower($row['letter']);
if ($this->pageIndexPrefix === $row['letter']) {
$row['current'] = true;
$currentset = true;
} else {
$row['current'] = false;
}
$tabs[] = $row;
}
}
$xoopsDB->freeRecordSet($result);
if (!$currentset) {
$this->pageIndexPrefix = $tabs[0]['letter'];
$tabs[0]['current'] = true;
}
return $tabs;
}
/**
* get a page index
*
* @param string|null $prefix if not null, limit index to a prefix
*
* @return string rendered index
*/
private function pageIndex($prefix = null)
{
global $xoopsDB;
$simplelayout = false;
$tablayout = false;
$body = '';
$pageselect = 'active=1 AND show_in_index=1 ';
if (!empty($prefix)) {
$pageselect .= ' AND keyword LIKE "' . $prefix . '%" ';
} else {
$sql = 'SELECT count(*) AS count FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' WHERE ' . $pageselect;
$result = $xoopsDB->query($sql);
$row = $xoopsDB->fetchArray($result);
$cnt = $row['count'];
$xoopsDB->freeRecordSet($result);
if ($cnt > 500) {
$tablayout = true;
$simplelayout = true; // tablayout is already grouped by first character
$tabs = $this->getIndexTabs();
$pageselect .= ' AND display_keyword LIKE "' . $this->pageIndexPrefix . '%" ';
}
}
$sql = 'SELECT keyword, display_keyword, title';
$sql .= ' FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' WHERE ' . $pageselect;
$sql .= ' ORDER BY display_keyword ';
// $sql.=' ORDER BY active, show_in_index, display_keyword ';
$result = $xoopsDB->query($sql);
$rowcnt = $xoopsDB->getRowsNum($result);
if ($rowcnt < 50) {
$simplelayout = true;
} // skip the fancy by letter breakout if this is a small index
if ($tablayout) {
$body .= '<div class="wikiindex"><div class="wikiindextabs"><ul>';
foreach ($tabs as $tab) {
$class = '';
if ($tab['current']) {
$class = ' id="wikiindextabactive"';
}
$url = \sprintf($this->wikiLinkURL, mb_strtolower($this->keyword . '(\\' . $tab['letter'] . ')'));
$letter = mb_strtoupper($tab['letter']);
$body .= "\n<li{$class}><a href=\"{$url}\">{$letter}</a></li>";
}
$body .= '</ul></div><div class="wikiindexbody">';
}
$lastletter = '';
if ($simplelayout) {
$body .= '<ul>';
}
while (false !== ($content = $xoopsDB->fetchArray($result))) {
$display_keyword = $content['display_keyword'];
if (empty($display_keyword)) {
$display_keyword = $content['keyword'];
}
if (!$simplelayout) {
if (\function_exists('mb_substr')) {
$testletter = mb_strtoupper(mb_substr($display_keyword, 0, 1, 'UTF-8'), 'UTF-8');
} else {
$testletter = mb_strtoupper(mb_substr($display_keyword, 0, 1));
}
if ('' === $lastletter) {
$lastletter = $testletter;
$body .= "<h3>{$lastletter}</h3><ul>";
}
if ($lastletter !== $testletter) {
$lastletter = $testletter;
$body .= "</ul><h3>{$lastletter}</h3><ul>";
}
}
$title = \htmlspecialchars($content['title'], \ENT_QUOTES | ENT_HTML5);
$display_keyword = \htmlspecialchars($display_keyword, \ENT_QUOTES | ENT_HTML5);
$url = \sprintf($this->wikiLinkURL, mb_strtolower($content['keyword']));
$link = \sprintf('<a href="%s" title="%s">%s%s</a>', $url, $title, $display_keyword, '');
$body .= '<li>' . $link . ' : ' . $title . '</li>';
}
$xoopsDB->freeRecordSet($result);
if ($tablayout) {
$body .= '</ul></div></div>';
} elseif ('' != $body) {
$body .= '</ul>';
}
return $body . "\n\n";
}
/**
* get a recently modfied page index
*
* @param string|null $prefix if not null, limit index to a prefix
*
* @return string rendered index
*/
private function recentIndex($prefix = null)
{
global $xoopsDB;
// only show active pages
$pageselect = 'active=1 AND show_in_index=1 ';
if (!empty($prefix)) {
$pageselect .= ' AND keyword LIKE "' . $prefix . '%" ';
}
$body = '';
$sql = 'SELECT keyword, display_keyword, title, lastmodified';
$sql .= ', FROM_UNIXTIME(lastmodified) as fmtlastmodified, uid';
$sql .= ' FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= ' WHERE ' . $pageselect;
$sql .= ' ORDER BY lastmodified DESC LIMIT ' . $this->numberOfRecentItems;
$result = $xoopsDB->query($sql);
$lastdate = '';
while (false !== ($content = $xoopsDB->fetchArray($result))) {
$testdate = mb_substr($content['fmtlastmodified'], 0, 10);
if ('' === $lastdate) {
$lastdate = $testdate;
$body .= "<h3>{$lastdate}</h3><ul>";
}
if ($lastdate !== $testdate) {
$lastdate = $testdate;
$body .= "</ul><h3>{$lastdate}</h3><ul>";
}
$title = \htmlspecialchars($content['title'], \ENT_QUOTES | ENT_HTML5);
$display_keyword = \htmlspecialchars($content['display_keyword'], \ENT_QUOTES | ENT_HTML5);
$url = \sprintf($this->wikiLinkURL, mb_strtolower($content['keyword']));
$link = \sprintf('<a href="%s" title="%s">%s%s</a>', $url, $title, $display_keyword, '');
$body .= '<li>' . $link . ' : ' . $title . '</li>';
}
$xoopsDB->freeRecordSet($result);
if ('' !== $body) {
$body .= '</ul>';
}
return $body . "\n\n";
}
/**
* callback render match specified index
*
* @param string[] $matches preg_replace_callback matches
*
* @return bool|string
*/
private function renderIndex($matches)
{
$type = $matches[1];
$parms = '';
if (isset($matches[2])) {
$parms = \trim($matches[2]);
}
if (0 === \strcasecmp($type, 'RecentChanges')) {
return $this->recentIndex($parms);
}
if (0 === \strcasecmp($type, 'PageIndex')) {
return $this->pageIndex($parms);
}
return false;
}
/**
* highlight search terms
* adapted from: http://stack:overflow.com/questions/2591046/highlight-text-except-html-tags
*
* @param string[] $capture matches
*
* @return string
*/
private function mon_rplc_callback($capture)
{
$haystack = $capture[1];
$p1 = mb_stripos($haystack, $this->highlightArg['needle']);
$l1 = mb_strlen($this->highlightArg['needle']);
$ret = '';
while (false !== $p1) {
$ret .= mb_substr($haystack, 0, $p1) . $this->highlightArg['pre'] . mb_substr($haystack, $p1, $l1) . $this->highlightArg['post'];
$haystack = mb_substr($haystack, $p1 + $l1);
$p1 = mb_stripos($haystack, $this->highlightArg['needle']);
}
$ret .= $haystack . $capture[2];
return $ret;
}
/**
* split string aware of html tags
*
* @param string $needle string to find
* @param string $pre string to include before each match
* @param string $post string to include after each match
* @param string $txt text to search
*
* @return string
*/
private function split_on_tag($needle, $pre, $post, $txt)
{
$this->highlightArg = \compact('needle', 'pre', 'post');
return \preg_replace_callback('#((?:(?!<[/a-z]).)*)([^>]*>|$)#si', [$this, 'mon_rplc_callback'], $txt);
}
/**
* highlight words in page
*
* @param string $words space separated words to match
*
* @return string rendered page with words highlighted
*/
public function highlightWords($words)
{
$words = \str_replace(' ', ' ', $words);
$words = \explode(' ', $words);
$body = $this->renderedPage;
foreach ($words as $word) {
$body = $this->split_on_tag($word, '<span class="wiki_search_term">', '</span>', $body);
}
return $body;
}
/**
* Hold content not to be processed for wiki markup, generate a unique tag to locate later
*
* @param string $type type of nowiki invocation (block, wcinline or inline)
* @param string $source content to hold
*
* @return string generated tag for held content
*/
private function noWikiHold($type, $source)
{
++$this->noWikiIndex;
switch ($type) {
case 'block':
$this->noWikiQueue[$this->noWikiIndex] = "<pre>\n{$source}\n</pre>";
break;
case 'wcinline':
$this->noWikiQueue[$this->noWikiIndex] = '<span class="wikinoinline">' . $source . '</span>';
break;
case 'inline':
default:
$this->noWikiQueue[$this->noWikiIndex] = $source;
break;
}
$ret = "{PdNlNw:{$this->noWikiIndex}}";
return $ret;
}
/**
* no wiki block callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function noWikiHoldBlock($matches)
{
return $this->noWikiHold('block', $matches[1]);
}
/**
* no wiki inline callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function noWikiHoldInline($matches)
{
return $this->noWikiHold('inline', $matches[1]);
}
/**
* no wiki inline (WikiCreole style) callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function noWikiHoldWCInline($matches)
{
return $this->noWikiHold('wcinline', $matches[1]);
}
/**
* no wiki for code block callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function noWikiHoldCode($matches)
{
return $matches[1] . $this->noWikiHold('block', $matches[2]) . $matches[3];
}
/**
* emit save nowiki content callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function noWikiEmit($matches)
{
$index = $matches[1];
return $this->noWikiQueue[$index];
}
/**
* table support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderTables($matches)
{
$source = $matches[0];
$rowcnt = 0;
$table = "<table class=\"wikitable\">\n";
$rows = \explode("\n", $source);
foreach ($rows as $i => $row) {
$row = \trim($row);
if (!empty($row)) {
if ('|' === $row[0]) {
$row = mb_substr($row, 1);
}
if ('|' === mb_substr($row, -1)) {
$row = mb_substr($row, 0, -1);
}
$cols = \explode('|', $row);
$table .= '<tr' . (($rowcnt % 2) ? ' class="even"' : ' class="odd"') . '>';
++$rowcnt;
foreach ($cols as $col) {
if (empty($col)) {
$table .= '<td> </td>';
} elseif ('=' === $col[0]) {
$table .= '<th>' . mb_substr($col, 1) . '</th>';
} elseif ('>' === $col[0]) {
$table .= '<td class="right">' . mb_substr($col, 1) . '</td>';
} elseif ('+' === $col[0]) {
$table .= '<td class="center">' . mb_substr($col, 1) . '</td>';
} elseif ('<' === mb_substr($col, 0, 4)) {
$table .= '<td class="left">' . mb_substr($col, 4) . '</td>';
} elseif (\preg_match('/^\s*[0-9.$+\-]+\s*$/', $col)) {
$class = 'number';
if ((float)\preg_replace('/[^-0-9\.]/', '', $col) < 0) {
$class = 'number negative';
}
$table .= '<td class="' . $class . '">' . \trim($col) . '</td>';
} else {
$table .= '<td>' . $col . '</td>';
}
}
$table .= "</tr>\n";
}
}
$table .= "</table>\n";
return $table;
}
/**
* link support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderLink($matches)
{
$source = \trim($matches[1]);
$pos = mb_strpos($source, '|');
if (false === $pos) { // no delimter - whole thing is the link
$link = $source;
$linktext = '';
// handle the pathological case of a possesive of a person page.
// Creole test includes "[[Ward Cunningham's]]" which leads to a
// wiki page WardCunningham. Included in spirit of compatibility.
if ("'s" === mb_substr($link, -2)) {
$templink = mb_substr($link, 0, -3); // quote is slashed
// only if a wiki page
if (\preg_match('/^([A-Za-z\x80-\xff0-9.:\- ]){2,}$/', $templink)) {
$linktext = $link;
$link = $templink;
}
}
} else {
$link = \trim(mb_substr($source, 0, $pos));
$linktext = \trim(mb_substr($source, $pos + 1));
}
if (\preg_match('/^([A-Za-z\x80-\xff0-9.:\- ]){2,}$/', $link)) {
//$link=str_replace (' ', '', $link);
if (empty($linktext)) {
$ret = $this->wikiLink($link);
} else {
$ret = $this->wikiLink($link, \stripslashes($linktext));
}
} else {
$ext = true;
if (0 === \strncasecmp($link, XOOPS_URL, mb_strlen(XOOPS_URL))) {
$ext = false;
} // matches our site
if (0 === \strcasecmp('siteurl:', mb_substr($link, 0, 8))) { // explicit reference to our site
$link = XOOPS_URL . mb_substr($link, 8);
$ext = false;
}
if (false === mb_strpos($link, ':')) {
$ext = false;
} // no protocol, assume relative url
if ('' === $linktext) {
$linktext = $link;
}
$linktext = \stripslashes($linktext);
// $linktext=$this->noWikiHold('inline',stripslashes($linktext));
$ret = "<a href=\"{$link}\" title=\"{$linktext}\">{$linktext}</a>";
if ($ext) {
$ret = '<a href="' . $link . '" target="_blank" title="' . \_MD_GWIKI_PAGE_EXT_LINK_TT . '">' . $linktext . '<span class="wikiextlink"> </span></a>';
}
}
return $ret;
}
/**
* header support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderHeader($matches)
{
$source = $matches[3];
$level = $matches[2];
$level = mb_strlen($level) + 1;
$this->tocQueue[$this->tocIndex] = ['level' => $level, 'name' => $source];
$toc = "\n<h" . $level . ' id="' . $this->tocIdPrefix . $this->tocIndex . '" >' . $source . '</h' . $level . ">\n";
++$this->tocIndex;
return $toc;
}
/**
* indent support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderIndent($matches)
{
$source = $matches[2];
$level = $matches[1];
$level = mb_strlen($level);
$ret = "\n<div class=\"wikiindent{$level}\">\n{$source}\n</div>";
return $ret;
}
/**
* table of contents support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderToc($matches)
{
$tocq = $this->tocQueue;
$toc = '';
foreach ($tocq as $i => $v) {
$toc .= '<li class="wikitoclevel' . $v['level'] . '"><a href="' . \sprintf($this->tocAnchorFmt, $this->tocIdPrefix . $i) . '">' . \strip_tags($v['name']) . '</a></li>';
}
if (!empty($toc)) {
$toc = '<div class="wikitoc"><div class="wikitocheader">' . \_MD_GWIKI_TOC . '</div><ul class="wikitoclist">' . $toc . '</ul></div>';
}
return $toc;
}
/**
* fetch table of contents for a page
*
* @param string $page keyword
*
* @return array|bool
*/
public function fetchPageSetToc(&$page)
{
global $xoopsDB;
$toc = false;
$q_page = $this->escapeForDB($page);
$sql = 'SELECT gwiki_id, keyword, display_keyword, page_set_home, page_set_order, toc_cache ';
$sql .= ' FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= " WHERE active=1 and keyword='{$q_page}' ";
$result = $xoopsDB->query($sql);
$rows = $xoopsDB->getRowsNum($result);
if ($rows) {
$row = $xoopsDB->fetchArray($result);
if (!empty($row['page_set_home'])) {
$page = $row['page_set_home']; // this is passed back up to caller!
$q_page = $this->escapeForDB($row['page_set_home']);
$xoopsDB->freeRecordSet($result);
$sql = 'SELECT gwiki_id, keyword, display_keyword, page_set_home, page_set_order, toc_cache ';
$sql .= ' FROM ' . $xoopsDB->prefix('gwiki_pages');
$sql .= " WHERE active=1 and page_set_home='{$q_page}' ";
$sql .= ' ORDER BY page_set_order, keyword ';
$result = $xoopsDB->query($sql);
while (false !== ($row = $xoopsDB->fetchArray($result))) {
$row['display_keyword'] = \strip_tags($row['display_keyword']);
if (!empty($row['toc_cache'])) {
$tmp = \unserialize($row['toc_cache']);
foreach ($tmp as $i => $v) {
$tmp[$i]['name'] = \strip_tags($tmp[$i]['name']);
}
$row['toc'] = $tmp;
$toc[] = $row;
}
}
}
}
$xoopsDB->freeRecordSet($result);
return $toc;
}
/**
* page set toc support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
public function renderPageSetTocWrapper($matches)
{
return $this->renderPageSetToc($this->keyword, 6);
}
/**
* render a table of contents
*
* @param string $page keyword
* @param int $level level limit
* @param string $tocclass base class for toc. Current level will be appended
*
* @return bool|string
*/
public function renderPageSetToc(&$page, $level, $tocclass = 'wikitocpage')
{
$toc = $this->fetchPageSetToc($page);
if (!$toc) {
return false;
}
$tocout = '';
foreach ($toc as $ti => $tv) {
//$link=sprintf($this->getWikiLinkURL(),$tv['keyword']);
foreach ($tv['toc'] as $i => $v) {
if ((int)$v['level'] <= $level) {
$tocout .= '<li class="wikitoclevel' . $v['level'] . '"><a href="' . \sprintf($this->getWikiLinkURL(), $tv['keyword'] . \sprintf($this->tocAnchorFmt, $this->tocIdPrefix . $i)) . '">' . $v['name'] . '</a></li>';
}
}
}
if (!empty($tocout)) {
$tocout = '<div class="' . $tocclass . '"><ul class="wikitoclist">' . $tocout . '</ul></div>';
}
return $tocout;
}
/**
* render navigation for a page set
* @param $page
*
* @return mixed
*/
public function renderPageSetNav($page)
{
$sethome = $page;
$toc = $this->fetchPageSetToc($sethome); // this will set home
if (!$toc) {
return false;
}
$home = -1;
$current = -1;
$prev = -1;
$next = -1;
foreach ($toc as $i => $v) {
if (0 === \strcasecmp($toc[$i]['keyword'], $page)) {
$current = $i;
}
if (0 === \strcasecmp($toc[$i]['keyword'], $sethome)) {
$home = $i;
}
}
if ($current > -1) {
$prev = $current - 1;
$next = $current + 1;
}
$first = 0;
$last = \count($toc) - 1;
// should these wrap instead?
if ($next > $last) {
$next = $last;
}
if ($prev < 0) {
$prev = 0;
}
if ($home < 0) {
$home = 0;
}
$pageset['first'] = [
'link' => \sprintf($this->getWikiLinkURL(), $toc[$first]['keyword']),
'text' => \htmlentities($toc[$first]['display_keyword'], \ENT_QUOTES),
'desc' => \_MD_GWIKI_PAGENAV_FIRST,
];
$pageset['prev'] = [
'link' => \sprintf($this->getWikiLinkURL(), $toc[$prev]['keyword']),
'text' => \htmlentities($toc[$prev]['display_keyword'], \ENT_QUOTES),
'desc' => \_MD_GWIKI_PAGENAV_PREV,
];
$pageset['home'] = [
'link' => \sprintf($this->getWikiLinkURL(), $toc[$home]['keyword']),
'text' => \htmlentities($toc[$home]['display_keyword'], \ENT_QUOTES),
'desc' => \_MD_GWIKI_PAGENAV_TOP,
];
$pageset['next'] = [
'link' => \sprintf($this->getWikiLinkURL(), $toc[$next]['keyword']),
'text' => \htmlentities($toc[$next]['display_keyword'], \ENT_QUOTES),
'desc' => \_MD_GWIKI_PAGENAV_NEXT,
];
$pageset['last'] = [
'link' => \sprintf($this->getWikiLinkURL(), $toc[$last]['keyword']),
'text' => \htmlentities($toc[$last]['display_keyword'], \ENT_QUOTES),
'desc' => \_MD_GWIKI_PAGENAV_LAST,
];
if (0 === \strcasecmp($toc[$first]['keyword'], $page)) {
$pageset['first']['link'] = 'javascript:void(0)';
}
if (0 === \strcasecmp($toc[$prev]['keyword'], $page)) {
$pageset['prev']['link'] = 'javascript:void(0)';
}
if (0 === \strcasecmp($toc[$home]['keyword'], $page)) {
$pageset['home']['link'] = 'javascript:void(0)';
}
if (0 === \strcasecmp($toc[$next]['keyword'], $page)) {
$pageset['next']['link'] = 'javascript:void(0)';
}
if (0 === \strcasecmp($toc[$last]['keyword'], $page)) {
$pageset['last']['link'] = 'javascript:void(0)';
}
return $pageset;
}
/**
* get image libraries for a page
*
* @param string $keyword keyword of page
*
* @return string[]
*/
public function getImageLib($keyword)
{
$lib = $this->imageLib;
\array_unshift($lib, $keyword);
return \array_unique($lib);
}
/**
* get an image relative to specified page
*
* return array includes:
* image_id
* keyword
* image_name
* image_alt_text
* image_file
*
* @param string $keyword keyword of page
* @param string $name image name
*
* @return array|bool image data or false if invalid or not found
*/
public function getPageImage($keyword, $name)
{
global $xoopsDB;
if (0 === \strncasecmp($name, 'http://', 7) || 0 === \strncasecmp($name, 'https://', 8)) {
return false;
}
$lib = $this->imageLib;
\array_unshift($lib, $keyword);
foreach ($lib as $page) {
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_page_images') . ' WHERE keyword=\'' . $this->escapeForDB($page) . '\' ';
$sql .= ' AND image_name=\'' . $this->escapeForDB($name) . '\' ';
$result = $xoopsDB->query($sql);
$image = $xoopsDB->fetchArray($result);
if ($image) {
return $image;
}
}
return false;
}
/**
* render image support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderImage($matches)
{
$source = \trim($matches[1]);
$pos = mb_strpos($source, '|');
//if($pos===false) $pos=strpos($source,' ');
if (false === $pos) { // no delimter - whole thing is the image url
$link = $source;
$parms = [];
} else {
$link = \trim(mb_substr($source, 0, $pos));
$parms = \explode('|', \trim(mb_substr($source, $pos + 1)));
foreach ($parms as $i => $parm) {
$parms[$i] = \trim($parm);
}
}
if (0 === \strcasecmp('siteurl:', mb_substr($link, 0, 8))) { // explicit reference to our site
$link = XOOPS_URL . mb_substr($link, 8);
}
$showthumb = false;
$showthumblink = false;
if (0 === \strcasecmp('thumb:', mb_substr($link, 0, 6))) { // explicit request for thumbnail, links to full image
$revertlink = $link;
$link = mb_substr($link, 6);
$showthumblink = true;
}
$alttext = empty($parms[0]) ? '' : $parms[0];
$align = empty($parms[1]) ? '' : $parms[1];
$maxpx = empty($parms[2]) ? '' : (int)$parms[2];
// align must be left, right, center or empty
$align = '';
if (0 === \strcasecmp($align, 'left')) {
$align = 'left';
} elseif (0 === \strcasecmp($align, 'right')) {
$align = 'right';
} elseif (0 === \strcasecmp($align, 'center')) {
$align = 'center';
}
$alignparm = '';
if ('left' === $align || 'right' === $align || 'center' === $align) {
$alignparm = ', ' . $align;
}
// look up link in page_images table, if found use that, otherwise just pass on link as is
$image = $this->getPageImage($this->keyword, $link);
if ($image) {
// image array includes:
// image_id
// keyword
// image_name
// image_alt_text
// image_file
// use_to_represent
$link = XOOPS_URL . '/uploads/' . $this->wikiDir . '/' . $image['image_file'];
if (empty($alttext)) {
$alttext = $image['image_alt_text'];
}
} else {
// thumbs don't apply, so put everything back the way it was
if ($showthumblink) {
$link = $revertlink;
$showthumblink = false;
}
}
$alt = '';
// $alttext=htmlspecialchars($alttext);
if (!empty($alttext)) {
$alt = " alt=\"{$alttext}\" title=\"{$alttext}\" ";
}
$maxpxstyle = '';
if (!empty($maxpx)) {
$maxpxstyle = " style=\"max-width:{$maxpx}px; max-height:{$maxpx}px; width:auto; height:auto;\" ";
$showthumb = true; // trigger automatic thumbnail use
}
if ($showthumb) {
$thumbsize = $this->defaultThumbSize;
if (!empty($maxpx)) {
$thumbsize = $maxpx;
}
$link = XOOPS_URL . '/modules/' . $this->wikiDir . '/getthumb.php?page=' . $image['keyword'] . '&name=' . \urlencode($image['image_name']) . '&size=' . $thumbsize;
}
if ($showthumblink) {
$ret = '';
$thumbsize = $this->defaultThumbSize;
if (!empty($maxpx)) {
$thumbsize = $maxpx;
}
$thumb = XOOPS_URL . '/modules/' . $this->wikiDir . '/getthumb.php?page=' . $image['keyword'] . '&name=' . \urlencode($image['image_name']) . '&size=' . $thumbsize;
$img = XOOPS_URL . '/uploads/' . $this->wikiDir . '/' . $image['image_file'];
$ret .= '<a href="' . $img . '" ' . $alt . '><img src="' . $thumb . '"' . $alt . $maxpxstyle . '></a>';
} else {
$ret = "<img class=\"wikiimage{$alignparm}\" src=\"{$link}\" {$alt}{$maxpxstyle}>";
}
if ('center' === $align) {
$ret = '<div style="margin: 0 auto; text-align: center;">' . $ret . '</div>';
}
return $ret;
}
/**
* gallery support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderGallery($matches)
{
global $xoopsDB;
$source = '';
if (isset($matches[1])) {
$source = $matches[1];
}
$maxpx = (int)\trim($source);
if ($maxpx < 10) {
$maxpx = $this->defaultThumbSize;
}
$page = $this->keyword;
$sql = 'SELECT * FROM ' . $xoopsDB->prefix('gwiki_page_images') . ' WHERE keyword = \'' . $page . '\' ' . ' ORDER BY image_name ';
$result = $xoopsDB->query($sql);
$dir = $this->wikiDir;
$body = '<div class="wikigallery"><ul class="wikigalleryimg">';
for ($i = 0, $iMax = $xoopsDB->getRowsNum($result); $i < $iMax; ++$i) {
$image = $xoopsDB->fetchArray($result);
$img = XOOPS_URL . '/uploads/' . $dir . '/' . $image['image_file'];
$thumb = XOOPS_URL . '/modules/' . $dir . '/getthumb.php?page=' . $image['keyword'] . '&name=' . \urlencode($image['image_name']) . '&size=' . $maxpx;
$alt = \htmlentities($image['image_alt_text'], \ENT_QUOTES);
$name = \htmlentities($image['image_name'], \ENT_QUOTES);
if (empty($alt)) {
$alt = $name;
}
$body .= '<li><a href="' . $img . '" title="' . $name . '"><img src="' . $thumb . '" alt="' . $alt . '" title="' . $alt . '"></a></li>' . "\n";
}
$body .= '</ul><br style="clear:both;"></div>';
return $body;
}
/**
* list support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderLists($matches)
{
$lines = \explode("\n", $matches[0]);
$last = '';
foreach ($lines as $i => $line) {
$line = \ltrim($line);
if (!empty($line)) {
$list = '';
$p = mb_strpos($line, ' ');
$current = mb_substr($line, 0, $p);
$x = 0;
while (!empty($last[$x]) && !empty($current[$x]) && $last[$x] === $current[$x]) {
++$x;
}
// $x is where the last and current list prefixes differ
// close anything from $x to end in last
$close = \strrev(mb_substr($last, $x));
$y = 0;
while (!empty($close[$y])) {
if ('*' === $close[$y]) {
$list .= '</li></ul>';
} //.($x>0?'</li>':'');
if ('#' === $close[$y]) {
$list .= '</li></ol>';
} //.($x>0?'</li>':'');
++$y;
}
// open anything from $x to end in
$open = mb_substr($current, $x);
$y = 0;
while (!empty($open[$y])) {
if ('*' === $open[$y]) {
$list .= '<ul class="wikiulist">';
}
if ('#' === $open[$y]) {
$list .= '<ol class="wikiolist">';
}
++$y;
}
$endli = ($last === $current) ? '</li>' : '';
$last = $current;
$lines[$i] = $list . $endli . "\n<li> " . mb_substr($line, $p + 1);
}
}
// put list back together
$list = "\n";
foreach ($lines as $line) {
if (!empty($line)) {
$list .= $line;
}
}
// close anything left open
$close = \strrev($last);
$y = 0;
while (!empty($close[$y])) {
if ('*' === $close[$y]) {
$list .= "</li></ul>\n";
}
if ('#' === $close[$y]) {
$list .= "</li></ol>\n";
}
++$y;
}
return $list;
}
/**
* reference support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderRef($matches)
{
$refinfo = $matches[1];
$source = $matches[2];
$first_ref = false;
$refs = \explode('|', \trim($refinfo) . '|||');
$rq['id'] = $refs[0];
$rq['first'] = $refs[1];
$rq['repeat'] = $refs[2];
$rq['source'] = $source;
$refid = (-1);
if (!empty($rq['id'])) {
foreach ($this->refQueue as $i => $v) {
if ($v['id'] === $rq['id']) {
$refid = $i;
}
}
}
if ($refid === (-1)) {
$refid = $this->refIndex;
$first_ref = true;
$this->refQueue[$this->refIndex] = $rq;
++$this->refIndex;
}
$paren_ref = false;
if (!empty($this->refQueue[$refid]['first'])) {
$paren_ref = true;
}
if ($paren_ref) {
$ref_text = $this->refQueue[$refid]['first'];
if (!$first_ref) {
if (!empty($this->refQueue[$refid]['repeat'])) {
$ref_text = $this->refQueue[$refid]['repeat'];
}
}
$r = '<span class="wikiparenref"><a href="#ref' . $refid . '">(' . $ref_text . ')</a></span>';
} else {
$r = '<span class="wikinumref"><a href="#ref' . $refid . '">' . ($refid + 1) . '</a></span>';
}
return $r;
}
/**
* reference list support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderRefList($matches)
{
$this->refShown = true;
$r = '<div class="wikicitelist">';
foreach ($this->refQueue as $i => $v) {
$refid = $i;
if (empty($v['first'])) {
$r .= '<div class="wikicitenumitem" id="ref' . $refid . '"><span class="wikicitenum">' . ($refid + 1) . '. </span>' . $v['source'] . "</div>\n";
} else {
$r .= '<div class="wikiciteparenitem" id="ref' . $refid . '">' . $v['source'] . "</div>\n";
}
}
$r .= '</div>';
return $r;
}
/**
* box support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderBox($matches)
{
$type = $matches[1];
$title = $matches[2];
$body = $matches[3];
// make sure we have a valid type
$type = mb_strtolower($type);
if (!('code' === $type || 'info' === $type || 'note' === $type || 'tip' === $type || 'warn' === $type
|| 'folded' === $type)) {
$type = 'info';
}
// $title may include options ( title | align ) :
// align: adds additonal class 'left' or 'right' to box so css can alter float, size, etc.
$title = \trim($title);
$eclass = '';
$ejs = '';
$etooltip = '';
$pos = mb_strpos($title, '|');
if (false !== $pos) { // if no delimiter - whole thing is the title
$parms = \explode('|', $title);
$title = $parms[0];
if (!empty($parms[1]) && ('left' === $parms[1] || 'right' === $parms[1])) {
$eclass = ' ' . $parms[1];
}
}
if ('folded' === $type) {
$foldclass = 'wikifolded' . $eclass;
$unfoldclass = 'wikiunfolded' . $eclass;
$ejs = ' onclick="var c=this.className; if(c==\'' . $foldclass . '\') this.className=\'' . $unfoldclass . '\'; else this.className=\'' . $foldclass . '\';"';
$etooltip = '<span>' . \_MD_GWIKI_FOLDED_TT . '</span>';
}
$ret = '<div class="wiki' . $type . $eclass . '"' . $ejs . '><div class="wiki' . $type . 'icon"></div><div class="wiki' . $type . 'title">' . $title . $etooltip . '</div><div class="wiki' . $type . 'inner">' . $body . '<br clear="all"></div></div>' . "\n\n";
return $ret;
}
/**
* Convert entities
*
* @param string $body wiki text to process
*
* @return string
*/
private function convertEntities($body)
{
// convert some entites
$sym = [];
$ent = [];
$sym[] = '{cent}';
$ent[] = '¢';
$sym[] = '{pound}';
$ent[] = '£';
$sym[] = '{yen}';
$ent[] = '¥';
$sym[] = '{euro}';
$ent[] = '€';
$sym[] = '{c}';
$ent[] = '©';
$sym[] = '(c)';
$ent[] = '©';
$sym[] = '{r}';
$ent[] = '®';
$sym[] = '(r)';
$ent[] = '®';
$sym[] = '{tm}';
$ent[] = '™';
$sym[] = '(tm)';
$ent[] = '™';
$sym[] = '{sm}';
// very poor font support for unicode code point for service mark, fake with markup
$ent[] = '<span style="font-size: 50%; vertical-align: super;">SM</span>';
$sym[] = '{nbsp}';
$ent[] = ' ';
$body = \str_ireplace($sym, $ent, $body);
return $body;
}
/**
* Render a teaser section. If page includes a {more} tag, teaser will be text that preceeds it.
* Otherwise try to break semantically at about 400 characters.
*
* @param string|null $body text to process, defaults to current page body
* @param string|null $title title to use
*
* @return string
*/
public function renderTeaser($body = null, $title = null)
{
// chop body at more tag if it is set
$splitsize = 400; // arbitrary size to use when no {more} tag
if (empty($body)) {
$body = $this->body;
}
$pos = mb_stripos($body, '{more}');
if (false === $pos && mb_strlen($body) > $splitsize) {
$search = "#\r\n?#";
$replace = "\n";
$body = \preg_replace($search, $replace, $body);
$pos = mb_stripos($body, "\n\n", $splitsize); // hopefully the end of a paragraph
}
if (false !== $pos) {
$body = mb_substr($body, 0, $pos);
$url = \sprintf($this->wikiLinkURL, $this->keyword);
}
$body = \str_ireplace('{toc}', '', $body);
$body = $this->renderPage($body, $title);
if (false !== $pos) {
$body .= '<a href="' . $url . '#more"><span class="wikimore">' . \_MD_GWIKI_MORE . '</span></a>';
}
return $body;
}
/**
* block quote support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderBlockquote($matches)
{
$src = \str_replace("\n", ' ', \preg_replace('#^> #m', '', $matches[0]));
return '<blockquote class=\"wikiquote\">' . $src . "</blockquote>\n";
}
/**
* preformatted support callback
*
* @param string[] $matches preg_replace_callback matches
*
* @return string
*/
private function renderPreformat($matches)
{
$src = \preg_replace('#^. #m', '', $matches[0]);
return '<pre>' . $src . "</pre>\n";
}
/**
* Render a page
*
* @param string|null $body text to process, defaults to current page body
* @param string|null $title title to use, defaults to current page title
*
* @return string
*/
public function renderPage($body = null, $title = null)
{
if (empty($body)) {
$body = $this->body;
}
$this->renderedPage = '';
$this->noWikiQueue = [];
$this->noWikiIndex = 0;
$this->refShown = false;
if (empty($title)) {
$title = $this->title;
}
// do first because title should always be #toc0 - set in template
$this->renderHeader(['', '', '', $title]);
$body .= "\n";
// eliminate double line endings
$search = "#\r\n?#";
$replace = "\n";
$body = \preg_replace($search, $replace, $body);
// neuter html tags
$search = '#<#';
$replace = '<';
$body = \preg_replace($search, $replace, $body);
// neuter single quotes
$search = "#'#";
$replace = "\\'";
$body = \preg_replace($search, $replace, $body);
// nowiki - tilde escape
$search = "#~([^ \t\r\n\v\f])#U";
$replace = [$this, 'noWikiHoldInline'];
$body = \preg_replace_callback($search, $replace, $body);
// nowiki content gwiki style
$search = '#{nowiki}(.*){endnowiki}#Umsi';
$replace = [$this, 'noWikiHoldInline'];
$body = \preg_replace_callback($search, $replace, $body);
// nowiki content block creole style (a nowiki that forces a style, how odd.)
$search = "#^{{{\n(.*)^}}}\n#Umsi";
$replace = [$this, 'noWikiHoldBlock'];
$body = \preg_replace_callback($search, $replace, $body);
// nowiki content inline creole style
$search = '#{{{(.*)}}}#U';
$replace = [$this, 'noWikiHoldWCInline'];
$body = \preg_replace_callback($search, $replace, $body);
// automatically nowiki content of code box - {code title}xxx{endcode}
$search = "#({code [^\"<\n]+?})(.*?)({endcode})#si";
$replace = [$this, 'noWikiHoldCode'];
$body = \preg_replace_callback($search, $replace, $body);
// center ++ xxx
$search = "#^(\+{2})(.*)(?=\n\n|\Z)#Usm";
$replace = "<div style=\"text-align: center;\" class=\"wikicenter\">\n\\2\n</div>\n";
$body = \preg_replace($search, $replace, $body);
// : indent up to 5 levels
$search = "#^(\:{1,5})\s(.*)(?=\n\n|\Z)#Usm";
$replace = [$this, 'renderIndent'];
$body = \preg_replace_callback($search, $replace, $body);
// lists
$search = "#^( *[*\#]{1,} (.*)\n)+#m";
$replace = [$this, 'renderLists'];
$body = \preg_replace_callback($search, $replace, $body);
// bold **xxx**
$search = "#\*{2}(.*?)(\*{2}|(?=\n\n))#s";
$replace = '<strong class="wikistrong">\\1</strong>';
$body = \preg_replace($search, $replace, $body);
// italic //xxx//
$search = "#(?<![:])/{2}(.*?[^:])(/{2}|(?=\n\n))#s";
$replace = '<em class="wikiem">\\1</em>';
$body = \preg_replace($search, $replace, $body);
// horizontal rule ---- (not an empty strikethru; creole says 4 or more so this needs to go first)
$search = '#^-{4,}$#m';
$replace = "\n<hr class=\"wikihr\">\n";
$body = \preg_replace($search, $replace, $body);
// strikethru --xxx-- (this does NOT cross lines, as '--' is a common typographic convention
$search = '#-{2}([^\s]{1}.*?)(-{2})#';
$replace = '<del class="wikidel">\\1</del>';
$body = \preg_replace($search, $replace, $body);
// underline __xxx__
$search = "#(?<=\s)_{2}(.*?)(_{2}|(?=\n\n))#s";
$replace = '<span class="wikiu">\\1</span>';
$body = \preg_replace($search, $replace, $body);
// superscript ^^xxx^^
$search = "#\^{2}(.*?)(\^{2}|(?=\n\n))#s";
$replace = '<sup class="wikisup">\\1</sup>';
$body = \preg_replace($search, $replace, $body);
// subscript ,,xxx,,
$search = "#,{2}(.*?)(,{2}|(?=\n\n))#s";
$replace = '<sub class="wikisub">\\1</sub>';
$body = \preg_replace($search, $replace, $body);
// monospace ##xxx##
$search = "#\#{2}(.*?)(\#{2}|(?=\n\n))#s";
$replace = '<span class="wikitt">\\1</span>';
$body = \preg_replace($search, $replace, $body);
// color !!color:xxx!!
$search = "#!{2}(\#{0,1}[0-9A-Za-z]*):(.*?)(!{2}|(?=\n\n))#s";
$replace = '<span style="color:\\1;">\\2</span>';
$body = \preg_replace($search, $replace, $body);
// color !!color,background:xxx!!
$search = "#!{2}(\#{0,1}[0-9A-Za-z]*),(\#{0,1}[0-9A-Za-z]*):(.*?)(!{2}|(?=\n\n))#s";
$replace = '<span style="color:\\1; background-color:\\2;">\\3</span>';
$body = \preg_replace($search, $replace, $body);
// forced line break creole style \\, just a bare break tag
$search = "#(\\\{2})#i";
$replace = '<br>';
$body = \preg_replace($search, $replace, $body);
// forced line break blog [[br]] or gwiki {break} styles, themed - by default clear all
$search = '#(\[\[BR\]\]|{break})#i';
$replace = '<br class="wikibreak">';
$body = \preg_replace($search, $replace, $body);
// image {{image url|alt text|align|max width in pixels}}
$search = '#\{{2}(.*)\}{2}#Um';
$replace = [$this, 'renderImage'];
$body = \preg_replace_callback($search, $replace, $body);
// info box {info title}xxx{endinfo}
$search = "#{(info) ([^\"<\n]+?)?}(.*?){endinfo}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// note box {note title}xxx{endnote}
$search = "#{(note) ([^\"<\n]+?)?}(.*?){endnote}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// tip box {tip title}xxx{endtip}
$search = "#{(tip) ([^\"<\n]+?)?}(.*?){endtip}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// warning box {warning title}xxx{endwarning}
$search = "#{(warn)ing ([^\"<\n]+?)?}(.*?){endwarning}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// code (preformatted) box {code title}xxx{endcode}
$search = "#{(code) ([^\"<\n]+?)?}(.*?){endcode}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// folded box {folded title}xxx{endfolded}
$search = "#{(folded) ([^\"<\n]+?)?}(.*?){endfolded}#si";
$replace = [$this, 'renderBox'];
$body = \preg_replace_callback($search, $replace, $body);
// urls - smells like a link
$search = '#(?<=\s)((http|https|ftp|ftps)://.{2,}\..*)(?=[,.?!:;]{0,1}\s)#Ui';
$replace = [$this, 'renderLink'];
$body = \preg_replace_callback($search, $replace, $body);
// link [[link|linktext]]
$search = '#\[{2}(.*)\]{2}#Um';
$replace = [$this, 'renderLink'];
$body = \preg_replace_callback($search, $replace, $body);
// email xxx@example.com
$search = '#(?<=\s)([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4})(?=\s)#i';
$replace = '<a href="mailto:\\1">\\1</a>';
$body = \preg_replace($search, $replace, $body);
// CamelCase wiki link "#^([A-Z][a-z\:]+){2,}\d*$#"
// Could be between whitespace on either end or between > on start and/or < on end
if ($this->useCamelCase) {
$search = '#(?<=\s|>)' . \_WIKI_CAMELCASE_REGEX . '(?=\s|</l|</t)#';
$replace = [$this, 'wikiCCLink'];
$body = \preg_replace_callback($search, $replace, $body);
}
// =====headings up to 5 levels
$search = '#(^\s{0,})(={1,5})([^=].*[^=])(={0,5})\s*$#Um';
$replace = [$this, 'renderHeader'];
$body = \preg_replace_callback($search, $replace, $body);
// blockquote > xxx
$search = "#^(> .*\n)+#m";
$replace = [$this, 'renderBlockquote'];
$body = \preg_replace_callback($search, $replace, $body);
// preformated .xxx
$search = "#^(\. .*\n)+#m";
$replace = [$this, 'renderPreformat'];
$body = \preg_replace_callback($search, $replace, $body);
// reference {ref id|first-ref}source{endref}
$search = "#{ref( [^\"<\n]+?)?}(.*?){endref}#si";
$replace = [$this, 'renderRef'];
$body = \preg_replace_callback($search, $replace, $body);
// forced line break blog [[br]] or gwiki {break} styles, themed - by default clear all
$search = '#({reflist})#i';
$replace = [$this, 'renderRefList'];
$body = \preg_replace_callback($search, $replace, $body);
// index or change list {pageindex prefix}
$search = "#{(PageIndex|RecentChanges)([^\"<\n]+?)?}#si";
$replace = [$this, 'renderIndex'];
$body = \preg_replace_callback($search, $replace, $body);
// table of contents
$search = '#\{toc\}#i';
$replace = [$this, 'renderToc'];
$body = \preg_replace_callback($search, $replace, $body);
// page set table of contents
$search = '#\{pagesettoc\}#i';
$replace = [$this, 'renderPageSetTocWrapper'];
$body = \preg_replace_callback($search, $replace, $body);
// image gallery {gallery size}
$search = "#{gallery([^\"<\n]+?)?}#si";
$replace = [$this, 'renderGallery'];
$body = \preg_replace_callback($search, $replace, $body);
// more anchor - indicates end of teaser/summary
$search = '#\{more\}#i';
$replace = '<span id="more"></span>';
$body = \preg_replace($search, $replace, $body);
// tables
$search = "#^( *\|((.*)\|){1,}\s*\n)+#m";
$replace = [$this, 'renderTables'];
$body = \preg_replace_callback($search, $replace, $body);
// paragraph on 2 consecutive newlines
$search = "#\n{2}#";
$replace = "\n<p>";
$body = \preg_replace($search, $replace, $body);
// restore cached nowiki content, all styles
// (if you need to use {PdNlNw:#} in your page, put it in a nowiki tag)
$search = '#{PdNlNw:([0-9]{1,})}#';
$replace = [$this, 'noWikiEmit'];
$body = \preg_replace_callback($search, $replace, $body);
if (false === $this->refShown && $this->refIndex > 0) {
$body .= $this->renderRefList(null);
}
$body = \stripslashes($this->convertEntities($body));
$this->renderedPage = $body;
return $this->renderedPage;
}
}