* * Copyright (c) 2004 bitweaver.org * Copyright (c) 2003 tikwiki.org * Copyright (c) 2002-2003, Luis Argerich, Garland Foster, Eduardo Polidor, et. al. * All Rights Reserved. See below for details and a complete list of authors. * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See http://www.gnu.org/copyleft/lesser.html for details */ /** * required setup */ namespace Bitweaver\Wiki; use Bitweaver\BitBase; use Bitweaver\KernelTools; use Bitweaver\BitCacheable; use Bitweaver\Liberty\LibertyContent; use Bitweaver\Liberty\LibertyMime; /** * @package wiki */ class BitPage extends LibertyMime implements BitCacheable { public $mPageId; public $mPageName; public function __construct( $pPageId=null, $pContentId=null ) { parent::__construct(); $this->registerContentType( BITPAGE_CONTENT_TYPE_GUID, [ 'content_type_guid' => BITPAGE_CONTENT_TYPE_GUID, 'content_name' => 'Wiki Page', 'handler_class' => 'BitPage', 'handler_package' => 'wiki', 'handler_file' => 'BitPage.php', 'maintainer_url' => 'https://www.bitweaver.org', ] ); $this->mPageId = (int)$pPageId; $this->mContentId = (int)$pContentId; $this->mContentTypeGuid = BITPAGE_CONTENT_TYPE_GUID; // Permission setup $this->mViewContentPerm = 'p_wiki_view_page'; $this->mCreateContentPerm = 'p_wiki_create_page'; $this->mUpdateContentPerm = 'p_wiki_update_page'; $this->mAdminContentPerm = 'p_wiki_admin'; } public static function isCacheableClass() { return true; } public function isCacheableObject() { return true; } public static function getCacheClass() { return 'BitPage'; } public function __sleep() { return array_merge( parent::__sleep(), [ 'mPageId', 'mPageName' ] ); } public static function findContentIdByPageId( int $pPageId ) { global $gBitDb; $ret = null; if( BitBase::verifyId( $pPageId ) ) { $ret = $gBitDb->getOne( "SELECT `content_id` FROM `".BIT_DB_PREFIX."wiki_pages` WHERE `page_id`=?", [ (int)$pPageId ] ); } return $ret; } public function findByPageName( string $pPageName, int $pUserId = -2 ) { $userWhere = ''; $bindVars = [ $pPageName, $this->mContentTypeGuid ]; if( BitBase::verifyId( $pUserId ) ) { $userWhere = " AND lc.`user_id`=?"; array_push( $bindVars, $pUserId ); } $ret = $this->mDb->getOne("select `page_id` from `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) where lc.`title`=? AND lc.`content_type_guid`=? $userWhere", $bindVars ); return $ret; } public static function lookupObject( $pLookupHash ) { if( !empty( $pLookupHash['page_id'] ) ) { $loadContentId = BitPage::findContentIdByPageId( $pLookupHash['page_id'] ); } elseif( !empty( $pLookupHash['content_id'] ) ) { $loadContentId = $pLookupHash['content_id']; } elseif( !empty( $pLookupHash['page'] ) ) { //handle legacy forms that use plain 'page' form variable name //if page had some special enities they were changed to HTML for for security reasons. //now we deal only with string so convert it back - so we can support this case: //You&Me --(detoxify in kernel)--> You&Me --(now)--> You&Me //we could do htmlspecialchars_decode but it allows <> marks here, so we just transform & to & - it's not so scary. $loadPage = str_replace("&", "&", $pLookupHash['page'] ); // Fix nignx mapping of '+' sign when doing rewrite $loadPage = str_replace("+", " ", $loadPage ); if( $loadPage && $existsInfo = static::pageExists( $loadPage ) ) { if (count($existsInfo)) { if (count($existsInfo) > 1) { // Perhaps something should be done on page conflicts } $loadPageId = $existsInfo[0]['page_id']; $loadContentId = $existsInfo[0]['content_id']; } } } if( !empty( $loadContentId ) ) { $ret = static::getLibertyObject( $loadContentId ); } if( empty( $ret ) || !is_object( $ret ) ) { $ret = new self(); } return $ret; } /** * Determines if a wiki page (row in wiki_pages) exists, and returns a hash of important info. If N pages exists with $pPageName, returned existsHash has a row for each unique pPageName row. * @param string $pPageName name of the wiki page * @param bool $pCaseSensitive look for case sensitive names * @param int $pContentId if you insert the content id of the currently viewed content, non-existing links can be created immediately * @return array $infoHash */ public static function pageExists( string $pPageName, bool $pCaseSensitive = false, $pContentId = 0 ) { global $gBitSystem; $ret = null; if( $gBitSystem->isPackageActive( 'wiki' ) ) { $columnExpression = $gBitSystem->mDb->getCaseLessColumn('lc.title'); $pageWhere = $pCaseSensitive ? 'lc.`title`' : $columnExpression; $bindVars = [ $pCaseSensitive ? $pPageName : strtoupper( $pPageName ) ]; $query = "SELECT `page_id`, wp.`content_id`, lcds.`data` AS `summary`, lc.`last_modified`, lc.`title` FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id`=wp.`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 $pageWhere = ?"; if( !$ret = $gBitSystem->mDb->getAll( $query, $bindVars ) ) { $query = "SELECT `page_id`, wp.`content_id`, lcds.`data` AS `summary`, lc.`last_modified`, lc.`title`, lal.`alias_title` FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id`=wp.`content_id`) INNER JOIN `".BIT_DB_PREFIX."liberty_aliases` lal ON (lc.`content_id`=lal.`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 ".$gBitSystem->mDb->getCaseLessColumn('lal.alias_title')." = ?"; $ret = $gBitSystem->mDb->getAll( $query, $bindVars ); } } return $ret; } /** * load * * @return bool true on success, false on failure - mErrors will contain reason for failure */ public function load() { if( $this->verifyId( $this->mPageId ) || $this->verifyId( $this->mContentId ) ) { global $gBitSystem; $lookupColumn = BitBase::verifyId( $this->mPageId ) ? 'page_id' : 'content_id'; $parse = $_REQUEST['parse'] ?? true; $bindVars = []; $selectSql = ''; $joinSql = ''; $whereSql = ''; array_push( $bindVars, $lookupId = BitBase::verifyId( $this->mPageId )? $this->mPageId : $this->mContentId ); $this->getServicesSql( 'content_load_sql_function', $selectSql, $joinSql, $whereSql, $bindVars ); $query = " SELECT wp.*, lc.*, lcds.`data` AS `summary`, uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name, uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name $selectSql FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) $joinSql LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_data` lcds ON (lc.`content_id` = lcds.`content_id` AND lcds.`data_type`='summary') LEFT JOIN `".BIT_DB_PREFIX."users_users` uue ON (uue.`user_id` = lc.`modifier_user_id`) LEFT JOIN `".BIT_DB_PREFIX."users_users` uuc ON (uuc.`user_id` = lc.`user_id`) WHERE wp.`$lookupColumn`=? $whereSql"; if( $this->mInfo = $this->mDb->getRow( $query, $bindVars ) ) { $this->mContentId = $this->mInfo['content_id']; $this->mPageId = $this->mInfo['page_id']; $this->mPageName = $this->mInfo['title']; $this->mInfo['display_url'] = $this->getDisplayUrl(); // TODO: this is a bad habbit and should not be done RoleUser::getDisplayName sorts out what name to display $this->mInfo['creator'] = $this->mInfo['creator_real_name'] ?? $this->mInfo['creator_user']; $this->mInfo['editor'] = $this->mInfo['modifier_real_name'] ?? $this->mInfo['modifier_user']; // Save some work if wiki_attachments are not active // get prefs before we parse the data that we know how to parse the data if( $gBitSystem->isFeatureActive( 'wiki_attachments' ) ) { LibertyMime::load(); } else { LibertyContent::load(); } if ( $parse ) { $this->getParsedData(); } } else { $this->mPageId = 0; } } return count( $this->mInfo ); } /** * This is the ONLY method that should be called in order to store (create or update) a wiki page! * It is very smart and will figure out what to do for you. It should be considered a black box. * * @param array pParamHash hash of values that will be used to store the page * @return bool true on success, false if store could not occur. If false, $this->mErrors will have reason why **/ public function store( array &$pParamHash): bool { $this->StartTrans(); if( $this->verify( $pParamHash ) && LibertyMime::store( $pParamHash ) ) { $pParamHash['page_store']['wiki_page_size'] = !empty( $pParamHash['edit'] ) ? strlen( $pParamHash['edit'] ) : 0; $table = BIT_DB_PREFIX."wiki_pages"; if( $this->verifyId( $this->mPageId ) ) { $result = $this->mDb->associateUpdate( $table, $pParamHash['page_store'], [ "page_id" => $this->mPageId ] ); } else { $pParamHash['page_store']['content_id'] = $pParamHash['content_id']; $pParamHash['page_store']['page_id'] = @$this->verifyId( $pParamHash['page_id'] ) ? $pParamHash['page_id'] : $this->mDb->GenID( 'wiki_pages_page_id_seq'); $this->mPageId = $pParamHash['page_store']['page_id']; $result = $this->mDb->associateInsert( $table, $pParamHash['page_store'] ); } // Access new data for notifications $this->load(); if( isset( $mailEvents ) ) { global $notificationlib, $gBitUser, $gBitSystem, $gBitSmarty; include_once KERNEL_PKG_INCLUDE_PATH.'notification_lib.php'; $notificationlib->post_content_event($this->mContentId, $this->mInfo['content_type_guid'], 'wiki', $this->mInfo['title'], $this->mInfo['modifier_user'], $this->mInfo['edit_comment'], $this->mInfo['data']); if( $gBitSystem->isFeatureActive( 'users_watches') ) { $nots = $gBitUser->get_event_watches( 'wiki_page_changed', $this->mPageId ); foreach ($nots as $not) { if ($wiki_watch_editor != 'y' && $not['user_id'] == $user) break; $gBitSmarty->assign('mail_site', $_SERVER["SERVER_NAME"]); $gBitSmarty->assign('mail_page', $this->mInfo['title']); $gBitSmarty->assign('mail_date', $gBitSystem->getUTCTime()); $gBitSmarty->assign('mail_user', $this->mInfo['modifier_user']); $gBitSmarty->assign('mail_comment', $this->mInfo['edit_comment']); $gBitSmarty->assign('mail_last_version', $this->mInfo['version'] - 1); $gBitSmarty->assign('mail_data', $this->mInfo['data']); $gBitSmarty->assign('mail_hash', $not['hash']); $foo = parse_url($_SERVER["REQUEST_URI"]); $machine = KernelTools::httpPrefix(); $gBitSmarty->assign('mail_machine', $machine); $parts = explode('/', $foo['path']); if (count($parts) > 1) unset ($parts[count($parts) - 1]); $gBitSmarty->assign('mail_machine_raw', KernelTools::httpPrefix(). implode('/', $parts)); $gBitSmarty->assign('mail_pagedata', $this->mInfo['data']); $mail_data = $gBitSmarty->fetch('bitpackage:wiki/user_watch_wiki_page_changed.tpl'); $email_to = $not['email']; @mail($email_to, KernelTools::tra('Wiki page'). ' ' . $this->mInfo['title'] . ' ' . KernelTools::tra('changed'), $mail_data, "From: ".$gBitSystem->getConfig( 'site_sender_email' )."\r\nContent-type: text/plain;charset=utf-8\r\n"); } } } } $this->CompleteTrans(); return count( $this->mErrors ) == 0; } /** * verify This function is responsible for data integrity and validation before any operations are performed with the $pParamHash * NOTE: This is a PRIVATE METHOD!!!! do not call outside this class, under penalty of death! * * @param array pParams reference to hash of values that will be used to store the page, they will be modified where necessary * * @return bool true on success, false if verify failed. If false, $this->mErrors will have reason why * * @access private **/ public function verify( array &$pParamHash ): bool { global $gBitUser, $gBitSystem; // make sure we're all loaded up of we have a mPageId if( $this->verifyId( $this->mPageId ) && empty( $this->mInfo ) ) { $this->load(); } if( isset( $this->mInfo['content_id'] ) && $this->verifyId( $this->mInfo['content_id'] ) ) { $pParamHash['content_id'] = $this->mInfo['content_id']; } // It is possible a derived class set this to something different if( empty( $pParamHash['content_type_guid'] ) ) { $pParamHash['content_type_guid'] = $this->mContentTypeGuid; } if( @$this->verifyId( $pParamHash['content_id'] ?? 0 ) ) { $pParamHash['page_store']['content_id'] = $pParamHash['content_id']; } // check for name issues, first truncate length if too long if( empty( $pParamHash['title'] ) ) { $this->mErrors['title'] = 'You must specify a title'; } elseif( !empty( $pParamHash['title']) || !empty($this->mPageName)) { if( !$this->verifyId( $this->mPageId ) ) { if( empty( $pParamHash['title'] ) ) { $this->mErrors['title'] = 'You must enter a name for this page.'; } else { $pParamHash['content_store']['title'] = substr( $pParamHash['title'], 0, 160 ); if ($gBitSystem->isFeatureActive( 'wiki_allow_dup_page_names')) { # silently allow pages with duplicate names to be created } else { if( $this->pageExists( $pParamHash['title'] ) ) { $this->mErrors['title'] = 'Page "'.$pParamHash['title'].'" already exists. Please choose a different name.'; } } } } else { $pParamHash['content_store']['title'] = isset( $pParamHash['title'] ) ? substr( $pParamHash['title'], 0, 160 ) : $this->mPageName; if ($gBitSystem->isFeatureActive( 'wiki_allow_dup_page_names')) { # silently allow pages with duplicate names to be created } else { if( $gBitUser->hasPermission( 'p_wiki_rename_page' ) && (isset( $this->mInfo['title'] ) && ($pParamHash['title'] != $this->mInfo['title'])) ) { if( $this->pageExists( $pParamHash['title'] ) ) { $this->mErrors['title'] = 'Page "'.$pParamHash['title'].'" already exists. Please choose a different name.'; } } } } } $pParamHash['page_store']['edit_comment'] = empty( $pParamHash['edit_comment'] ) ? null : substr( $pParamHash['edit_comment'], 0, 200 ); if( !empty( $pParamHash['minor'] ) && $this->isValid() ) { // we can only minor save over our own versions if( !$gBitUser->isRegistered() || ($this->mInfo['modifier_user_id'] != $gBitUser->mUserId && !$gBitUser->isAdmin()) ) { unset( $pParamHash['minor'] ); } } // if we have an error we get them all by checking parent classes for additional errors if( count( $this->mErrors ) > 0 ){ parent::verify( $pParamHash ); } return count( $this->mErrors ) == 0; } /** * Remove page from database */ public function expunge(): bool { if( $this->isValid() ) { $this->StartTrans(); $this->expungeVersion(); // will nuke all versions $query = "DELETE FROM `".BIT_DB_PREFIX."wiki_pages` WHERE `content_id` = ?"; $result = $this->mDb->query( $query, [ $this->mContentId ] ); if( LibertyMime::expunge() ) { $this->CompleteTrans(); } else { $this->mDb->RollbackTrans(); } } return true; } public function isUserPage() { $ret = false; if( $this->mPageName ) { $ret = preg_match( '/^UserPage(.*)/', $this->mPageName, $matches ); } return $ret; } public function isValid() { return $this->verifyId( $this->mPageId ); } public function isLocked(): bool { $ret = false; if( $this->verifyId( $this->mPageId ) ) { if( empty( $this->mInfo ) ) { $this->load(); } $ret = $this->getField( 'flag', false ); } return $ret; } public function isCommentable() { global $gBitSystem; return $gBitSystem->isFeatureActive( 'wiki_comments' ); } public function setLock( $pLock, $pModUserId=null ) { if( $this->verifyId( $this->mPageId ) ) { $bindVars = []; $userSql = ''; if( $pModUserId ) { $userSql = "`modifier_user_id`=?, "; array_push( $bindVars, $pModUserId ); } array_push( $bindVars, $pLock, $this->mPageId ); $query = "update `".BIT_DB_PREFIX."wiki_pages` SET $userSql `flag`=? where `page_id`=?"; $result = $this->mDb->query($query, $bindVars ); $this->mInfo['flag'] = $pLock; } return true; } public function lock( $pModUserId=null ) { return $this->setLock( 'L', $pModUserId ); } public function unlock( $pModUserId=null ) { return $this->setLock( null, $pModUserId ); } // ********* Footnote functions for the wiki ********** // /** * Store footnote */ public function storeFootnote($pUserId, $pData) { if( $this->verifyId( $this->mPageId ) ) { $querydel = "delete from `".BIT_DB_PREFIX."wiki_footnotes` where `user_id`=? and `page_id`=?"; $this->mDb->query( $querydel, [ $pUserId, $this->mPageId ] ); $query = "insert into `".BIT_DB_PREFIX."wiki_footnotes`(`user_id`,`page_id`,`data`) values(?,?,?)"; $this->mDb->query( $query, [ $pUserId, $this->mPageId, $pData ] ); } } /** * Delete footnote */ public function expungeFootnote( $pUserId ) { if( $this->verifyId( $this->mPageId ) ) { $query = "delete from `".BIT_DB_PREFIX."wiki_footnotes` where `user_id`=? and `page_id`=?"; $this->mDb->query( $query, [$pUserId,$this->mPageId] ); } } /** * Get footnote */ public function getFootnote( int $pUserId ) { if( $this->verifyId( $this->mPageId ) ) { $count = $this->mDb->getOne( "select count(*) from `".BIT_DB_PREFIX."wiki_footnotes` where `user_id`=? and `page_id`=?", [ $pUserId, $this->mPageId ] ); if( $count ) { return $this->mDb->getOne("select `data` from `".BIT_DB_PREFIX."wiki_footnotes` where `user_id`=? and `page_id`=?",[ $pUserId, $this->mPageId ] ); } } return false; } /** * Generates a link to a wiki page within lists of pages * @return string the link to display the page. */ public function getListLink( $pParamHash ) { return BitPage::getPageLink( $pParamHash['title'], null ); } /** * Returns include file that will * @return string the fully specified path to file to be included */ public function getRenderFile() { return WIKI_PKG_INCLUDE_PATH."display_bitpage_inc.php"; } /** * Returns the center template for the view selected */ public function getViewTemplate( $pAction ){ $ret = null; switch ( $pAction ){ case "view": $ret = "bitpackage:wiki/center_wiki_page.tpl"; break; case "list": $ret = "bitpackage:liberty/center_".$pAction."_generic.tpl"; break; } return $ret; } /** * Create the generic title for a content item * * This will normally be overwriten by extended classes to provide * an appropriate title string * @return string Descriptive title for the page */ public function getTitle() { $ret = $this->getField('title'); if( $this->isValid() ) { $ret = static::getTitleFromHash( $this->mInfo ); $requestPage = strtoupper( self::getParameter( $_REQUEST, 'page' ) ?? '' ); if( $requestPage && $requestPage != strtoupper( $this->mInfo['title'] ) ) { $aliases = $this->getAliases( true ); if( in_array( $requestPage, $aliases ) ) { $ret = $_REQUEST['page']; } } } return $ret; } /** * Generates the URL to this wiki page * @param$pExistsHash the hash that was returned by LibertyContent::pageExists * @return string the link to display the page. */ public static function getDisplayUrlFromHash( &$pParamHash ) { global $gBitSystem; if( !empty( $pParamHash['title'] ) ) { if( $gBitSystem->isFeatureActive( 'pretty_urls' ) || $gBitSystem->isFeatureActive( 'pretty_urls_extended' ) ) { $rewrite_tag = $gBitSystem->isFeatureActive( 'pretty_urls_extended' ) ? 'view/':''; $prettyPageName = preg_replace( '/ /', '+', $pParamHash['title'] ); $ret = WIKI_PKG_URL.$rewrite_tag.$prettyPageName; } else { $ret = WIKI_PKG_URL.'index.php?page='.urlencode( $pParamHash['title'] ); } } else { $ret = parent::getDisplayUrlFromHash( $pParamHash ); } return $ret; } /** * Returns HTML link to display a page if it exists, or to create if not * @param string $pLinkText * @param string|array $pExistsHash the hash that was returned by LibertyContent::pageExists * @param string $pAnchor * @return string the link to display the page. */ public static function getPageLink( string $pLinkText = '', string|array $pExistsHash = '', string $pAnchor = '' ): string { global $gBitSystem, $gBitUser; $ret = $pLinkText; if( $gBitSystem->isPackageActive( 'wiki' ) ) { if( !empty($pExistsHash ) && is_array($pExistsHash ) ) { if( is_array( current($pExistsHash ) ) ) { $exists =$pExistsHash[0]; $multiple = true; } else { $exists =$pExistsHash; $multiple = false; } // we have a multi-demensional array (likely returned from LibertyContent::pageExists() ) - meaning we potentially have multiple pages with the same name $desc = $multiple ? KernelTools::tra( 'Multiple pages with this name' ) : ( empty( $exists['summary'] ) ? $exists['title'] : $exists['summary'] ); $ret = ''.htmlspecialchars( $exists['title'] ).''; } else { $ret = $gBitUser->hasPermission( 'p_wiki_create_page' ) ? ''.htmlspecialchars( $pLinkText ).'' : $pLinkText; } } return $ret; } /** * Returns content_id's that link to this page * @return array hash of content */ public function getBacklinks() { $ret = []; if( $this->isValid() ) { $to_title = $this->mInfo['title']; $query = "SELECT `content_id`, `title`, `to_title`, url FROM ( SELECT lc.`content_id`, lc.`title`, lcl.`to_title`, 'wiki/' || lc.`title` AS url FROM `".BIT_DB_PREFIX."liberty_content_links` lcl INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lcl.`from_content_id` = lc.`content_id`) LEFT JOIN `".BIT_DB_PREFIX."wiki_pages` wp ON (lcl.`from_content_id` = wp.`content_id`) WHERE lc.`content_type_guid` = 'bitpage' UNION ALL SELECT lc.`content_id`, COALESCE( lc.`title`, CAST ( DATEADD( SECOND, bl.`publish_date`, timestamp '1/1/1970 00:00:00' ) AS DATE ) ) AS TITLE, lcb.`to_title`, 'blogs/post/' || BL.`post_id` AS url FROM ".BIT_DB_PREFIX."liberty_content_links lcb INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lcb.`from_content_id` = lc.`content_id`) LEFT JOIN `".BIT_DB_PREFIX."blog_posts` bl ON (lcb.`from_content_id` = bl.`content_id`) WHERE lc.`content_type_guid` = 'bitblogpost' ) WHERE `to_title` = ? ORDER BY `title`"; $ret = $this->mDb->getAssoc( $query, [ $to_title ] ); } return $ret; } /** * Roll back to a specific version of a page * @param int $pVersion Version number to roll back to * @param string $comment Comment text to be added to the action log * @return bool true if completed successfully */ public function rollbackVersion( int $pVersion, string $comment = '' ): bool { global $gBitSystem; $ret = false; if( parent::rollbackVersion( $pVersion, $comment ) ) { $action = "Changed actual version to $pVersion"; $t = $gBitSystem->getUTCTime(); $query = "insert into `".BIT_DB_PREFIX."liberty_action_log`(`log_message`,`content_id`,`last_modified`,`user_id`,`ip`,`error_message`) values(?,?,?,?,?,?)"; $result = $this->mDb->query($query,[$action,$this->mContentId,$t,ROOT_USER_ID,$_SERVER["REMOTE_ADDR"],$comment]); $ret = true; } return $ret; } /** * getList * * @param array $pListHash array of list parameters * - boolean ['orphans_only'] only return orphan wiki pages * - boolean ['extras'] load extra infrmation such as backlinks and links * - boolean ['get_data'] return the wiki page data along with the listed information * - string ['find_title'] filter by the page title * - string ['find_author'] filter by the login name of the page author * - string ['find_last_editor'] filter by the login name of the last editor of the page * @access public * @return array of wiki pages */ public function getList( array &$pListHash ): array { global $gBitSystem, $gBitUser; LibertyContent::prepGetList( $pListHash ); if( $pListHash['sort_mode'] == 'size_asc' || $pListHash['sort_mode'] == 'size_desc' ) { $pListHash['sort_mode'] = str_replace( 'size', 'wiki_page_size', $pListHash['sort_mode'] ); } $specialSort = [ 'versions_desc', 'versions_asc', 'links_asc', 'links_desc', 'backlinks_asc', 'backlinks_desc', ]; if( in_array( $pListHash['sort_mode'], $specialSort ) ) { $originalListHash = $pListHash; // now we can set the new values in the pListHash $pListHash['sort_mode'] = 'modifier_user_desc'; $pListHash['offset'] = 0; $pListHash['max_records'] = -1; } $whereSql = $joinSql = $selectSql = ''; $bindVars = []; $this->getServicesSql( 'content_list_sql_function', $selectSql, $joinSql, $whereSql, $bindVars, null, $pListHash ); if ( !empty( $pListHash['content_type_guid'] ) ) { $whereSql .= " AND lc.`content_type_guid`=? "; $bindVars[] = $pListHash['content_type_guid']; } // make find_title compatible with {minifind} if( empty( $pListHash['find_title'] )) { $pListHash['find_title'] = $pListHash['find']; } // use an array or string to search for wiki page titles if( is_array( $pListHash['find_title'] )) { $whereSql .= " AND lc.`title` IN (".implode(',',array_fill( 0, count( $pListHash['find_title'] ), '?' )).")"; $bindVars = array_merge( $bindVars, $pListHash['find_title'] ); } elseif( !empty( $pListHash['find_title'] ) && is_string( $pListHash['find_title'] )) { $whereSql .= " AND UPPER(lc.`title`) LIKE ? "; $bindVars = array_merge( $bindVars, [ '%'.strtoupper( $pListHash['find_title'] ) . '%' ]); } // limit by user id if( BitBase::verifyId( $pListHash['user_id'] ?? 0 )) { $whereSql .= " AND lc.`user_id` = ? "; $bindVars = array_merge( $bindVars, [ $pListHash['user_id'] ]); } // filter pages by author login if( !empty( $pListHash['find_author'] )) { $whereSql .= " AND UPPER(uuc.`login`) = ? "; $bindVars = array_merge( $bindVars, [ strtoupper( $pListHash['find_author'] ) ] ); } // filter pages by last editor if( !empty( $pListHash['find_last_editor'] )) { $whereSql .= " AND UPPER(uue.`login`) = ? "; $bindVars = array_merge( $bindVars, [ strtoupper( $pListHash['find_last_editor'] ) ] ); } $get_data = ''; if( !empty( $pListHash['get_data'] )) { $get_data = ', lc.`data`'; } if( empty( $pListHash['orphans_only'] )) { $whereSql = preg_replace('/^ AND */',' WHERE ', $whereSql); $query = "SELECT uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name, uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name, wp.`page_id`, wp.`wiki_page_size` as `len`, lcds.`data` AS `summary`, wp.`edit_comment`, wp.`content_id`, wp.`flag`, lc.`title`, lc.`format_guid`, lc.`last_modified`, lc.`created`, lc.`ip`, lc.`version`, lch.`hits` $get_data $selectSql FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) INNER JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` ) INNER JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`modifier_user_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_data` lcds ON (lc.`content_id` = lcds.`content_id` AND lcds.`data_type`='summary') LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_hits` lch ON (lc.`content_id` = lch.`content_id`) $joinSql $whereSql ORDER BY ".$this->mDb->convertSortmode( $pListHash['sort_mode'] ); $query_cant = " SELECT COUNT(*) FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) INNER JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` ) INNER JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`modifier_user_id` ) $joinSql $whereSql "; } else { $whereSql .= ' AND lcl.`to_content_id` is null '; $whereSql = preg_replace('/^ AND */',' WHERE ', $whereSql); $query = "SELECT uue.`login` AS modifier_user, uue.`real_name` AS modifier_real_name, uuc.`login` AS creator_user, uuc.`real_name` AS creator_real_name, wp.`page_id`, wp.`wiki_page_size` AS `len`,lcds.`data` AS `summary`, wp.`edit_comment`, wp.`content_id`, wp.`flag`, lc.`title`, lc.`format_guid`, lc.`last_modified`, lc.`created`, lc.`ip`, lc.`version`, lch.`hits` $get_data $selectSql FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) INNER JOIN `".BIT_DB_PREFIX."users_users` uuc ON ( uuc.`user_id` = lc.`user_id` ) INNER JOIN `".BIT_DB_PREFIX."users_users` uue ON ( uue.`user_id` = lc.`user_id` ) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_links` lcl ON (wp.`content_id` = lcl.`to_content_id`) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_hits` lch ON (lc.`content_id` = lch.`content_id`) LEFT OUTER JOIN `".BIT_DB_PREFIX."liberty_content_data` lcds ON (lc.`content_id` = lcds.`content_id` AND lcds.`data_type`='summary') $joinSql $whereSql ORDER BY ".$this->mDb->convertSortmode( $pListHash['sort_mode'] ); $query_cant = " SELECT COUNT(*) FROM `".BIT_DB_PREFIX."wiki_pages` wp LEFT JOIN `".BIT_DB_PREFIX."liberty_content_links` lcl ON (wp.`content_id` = lcl.`to_content_id`) INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) $joinSql $whereSql"; } // If sort mode is versions then offset is 0, max_records is -1 (again) and sort_mode is nil // If sort mode is links then offset is 0, max_records is -1 (again) and sort_mode is nil // If sort mode is backlinks then offset is 0, max_records is -1 (again) and sort_mode is nil $ret = []; $this->StartTrans(); # get count of total number of items available $cant = $this->mDb->getOne( $query_cant, $bindVars ); $pListHash["cant"] = $cant; # Check for offset out of range if ( $pListHash['offset'] < 0 ) { $pListHash['offset'] = 0; } elseif ( $pListHash['offset'] > $pListHash["cant"] ) { $lastPageNumber = ceil ( $pListHash["cant"] / $pListHash['max_records'] ) - 1; $pListHash['offset'] = $pListHash['max_records'] * $lastPageNumber; } $result = $this->mDb->query( $query, $bindVars, $pListHash['max_records'], $pListHash['offset'] ); $this->CompleteTrans(); while( $res = $result->fetchRow() ) { $aux = []; $aux = $res; $aux['creator'] = $res['creator_real_name'] ?? $res['creator_user']; $aux['editor'] = $res['modifier_real_name'] ?? $res['modifier_user']; $aux['flag'] = $res["flag"] == 'L' ? 'locked' : 'unlocked'; $aux['display_url'] = static::getDisplayUrlFromHash( $aux ); // display_link does not seem to be used when getList is called //$aux['display_link'] = $this->getDisplayLink( $aux['title'] ); //WIKI_PKG_URL."index.php?page_id=".$res['page_id']; if( !empty( $pListHash['extras'] )) { // USE SPARINGLY!!! This gets expensive fast // $aux['versions"] = $this->mDb->getOne( "SELECT COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content_history` WHERE `page_id`=?", [ $res["page_id"] )); $aux['links'] = $this->mDb->getOne( "SELECT COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE `from_content_id`=?", [ $res["content_id"] ]); $aux['backlinks'] = $this->mDb->getOne( "select COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content_links` WHERE `to_title`=?", [ $aux['title'] ]); } $ret[] = $aux; } // apply the custom sorting options if needed if( !empty( $originalListHash )) { if( $originalListHash['sort_mode'] == 'versions_asc' && !empty( $ret['versions'] )) { usort( $ret, 'compare_versions'); } elseif( $originalListHash['sort_mode'] == 'versions_desc' && !empty( $ret['versions'] )) { usort( $ret, 'r_compare_versions'); } elseif( $originalListHash['sort_mode'] == 'links_desc' && !empty( $ret['links'] )) { usort( $ret, 'compare_links'); } elseif( $originalListHash['sort_mode'] == 'links_asc' && !empty( $ret['links'] )) { usort( $ret, 'r_compare_links'); } elseif( $originalListHash['sort_mode'] == 'backlinks_desc' && !empty( $ret['backlinks'] )) { usort($ret, 'compare_backlinks'); } elseif( $originalListHash['sort_mode'] == 'backlinks_asc' && !empty( $ret['backlinks'] )) { usort($ret, 'r_compare_backlinks'); } // return only requested values if( in_array( $originalListHash['sort_mode'], $specialSort )) { $ret = array_slice( $ret, $originalListHash['offset'], $originalListHash['max_records'] ); } // load original listHash $pListHash = $originalListHash; } LibertyContent::postGetList( $pListHash ); return $ret; } /* Update a page * $pHashOld the where conmdition : page_id * $pHashNew the new fields: title, data */ public function update( $pHashOld, $pHashNew) { $set = []; $where = []; if (!empty($pHashNew['title'])) { $set[] = "lc.`title`=?"; $bindVars[] = $pHashNew['title']; } if (!empty($pHashNew['data'])) { $set[] = "lc.`data`=?"; $bindVars[] = $pHashNew['data']; } if (!empty($pHashOld['page_id'])) { $where[] = "wp.`page_id`=?"; $bindVars[] = $pHashOld['page_id'] ; } if (empty($where)) { $this->mErrors['page_id'] = "You must specify a where condition"; return false; } $query = "update `".BIT_DB_PREFIX."liberty_content` lc LEFT JOIN `".BIT_DB_PREFIX."wiki_pages` wp on (wp.`content_id`= lc.`content_id`) SET ".implode(',', $set)." WHERE ".implode (" AND ", $where); $this->mDb->query( $query, $bindVars); return true; } // ...page... functions public function countSubPages( $pData ) { return preg_match_all( '/'.( defined( 'PAGE_SEP' ) ? preg_quote( PAGE_SEP ) : '\.\.\.page\.\.\.').'/', $pData ?? ''); // , $matches + 1 ); } /** * getSubPage * * @param string $pData * @param int $pPageNumber * @return string SubPage */ public function getSubPage( string $pData, int $pPageNumber ): string { // Get slides $parts = explode( defined( 'PAGE_SEP' ) ? PAGE_SEP : "...page...", $pData ); $ret = substr( $parts[$pPageNumber - 1], 1, 5 ) == "
" ? substr( $parts[$pPageNumber - 1], 6 ) : $parts[$pPageNumber - 1]; return $ret; } /** * getLikePages Like pages are pages that share a word in common with the current page * * @param string $pPageTitle * @return array */ public function getLikePages( string $pPageTitle ): array { $ret = []; if( !empty( $pPageName ) ) { preg_match_all("/([A-Z])([a-z]+)/", $pPageTitle, $words); // Add support to ((x)) in either strict or full modes preg_match_all("/(([A-Za-z]|[\x80-\xFF])+)/", $pPageTitle, $words2); $words = array_unique(array_merge($words[0], $words2[0])); $exps = []; $bindVars=[]; foreach ($words as $word) { $exps[] = "`title` like ?"; $bindVars[] = "%$word%"; } $selectSql = ''; $joinSql = ''; $whereSql = implode(" or ", $exps); array_push( $bindVars, $this->mContentTypeGuid ); $this->getServicesSql( 'content_list_sql_function', $selectSql, $joinSql, $whereSql, $bindVars ); $query = "SELECT lc.`title` FROM `".BIT_DB_PREFIX."wiki_pages` wp INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON (lc.`content_id` = wp.`content_id`) $joinSql WHERE $whereSql"; $result = $this->mDb->query($query,$bindVars); while ($res = $result->fetchRow()) { $ret[] = $res["title"]; } } return $ret; } /** * getStats getStats is always used by the stats package to display various stats of your package. * * @return array */ public function getStats(): array { global $gBitSystem; $ret = []; $query = "SELECT COUNT(*) FROM `".BIT_DB_PREFIX."wiki_pages`"; $ret['pages'] = [ 'label' => "Number of pages", 'value' => $this->mDb->getOne( $query ), ]; $listHash = [ 'orphans_only' => true ]; $this->getList( $listHash ); $ret['orphans'] = [ 'label' => 'Orphan Pages', 'value' => $listHash['total_records'], ]; $query = "SELECT SUM(`wiki_page_size`) FROM `".BIT_DB_PREFIX."wiki_pages`"; $ret['size'] = [ 'label' => "Combined size", 'value' => $this->mDb->getOne( $query ), 'modifier' => 'display_bytes', ]; $ret['average_size'] = [ 'label' => 'Average page size', 'value' => $ret['size']['value'] / $ret['pages']['value'], 'modifier' => 'display_bytes', ]; $query = " SELECT COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content_history` lch INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lch.`content_id` = lc.`content_id` ) WHERE lc.`content_type_guid` = ?"; $ret['versions'] = [ 'label' => "Versions", 'value' => $this->mDb->getOne( $query, [ BITPAGE_CONTENT_TYPE_GUID ]), ]; $ret['average_versions'] = [ 'label' => 'Average versions per page', 'value' => round( $ret['versions']['value'] / $ret['pages']['value'], 3 ), ]; $query = " SELECT COUNT(*) FROM `".BIT_DB_PREFIX."liberty_content_links` lcl INNER JOIN `".BIT_DB_PREFIX."liberty_content` lc ON( lcl.`from_content_id` = lc.`content_id` OR lcl.`from_content_id` = lc.`content_id` ) WHERE lc.`content_type_guid` = ?"; $ret['links'] = [ 'label' => "Total wiki links", 'value' => $this->mDb->getOne( $query, [ BITPAGE_CONTENT_TYPE_GUID ]), ]; $ret['average_links'] = [ 'label' => 'Average links per page', 'value' => round( $ret['links']['value'] / $ret['pages']['value'], 3 ), ]; return $ret; } // {{{ ==================================== GraphViz wiki graph methods ==================================== /** * linkStructureGraph * * @param object $pGraphViz * @param array $pLinkStructure * @param array $pParams * @return void */ public function linkStructureGraph( &$pGraphViz, $pLinkStructure = [], $pParams = [] ) { if( !empty( $pLinkStructure ) && !empty( $pGraphViz )) { $pParams['graph']['URL'] = WIKI_PKG_URL.'index.php'; $pGraphViz->addAttributes( $pParams['graph'] ); $pLinkStructure['title'] = $pLinkStructure['name']; $pParams['node']['URL'] = static::getDisplayUrlFromHash( $pLinkStructure ); $pGraphViz->addNode( $pLinkStructure['name'], $pParams['node'] ); foreach( $pLinkStructure['pages'] as $node ) { $this->linkStructureGraph( $pGraphViz,$node, $pParams ); $pGraphViz->addEdge( [ $pLinkStructure['name'] => $node['name'] ], $pParams['node'] ); } } } /** * linkStructureMap * * @param array $pPageName * @param int $pLevel * @param array $pParams * @access public * @return string */ public function linkStructureMap( $pPageName, $pLevel = 0, $pParams = [] ) { if( !empty( $pPageName ) && @include_once 'Image/GraphViz.php') { $graph = new \Image_GraphViz(); $this->linkStructureGraph( $graph,$this->getLinkStructure( $pPageName, $pLevel ), $pParams ); return $graph->fetch( 'cmap' ); } return ''; } /** * getLinkStructure * * @param string $pPageName * @param int $pLevel * @access public * @return array of links */ public function getLinkStructure( $pPageName, $pLevel = 0 ) { $query = " SELECT lc2.`title` FROM `".BIT_DB_PREFIX."liberty_content_links` lcl INNER JOIN liberty_content lc1 ON( lc1.`content_id` = lcl.`from_content_id` AND lc1.`content_status_id` > 49 ) INNER JOIN liberty_content lc2 ON( lc2.`content_id` = lcl.`to_content_id` AND lc2.`content_status_id` > 49 ) WHERE lc1.`title` = ? AND lcl.`from_content_id` <> lcl.`to_content_id`"; $result = $this->mDb->query( $query, [$pPageName]); $ret['pages'] = []; $ret['name'] = $pPageName; while( $res = $result->fetchRow() ) { $ret['pages'][] = !empty( $pLevel ) ? $this->getLinkStructure( $res['title'], $pLevel - 1 ) : [ 'name' => $res['title'], 'pages' => [] ]; } return $ret; } // }}} }