author | Dan |
Tue, 30 Mar 2010 11:37:00 -0400 | |
changeset 1231 | 4797a4a88533 |
parent 1227 | bdac73ed481e |
child 1252 | e34c23a35dc9 |
permissions | -rw-r--r-- |
<?php /** * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between * Copyright (C) 2006-2009 Dan Fuhry * paths.php - The part of Enano that actually manages content. Everything related to page handling and namespaces is in here. * * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. * * @package Enano * @subpackage PathManager * @see http://enanocms.org/Help:API_Documentation */ class pathManager { public $title, $pages, $custom_page, $cpage, $page, $fullpage, $page_exists, $page_id, $namespace, $nslist, $admin_tree, $wiki_mode, $page_protected, $template_cache, $external_api_page; /** * List of custom processing functions for namespaces. This is protected so trying to do anything with it will throw an error. * @access private * @var array */ protected $namespace_processors; function __construct() { global $db, $session, $paths, $template, $plugins; // Common objects $GLOBALS['paths'] =& $this; $this->pages = Array(); // DEFINE NAMESPACES HERE // The key names should NOT EVER be changed, or Enano will be very broken $namespace_delimiter = ( defined('WINDOWS_MOD_REWRITE_WORKAROUNDS') ) ? '.' : ':'; $this->nslist = Array( 'Article' => '', 'User' => 'User' . $namespace_delimiter, 'File' => 'File' . $namespace_delimiter, 'Help' => 'Help' . $namespace_delimiter, 'Admin' => 'Admin' . $namespace_delimiter, 'Special' => 'Special' . $namespace_delimiter, 'System' => 'Enano' . $namespace_delimiter, 'Template' => 'Template' . $namespace_delimiter, 'Category' => 'Category' . $namespace_delimiter, 'API' => 'SystemAPI' . $namespace_delimiter, 'Project' => sanitize_page_id(getConfig('site_name')) . $namespace_delimiter, ); // ACL types // These can also be added from within plugins $session->register_acl_type('read', AUTH_ALLOW, 'perm_read'); $session->register_acl_type('post_comments', AUTH_ALLOW, 'perm_post_comments', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('edit_comments', AUTH_ALLOW, 'perm_edit_comments', Array('post_comments'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('edit_page', AUTH_WIKIMODE, 'perm_edit_page', Array('view_source'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('edit_wysiwyg', AUTH_ALLOW, 'perm_edit_wysiwyg', Array(), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('view_source', AUTH_WIKIMODE, 'perm_view_source', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); // Only used if the page is protected $session->register_acl_type('mod_comments', AUTH_DISALLOW, 'perm_mod_comments', Array('edit_comments'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('history_view', AUTH_WIKIMODE, 'perm_history_view', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('history_rollback', AUTH_DISALLOW, 'perm_history_rollback', Array('history_view'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('history_rollback_extra', AUTH_DISALLOW, 'perm_history_rollback_extra', Array('history_rollback'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('protect', AUTH_DISALLOW, 'perm_protect', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('rename', AUTH_WIKIMODE, 'perm_rename', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('clear_logs', AUTH_DISALLOW, 'perm_clear_logs', Array('read', 'protect', 'even_when_protected'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('vote_delete', AUTH_ALLOW, 'perm_vote_delete', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('vote_reset', AUTH_DISALLOW, 'perm_vote_reset', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('delete_page', AUTH_DISALLOW, 'perm_delete_page', Array(), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('tag_create', AUTH_ALLOW, 'perm_tag_create', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('tag_delete_own', AUTH_ALLOW, 'perm_tag_delete_own', Array('read', 'tag_create'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('tag_delete_other', AUTH_DISALLOW, 'perm_tag_delete_other', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('set_wiki_mode', AUTH_DISALLOW, 'perm_set_wiki_mode', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('password_set', AUTH_DISALLOW, 'perm_password_set', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('password_reset', AUTH_DISALLOW, 'perm_password_reset', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('mod_misc', AUTH_DISALLOW, 'perm_mod_misc', Array(), 'All'); $session->register_acl_type('edit_cat', AUTH_WIKIMODE, 'perm_edit_cat', Array('read'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('even_when_protected', AUTH_DISALLOW, 'perm_even_when_protected', Array('edit_page', 'rename', 'mod_comments', 'edit_cat'), 'Article|User|Project|Template|File|Help|System|Category'); $session->register_acl_type('create_page', AUTH_WIKIMODE, 'perm_create_page', Array(), 'All'); $session->register_acl_type('upload_files', AUTH_DISALLOW, 'perm_upload_files', Array('create_page'), 'All'); $session->register_acl_type('upload_new_version', AUTH_WIKIMODE, 'perm_upload_new_version', Array('upload_files'), 'All'); $session->register_acl_type('html_in_pages', AUTH_DISALLOW, 'perm_html_in_pages', Array('edit_page'), 'Article|User|Project|Template|File|Help|System|Category|Admin'); $session->register_acl_type('php_in_pages', AUTH_DISALLOW, 'perm_php_in_pages', Array('edit_page', 'html_in_pages'), 'Article|User|Project|Template|File|Help|System|Category|Admin'); $session->register_acl_type('custom_user_title', AUTH_DISALLOW, 'perm_custom_user_title', Array(), 'User|Special'); $session->register_acl_type('edit_acl', AUTH_DISALLOW, 'perm_edit_acl', Array()); // DO NOT add new admin pages here! Use a plugin to call $paths->addAdminNode(); $this->addAdminNode('adm_cat_general', 'adm_page_general_config', 'GeneralConfig', array(2, 2)); $this->addAdminNode('adm_cat_general', 'adm_page_file_uploads', 'UploadConfig', array(2, 5)); $this->addAdminNode('adm_cat_general', 'adm_page_file_types', 'UploadAllowedMimeTypes', array(1, 5)); $this->addAdminNode('adm_cat_content', 'adm_page_manager', 'PageManager', array(1, 4)); $this->addAdminNode('adm_cat_content', 'adm_page_editor', 'PageEditor', array(3, 3)); $this->addAdminNode('adm_cat_content', 'adm_page_pg_groups', 'PageGroups', array(4, 3)); $this->addAdminNode('adm_cat_appearance', 'adm_page_themes', 'ThemeManager', array(4, 4)); $this->addAdminNode('adm_cat_appearance', 'adm_page_plugins', 'PluginManager', array(2, 4)); $this->addAdminNode('adm_cat_appearance', 'adm_page_db_backup', 'DBBackup', array(1, 2)); $this->addAdminNode('adm_cat_appearance', 'adm_page_lang_manager', 'LangManager', array(1, 3)); $this->addAdminNode('adm_cat_appearance', 'adm_page_cache_manager', 'CacheManager', array(3, 1)); $this->addAdminNode('adm_cat_users', 'adm_page_users', 'UserManager', array(3, 5)); $this->addAdminNode('adm_cat_users', 'adm_page_user_groups', 'GroupManager', array(3, 2)); $this->addAdminNode('adm_cat_users', 'adm_page_coppa', 'COPPA', array(4, 1)); $this->addAdminNode('adm_cat_users', 'adm_page_mass_email', 'MassEmail', array(2, 3)); $this->addAdminNode('adm_cat_users', 'adm_page_user_ranks', 'UserRanks', array(4, 5)); $this->addAdminNode('adm_cat_security', 'adm_page_security_log', 'SecurityLog', array(3, 4)); $this->addAdminNode('adm_cat_security', 'adm_page_ban_control', 'BanControl', array(2, 1)); $code = $plugins->setHook('acl_rule_init'); foreach ( $code as $cmd ) { eval($cmd); } $this->wiki_mode = ( getConfig('wiki_mode') == '1' ) ? 1 : 0; $this->template_cache = Array(); } function init($title) { global $db, $session, $paths, $template, $plugins; // Common objects global $lang; global $cache; $code = $plugins->setHook('paths_init_before'); foreach ( $code as $cmd ) { eval($cmd); } if ( defined('ENANO_INTERFACE_INDEX') || defined('ENANO_INTERFACE_AJAX') || defined('IN_ENANO_UPGRADE') ) { if ( empty($title) ) $title = get_title(); if ( empty($title) && !have_blank_urlname_page() ) { $this->main_page(); } if ( strstr($title, ' ') || strstr($title, '+') || strstr($title, '%20') ) { $title = sanitize_page_id($title); redirect(makeUrl($title), '', '', 0); } $title = sanitize_page_id($title); // We've got the title, pull the namespace from it $namespace = 'Article'; $page_id = $title; foreach ( $this->nslist as $ns => $prefix ) { $prefix_len = strlen($prefix); if ( substr($title, 0, $prefix_len) == $prefix ) { $page_id = substr($title, $prefix_len); $namespace = $ns; } } $this->namespace = $namespace; $this->fullpage = $title; if ( $namespace == 'Special' || $namespace == 'Admin' ) { list($page_id) = explode('/', $page_id); } $this->page = $this->nslist[$namespace] . $page_id; $this->page_id = $page_id; // die("All done setting parameters. What we've got:<br/>namespace: $namespace<br/>fullpage: $this->fullpage<br/>page: $this->page<br/>page_id: $this->page_id"); } else { // Starting up Enano with the API from a page that wants to do its own thing. Generate // metadata for an anonymous page and avoid redirection at all costs. if ( isset($GLOBALS['title']) ) { $title =& $GLOBALS['title']; } else { $title = basename($_SERVER['SCRIPT_NAME']); } $base_uri = scriptPath == '' ? ltrim($_SERVER['SCRIPT_NAME'], '/') : str_replace( scriptPath . '/', '', $_SERVER['SCRIPT_NAME'] ); $this->page = $this->nslist['API'] . sanitize_page_id($base_uri); $this->fullpage = $this->nslist['API'] . sanitize_page_id($base_uri); $this->namespace = 'API'; $this->cpage = array( 'name' => $title, 'urlname' => sanitize_page_id($base_uri), 'namespace' => 'API', 'special' => 1, 'visible' => 1, 'comments_on' => 1, 'protected' => 1, 'delvotes' => 0, 'delvote_ips' => '', 'page_format' => getConfig('default_page_format', 'wikitext') ); $this->external_api_page = true; $code = $plugins->setHook('paths_external_api_page'); foreach ( $code as $cmd ) { eval($cmd); } } $this->page = sanitize_page_id($this->page); $this->fullpage = sanitize_page_id($this->fullpage); if(isset($this->pages[$this->page])) { $this->page_exists = true; $this->cpage = $this->pages[$this->page]; $this->page_id =& $this->cpage['urlname_nons']; $this->namespace = $this->cpage['namespace']; if(!isset($this->cpage['wiki_mode'])) $this->cpage['wiki_mode'] = 2; // Determine the wiki mode for this page, now that we have this->cpage established if($this->cpage['wiki_mode'] == 2) { $this->wiki_mode = (int)getConfig('wiki_mode'); } else { $this->wiki_mode = $this->cpage['wiki_mode']; } // Allow the user to create/modify his user page uncondtionally (admins can still protect the page) if($this->page == $this->nslist['User'].str_replace(' ', '_', $session->username)) { $this->wiki_mode = true; } // And above all, if the site requires wiki mode to be off for non-logged-in users, disable it now if(getConfig('wiki_mode_require_login')=='1' && !$session->user_logged_in) { $this->wiki_mode = false; } if($this->cpage['protected'] == 2) { // The page is semi-protected, determine permissions if($session->user_logged_in && $session->reg_time + 60*60*24*4 < time()) { $this->page_protected = 0; } else { $this->page_protected = 1; } } else { $this->page_protected = $this->cpage['protected']; } } else { $this->page_exists = false; $page_name = dirtify_page_id($this->page); $page_name = str_replace('_', ' ', $page_name); $pid_cleaned = sanitize_page_id($this->page); if ( $pid_cleaned != $this->page ) { redirect(makeUrl($pid_cleaned), 'Sanitizer message', 'page id sanitized', 0); } if ( !is_array($this->cpage) ) { $this->cpage = Array( 'name' => $page_name, 'urlname' => $this->page, 'namespace' => 'Article', 'special' => 0, 'visible' => 0, 'comments_on' => 1, 'protected' => 0, 'delvotes' => 0, 'delvote_ips' => '', 'wiki_mode' => 2, 'page_format' => getConfig('default_page_format', 'wikitext') ); } // Look for a namespace prefix in the urlname, and assign a different namespace, if necessary $k = array_keys($this->nslist); for($i=0;$i<sizeof($this->nslist);$i++) { $ln = strlen($this->nslist[$k[$i]]); if( substr($this->page, 0, $ln) == $this->nslist[$k[$i]] ) { $this->cpage['namespace'] = $k[$i]; $this->cpage['urlname_nons'] = substr($this->page, strlen($this->nslist[$this->cpage['namespace']]), strlen($this->page)); if(!isset($this->cpage['wiki_mode'])) { $this->cpage['wiki_mode'] = 2; } } } $this->namespace = $this->cpage['namespace']; $this->page_id =& $this->cpage['urlname_nons']; if($this->namespace=='System') { $this->cpage['protected'] = 1; } if($this->namespace == 'Special' && !$this->external_api_page) { // Can't load nonexistent pages if( is_string(get_main_page()) ) { $main_page = makeUrl(get_main_page()); } else { $main_page = makeUrl($this->pages[0]['urlname']); } redirect($main_page, $lang->get('page_msg_special_404_title'), $lang->get('page_msg_special_404_body', array('sp_link' => makeUrlNS('Special', 'SpecialPages'))), 15); exit; } // Allow the user to create/modify his user page uncondtionally (admins can still protect the page) if($this->page == $this->nslist['User'].str_replace(' ', '_', $session->username)) { $this->wiki_mode = true; } } // This is used in the admin panel to keep track of form submission targets $this->cpage['module'] = $this->cpage['urlname']; $this->cpage['require_admin'] = ( $this->cpage['namespace'] === 'Admin' ); // Page is set up, call any hooks $code = $plugins->setHook('page_set'); foreach ( $code as $cmd ) { eval($cmd); } profiler_log('Paths and CMS core initted'); $session->init_permissions(); } /** * Fetch cdata (metadata) for a page. * @param string Page ID * @param string Namespace * @return array */ function get_cdata($page_id, $namespace) { global $db, $session, $paths, $template, $plugins; // Common objects $pathskey = $this->get_pathskey($page_id, $namespace); if ( isset($this->pages[$pathskey]) ) return $this->pages[$pathskey]; $page = namespace_factory($page_id, $namespace); $cdata = $page->get_cdata(); $this->pages[$pathskey] = $cdata; return $cdata; } /** * For a given page ID and namespace, generate a flat string that can be used to access $paths->pages. * @param string Page ID * @param string Namespace */ function get_pathskey($page_id, $namespace) { return ( isset($this->nslist[$namespace]) ) ? "{$this->nslist[$namespace]}{$page_id}" : "{$namespace}:{$page_id}"; } function add_page($flags) { global $lang; $flags['urlname_nons'] = $flags['urlname']; $flags['urlname'] = $this->nslist[$flags['namespace']] . $flags['urlname']; // Applies the User:/File:/etc prefixes to the URL names if ( is_object($lang) ) { if ( preg_match('/^[a-z0-9]+_[a-z0-9_]+$/', $flags['name']) ) $flags['name'] = $lang->get($flags['name']); } $flags['require_admin'] = ( $flags['namespace'] === 'Admin' ); $flags['page_exists'] = true; $this->pages[$flags['urlname']] = $flags; } function main_page() { if( is_string(get_main_page()) ) { $main_page = makeUrl(get_main_page()); } else { $main_page = makeUrl($this->pages[0]['urlname']); } redirect($main_page, 'Redirecting...', 'Invalid request, redirecting to main page', 0); exit; } function sysmsg($n) { global $db, $session, $paths, $template, $plugins; // Common objects static $sm_cache = array(); if ( isset($sm_cache[$n]) ) return $sm_cache[$n]; // sometimes this gets called during die_semicritical()... if ( !is_object($db) ) return false; if ( !@$db->_conn ) return false; $q = $db->sql_query('SELECT page_text, char_tag FROM '.table_prefix.'page_text WHERE page_id=\''.$db->escape(sanitize_page_id($n)).'\' AND namespace=\'System\''); if( !$q ) { $db->_die('Error during generic selection of system page data.'); } if($db->numrows() < 1) { return false; //$db->_die('Error during generic selection of system page data: there were no rows in the text table that matched the page text query.'); } $r = $db->fetchrow(); $db->free_result(); $message = $r['page_text']; $message = preg_replace('/<noinclude>(.*?)<\/noinclude>/is', '', $message); $message = preg_replace('/<nodisplay>(.*?)<\/nodisplay>/is', '\\1', $message); $sm_cache[$n] = $message; return $message; } function get_pageid_from_url() { return get_title(true, true); } // Parses a (very carefully formed) array into Javascript code compatible with the Tigra Tree Menu used in the admin menu function parseAdminTree() { global $lang; $k = array_keys($this->admin_tree); $i = 0; $ret = ''; $icon = $this->make_sprite_icon(4, 2); $ret .= "var TREE_ITEMS = "; $tree = array( array($icon . $lang->get('adm_btn_home'), "javascript:ajaxPage('{$this->nslist['Admin']}Home');") ); $root =& $tree[0]; foreach($k as $key) { $i++; $name = $lang->get($key); $group = array($name, "javascript:trees[0].toggle($i)"); foreach($this->admin_tree[$key] as $nodeid => $c) { $i++; $name = $lang->get($c['name']); if ( $c['icon'] && $c['icon'] != cdnPath . '/images/spacer.gif' ) { if ( is_array($c['icon']) ) { // this is a sprite reference list($ix, $iy) = $c['icon']; $icon = $this->make_sprite_icon($ix, $iy); } else { $icon = "<img alt=\"\" src=\"{$c['icon']}\" style=\"border-width: 0; margin-right: 3px;\" /> "; } } else { $icon = ''; } $group[] = array("$icon$name", "javascript:ajaxPage('{$this->nslist['Admin']}{$c['pageid']}');"); } $root[] = $group; } $icon = $this->make_sprite_icon(1, 1); $root[] = array( $icon . $lang->get('adm_btn_logout'), "javascript:ajaxPage('{$this->nslist['Admin']}AdminLogout');" ); $root[] = array( "<span id=\"keepalivestat\">" . $lang->get('adm_btn_keepalive_loading') . "</span>", "javascript:ajaxToggleKeepalive();", array( $lang->get('adm_btn_keepalive_about'), "javascript:aboutKeepAlive();" ) ); $ret .= enano_json_encode($tree) . ';'; // I used this while I painstakingly wrote the Runt code that auto-expands certain nodes based on the value of a bitfield stored in a cookie. *shudders* // $ret .= " ['(debug) Clear menu bitfield', 'javascript:createCookie(\\'admin_menu_state\\', \\'1\\', 365);'],\n"; return $ret; } /** * Internal function to generate HTML code for an icon in the admin panel tree which is sprited. * @param int X index of icon * @param int Y index of icon * @return string */ function make_sprite_icon($ix, $iy) { $xpos = 16 * ( $ix - 1 ); $ypos = 16 * ( $iy - 1 ); return "<img alt=\"\" src=\"" . cdnPath . "/images/spacer.gif\" class=\"adminiconsprite\" style=\"border-width: 0; margin-right: 3px; background-position: -{$xpos}px -{$ypos}px;\" /> "; } /** * Creates a new entry in the administration panel's navigation tree. * @param string Section name - if this is a language string identifier, it will be sent through $lang->get() * @param string The title of the page, also may be a language string identifier * @param string The page ID of the admin page, the namespace Admin is assumed * @param string Optional. The path to a 16x16 image that will be displayed as the icon for this admin page */ function addAdminNode($section, $page_title, $url, $icon = false) { if ( !$icon ) { $icon = cdnPath . '/images/spacer.gif'; } if(!isset($this->admin_tree[$section])) { $this->admin_tree[$section] = Array(); } $this->admin_tree[$section][] = Array( 'name' => $page_title, 'pageid' => $url, 'icon' => $icon ); } function getParam($id = 0) { $title = $this->fullpage; list(, $ns) = RenderMan::strToPageID($title); $title = substr($title, strlen($this->nslist[$ns])); $regex = '/^' . str_replace('/', '\\/', preg_quote($this->nslist[$ns])) . '\\/?/'; $title = preg_replace($regex, '', $title); $title = explode('/', $title); $id = $id + 1; return ( isset($title[$id]) ) ? $title[$id] : false; } function getAllParams() { $title = $this->fullpage; $regex = '/^' . str_replace('/', '\\/', preg_quote($this->nslist[$this->namespace])) . '\\/?/'; $title = preg_replace($regex, '', $title); $title = explode('/', $title); unset($title[0]); return implode('/', $title); } /** * Creates a new namespace in memory * @param string $id the namespace ID * @param string $prefix the URL prefix, must not be blank or already used * @return bool true on success false on failure */ function create_namespace($id, $prefix) { if(in_array($prefix, $this->nslist)) { // echo '<b>Warning:</b> pathManager::create_namespace: Prefix "'.$prefix.'" is already taken<br />'; return false; } if( isset($this->nslist[$id]) ) { // echo '<b>Warning:</b> pathManager::create_namespace: Namespace ID "'.$prefix.'" is already taken<br />'; return false; } $this->nslist[$id] = $prefix; } /** * Deprecated. */ function update_metadata_cache() { global $db, $session, $paths, $template, $plugins; // Common objects return false; } /** * Takes a result row from the pages table and calculates correct values for it. * @param array * @return array */ function calculate_metadata_from_row($r) { return Namespace_Default::bake_cdata($r); } /** * Registers a handler to manually process a namespace instead of the default PageProcessor behavior. * The first and only parameter passed to the processing function will be the PageProcessor instance. * @param string Namespace to process * @param mixed Function address. Either a function name or an array of the form array(0 => mixed (string:class name or object), 1 => string:method) */ function register_namespace_processor($namespace, $function) { if ( isset($this->namespace_processors[$namespace]) ) { $processorname = ( is_string($this->namespace_processors[$namespace]) ) ? $this->namespace_processors[$namespace] : ( is_object($this->namespace_processors[$namespace][0]) ? get_class($this->namespace_processors[$namespace][0]) : $this->namespace_processors[$namespace][0] ) . '::' . $this->namespace_processors[$namespace][1]; trigger_error("Namespace \"$namespace\" is already being processed by $processorname - replacing caller", E_USER_WARNING); } if ( !is_string($function) ) { if ( !is_array($function) ) return false; if ( count($function) != 2 ) return false; if ( !is_string($function[0]) && !is_object($function[0]) ) return false; if ( !is_string($function[1]) ) return false; } // security: don't allow Special or Admin namespaces to be overridden if ( $namespace == 'Special' || $namespace == 'Admin' ) { trigger_error("Security manager denied attempt to override processor for $namespace", E_USER_ERROR); } $this->namespace_processors[$namespace] = $function; } /** * Returns a namespace processor if one exists, otherwise returns false. * @param string Namespace * @return mixed */ function get_namespace_processor($namespace) { return ( isset($this->namespace_processors[$namespace]) ) ? $this->namespace_processors[$namespace] : false; } /** * Fetches the page texts for searching */ function fetch_page_search_texts() { global $db, $session, $paths, $template, $plugins; // Common objects $texts = Array(); $q = $db->sql_query('SELECT t.page_id,t.namespace,t.page_text,t.char_tag FROM '.table_prefix.'page_text AS t LEFT JOIN '.table_prefix.'pages AS p ON t.page_id=p.urlname WHERE p.namespace=t.namespace AND ( p.password=\'\' OR p.password=\'da39a3ee5e6b4b0d3255bfef95601890afd80709\' ) AND p.visible=1;'); // Only indexes "visible" pages if( !$q ) { return false; } while($row = $db->fetchrow()) { $pid = $this->nslist[$row['namespace']] . $row['page_id']; $texts[$pid] = $row['page_text']; } $db->free_result(); return $texts; } /** * Generates an SQL query to grab all of the text */ function fetch_page_search_resource() { global $db, $session, $paths, $template, $plugins; // Common objects // sha1('') returns "da39a3ee5e6b4b0d3255bfef95601890afd80709" $concat_column = ( ENANO_DBLAYER == 'MYSQL' ) ? 'CONCAT(\'ns=\',t.namespace,\';pid=\',t.page_id)' : "'ns=' || t.namespace || ';pid=' || t.page_id"; $texts = 'SELECT t.page_text, ' . $concat_column . ' AS page_idstring, t.page_id, t.namespace FROM '.table_prefix.'page_text AS t LEFT JOIN '.table_prefix.'pages AS p ON ( t.page_id=p.urlname AND t.namespace=p.namespace ) WHERE p.namespace=t.namespace AND ( p.password=\'\' OR p.password=\'da39a3ee5e6b4b0d3255bfef95601890afd80709\' ) AND p.visible=1;'; // Only indexes "visible" pages return $texts; } /** * Builds a word list for search indexing. * @param string Text to index * @param string Page ID of the page being indexed * @param string Title of the page being indexed * @return array List of words */ function calculate_word_list($text, $page_id, $page_name) { $page_id = dirtify_page_id($page_id); $text = preg_replace('/[^a-z0-9\']/i', ' ', $text); $page_id = preg_replace('/[^a-z0-9\']/i', ' ', $page_id); $page_name = preg_replace('/[^a-z0-9\']/i', ' ', $page_name); $text .= " $page_id $page_name"; $text = explode(' ', $text); foreach ( $text as $i => &$word ) { if ( strstr($word, "''") ) $word = preg_replace("/[']{2,}/", '', $word); if ( strlen($word) < 2 ) unset($text[$i]); } $text = array_unique(array_values($text)); // for debugging purposes (usually XSS safe because of character stripping) // echo ' ' . implode(' ', $text) . '<br />'; return $text; } /** * Rebuilds the site's entire search index. Considerably more exciting if run from the command line. * @param bool If true, verbose output. * @param bool If true, verbose + debugging output. */ function rebuild_search_index($verbose = false, $debug = false) { global $db, $session, $paths, $template, $plugins; // Common objects require_once(ENANO_ROOT . '/includes/search.php'); $progress = false; if ( class_exists('ProgressBar') ) { // CLI only. $progress = new ProgressBar('Rebuilding search index: [', ']', 'Initializing...', 'green', 'blue', 'white', 'yellow'); $verbose = false; $debug = false; $progress->start(); } @set_time_limit(0); $q = $db->sql_query('DELETE FROM ' . table_prefix . 'search_index;'); if ( !$q ) $db->_die(); $sha1_blank = sha1(''); // // Index $pages_in_batch pages at a time // $pages_in_batch = 15; // First find out how many pages there are $q = $db->sql_query('SELECT COUNT(p.urlname) AS num_pages FROM ' . table_prefix . "page_text AS t\n" . " LEFT JOIN " . table_prefix . "pages AS p\n" . " ON ( p.urlname = t.page_id AND p.namespace = t.namespace )\n" . " WHERE ( p.password = '' OR p.password = '$sha1_blank' )\n" . " AND ( p.visible = 1 );"); if ( !$q ) $db->_die(); list($num_pages) = $db->fetchrow_num(); $num_pages = intval($num_pages); $loops = ceil($num_pages / $pages_in_batch); $master_word_list = array(); $stopwords = get_stopwords(); for ( $j = 0; $j < $loops; ) { $offset = $j * $pages_in_batch; $j++; if ( $verbose && $debug ) { echo "Running indexing round $j of $loops (offset $offset)\n" . ( isset($_SERVER['REQUEST_URI']) ? '<br />' : '' ); } // this is friendly to both MySQL and PostgreSQL. $texts = $db->sql_query('SELECT p.name, p.visible, t.page_id, t.namespace, t.page_text FROM ' . table_prefix . "page_text AS t\n" . " LEFT JOIN " . table_prefix . "pages AS p\n" . " ON ( p.urlname = t.page_id AND p.namespace = t.namespace )\n" . " WHERE ( p.password = '' OR p.password = '$sha1_blank' )\n" . " AND ( p.visible = 1 )\n" . " LIMIT $pages_in_batch OFFSET $offset;", false); if ( !$texts ) $db->_die(); $k = $offset; if ( $row = $db->fetchrow($texts) ) { do { $k++; if ( $verbose ) { $mu = memory_get_usage(); echo " Indexing page $k of $num_pages: {$row['namespace']}:{$row['page_id']}"; if ( $debug ) echo ", mem = $mu..."; flush(); } else if ( is_object($progress) ) { $progress->update_text_quiet("$k/$num_pages {$row['namespace']}:{$row['page_id']}"); $progress->set($k, $num_pages); } // skip this page if it's not supposed to be indexed if ( $row['visible'] == 0 ) { if ( $verbose ) { echo "skipped"; if ( isset($_SERVER['REQUEST_URI']) ) echo '<br />'; echo "\n"; } continue; } // Indexing identifier for the page in the DB $page_uniqid = "ns={$row['namespace']};pid=" . sanitize_page_id($row['page_id']); $page_uniqid = $db->escape($page_uniqid); // List of words on the page $wordlist = $this->calculate_word_list($row['page_text'], $row['page_id'], $row['name']); // Index calculation complete -- run inserts $inserts = array(); foreach ( $wordlist as $word ) { if ( in_array($word, $stopwords) || strval(intval($word)) === $word || strlen($word) < 3 ) continue; $word_db = $db->escape($word); $word_db_lc = $db->escape(strtolower($word)); if ( !in_array($word, $master_word_list) ) { $inserts[] = "( '$word_db', '$word_db_lc', '$page_uniqid' )"; } else { if ( $verbose && $debug ) echo '.'; $pid_col = ( ENANO_DBLAYER == 'MYSQL' ) ? "CONCAT( page_names, ',$page_uniqid' )": "page_names || ',$page_uniqid'"; $q = $db->sql_query('UPDATE ' . table_prefix . "search_index SET page_names = $pid_col WHERE word = '$word_db';", false); if ( !$q ) $db->_die(); } } if ( count($inserts) > 0 ) { if ( $verbose && $debug ) echo 'i'; $inserts = implode(",\n ", $inserts); $q = $db->sql_query('INSERT INTO ' . table_prefix . "search_index(word, word_lcase, page_names) VALUES\n $inserts;", false); if ( !$q ) $db->_die(); } $master_word_list = array_unique(array_merge($master_word_list, $wordlist)); if ( $verbose ) { if ( isset($_SERVER['REQUEST_URI']) ) echo '<br />'; echo "\n"; } unset($inserts, $wordlist, $page_uniqid, $word_db, $q, $word, $row); } while ( $row = $db->fetchrow($texts) ); } $db->free_result($texts); } if ( $verbose ) { echo "Indexing complete."; if ( isset($_SERVER['REQUEST_URI']) ) echo '<br />'; echo "\n"; } else if ( is_object($progress) ) { $progress->update_text('Complete.'); $progress->end(); } return true; } /** * Partially rebuilds the search index, removing/inserting entries only for the current page * @param string $page_id * @param string $namespace */ function rebuild_page_index($page_id, $namespace) { global $db, $session, $paths, $template, $plugins; // Common objects require_once(ENANO_ROOT . '/includes/search.php'); if(!$db->sql_query('SELECT page_text FROM '.table_prefix.'page_text WHERE page_id=\''.$db->escape($page_id).'\' AND namespace=\''.$db->escape($namespace).'\';')) { return $db->get_error(); } if ( $db->numrows() < 1 ) return 'E: No rows'; $idstring = $this->nslist[$namespace] . sanitize_page_id($page_id); if ( !isset($this->pages[$idstring]) ) { return 'E: Can\'t find page metadata'; } $row = $db->fetchrow(); $db->free_result(); $search = new Searcher(); // if the page shouldn't be indexed, send a blank set of strings to the indexing engine if ( $this->pages[$idstring]['visible'] == 0 ) { $search->buildIndex(Array("ns={$namespace};pid={$page_id}"=>'')); } else { $search->buildIndex(Array("ns={$namespace};pid={$page_id}"=>$row['page_text'] . ' ' . $this->pages[$idstring]['name'])); } $new_index = $search->index; if ( count($search->index) == 0 ) // o_O // nothing indexed. return true; if ( ENANO_DBLAYER == 'MYSQL' ) { $keys = array_keys($search->index); foreach($keys as $i => $k) { $c =& $keys[$i]; $c = hexencode($c, '', ''); } $keys = "word=0x" . implode ( " OR word=0x", $keys ) . ""; } else { $keys = array_keys($search->index); foreach($keys as $i => $k) { $c =& $keys[$i]; $c = $db->escape($c); } $keys = "word='" . implode ( "' OR word='", $keys ) . "'"; } $query = $db->sql_query('SELECT word,page_names FROM '.table_prefix.'search_index WHERE '.$keys.';'); while($row = $db->fetchrow()) { $row['word'] = rtrim($row['word'], "\0"); $new_index[ $row['word'] ] = $row['page_names'] . ',' . $search->index[ $row['word'] ]; } $db->free_result(); $db->sql_query('DELETE FROM '.table_prefix.'search_index WHERE '.$keys.';'); $secs = Array(); $q = 'INSERT INTO '.table_prefix.'search_index(word,word_lcase,page_names) VALUES'; foreach($new_index as $word => $pages) { $secs[] = '(\''.$db->escape($word).'\', \'' . $db->escape(strtolower($word)) . '\', \''.$db->escape($pages).'\')'; } $q .= implode(',', $secs); unset($secs); $q .= ';'; if(!$db->check_query($q)) { die('BUG: PathManager::rebuild_page_index: Query rejected by SQL parser:<pre>'.$q.'</pre>'); } $result = $db->sql_query($q); if($result) return true; else $db->_die('The search index was trying to rebuild itself when the error occured.'); } /** * Returns a list of groups that a given page is a member of. * @param string Page ID * @param string Namespace * @return array */ function get_page_groups($page_id, $namespace) { global $db, $session, $paths, $template, $plugins; // Common objects static $cache = array(); if ( count($cache) == 0 ) { foreach ( $this->nslist as $key => $_ ) { $cache[$key] = array(); } } if ( !isset($this->nslist[$namespace]) ) die('$paths->get_page_groups(): HACKING ATTEMPT: namespace "'. htmlspecialchars($namespace) .'" doesn\'t exist'); $page_id_unescaped = $paths->nslist[$namespace] . dirtify_page_id($page_id); $page_id_str = $paths->nslist[$namespace] . sanitize_page_id($page_id); $page_id = $db->escape(sanitize_page_id($page_id)); if ( isset($cache[$namespace][$page_id]) ) { return $cache[$namespace][$page_id]; } $group_list = array(); // What linked categories have this page? $q = $db->sql_unbuffered_query('SELECT g.pg_id, g.pg_type, g.pg_target FROM '.table_prefix.'page_groups AS g LEFT JOIN '.table_prefix.'categories AS c ON ( ( c.category_id = g.pg_target AND g.pg_type = ' . PAGE_GRP_CATLINK . ' ) OR c.category_id IS NULL ) LEFT JOIN '.table_prefix.'page_group_members AS m ON ( ( g.pg_id = m.pg_id AND g.pg_type = ' . PAGE_GRP_NORMAL . ' ) OR ( m.pg_id IS NULL ) ) LEFT JOIN '.table_prefix.'tags AS t ON ( ( t.tag_name = g.pg_target AND pg_type = ' . PAGE_GRP_TAGGED . ' ) OR t.tag_name IS NULL ) WHERE ( c.page_id=\'' . $page_id . '\' AND c.namespace=\'' . $namespace . '\' ) OR ( t.page_id=\'' . $page_id . '\' AND t.namespace=\'' . $namespace . '\' ) OR ( m.page_id=\'' . $page_id . '\' AND m.namespace=\'' . $namespace . '\' ) OR ( g.pg_type = ' . PAGE_GRP_REGEX . ' );'); if ( !$q ) $db->_die(); while ( $row = $db->fetchrow() ) { if ( $row['pg_type'] == PAGE_GRP_REGEX ) { //echo "<debug> matching page " . htmlspecialchars($page_id_unescaped) . " against regex <tt>" . htmlspecialchars($row['pg_target']) . "</tt>."; if ( @preg_match($row['pg_target'], $page_id_unescaped) || @preg_match($row['pg_target'], $page_id_str) ) { //echo "..matched"; $group_list[] = $row['pg_id']; } //echo "<br />"; } else { $group_list[] = $row['pg_id']; } } $db->free_result(); $cache[$namespace][$page_id] = $group_list; return $group_list; } } /** * Register a special page. * @param string urlname of the page ("Administration" in "Special:Administration") * @param string Page title; if in the right format, will be treated as a l10n string identifier * @param bool If true, page is visible (shown in lists/search). Otherwise, hidden. Defaults to true. */ function register_special_page($urlname, $name, $visible = true) { global $paths; return $paths->add_page(Array( 'name' => $name, 'urlname' => $urlname, 'namespace' => 'Special', 'special' => 0, 'visible' => $visible ? 1 : 0, 'comments_on' => 0, 'protected' => 1, 'delvotes' => 0, 'delvote_ips' => '', )); } ?>