'WikiLinks',
'description' => 'If you use links of the format ((Wiki Page)) this filter will convert that to a link to a wiki page entitled Wiki Page',
'auto_activate' => true,
'plugin_type' => FILTER_PLUGIN,
// filter functions
'presplit_function' => '\Bitweaver\Liberty\bitlinks_prefilter',
'preparse_function' => '\Bitweaver\Liberty\bitlinks_prefilter',
'postsplit_function' => '\Bitweaver\Liberty\bitlinks_postfilter',
'postparse_function' => '\Bitweaver\Liberty\bitlinks_postfilter',
'poststore_function' => '\Bitweaver\Liberty\bitlinks_storefilter',
'expunge_function' => 'bitlinks_expungefilter',
];
$gLibertySystem->registerPlugin( PLUGIN_GUID_FILTERWIKILINKS, $pluginParams );
define( 'WIKI_WORDS_REGEX', '[A-z0-9]{2}[\w\d_\-]+[A-Z_][\w\d_\-]+[A-z0-9]+' );
/**
* bitlinks_prefilter
*
* @param string $pData
* @param string $pFilterHash
* @param array $pObject
* @access public
* @return void
*/
function bitlinks_prefilter( &$pData, &$pFilterHash, $pObject ) {
static $sBitLinks;
if( empty( $sBitLinks )) {
$sBitLinks = new BitLinks();
}
// extract ((Page|Description)) type links that they don't enter the parser.
// these can cause problems in various places such as tiki tables due to the |
preg_match_all( "@\({2}({$sBitLinks->mWikiWordRegex})\|(.+?)\){2}@", $pData, $protected );
if( !empty( $protected )) {
foreach( $protected[0] as $i => $prot ) {
$key = md5( mt_rand() );
$pFilterHash['bitlinks']['replacements'][$key] = $protected[0][$i];
$pData = str_replace( $prot, $key, $pData );
}
}
}
/**
* convert wiki links to html links e.g.: ((Wiki Page)) --> Wiki Page
*
* @param string $pData
* @param array $pFilterHash
* @param bool $pCase
* @access public
* @return void
*/
function bitlinks_postfilter( &$pData, &$pFilterHash, $pCase ) {
static $sBitLinks;
if( empty( $sBitLinks )) {
$sBitLinks = new BitLinks();
}
// first we need to put the ((Page|Description)) type links back in that we can parse them below
if( !empty( $pFilterHash['bitlinks']['replacements'] )) {
foreach( $pFilterHash['bitlinks']['replacements'] as $key => $replace ) {
$pData = str_replace( $key, $replace, $pData );
}
}
$pFilterHash['data'] = $pData;
$pData = $sBitLinks->parseLinks( $pData, $pFilterHash, $pCase );
}
/**
* store links to existing wiki pages in the database
*
* @param string $pData
* @param array $pFilterHash
* @param object $pObject
* @access public
* @return void
*/
function bitlinks_storefilter( &$pData, &$pFilterHash, $pObject ) {
global $gBitSystem;
static $sBitLinks;
if( empty( $sBitLinks )) {
$sBitLinks = new BitLinks();
}
$sBitLinks->storeLinks( $pData, $pFilterHash );
// if the title of this object was changed, we need to update links to it
if(
$gBitSystem->isPackageActive( 'wiki' )
&& $pObject->mContentTypeGuid == BITPAGE_CONTENT_TYPE_GUID
&& !empty( $pFilterHash['title'] )
&& !empty( $pObject->mInfo['title'] )
&& $pFilterHash['title'] != $pObject->mInfo['title']
) {
$sBitLinks->renameLinks( $pObject->mContentId, $pObject->mInfo['title'], $pFilterHash['title'] );
}
}
/**
* expunge bitlinks in the database
*
* @param string $pData
* @param array $pFilterHash
* @param object $pObject
* @access public
* @return void
*/
function bitlinks_expungefilter( &$pData, &$pFilterHash, $pObject ) {
static $sBitLinks;
if( empty( $sBitLinks )) {
$sBitLinks = new BitLinks();
}
$sBitLinks->expungeLinks( $pObject->mContentId );
}
/**
* BitLinks class
*
* @package liberty
* @uses BitBase
*/
class BitLinks extends BitBase {
/**
* mLinks
*
* @var array of links pointing to this page
* @access public
*/
public $mLinks = null;
public $mWikiWordRegex = '';
/**
* Initiate class
*
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
public function __construct() {
parent::__construct();
global $gBitSystem;
$mode = $gBitSystem->getConfig( 'wiki_page_regex', 'strict' );
switch ( $mode ) {
case 'strict':
$this->mWikiWordRegex = "([A-Za-z0-9_])([\'\.: A-Za-z0-9_\-])*([\.:A-Za-z0-9_])";
break;
case 'full':
$this->mWikiWordRegex = "([A-Za-z0-9_]|[\x80-\xFF])([\'\.: A-Za-z0-9_\-]|[\x80-\xFF])*([\.:A-Za-z0-9_]|[\x80-\xFF])";
break;
default:
// This is just evil. The middle section means "anything, as long
// as it's not a | and isn't followed by ))". -rlpowell
$this->mWikiWordRegex = "([^|\(\)])([^|](?!\)\)))*?([^|\(\)])";
}
// append anchor to regex
$this->mWikiWordRegex .= "(#\w+)?";
}
/**
* Get all pages linking to a given content id
*
* @param int $pContentId
* @access public
* @return array
*/
public function getAllPages( $pContentId ) {
global $gBitSystem;
$ret = [];
if( $gBitSystem->isPackageActive( 'wiki' ) && BitBase::verifyId( $pContentId )) {
$query = "SELECT `page_id`, lc.`content_id`, lc.`last_modified`, lc.`title`, lcds.`data` AS `summary`
FROM `".BIT_DB_PREFIX."liberty_content_links` lcl
INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lcl.`to_content_id`=lc.`content_id` )
INNER JOIN `".BIT_DB_PREFIX."wiki_pages` wp ON( wp.`content_id`=lc.`content_id` )
LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_data` lcds ON (lc.`content_id` = lcds.`content_id` AND lcds.`data_type`='summary')
WHERE lcl.`from_content_id`=? ORDER BY lc.`title`";
if( $result = $this->mDb->query( $query, [ $pContentId ] ) ) {
$lastTitle = '';
while( $row = $result->fetchRow() ) {
if( array_key_exists( strtolower( $row['title'] ), $ret )) {
$row['description'] = KernelTools::tra( 'Multiple pages with this name' );
}
$ret[strtolower( $row['title'] )] = $row;
}
}
}
return $ret;
}
/**
* see if page has already been created and stored
*
* @param string $pTitle title of the page
* @param bool $pCaseSensitive
* @param int $pContentId content_id of the current page - sometimes we don't have the object but a content_id to work with
* @access public
* @return bool true on success, false on failure - mErrors will contain reason for failure
*/
public function pageExists( $pTitle, $pCaseSensitive, $pContentId ) {
global $gBitSystem;
// only update this hash once - this is initiated as null and will be set to at least array after first call
if( $this->mLinks === null ) {
$this->mLinks = $this->getAllPages( $pContentId );
}
$ret = false;
if( !empty( $pTitle ) && !empty( $this->mLinks )) {
if( array_key_exists( strtolower( $pTitle ), $this->mLinks )) {
$ret = $this->mLinks[strtolower( $pTitle )];
}
}
// final attempt to get page details
if( empty( $ret ) && $gBitSystem->isPackageActive( 'wiki' ) ) {
if( $ret = BitPage::pageExists( $pTitle, false, $pContentId )) {
if( count( $ret ) > 1 ) {
$ret[0]['description'] = KernelTools::tra( 'Multiple pages with this name' );
}
$ret = $ret[0];
}
}
return $ret;
}
/**
* extractWikiWords
*
* @param string $pData
* @access public
* @return array of wiki words in the data string
*/
public function extractWikiWords( $pData ) {
global $gBitSystem;
// we need to remove text that might contain unexpected wiki words
$protect = [
"!]*>.*?!si", // links
"!<[^>]*>!", // any html tags
];
$tmpData = preg_replace( $protect, "", $pData );
$words1[1] = $words2[1] = $words3[1] = [];
preg_match_all( "@\({2}($this->mWikiWordRegex)\){2}@", $tmpData, $words2 );
preg_match_all( "@\({2}($this->mWikiWordRegex)\|(.+?)\){2}@", $tmpData, $words3 );
if( $gBitSystem->isFeatureActive( 'wiki_words' )) {
preg_match_all( '/\b('.WIKI_WORDS_REGEX.')\b/', $tmpData, $words1 );
}
return array_unique( array_merge( $words1[1], $words2[1], $words3[1] ));
}
/**
* convert wiki links to html links
*
* @param string $pData
* @param array $pParamHash
* @param bool $pCase
* @access public
* @return string
*/
public function parseLinks( $pData, $pParamHash, $pCase ) {
global $gBitSystem, $gLibertySystem;
// if wiki isn't active, there isn't much we can do here
if( !$gBitSystem->isPackageActive( 'wiki' )) {
return $pData;
}
// fetch BitPage in case it hasn't been loaded yet
$gLibertySystem->getContentClassName( 'bitpage' );
// We need to remove ))WikiWords(( before links get made.
// users just need to be strict about not inserting spaces between
// words and brackets
preg_match_all( "@\){2}(".WIKI_WORDS_REGEX.")\({2}@", $pData, $protected );
// this array is used to fill the text with temporary placeholders that get replaced back in further down
$replacements = [];
if( !empty( $protected )) {
foreach( $protected[0] as $i => $prot ) {
$key = md5( mt_rand() );
$replacements[$key] = $protected[1][$i];
$pData = str_replace( $prot, $key, $pData );
}
}
// Process ((Wiki Page|Wiki Page Description)) type links first. Here
// we don't handle plurals and the like since the user should know what
// he's linking to when using these links
preg_match_all( "@\({2}({$this->mWikiWordRegex})\|(.+?)\){2}@", $pData, $pages );
for( $i = 0; $i < count( $pages[1] ); $i++ ) {
$page = str_replace( $pages[5][$i], "", $pages[1][$i] );
$exists = $this->pageExists( $page, $pCase, $pParamHash['content_id'] );
// anchor
$repl = !empty( $pages[5][$i] )
? preg_replace( '!href="([^"]*)"!', "href=\"$1{$pages[5][$i]}\"", BitPage::getPageLink( $page, $exists ))
: BitPage::getPageLink( $page, $exists );
// alternate title
if( strlen( trim( $pages[6][$i] )) > 0 ) {
$repl = str_replace( $page."", "{$pages[6][$i]}", $repl );
}
$key = md5( mt_rand() );
$replacements[$key] = $repl;
$pData = str_replace( $pages[0][$i], $key, $pData );
}
// Process the simpler ((Wiki Page)) type links without the description
preg_match_all( "@\({2}({$this->mWikiWordRegex})\){2}@", $pData, $pages );
foreach( array_unique( $pages[1] ) as $i => $page ) {
$page = str_replace( $pages[5][$i], "", $pages[1][$i] );
$exists = $this->pageExists( $page, $pCase, $pParamHash['content_id'] );
$repl = !empty( $pages[5][$i] )
? preg_replace( '!href="([^"]*)"!', "href=\"$1{$pages[5][$i]}\"", BitPage::getPageLink( $page, $exists ))
: BitPage::getPageLink( $page, $exists );
$key = md5( mt_rand() );
$replacements[$key] = $repl;
$pData = str_replace( "(({$pages[1][$i]}))", $key, $pData );
}
// Finally we deal with WikiWord links
if( $gBitSystem->isFeatureActive( 'wiki_words' )) {
$pages = $this->extractWikiWords( $pData );
foreach( $pages as $page) {
if( $exists = $this->pageExists( $page, $pCase, $pParamHash['content_id'] )) {
$repl = BitPage::getPageLink( $page, $exists );
} elseif( $gBitSystem->isFeatureActive( 'wiki_plurals' ) && $this->getLocale() == 'en_US' ) {
// Link plural topic names to singular topic names if the plural
// doesn't exist, and the language is english
$plural_tmp = $page;
// Plurals like policy / policies
$plural_tmp = preg_replace( "/ies$/", "y", $plural_tmp );
// Plurals like address / addresses
$plural_tmp = preg_replace( "/sses$/", "ss", $plural_tmp );
// Plurals like box / boxes
$plural_tmp = preg_replace( "/([Xx])es$/", "$1", $plural_tmp );
// Others, excluding ending ss like address(es)
$plural_tmp = preg_replace( "/([A-Za-rt-z])s$/", "$1", $plural_tmp );
// prevent redundant pageExists calls if plurals are on, and plural is same as original word
if( $page != $plural_tmp ) {
$exists = $this->pageExists( $plural_tmp, $pCase, $pParamHash['content_id'] );
}
$repl = BitPage::getPageLink( $plural_tmp, $exists );
} else {
$repl = BitPage::getPageLink( $page, $exists );
}
// old code
//$slashed = preg_replace( "/([\/\[\]\(\)])/", "\\\\$1", $page_parse );
//$data = preg_replace( "#([\s\,\;])\b$slashed\b([\s\,\;\.])#", "$1 ".$repl."$2", $data);
// new code
// i never understood why the simple stuff never worked but it
// seems to work now - xing - Sunday Jul 22, 2007 17:37:17 CEST
$pData = preg_replace( "/\b".preg_quote( $page, "/" )."\b/", $repl, $pData );
}
}
// replace protection keys with original words
foreach( $replacements as $key => $replace ) {
$pData = str_replace( $key, $replace, $pData );
}
return $pData;
}
/**
* getLocale
*
* @access public
* @return string locale
*/
public function getLocale() {
static $locales = [
'cs' => 'cs_CZ',
'de' => 'de_DE',
'dk' => 'da_DK',
'en' => 'en_US',
'fr' => 'fr_FR',
'he' => 'he_IL', # hebrew
'it' => 'it_IT', # italian
'pl' => 'pl_PL', # polish
'po' => 'po',
'ru' => 'ru_RU',
'es' => 'es_ES',
'sw' => 'sw_SW', # swahili
'tw' => 'tw_TW',
];
if( empty( $locale )) {
$locale = '';
if( isset( $locales[$this->getLanguage()] )) {
$locale = $locales[$this->getLanguage()];
}
}
return $locale;
}
/**
* getLanguage
*
* @access public
* @return string language
*/
public function getLanguage() {
static $sBitLanguage = false;
global $gBitUser, $gBitSystem;
if( empty( $sBitLanguage )) {
$sBitLanguage = $gBitUser->isValid()
? $gBitUser->getPreference( 'bitLanguage', 'en' )
: $gBitSystem->getPreference( 'bitLanguage', 'en' );
}
return $sBitLanguage;
}
/**
* storeLinks
*
* @param string $pData
* @param array $pFilterHash
* @access public
* @return bool store wiki links in database
*/
public function storeLinks( $pData, $pFilterHash ) {
global $gBitSystem;
// if we don't have a content_id or wiki isn't active, get out of here.
if( empty( $pFilterHash['content_id'] ) || !$gBitSystem->isPackageActive( 'wiki' )) {
return false;
}
$from_content_id = $pFilterHash['content_id'];
$from_title = $pFilterHash['title'] ?? '';
// we need to remove the cache of any pages pointing to this one
$query = "SELECT `from_content_id` FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE ( `to_content_id` = ? OR `to_content_id` IS null ) AND `to_title` = ?";
$clearCache = $gBitSystem->mDb->getCol( $query, [ $from_content_id, $from_title ] );
if( is_array( $clearCache )) {
foreach( $clearCache as $content_id ) {
LibertyContent::expungeCacheFile( $content_id );
}
}
// if this is a new page, fix up any links that may already point to it
$query = "UPDATE `".BIT_DB_PREFIX."liberty_content_links` SET `to_content_id` = ? WHERE ( `to_content_id` = ? OR `to_content_id` IS null ) AND `to_title` = ?";
$gBitSystem->mDb->query( $query, [ $from_content_id, 0, $from_title ] );
// get all the current links from this page
$query = "SELECT LOWER( `to_title` ), `to_content_id` FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE `from_content_id` = ?";
$oldLinks = $gBitSystem->mDb->getAssoc( $query, [ $from_content_id ] );
// get list of all wiki links on this page
$extractedWikiWords = $this->extractWikiWords( $pData );
// wiki links with anchors are pulled out as well. we'll make copies of the wiki pagenames without the anchor to process these correctly as well
foreach( $extractedWikiWords as $link ) {
if( strstr( $link, '#' ) !== false ) {
$extractedWikiWords[] = preg_replace( "!#.*!", "", $link );
}
}
// create list of unique new wiki links on this page
$uniqueNewWikiLinks = [];
foreach( $extractedWikiWords as $to_title ) {
if( !empty( $to_title ) && !isset( $oldLinks[strtolower($to_title)] )) {
$uniqueNewWikiLinks[] = $to_title;
}
}
// remove duplicates
array_unique( $uniqueNewWikiLinks );
// get list of all new links that point to existing content
if( !empty( $uniqueNewWikiLinks )) {
$inSql = '?'.str_repeat( ',?', count( $uniqueNewWikiLinks ) - 1 );
// All arguments to MySQL in() function have to be passed as strings or it doesn't work correctly
$bindVars = [];
foreach( $uniqueNewWikiLinks as $var ) {
$bindVars[] = ( string )$var;
}
$bindVars[] = BITPAGE_CONTENT_TYPE_GUID;
$query = "SELECT LOWER( `title` ), `title` FROM `".BIT_DB_PREFIX."liberty_content` WHERE `title` IN( $inSql ) AND `content_type_guid` = ?";
$newLinksPointingToExistingContent = $gBitSystem->mDb->getAssoc( $query, $bindVars );
// insert all new links pointing to existing content
if( !empty( $newLinksPointingToExistingContent )) {
// All arguments to MySQL in() function have to be passed as strings or it doesn't work correctly
$bindVars = [];
foreach( $newLinksPointingToExistingContent as $var ) {
$bindVars[] = ( string )$var;
}
$bindVars[] = BITPAGE_CONTENT_TYPE_GUID;
$inSql = '?'.str_repeat( ',?', count( $newLinksPointingToExistingContent ) - 1 );
$query = "
INSERT INTO `".BIT_DB_PREFIX."liberty_content_links` ( `from_content_id`, `to_content_id`, `to_title` )
SELECT ?, `content_id`, `title` FROM `".BIT_DB_PREFIX."liberty_content`
WHERE `title` IN( $inSql ) AND `content_type_guid` = ?";
array_unshift( $bindVars, $from_content_id );
$result = $gBitSystem->mDb->query( $query, $bindVars );
}
// insert all new links pointing to non-existing content and that are not in the db yet
foreach( $uniqueNewWikiLinks as $to_title ) {
if( !isset( $newLinksPointingToExistingContent[strtolower( $to_title )] ) && !in_array( strtolower( $to_title ), array_keys( $oldLinks ))) {
$query = "INSERT INTO `".BIT_DB_PREFIX."liberty_content_links` ( `from_content_id`, `to_title` ) VALUES( ?, ? )";
$result = $gBitSystem->mDb->query( $query, [ $from_content_id, $to_title ] );
}
}
}
// now delete any links no longer on page
foreach( $extractedWikiWords as $to_title ) {
$obsoleteLinks[strtolower( $to_title )] = 1;
}
foreach( array_keys( $oldLinks ) as $to_title ) {
if( !isset( $obsoleteLinks[$to_title] )) {
$query = "DELETE FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE `from_content_id`=? AND LOWER( `to_title` ) = ?";
$result = $gBitSystem->mDb->query( $query, [ $from_content_id, $to_title ] );
}
}
return true;
}
/**
* renameLinks
*
* @param int $pContentId
* @param string $pOldName
* @param string $pNewName
* @access public
* @return void
*/
public function renameLinks( $pContentId, $pOldName, $pNewName ) {
$query = "
SELECT `from_content_id`, `data`
FROM `".BIT_DB_PREFIX."liberty_content_links` lcl
INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lcl.`from_content_id`=lc.`content_id` )
WHERE `to_content_id` = ?";
// --- ((Wiki Page|Description))
$pattern = [ "!\({2}\b$pOldName\b\|([^\)]*)\){2}!" ];
$replace = [ "(($pNewName|$1))" ];
// --- ((Wiki Page)) or WikiPage
$pattern[] = "!(\({2})?\b$pOldName\b(\){2})?!";
$replace[] = preg_match( "! !", $pNewName )
? "(($pNewName))"
: "$1$pNewName$2";
if( $result = $this->mDb->query( $query, [ $pContentId ] ) ) {
while( $row = $result->fetchRow() ) {
$data = preg_replace( $pattern, $replace, $row['data'] );
if( md5( $data ) != md5( $row['data'] ) ) {
$query = "UPDATE `".BIT_DB_PREFIX."liberty_content` SET `data`=? WHERE `content_id`=?";
$this->mDb->query( $query, [ $data, $row['from_content_id'] ] );
// remove any chached files pointing here
LibertyContent::expungeCacheFile( $row['from_content_id'] );
}
}
}
# Fix up titles in the link table
$query = "UPDATE `".BIT_DB_PREFIX."liberty_content_links` SET `to_title`=? WHERE `to_content_id`=?";
$this->mDb->query( $query, [ $pNewName, $pContentId ] );
}
/**
* expunge bitlinks in the database
*
* @param numeric $pContentId
* @access public
* @return void
*/
public function expungeLinks( $pContentId ) {
if( !empty( $pContentId )) {
// remove any cached file pointing to this page
$links = $this->mDb->getCol( "SELECT `from_content_id` FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE to_content_id=?", [ $pContentId ] );
foreach( $links as $content_id ) {
LibertyContent::expungeCacheFile( $content_id );
}
$this->mDb->query( "DELETE FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE from_content_id=? OR to_content_id=?", [ $pContentId, $pContentId ] );
}
}
}